Download src/App.jsx from shutajb/agent: direct link, hf CLI and curl.
- Browser
- Download file 7.68 kB
-
https://huggingface.co/spaces/shutajb/agent/resolve/main/src/App.jsx
- Command line
-
hf download hf://spaces/shutajb/agent/src/App.jsx
-
curl -L -o App.jsx https://huggingface.co/spaces/shutajb/agent/resolve/main/src/App.jsx
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 | |