free-coding-models 0.5.5 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +19 -11
  2. package/bin/free-coding-models.js +6 -0
  3. package/changelog/v0.5.5.md +1 -0
  4. package/changelog/v0.5.6.md +24 -0
  5. package/changelog/v0.5.7.md +20 -0
  6. package/package.json +4 -4
  7. package/src/core/changelog-loader.js +5 -1
  8. package/src/core/endpoint-installer.js +3 -3
  9. package/src/core/router-daemon.js +11 -0
  10. package/src/core/tool-launchers.js +2 -2
  11. package/web/dist/assets/index-DKmmiDip.css +1 -0
  12. package/web/dist/assets/index-DzVt42Nf.js +39 -0
  13. package/web/dist/index.html +2 -2
  14. package/web/server.js +532 -2
  15. package/web/src/App.jsx +235 -73
  16. package/web/src/components/analytics/AnalyticsView.jsx +4 -0
  17. package/web/src/components/analytics/TokenUsagePanel.jsx +105 -0
  18. package/web/src/components/analytics/TokenUsagePanel.module.css +126 -0
  19. package/web/src/components/atoms/TierBadge.module.css +3 -3
  20. package/web/src/components/changelog/ChangelogView.jsx +135 -0
  21. package/web/src/components/changelog/ChangelogView.module.css +160 -0
  22. package/web/src/components/dashboard/DetailPanel.jsx +29 -4
  23. package/web/src/components/dashboard/DetailPanel.module.css +26 -0
  24. package/web/src/components/dashboard/FilterBar.jsx +17 -2
  25. package/web/src/components/dashboard/FilterBar.module.css +17 -0
  26. package/web/src/components/dashboard/ModelTable.jsx +17 -5
  27. package/web/src/components/dashboard/ModelTable.module.css +14 -1
  28. package/web/src/components/help/HelpView.jsx +188 -0
  29. package/web/src/components/help/HelpView.module.css +157 -0
  30. package/web/src/components/install/InstallEndpointsView.jsx +278 -0
  31. package/web/src/components/install/InstallEndpointsView.module.css +351 -0
  32. package/web/src/components/installed/InstalledModelsView.jsx +89 -0
  33. package/web/src/components/installed/InstalledModelsView.module.css +200 -0
  34. package/web/src/components/launch/IncompatibleFallbackModal.jsx +69 -0
  35. package/web/src/components/launch/LaunchButton.jsx +30 -0
  36. package/web/src/components/launch/LaunchButton.module.css +35 -0
  37. package/web/src/components/launch/LaunchModal.module.css +125 -0
  38. package/web/src/components/layout/Header.jsx +78 -14
  39. package/web/src/components/layout/Header.module.css +62 -2
  40. package/web/src/components/palette/CommandPalette.jsx +228 -74
  41. package/web/src/components/recommend/RecommendView.jsx +121 -0
  42. package/web/src/components/recommend/RecommendView.module.css +55 -0
  43. package/web/src/components/router/RouterView.jsx +286 -0
  44. package/web/src/components/router/RouterView.module.css +357 -0
  45. package/web/src/components/settings/SettingsView.jsx +281 -8
  46. package/web/src/components/settings/SettingsView.module.css +174 -0
  47. package/web/src/components/tools/ToolPicker.jsx +86 -0
  48. package/web/src/components/tools/ToolPicker.module.css +84 -0
  49. package/web/src/components/update/UpdateChip.jsx +104 -0
  50. package/web/src/components/update/UpdateChip.module.css +146 -0
  51. package/web/src/global.css +23 -0
  52. package/web/src/hooks/urlState.constants.js +28 -0
  53. package/web/src/hooks/useChangelog.js +51 -0
  54. package/web/src/hooks/useInstalledModels.js +42 -0
  55. package/web/src/hooks/useRecommend.js +67 -0
  56. package/web/src/hooks/useRouterDashboard.js +133 -0
  57. package/web/src/hooks/useTokenUsage.js +103 -0
  58. package/web/src/hooks/useToolMode.js +77 -0
  59. package/web/src/hooks/useUpdateChecker.js +91 -0
  60. package/web/src/hooks/useUrlState.js +123 -62
  61. package/web/src/utils/m3.js +55 -0
  62. package/web/dist/assets/index-uD3faN3G.js +0 -39
  63. package/web/dist/assets/index-uTifVKX1.css +0 -1
@@ -0,0 +1,278 @@
1
+ /**
2
+ * @file web/src/components/install/InstallEndpointsView.jsx
3
+ * @description Install Endpoints wizard — 4-step modal: provider → tool → models → install.
4
+ * 📖 M4: Full TUI parity for the install endpoints overlay.
5
+ * 📖 Uses existing /api/install-endpoints/providers, /api/install-endpoints/catalog,
6
+ * 📖 and /api/install-endpoints/wizard endpoints.
7
+ */
8
+ import { useState, useEffect, useCallback } from 'react'
9
+ import {
10
+ IconPlug, IconChevronRight, IconChevronLeft, IconCheck,
11
+ IconLoader,
12
+ } from '@tabler/icons-react'
13
+ import styles from './InstallEndpointsView.module.css'
14
+
15
+ const STEPS = ['Provider', 'Tool', 'Models', 'Install']
16
+
17
+ export default function InstallEndpointsView({ onClose, onToast }) {
18
+ const [step, setStep] = useState(0)
19
+ const [providers, setProviders] = useState([])
20
+ const [selectedProvider, setSelectedProvider] = useState(null)
21
+ const [catalogModels, setCatalogModels] = useState([])
22
+ const [selectedTool, setSelectedTool] = useState(null)
23
+ const [selectedModels, setSelectedModels] = useState([])
24
+ const [scope, setScope] = useState('all')
25
+ const [installing, setInstalling] = useState(false)
26
+ const [installResult, setInstallResult] = useState(null)
27
+ const [loading, setLoading] = useState(true)
28
+
29
+ // 📖 Available install targets from ToolPicker's mode list
30
+ const TOOLS = [
31
+ { id: 'opencode', label: 'OpenCode CLI', emoji: '💻' },
32
+ { id: 'opencode-desktop', label: 'OpenCode Desktop', emoji: '🖥️' },
33
+ { id: 'openclaw', label: 'OpenClaw', emoji: '🦞' },
34
+ { id: 'crush', label: 'Crush', emoji: '💘' },
35
+ { id: 'goose', label: 'Goose', emoji: '🪿' },
36
+ { id: 'pi', label: 'Pi', emoji: 'π' },
37
+ { id: 'aider', label: 'Aider', emoji: '🛠' },
38
+ { id: 'qwen', label: 'Qwen', emoji: '🐉' },
39
+ { id: 'openhands', label: 'OpenHands', emoji: '🤲' },
40
+ { id: 'amp', label: 'Amp', emoji: '⚡' },
41
+ { id: 'forgecode', label: 'ForgeCode', emoji: '🔥' },
42
+ { id: 'fcm_router', label: 'FCM Router', emoji: '🔄' },
43
+ ]
44
+
45
+ // Load providers on mount
46
+ useEffect(() => {
47
+ fetch('/api/install-endpoints/providers')
48
+ .then(r => r.json())
49
+ .then(data => {
50
+ setProviders(data.providers || [])
51
+ setLoading(false)
52
+ })
53
+ .catch(() => setLoading(false))
54
+ }, [])
55
+
56
+ // Load catalog when provider selected
57
+ useEffect(() => {
58
+ if (!selectedProvider) return
59
+ fetch(`/api/install-endpoints/catalog?provider=${selectedProvider.providerKey}`)
60
+ .then(r => r.json())
61
+ .then(data => setCatalogModels(data.models || []))
62
+ .catch(() => setCatalogModels([]))
63
+ }, [selectedProvider])
64
+
65
+ const toggleModel = useCallback((modelId) => {
66
+ setSelectedModels(prev =>
67
+ prev.includes(modelId)
68
+ ? prev.filter(id => id !== modelId)
69
+ : [...prev, modelId]
70
+ )
71
+ }, [])
72
+
73
+ const selectAll = useCallback(() => {
74
+ setSelectedModels(catalogModels.map(m => m.modelId))
75
+ setScope('all')
76
+ }, [catalogModels])
77
+
78
+ const selectNone = useCallback(() => {
79
+ setSelectedModels([])
80
+ setScope('selected')
81
+ }, [])
82
+
83
+ const canNext = () => {
84
+ if (step === 0) return !!selectedProvider
85
+ if (step === 1) return !!selectedTool
86
+ if (step === 2) return scope === 'all' || selectedModels.length > 0
87
+ return true
88
+ }
89
+
90
+ const handleInstall = async () => {
91
+ setInstalling(true)
92
+ try {
93
+ const resp = await fetch('/api/install-endpoints/wizard', {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({
97
+ providerKey: selectedProvider.providerKey,
98
+ toolMode: selectedTool,
99
+ scope,
100
+ modelIds: scope === 'selected' ? selectedModels : [],
101
+ }),
102
+ })
103
+ const data = await resp.json()
104
+ if (data.success) {
105
+ setInstallResult(data)
106
+ onToast?.(`Installed ${data.modelCount} models for ${selectedProvider.label} into ${TOOLS.find(t => t.id === selectedTool)?.label || selectedTool}.`, 'success')
107
+ } else {
108
+ onToast?.(`Install failed: ${data.error || 'unknown'}`, 'error')
109
+ }
110
+ } catch (err) {
111
+ onToast?.(`Install error: ${err.message}`, 'error')
112
+ } finally {
113
+ setInstalling(false)
114
+ }
115
+ }
116
+
117
+ const handleNext = () => {
118
+ if (step === 2) {
119
+ handleInstall()
120
+ return
121
+ }
122
+ setStep(s => Math.min(s + 1, 3))
123
+ }
124
+
125
+ const handleBack = () => {
126
+ setStep(s => Math.max(s - 1, 0))
127
+ }
128
+
129
+ return (
130
+ <div className={styles.overlay} onClick={(e) => e.target === e.currentTarget && onClose()}>
131
+ <div className={styles.modal}>
132
+ <div className={styles.header}>
133
+ <h2 className={styles.title}>
134
+ <IconPlug size={20} stroke={1.5} />
135
+ Install Endpoints
136
+ </h2>
137
+ <button className={styles.closeBtn} onClick={onClose} aria-label="Close">✕</button>
138
+ </div>
139
+
140
+ {/* Step indicator */}
141
+ <div className={styles.steps}>
142
+ {STEPS.map((label, i) => (
143
+ <div key={i} className={`${styles.step} ${i === step ? styles.stepActive : i < step ? styles.stepDone : ''}`}>
144
+ <span className={styles.stepNum}>{i < step ? <IconCheck size={12} /> : i + 1}</span>
145
+ <span className={styles.stepLabel}>{label}</span>
146
+ </div>
147
+ ))}
148
+ </div>
149
+
150
+ <div className={styles.body}>
151
+ {/* Step 0: Select Provider */}
152
+ {step === 0 && (
153
+ <div className={styles.stepContent}>
154
+ <p className={styles.stepDesc}>Choose a provider with a configured API key.</p>
155
+ {loading && <div className={styles.loading}>Loading providers…</div>}
156
+ {!loading && providers.length === 0 && (
157
+ <div className={styles.empty}>No configured providers found. Add API keys in Settings first.</div>
158
+ )}
159
+ <div className={styles.grid}>
160
+ {providers.map((p) => (
161
+ <button
162
+ key={p.providerKey}
163
+ className={`${styles.pickCard} ${selectedProvider?.providerKey === p.providerKey ? styles.pickActive : ''}`}
164
+ onClick={() => { setSelectedProvider(p); setStep(1) }}
165
+ >
166
+ <span className={styles.pickLabel}>{p.label}</span>
167
+ <span className={styles.pickMeta}>{p.modelCount} models</span>
168
+ </button>
169
+ ))}
170
+ </div>
171
+ </div>
172
+ )}
173
+
174
+ {/* Step 1: Select Tool */}
175
+ {step === 1 && (
176
+ <div className={styles.stepContent}>
177
+ <p className={styles.stepDesc}>
178
+ Install <strong>{selectedProvider?.label}</strong> models into which tool?
179
+ </p>
180
+ <div className={styles.grid}>
181
+ {TOOLS.map((tool) => (
182
+ <button
183
+ key={tool.id}
184
+ className={`${styles.pickCard} ${selectedTool === tool.id ? styles.pickActive : ''}`}
185
+ onClick={() => { setSelectedTool(tool.id); setStep(2) }}
186
+ >
187
+ <span className={styles.pickEmoji}>{tool.emoji}</span>
188
+ <span className={styles.pickLabel}>{tool.label}</span>
189
+ </button>
190
+ ))}
191
+ </div>
192
+ </div>
193
+ )}
194
+
195
+ {/* Step 2: Select Models */}
196
+ {step === 2 && (
197
+ <div className={styles.stepContent}>
198
+ <p className={styles.stepDesc}>
199
+ Choose models to install from <strong>{selectedProvider?.label}</strong> into <strong>{TOOLS.find(t => t.id === selectedTool)?.label}</strong>.
200
+ </p>
201
+ <div className={styles.scopeToggle}>
202
+ <button className={`${styles.scopeBtn} ${scope === 'all' ? styles.scopeActive : ''}`} onClick={() => { setScope('all'); selectAll() }}>All Models</button>
203
+ <button className={`${styles.scopeBtn} ${scope === 'selected' ? styles.scopeActive : ''}`} onClick={() => setScope('selected')}>Select Models</button>
204
+ </div>
205
+ {scope === 'selected' && (
206
+ <div className={styles.modelGrid}>
207
+ {catalogModels.map((model) => (
208
+ <button
209
+ key={model.modelId}
210
+ className={`${styles.modelCard} ${selectedModels.includes(model.modelId) ? styles.modelSelected : ''}`}
211
+ onClick={() => toggleModel(model.modelId)}
212
+ >
213
+ <span className={styles.modelName}>{model.label}</span>
214
+ <span className={styles.modelTier}>{model.tier}</span>
215
+ </button>
216
+ ))}
217
+ </div>
218
+ )}
219
+ {scope === 'all' && (
220
+ <div className={styles.allNotice}>
221
+ All {catalogModels.length} models from {selectedProvider?.label} will be installed.
222
+ </div>
223
+ )}
224
+ </div>
225
+ )}
226
+
227
+ {/* Step 3: Installing / Done */}
228
+ {step === 3 && (
229
+ <div className={styles.stepContent}>
230
+ {installing && (
231
+ <div className={styles.installingState}>
232
+ <IconLoader size={24} className={styles.spinner} />
233
+ <span>Installing {scope === 'all' ? 'all' : selectedModels.length} models…</span>
234
+ </div>
235
+ )}
236
+ {installResult && (
237
+ <div className={styles.doneState}>
238
+ <IconCheck size={32} color="#00c864" />
239
+ <h3>Installation Complete</h3>
240
+ <p>{installResult.modelCount} models installed into {installResult.toolLabel}</p>
241
+ <code className={styles.installPath}>{installResult.path}</code>
242
+ </div>
243
+ )}
244
+ </div>
245
+ )}
246
+ </div>
247
+
248
+ {/* Footer nav */}
249
+ <div className={styles.footer}>
250
+ {step > 0 && step < 3 && (
251
+ <button className={styles.backBtn} onClick={handleBack}>
252
+ <IconChevronLeft size={14} />
253
+ Back
254
+ </button>
255
+ )}
256
+ {step < 3 && (
257
+ <button className={styles.nextBtn} onClick={handleNext} disabled={!canNext()}>
258
+ {step === 2 ? (
259
+ <>
260
+ <IconPlug size={14} />
261
+ Install
262
+ </>
263
+ ) : (
264
+ <>
265
+ Next
266
+ <IconChevronRight size={14} />
267
+ </>
268
+ )}
269
+ </button>
270
+ )}
271
+ {step === 3 && installResult && (
272
+ <button className={styles.doneBtn} onClick={onClose}>Done</button>
273
+ )}
274
+ </div>
275
+ </div>
276
+ </div>
277
+ )
278
+ }
@@ -0,0 +1,351 @@
1
+ .overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ z-index: 200;
5
+ background: rgba(0, 0, 0, 0.6);
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ animation: fadeIn 0.15s ease;
10
+ }
11
+
12
+ .modal {
13
+ background: var(--bg-primary, #0f0f17);
14
+ border: 1px solid var(--border, #1e1e2e);
15
+ border-radius: 12px;
16
+ width: 680px;
17
+ max-width: 95vw;
18
+ max-height: 85vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
22
+ }
23
+
24
+ .header {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ padding: 16px 20px;
29
+ border-bottom: 1px solid var(--border, #1e1e2e);
30
+ }
31
+
32
+ .title {
33
+ margin: 0;
34
+ font-size: 16px;
35
+ font-weight: 600;
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 8px;
39
+ color: var(--text-primary, #e0e0e0);
40
+ }
41
+
42
+ .closeBtn {
43
+ background: none;
44
+ border: none;
45
+ color: var(--text-muted, #888);
46
+ font-size: 18px;
47
+ cursor: pointer;
48
+ padding: 4px 8px;
49
+ border-radius: 6px;
50
+ &:hover { background: var(--bg-hover, #1a1a2e); color: var(--text-primary, #e0e0e0); }
51
+ }
52
+
53
+ /* Steps */
54
+ .steps {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ gap: 24px;
59
+ padding: 14px 20px;
60
+ border-bottom: 1px solid var(--border, #1e1e2e);
61
+ }
62
+
63
+ .step {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 6px;
67
+ font-size: 12px;
68
+ color: var(--text-muted, #555);
69
+ }
70
+
71
+ .stepActive {
72
+ color: var(--text-primary, #e0e0e0);
73
+ font-weight: 600;
74
+ }
75
+
76
+ .stepDone {
77
+ color: #00c864;
78
+ }
79
+
80
+ .stepNum {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ width: 22px;
85
+ height: 22px;
86
+ border-radius: 50%;
87
+ font-size: 11px;
88
+ font-weight: 700;
89
+ background: var(--bg-secondary, #14141f);
90
+ border: 1px solid var(--border, #1e1e2e);
91
+ }
92
+
93
+ .stepActive .stepNum {
94
+ background: rgba(0, 200, 100, 0.15);
95
+ border-color: rgba(0, 200, 100, 0.3);
96
+ color: #00c864;
97
+ }
98
+
99
+ .stepDone .stepNum {
100
+ background: rgba(0, 200, 100, 0.15);
101
+ border-color: #00c864;
102
+ color: #00c864;
103
+ }
104
+
105
+ .stepLabel {
106
+ text-transform: uppercase;
107
+ letter-spacing: 0.3px;
108
+ }
109
+
110
+ /* Body */
111
+ .body {
112
+ padding: 20px;
113
+ overflow-y: auto;
114
+ flex: 1;
115
+ min-height: 300px;
116
+ }
117
+
118
+ .stepContent {}
119
+
120
+ .stepDesc {
121
+ font-size: 14px;
122
+ color: var(--text-secondary, #aaa);
123
+ margin: 0 0 16px 0;
124
+ }
125
+
126
+ .loading, .empty {
127
+ color: var(--text-muted, #666);
128
+ font-size: 13px;
129
+ text-align: center;
130
+ padding: 30px 0;
131
+ }
132
+
133
+ /* Pick Grid (Provider + Tool) */
134
+ .grid {
135
+ display: grid;
136
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
137
+ gap: 8px;
138
+ }
139
+
140
+ .pickCard {
141
+ display: flex;
142
+ flex-direction: column;
143
+ align-items: center;
144
+ gap: 4px;
145
+ padding: 14px 10px;
146
+ background: var(--bg-secondary, #14141f);
147
+ border: 1px solid var(--border, #1e1e2e);
148
+ border-radius: 8px;
149
+ cursor: pointer;
150
+ color: var(--text-secondary, #aaa);
151
+ font-size: 13px;
152
+ transition: all 0.15s ease;
153
+ &:hover { border-color: var(--text-muted, #666); color: var(--text-primary, #e0e0e0); }
154
+ }
155
+
156
+ .pickActive {
157
+ border-color: #00c864;
158
+ background: rgba(0, 200, 100, 0.08);
159
+ color: #00c864;
160
+ }
161
+
162
+ .pickEmoji {
163
+ font-size: 20px;
164
+ }
165
+
166
+ .pickLabel {
167
+ font-weight: 600;
168
+ font-size: 12px;
169
+ }
170
+
171
+ .pickMeta {
172
+ font-size: 10px;
173
+ color: var(--text-muted, #888);
174
+ }
175
+
176
+ /* Scope Toggle */
177
+ .scopeToggle {
178
+ display: flex;
179
+ gap: 8px;
180
+ margin-bottom: 12px;
181
+ }
182
+
183
+ .scopeBtn {
184
+ padding: 6px 14px;
185
+ border: 1px solid var(--border, #1e1e2e);
186
+ border-radius: 6px;
187
+ background: var(--bg-secondary, #14141f);
188
+ color: var(--text-secondary, #aaa);
189
+ font-size: 12px;
190
+ cursor: pointer;
191
+ &:hover { border-color: var(--text-muted, #888); }
192
+ }
193
+
194
+ .scopeActive {
195
+ background: rgba(0, 200, 100, 0.1);
196
+ border-color: rgba(0, 200, 100, 0.3);
197
+ color: #00c864;
198
+ font-weight: 600;
199
+ }
200
+
201
+ /* Model Selection */
202
+ .modelGrid {
203
+ display: grid;
204
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
205
+ gap: 6px;
206
+ max-height: 240px;
207
+ overflow-y: auto;
208
+ }
209
+
210
+ .modelCard {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: space-between;
214
+ padding: 8px 10px;
215
+ background: var(--bg-secondary, #14141f);
216
+ border: 1px solid var(--border, #1e1e2e);
217
+ border-radius: 6px;
218
+ cursor: pointer;
219
+ color: var(--text-secondary, #aaa);
220
+ font-size: 12px;
221
+ &:hover { border-color: var(--text-muted, #666); }
222
+ }
223
+
224
+ .modelSelected {
225
+ border-color: #00c864;
226
+ background: rgba(0, 200, 100, 0.08);
227
+ color: #00c864;
228
+ }
229
+
230
+ .modelName {
231
+ overflow: hidden;
232
+ text-overflow: ellipsis;
233
+ white-space: nowrap;
234
+ }
235
+
236
+ .modelTier {
237
+ font-size: 10px;
238
+ font-weight: 700;
239
+ padding: 1px 4px;
240
+ background: rgba(0, 200, 255, 0.1);
241
+ color: #00c8ff;
242
+ border-radius: 3px;
243
+ margin-left: 6px;
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ .allNotice {
248
+ color: var(--text-muted, #888);
249
+ font-size: 13px;
250
+ text-align: center;
251
+ padding: 30px 0;
252
+ }
253
+
254
+ /* Install State */
255
+ .installingState {
256
+ display: flex;
257
+ flex-direction: column;
258
+ align-items: center;
259
+ justify-content: center;
260
+ gap: 12px;
261
+ padding: 40px 0;
262
+ color: var(--text-secondary, #aaa);
263
+ }
264
+
265
+ .spinner {
266
+ animation: spin 1s linear infinite;
267
+ }
268
+
269
+ .doneState {
270
+ display: flex;
271
+ flex-direction: column;
272
+ align-items: center;
273
+ gap: 8px;
274
+ padding: 30px 0;
275
+ text-align: center;
276
+ h3 { margin: 0; color: #00c864; font-size: 18px; }
277
+ p { margin: 0; color: var(--text-secondary, #aaa); font-size: 13px; }
278
+ }
279
+
280
+ .installPath {
281
+ font-size: 11px;
282
+ color: var(--text-muted, #888);
283
+ background: var(--bg-secondary, #14141f);
284
+ padding: 4px 10px;
285
+ border-radius: 4px;
286
+ max-width: 100%;
287
+ overflow: hidden;
288
+ text-overflow: ellipsis;
289
+ }
290
+
291
+ /* Footer */
292
+ .footer {
293
+ display: flex;
294
+ justify-content: space-between;
295
+ padding: 14px 20px;
296
+ border-top: 1px solid var(--border, #1e1e2e);
297
+ }
298
+
299
+ .backBtn {
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 4px;
303
+ padding: 8px 14px;
304
+ background: var(--bg-secondary, #14141f);
305
+ border: 1px solid var(--border, #1e1e2e);
306
+ border-radius: 8px;
307
+ color: var(--text-secondary, #aaa);
308
+ font-size: 13px;
309
+ cursor: pointer;
310
+ &:hover { color: var(--text-primary, #e0e0e0); }
311
+ }
312
+
313
+ .nextBtn {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 4px;
317
+ padding: 8px 18px;
318
+ background: #00c864;
319
+ color: #000;
320
+ border: none;
321
+ border-radius: 8px;
322
+ font-size: 13px;
323
+ font-weight: 600;
324
+ cursor: pointer;
325
+ margin-left: auto;
326
+ &:hover { background: #00d874; }
327
+ &:disabled { opacity: 0.4; cursor: not-allowed; }
328
+ }
329
+
330
+ .doneBtn {
331
+ padding: 8px 24px;
332
+ background: #00c864;
333
+ color: #000;
334
+ border: none;
335
+ border-radius: 8px;
336
+ font-size: 13px;
337
+ font-weight: 600;
338
+ cursor: pointer;
339
+ margin-left: auto;
340
+ &:hover { background: #00d874; }
341
+ }
342
+
343
+ @keyframes fadeIn {
344
+ from { opacity: 0; }
345
+ to { opacity: 1; }
346
+ }
347
+
348
+ @keyframes spin {
349
+ from { transform: rotate(0deg); }
350
+ to { transform: rotate(360deg); }
351
+ }