agent / src /App.jsx
shutajb's picture
Upload 22 files
e5faeaf verified
Raw History Blame Contribute Delete
7.68 kB
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