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