File size: 7,681 Bytes
e5faeaf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | import { useState, useEffect } from 'react'
import { Settings, MessageSquare, Play, Sun, Moon, Download, Save } from 'lucide-react'
import ChatInterface from './components/ChatInterface'
import AgentInterface from './components/AgentInterface'
import ExportDialog from './components/ExportDialog'
const DEFAULT_CONFIG = {
supplier: 'https://openrouter.ai/api/v1',
apiKey: '',
model: 'nvidia/nemotron-3-ultra-550b-a55b:free',
}
const CONFIG_KEY = 'agentStudioConfig'
const CONFIGS_KEY = 'agentStudioConfigs'
function loadInitialConfig() {
try {
const raw = localStorage.getItem(CONFIG_KEY)
if (raw) return { ...DEFAULT_CONFIG, ...JSON.parse(raw) }
} catch { /* 讀取失敗就用預設值 */ }
return DEFAULT_CONFIG
}
function SettingsModal({ apiConfig, onApply, onDuplicate, savedConfigs, onLoadSaved, onClose }) {
const [draft, setDraft] = useState(apiConfig)
const set = (field, value) => setDraft({ ...draft, [field]: value })
return (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
<div className="bg-panel p-8 rounded-3xl w-full max-w-md border border-edge">
<h2 className="text-2xl font-bold mb-6">API 連線設定</h2>
<div className="space-y-5">
<div>
<label className="block text-sm mb-1">供應商 API URL</label>
<input
type="text"
value={draft.supplier}
onChange={(e) => set('supplier', e.target.value)}
placeholder="https://openrouter.ai/api/v1"
className="w-full bg-field px-4 py-3 rounded-2xl focus:outline-none"
/>
</div>
<div>
<label className="block text-sm mb-1">API Key</label>
<input
type="password"
value={draft.apiKey}
onChange={(e) => set('apiKey', e.target.value)}
placeholder="sk-..."
className="w-full bg-field px-4 py-3 rounded-2xl focus:outline-none"
/>
<p className="text-xs text-muted mt-1">
只存在你的瀏覽器 localStorage,不會送到任何伺服器。
</p>
</div>
<div>
<label className="block text-sm mb-1">Model</label>
<input
type="text"
value={draft.model}
onChange={(e) => set('model', e.target.value)}
placeholder="nvidia/nemotron-3-ultra-550b-a55b:free"
className="w-full bg-field px-4 py-3 rounded-2xl focus:outline-none"
/>
</div>
</div>
<div className="flex gap-3 mt-7">
<button
onClick={() => onApply({ ...draft })}
className="flex-1 flex items-center justify-center gap-2 bg-purple-600 py-3 rounded-2xl font-medium"
>
<Save className="w-4 h-4" /> 套用並關閉
</button>
<button
onClick={() => onDuplicate({ ...draft })}
className="px-4 py-3 bg-field rounded-2xl"
title="另存為可切換的配置"
>
<Save className="w-4 h-4" />
</button>
<button onClick={onClose} className="px-6 bg-red-600 rounded-2xl">關閉</button>
</div>
{savedConfigs.length > 0 && (
<div className="mt-6">
<p className="text-xs text-muted mb-2">已儲存配置(點一下套用):</p>
<div className="space-y-1">
{savedConfigs.map((c, i) => (
<button
key={i}
onClick={() => onLoadSaved(c)}
className="w-full text-left text-xs text-blue-400 hover:underline break-all"
>
{c.model || '(無模型)'} · {String(c.apiKey).slice(0, 6)}…
</button>
))}
</div>
</div>
)}
</div>
</div>
)
}
function App() {
const [view, setView] = useState('chat')
const [showSettings, setShowSettings] = useState(false)
const [showExport, setShowExport] = useState(false)
const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'dark')
const [apiConfig, setApiConfig] = useState(loadInitialConfig)
const [savedConfigs, setSavedConfigs] = useState(() => {
try {
return JSON.parse(localStorage.getItem(CONFIGS_KEY) || '[]')
} catch {
return []
}
})
useEffect(() => {
document.documentElement.classList.remove('theme-dark', 'theme-light')
document.documentElement.classList.add(`theme-${theme}`)
localStorage.setItem('theme', theme)
}, [theme])
// 金鑰與模型設定即時寫回 localStorage,重新整理不丟失
useEffect(() => {
localStorage.setItem(CONFIG_KEY, JSON.stringify(apiConfig))
}, [apiConfig])
const applyConfig = (config) => {
setApiConfig(config)
setShowSettings(false)
}
const duplicateConfig = () => {
const updated = [...savedConfigs, apiConfig]
setSavedConfigs(updated)
localStorage.setItem(CONFIGS_KEY, JSON.stringify(updated))
setShowSettings(false)
}
return (
<div className="min-h-screen bg-app text-ink">
<div className="max-w-7xl mx-auto p-4">
<div className="flex items-center justify-between gap-3 flex-wrap mb-6">
<div className="flex items-center gap-3">
<img src="./vite.svg" className="w-8 h-8" alt="Agent" />
<h1 className="text-3xl font-bold">Agent Studio</h1>
</div>
<div className="flex gap-2 flex-wrap">
<button
onClick={() => setView('chat')}
className={`px-4 py-2 rounded-xl flex items-center gap-2 ${view === 'chat' ? 'bg-blue-600' : 'bg-field'}`}
>
<MessageSquare className="w-4 h-4" /> 聊天
</button>
<button
onClick={() => setView('agent')}
className={`px-4 py-2 rounded-xl flex items-center gap-2 ${view === 'agent' ? 'bg-purple-600' : 'bg-field'}`}
>
<Play className="w-4 h-4" /> Agent
</button>
<button
onClick={() => setShowSettings(true)}
className="flex items-center gap-2 px-4 py-2 bg-field rounded-xl"
>
<Settings className="w-4 h-4" /> 設定
</button>
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="flex items-center gap-2 px-4 py-2 bg-field rounded-xl"
title={theme === 'dark' ? '切換亮色' : '切換深色'}
>
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
</button>
<button
onClick={() => setShowExport(true)}
className="flex items-center gap-2 px-4 py-2 bg-field rounded-xl"
>
<Download className="w-4 h-4" /> 匯出
</button>
</div>
</div>
{view === 'chat' ? (
<ChatInterface apiConfig={apiConfig} onChange={setApiConfig} />
) : (
<AgentInterface apiConfig={apiConfig} onOpenSettings={() => setShowSettings(true)} />
)}
</div>
{showSettings && (
<SettingsModal
apiConfig={apiConfig}
onApply={applyConfig}
onDuplicate={duplicateConfig}
savedConfigs={savedConfigs}
onLoadSaved={(c) => applyConfig(c)}
onClose={() => setShowSettings(false)}
/>
)}
{showExport && <ExportDialog onClose={() => setShowExport(false)} />}
</div>
)
}
export default App
|