ideaco 1.1.5

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 (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,551 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { useI18n } from '@/lib/i18n';
6
+ import TalentMarket from './TalentMarket';
7
+
8
+ const CATEGORY_ICONS = { general: '💬', drawing: '🎨', music: '🎵', video: '🎬', cli: '🖥️', browser: '🌐' };
9
+ const CATEGORY_ORDER = { cli: 0, browser: 1, general: 2, drawing: 3, music: 4, video: 5 };
10
+ const PRICE_COLORS = ['text-green-400', 'text-yellow-400', 'text-red-400'];
11
+ const PRICE_DOTS = ['bg-green-500', 'bg-yellow-500', 'bg-red-500'];
12
+ const RATING_COLORS = { high: 'text-green-400', mid: 'text-yellow-400', low: 'text-orange-400' };
13
+
14
+ function RatingBadge({ rating }) {
15
+ const color = rating >= 90 ? RATING_COLORS.high : rating >= 80 ? RATING_COLORS.mid : RATING_COLORS.low;
16
+ return <span className={`text-xs font-bold ${color}`}>⭐ {rating}</span>;
17
+ }
18
+
19
+ /**
20
+ * Shared provider grid with all config modals.
21
+ * Used by both ProvidersBoard and SystemMonitor.
22
+ *
23
+ * @param {object} props
24
+ * @param {boolean} [props.showHeader] - show title + subtitle
25
+ * @param {boolean} [props.showDescription] - show hint card + talent market
26
+ * @param {boolean} [props.showSecretary] - show secretary provider card
27
+ * @param {boolean} [props.showStatusDot] - show green/red dot per category header
28
+ * @param {function} [props.onCLIDetected] - callback after CLI detect/register (for local state refresh)
29
+ */
30
+ export default function ProviderGrid({
31
+ showHeader = false,
32
+ showDescription = true,
33
+ showSecretary = true,
34
+ showStatusDot = false,
35
+ onCLIDetected,
36
+ }) {
37
+ const { t } = useI18n();
38
+ const { company, configureProvider, fetchCLIBackends, detectCLIBackends, manageCLIBackend, updateSecretarySettings } = useStore();
39
+
40
+ // CLI management state
41
+ const [cliDetecting, setCLIDetecting] = useState(false);
42
+ const [showRegisterCLI, setShowRegisterCLI] = useState(false);
43
+ const [newCLI, setNewCLI] = useState({ id: '', name: '', execCommand: '', execArgs: '-p,{prompt},-y', detectCommand: '', memoryDir: '', memoryFile: 'MEMORY.md', nvmNode: '' });
44
+
45
+ // API Key config state
46
+ const [configTarget, setConfigTarget] = useState(null);
47
+ const [apiKey, setApiKey] = useState('');
48
+
49
+ // Web agent config state
50
+ const [webConfigTarget, setWebConfigTarget] = useState(null);
51
+ const [cookieValue, setCookieValue] = useState('');
52
+ const [cookieTestResult, setCookieTestResult] = useState(null);
53
+ const [cookieTesting, setCookieTesting] = useState(false);
54
+ const [cookieLogging, setCookieLogging] = useState(false);
55
+ const isElectron = typeof window !== 'undefined' && window.electronAPI?.isElectron;
56
+
57
+ // Selector calibration state
58
+ const [calibrating, setCalibrating] = useState(false);
59
+ const [selectorStatus, setSelectorStatus] = useState(null);
60
+
61
+ // Secretary state
62
+ const [showSecretaryPicker, setShowSecretaryPicker] = useState(false);
63
+ const [secretaryProviderId, setSecretaryProviderId] = useState(company?.secretary?.providerId || '');
64
+ const [savingSecretary, setSavingSecretary] = useState(false);
65
+
66
+ // Talent Market
67
+ const [showTalentMarket, setShowTalentMarket] = useState(false);
68
+
69
+ const categoryLabels = {
70
+ general: t('providers.categories.general'),
71
+ drawing: t('providers.categories.drawing'),
72
+ music: t('providers.categories.music'),
73
+ video: t('providers.categories.video'),
74
+ cli: t('providers.categories.cli'),
75
+ browser: t('providers.categories.browser'),
76
+ };
77
+
78
+ if (!company) return null;
79
+
80
+ const dashboard = company.providerDashboard || {};
81
+
82
+ const handleConfigure = async () => {
83
+ if (!configTarget || !apiKey) return;
84
+ try {
85
+ await configureProvider(configTarget.id, apiKey);
86
+ setConfigTarget(null);
87
+ setApiKey('');
88
+ } catch (e) { /* handled */ }
89
+ };
90
+
91
+ const handleCLIDetect = async () => {
92
+ setCLIDetecting(true);
93
+ await detectCLIBackends();
94
+ const updated = await fetchCLIBackends();
95
+ onCLIDetected?.(updated);
96
+ setCLIDetecting(false);
97
+ };
98
+
99
+ const handleCLIRegister = async () => {
100
+ if (!newCLI.id || !newCLI.execCommand) return;
101
+ const config = {
102
+ ...newCLI,
103
+ execArgs: newCLI.execArgs.split(',').map(s => s.trim()),
104
+ detectCommand: newCLI.detectCommand || `${newCLI.execCommand} --version`,
105
+ memoryDir: newCLI.memoryDir || `.${newCLI.id}`,
106
+ nvmNode: newCLI.nvmNode || null,
107
+ };
108
+ const updated = await manageCLIBackend('register', { config });
109
+ onCLIDetected?.(updated);
110
+ setShowRegisterCLI(false);
111
+ setNewCLI({ id: '', name: '', execCommand: '', execArgs: '-p,{prompt},-y', detectCommand: '', memoryDir: '', memoryFile: 'MEMORY.md', nvmNode: '' });
112
+ };
113
+
114
+ return (
115
+ <>
116
+ {showHeader && (
117
+ <div>
118
+ <h1 className="text-2xl font-bold">{t('providers.title')}</h1>
119
+ <p className="text-sm text-[var(--muted)] mt-1">{t('providers.subtitle')}</p>
120
+ </div>
121
+ )}
122
+
123
+ {/* Description card */}
124
+ {showDescription && (
125
+ <div className="card bg-gradient-to-r from-blue-900/10 to-purple-900/10 border-blue-500/20">
126
+ <div className="flex items-start gap-3">
127
+ <span className="text-2xl">💡</span>
128
+ <div className="text-sm text-[var(--muted)] flex-1">
129
+ <p className="font-medium text-[var(--foreground)] mb-1">{t('providers.hint.title')}</p>
130
+ <p dangerouslySetInnerHTML={{ __html: t('providers.hint.desc') }} />
131
+ </div>
132
+ <button
133
+ onClick={() => setShowTalentMarket(true)}
134
+ className="shrink-0 flex items-center gap-2 px-3 py-2 rounded-lg bg-yellow-900/20 border border-yellow-500/20 text-yellow-400 hover:bg-yellow-900/30 transition-all text-sm"
135
+ >
136
+ <span>🏪</span>
137
+ <span>{t('providers.talentMarket.btn')}</span>
138
+ {(company?.talentMarket?.length || 0) > 0 && (
139
+ <span className="text-xs bg-yellow-500/20 px-1.5 py-0.5 rounded-full">{company.talentMarket.length}</span>
140
+ )}
141
+ </button>
142
+ </div>
143
+ </div>
144
+ )}
145
+
146
+ {/* Secretary provider card */}
147
+ {showSecretary && (
148
+ <div className="card bg-gradient-to-r from-purple-900/10 to-blue-900/10 border-purple-500/20">
149
+ <div className="flex items-center justify-between">
150
+ <div className="flex items-center gap-3">
151
+ <span className="text-2xl">🤖</span>
152
+ <div>
153
+ <h3 className="font-semibold">{t('providers.secretaryProvider.title')}</h3>
154
+ <p className="text-xs text-[var(--muted)] mt-0.5 max-w-md">{t('providers.secretaryProvider.desc')}</p>
155
+ </div>
156
+ </div>
157
+ <div className="flex items-center gap-3 shrink-0">
158
+ <div className="text-right">
159
+ <div className="text-sm font-medium">
160
+ {company?.secretary?.provider && company?.secretary?.providerId !== 'none'
161
+ ? <span className="text-green-400">{t('providers.secretaryProvider.current', { name: company.secretary.provider })}</span>
162
+ : <span className="text-yellow-400">{t('providers.secretaryProvider.noneConfigured')}</span>
163
+ }
164
+ </div>
165
+ </div>
166
+ <button
167
+ onClick={() => {
168
+ const avail = company?.secretary?.availableProviders || [];
169
+ const currentId = company?.secretary?.providerId || '';
170
+ const inList = avail.some(p => p.id === currentId);
171
+ setSecretaryProviderId(inList ? currentId : (avail[0]?.id || ''));
172
+ setShowSecretaryPicker(true);
173
+ }}
174
+ className="px-3 py-2 rounded-lg text-xs bg-[var(--accent)]/10 text-[var(--accent)] hover:bg-[var(--accent)]/20 transition-all"
175
+ >
176
+ {t('providers.secretaryProvider.changeBtn')}
177
+ </button>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ )}
182
+
183
+ {/* Secretary provider picker modal */}
184
+ {showSecretaryPicker && company?.secretary?.availableProviders && (
185
+ <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 !m-0" onClick={() => setShowSecretaryPicker(false)}>
186
+ <div className="card max-w-sm w-full mx-4 space-y-4" onClick={e => e.stopPropagation()}>
187
+ <h3 className="text-lg font-semibold">{t('secretarySettings.providerLabel')}</h3>
188
+ <p className="text-xs text-[var(--muted)]">{t('secretarySettings.providerDesc')}</p>
189
+ {company.secretary?.availableProviders?.length > 0 ? (
190
+ <select
191
+ className="input w-full"
192
+ value={secretaryProviderId}
193
+ onChange={e => setSecretaryProviderId(e.target.value)}
194
+ >
195
+ {company.secretary.availableProviders.map(p => (
196
+ <option key={p.id} value={p.id}>{p.name}</option>
197
+ ))}
198
+ </select>
199
+ ) : (
200
+ <div className="text-xs text-yellow-400 p-2 rounded bg-yellow-400/10 border border-yellow-400/20">
201
+ {t('secretarySettings.noProviders')}
202
+ </div>
203
+ )}
204
+ <div className="flex gap-2">
205
+ <button className="btn-secondary flex-1" onClick={() => setShowSecretaryPicker(false)}>{t('common.cancel')}</button>
206
+ <button
207
+ className="btn-primary flex-1"
208
+ disabled={!secretaryProviderId || savingSecretary}
209
+ onClick={async () => {
210
+ setSavingSecretary(true);
211
+ try {
212
+ await updateSecretarySettings({ providerId: secretaryProviderId });
213
+ setShowSecretaryPicker(false);
214
+ } catch {}
215
+ setSavingSecretary(false);
216
+ }}
217
+ >
218
+ {savingSecretary ? t('secretarySettings.saving') : t('common.save')}
219
+ </button>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ )}
224
+
225
+ {/* Category grids */}
226
+ {Object.entries(dashboard)
227
+ .sort(([a], [b]) => (CATEGORY_ORDER[a] ?? 99) - (CATEGORY_ORDER[b] ?? 99))
228
+ .map(([category, info]) => (
229
+ <div key={category} className="card">
230
+ <div className="flex items-center justify-between mb-4">
231
+ <div className="flex items-center gap-2">
232
+ <span className="text-2xl">{CATEGORY_ICONS[category]}</span>
233
+ <div>
234
+ <h3 className="font-semibold">{categoryLabels[category] || category}</h3>
235
+ <div className="text-xs text-[var(--muted)]">
236
+ {t('providers.enabled', { n: info.enabled, total: info.total })}
237
+ </div>
238
+ </div>
239
+ </div>
240
+ {showStatusDot && (
241
+ <div className={`w-3 h-3 rounded-full ${info.enabled > 0 ? 'bg-green-500' : 'bg-red-500'}`} />
242
+ )}
243
+ {category === 'cli' && (
244
+ <div className="flex gap-2 ml-auto mr-3">
245
+ <button
246
+ onClick={handleCLIDetect}
247
+ disabled={cliDetecting}
248
+ className="px-2.5 py-1 rounded text-[10px] bg-[var(--accent)] text-white hover:opacity-90 transition-all disabled:opacity-50"
249
+ >
250
+ {cliDetecting ? t('systemSettings.cliBackends.detecting') : `🔍 ${t('systemSettings.cliBackends.detectAll')}`}
251
+ </button>
252
+ <button
253
+ onClick={() => setShowRegisterCLI(!showRegisterCLI)}
254
+ className="px-2.5 py-1 rounded text-[10px] bg-white/10 text-[var(--muted)] hover:bg-white/20 transition-all"
255
+ >
256
+ + {t('systemSettings.cliBackends.registerCustom')}
257
+ </button>
258
+ </div>
259
+ )}
260
+ </div>
261
+
262
+ {/* CLI category: register custom CLI form */}
263
+ {category === 'cli' && showRegisterCLI && (
264
+ <div className="p-4 rounded-lg bg-white/5 border border-[var(--border)] mb-4 space-y-3 animate-fade-in">
265
+ <h4 className="text-xs font-semibold">{t('systemSettings.cliBackends.registerCustom')}</h4>
266
+ <div className="grid grid-cols-2 gap-3">
267
+ <input className="input text-xs" placeholder={t('systemSettings.cliBackends.form.id')} value={newCLI.id} onChange={e => setNewCLI({...newCLI, id: e.target.value})} />
268
+ <input className="input text-xs" placeholder={t('systemSettings.cliBackends.form.name')} value={newCLI.name} onChange={e => setNewCLI({...newCLI, name: e.target.value})} />
269
+ <input className="input text-xs" placeholder={t('systemSettings.cliBackends.form.execCommand')} value={newCLI.execCommand} onChange={e => setNewCLI({...newCLI, execCommand: e.target.value})} />
270
+ <input className="input text-xs" placeholder={t('systemSettings.cliBackends.form.execArgs')} value={newCLI.execArgs} onChange={e => setNewCLI({...newCLI, execArgs: e.target.value})} />
271
+ <input className="input text-xs" placeholder={t('systemSettings.cliBackends.form.detectCommand')} value={newCLI.detectCommand} onChange={e => setNewCLI({...newCLI, detectCommand: e.target.value})} />
272
+ <input className="input text-xs" placeholder={t('systemSettings.cliBackends.form.memoryDir')} value={newCLI.memoryDir} onChange={e => setNewCLI({...newCLI, memoryDir: e.target.value})} />
273
+ <input className="input text-xs" placeholder={t('systemSettings.cliBackends.form.memoryFile')} value={newCLI.memoryFile} onChange={e => setNewCLI({...newCLI, memoryFile: e.target.value})} />
274
+ <div>
275
+ <input className="input text-xs" placeholder={t('systemSettings.cliBackends.form.nvmNode')} value={newCLI.nvmNode} onChange={e => setNewCLI({...newCLI, nvmNode: e.target.value})} />
276
+ <p className="text-[10px] text-[var(--muted)] mt-0.5">{t('systemSettings.cliBackends.form.nvmNodeHint')}</p>
277
+ </div>
278
+ </div>
279
+ <div className="flex gap-2 justify-end">
280
+ <button onClick={() => setShowRegisterCLI(false)} className="px-3 py-1 text-xs text-[var(--muted)] hover:text-white transition-all">
281
+ {t('common.cancel')}
282
+ </button>
283
+ <button
284
+ onClick={handleCLIRegister}
285
+ disabled={!newCLI.id || !newCLI.execCommand}
286
+ className="px-3 py-1 rounded-lg text-xs bg-[var(--accent)] text-white hover:opacity-90 disabled:opacity-50 transition-all"
287
+ >
288
+ {t('systemSettings.cliBackends.registerCustom')}
289
+ </button>
290
+ </div>
291
+ </div>
292
+ )}
293
+
294
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
295
+ {info.providers.map((p) => (
296
+ <div
297
+ key={p.id}
298
+ className={`p-3 rounded-lg border transition-all ${
299
+ p.enabled
300
+ ? 'border-green-500/30 bg-green-900/10'
301
+ : 'border-[var(--border)] bg-[var(--background)]'
302
+ }`}
303
+ >
304
+ <div className="flex items-center justify-between mb-2">
305
+ <div className="flex items-center gap-2 min-w-0">
306
+ {p.cliIcon && <span className="text-sm">{p.cliIcon}</span>}
307
+ <span className={`status-dot ${p.enabled ? 'active' : 'idle'}`} />
308
+ <span className="text-sm font-medium truncate">{p.name}</span>
309
+ </div>
310
+ {p.isCLI ? (
311
+ <button
312
+ className={`text-xs px-2.5 py-1 rounded transition-all shrink-0 ${
313
+ p.enabled
314
+ ? 'bg-green-900/30 text-green-400 hover:bg-red-900/30 hover:text-red-400'
315
+ : 'bg-[var(--accent)]/10 text-[var(--accent)] hover:bg-green-900/30 hover:text-green-400'
316
+ }`}
317
+ onClick={async () => {
318
+ await configureProvider(p.id, p.enabled ? '' : 'cli-local');
319
+ }}
320
+ >
321
+ {p.enabled ? t('common.disable') : t('common.enable')}
322
+ </button>
323
+ ) : p.isWeb ? (
324
+ <button
325
+ className={`text-xs px-2.5 py-1 rounded transition-all shrink-0 ${
326
+ p.enabled
327
+ ? 'bg-green-900/30 text-green-400 hover:bg-green-900/50'
328
+ : 'bg-cyan-900/20 text-cyan-400 hover:bg-cyan-900/30'
329
+ }`}
330
+ onClick={async () => {
331
+ setWebConfigTarget(p); setCookieValue(''); setCookieTestResult(null);
332
+ if (isElectron && window.electronAPI.getSelectorStatus) {
333
+ try { setSelectorStatus(await window.electronAPI.getSelectorStatus()); } catch {}
334
+ }
335
+ }}
336
+ >
337
+ {p.enabled ? t('common.manage') : '🌐 Login'}
338
+ </button>
339
+ ) : (
340
+ <button
341
+ className={`text-xs px-2.5 py-1 rounded transition-all shrink-0 ${
342
+ p.enabled
343
+ ? 'bg-green-900/30 text-green-400 hover:bg-green-900/50'
344
+ : 'bg-[var(--accent)]/10 text-[var(--accent)] hover:bg-[var(--accent)]/20'
345
+ }`}
346
+ onClick={() => { setConfigTarget(p); setApiKey(''); }}
347
+ >
348
+ {p.enabled ? t('common.manage') : t('common.configure')}
349
+ </button>
350
+ )}
351
+ </div>
352
+ <div className="text-xs text-[var(--muted)] mb-2">
353
+ {p.provider}
354
+ {p.cliVersion && <span className="ml-2 text-[10px] opacity-60">v{p.cliVersion}</span>}
355
+ {p.cliState && p.cliState !== 'detected' && (
356
+ <span className="ml-2 text-[10px] text-yellow-400">({t(`systemSettings.cliBackends.status.${p.cliState}`)})</span>
357
+ )}
358
+ </div>
359
+ <div className="flex items-center gap-3 mb-2">
360
+ <RatingBadge rating={p.rating} />
361
+ <div className="flex items-center gap-1">
362
+ <span className={`w-1.5 h-1.5 rounded-full ${PRICE_DOTS[(p.priceLevel || 1) - 1]}`} />
363
+ <span className={`text-xs ${PRICE_COLORS[(p.priceLevel || 1) - 1]}`}>
364
+ {p.priceLabel || t('providers.unknown')}
365
+ </span>
366
+ </div>
367
+ </div>
368
+ {p.capabilities && (
369
+ <div className="flex gap-1 flex-wrap">
370
+ {p.capabilities.slice(0, 4).map((c, i) => (
371
+ <span key={i} className="text-[9px] bg-white/5 text-[var(--muted)] px-1.5 py-0.5 rounded">{c}</span>
372
+ ))}
373
+ </div>
374
+ )}
375
+ </div>
376
+ ))}
377
+ </div>
378
+ </div>
379
+ ))}
380
+
381
+ {/* API Key config modal */}
382
+ {configTarget && (
383
+ <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 !m-0" onClick={() => setConfigTarget(null)}>
384
+ <div className="card max-w-sm w-full mx-4 space-y-4" onClick={e => e.stopPropagation()}>
385
+ <h3 className="text-lg font-semibold">{t('providers.configure.title', { name: configTarget.name })}</h3>
386
+ <div className="flex items-center justify-between text-sm">
387
+ <span className="text-[var(--muted)]">{t('providers.configure.provider', { name: configTarget.provider })}</span>
388
+ <div className="flex items-center gap-2">
389
+ <span className="text-yellow-400">⭐ {configTarget.rating}</span>
390
+ <span className={`${PRICE_COLORS[(configTarget.priceLevel || 1) - 1]}`}>
391
+ {configTarget.priceLabel}
392
+ </span>
393
+ </div>
394
+ </div>
395
+ {configTarget.description && (
396
+ <p className="text-xs text-[var(--muted)]">{configTarget.description}</p>
397
+ )}
398
+ <div>
399
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('providers.apiKeyLabel')}</label>
400
+ <input
401
+ type="password"
402
+ className="input w-full"
403
+ placeholder={t('providers.configure.apiKeyPlaceholder')}
404
+ value={apiKey}
405
+ onChange={e => setApiKey(e.target.value)}
406
+ />
407
+ </div>
408
+ <div className="flex gap-2">
409
+ <button className="btn-secondary flex-1" onClick={() => setConfigTarget(null)}>{t('common.cancel')}</button>
410
+ {configTarget.enabled && (
411
+ <button
412
+ className="btn-danger flex-1"
413
+ onClick={async () => { await configureProvider(configTarget.id, ''); setConfigTarget(null); }}
414
+ >
415
+ {t('common.disable')}
416
+ </button>
417
+ )}
418
+ <button className="btn-primary flex-1" disabled={!apiKey} onClick={handleConfigure}>
419
+ {configTarget.enabled ? t('common.update') : t('common.enable')}
420
+ </button>
421
+ </div>
422
+ </div>
423
+ </div>
424
+ )}
425
+
426
+ {/* Web Agent config modal */}
427
+ {webConfigTarget && (
428
+ <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 !m-0" onClick={() => setWebConfigTarget(null)}>
429
+ <div className="card max-w-md w-full mx-4 space-y-4" onClick={e => e.stopPropagation()}>
430
+ <h3 className="text-lg font-semibold">{t('providers.webConfigure.title', { name: webConfigTarget.name })}</h3>
431
+ <div className="flex items-center justify-between text-sm">
432
+ <span className="text-[var(--muted)]">{t('providers.configure.provider', { name: webConfigTarget.provider })}</span>
433
+ <div className="flex items-center gap-2">
434
+ <span className="text-yellow-400">⭐ {webConfigTarget.rating}</span>
435
+ <span className="text-green-400">{webConfigTarget.priceLabel}</span>
436
+ </div>
437
+ </div>
438
+ {webConfigTarget.description && (
439
+ <p className="text-xs text-[var(--muted)]">{webConfigTarget.description}</p>
440
+ )}
441
+ {/* One-click login button */}
442
+ <button
443
+ className="w-full px-4 py-3 rounded-lg text-sm font-medium bg-gradient-to-r from-green-900/30 to-cyan-900/30 border border-green-500/30 text-green-400 hover:from-green-900/50 hover:to-cyan-900/50 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
444
+ disabled={cookieLogging}
445
+ onClick={async () => {
446
+ if (!isElectron) {
447
+ setCookieTestResult({ ok: false, error: t('providers.webConfigure.desktopOnly') });
448
+ return;
449
+ }
450
+ setCookieLogging(true);
451
+ setCookieTestResult(null);
452
+ try {
453
+ const result = await window.electronAPI.loginChatGPT();
454
+ if (result.ok && result.cookie) {
455
+ setCookieValue(result.cookie);
456
+ await configureProvider(webConfigTarget.id, result.cookie);
457
+ setWebConfigTarget(null);
458
+ } else {
459
+ setCookieTestResult({ ok: false, error: result.error || 'Login cancelled' });
460
+ }
461
+ } catch (e) {
462
+ setCookieTestResult({ ok: false, error: e.message });
463
+ }
464
+ setCookieLogging(false);
465
+ }}
466
+ >
467
+ {cookieLogging ? (
468
+ <>{t('providers.webConfigure.loggingIn')}</>
469
+ ) : (
470
+ <>🚀 {t('providers.webConfigure.autoLoginBtn')}</>
471
+ )}
472
+ </button>
473
+ <p className="text-[10px] text-[var(--muted)]">{t('providers.webConfigure.loginHint')}</p>
474
+ {/* Selector Calibration */}
475
+ {isElectron && (
476
+ <div className="p-3 rounded-lg bg-white/5 border border-[var(--border)] space-y-2">
477
+ <div className="flex items-center justify-between">
478
+ <div>
479
+ <h4 className="text-xs font-medium">{t('providers.webConfigure.calibrate.title')}</h4>
480
+ <p className="text-[10px] text-[var(--muted)] mt-0.5">{t('providers.webConfigure.calibrate.desc')}</p>
481
+ </div>
482
+ <div className="flex gap-2 shrink-0">
483
+ <button
484
+ className="px-3 py-1.5 rounded-lg text-xs bg-purple-900/30 border border-purple-500/30 text-purple-400 hover:bg-purple-900/50 transition-all disabled:opacity-50"
485
+ disabled={calibrating}
486
+ onClick={async () => {
487
+ setCalibrating(true);
488
+ try {
489
+ const result = await window.electronAPI.calibrateSelectors();
490
+ if (result.ok) {
491
+ setSelectorStatus({ recorded: result.selectors, timestamp: new Date().toISOString() });
492
+ }
493
+ } catch {}
494
+ setCalibrating(false);
495
+ }}
496
+ >
497
+ {calibrating ? t('providers.webConfigure.calibrate.running') : t('providers.webConfigure.calibrate.btn')}
498
+ </button>
499
+ {selectorStatus?.recorded && Object.keys(selectorStatus.recorded).length > 0 && (
500
+ <button
501
+ className="px-2 py-1.5 rounded-lg text-[10px] bg-red-900/20 text-red-400 hover:bg-red-900/40 transition-all"
502
+ onClick={async () => {
503
+ await window.electronAPI.resetSelectors();
504
+ setSelectorStatus(null);
505
+ }}
506
+ >
507
+ {t('providers.webConfigure.calibrate.reset')}
508
+ </button>
509
+ )}
510
+ </div>
511
+ </div>
512
+ {selectorStatus?.recorded && Object.keys(selectorStatus.recorded).length > 0 && (
513
+ <div className="text-[10px] space-y-1 p-2 rounded bg-green-900/10 border border-green-500/20">
514
+ <div className="text-green-400 font-medium">{t('providers.webConfigure.calibrate.recorded')}</div>
515
+ {Object.entries(selectorStatus.recorded).map(([role, sel]) => (
516
+ <div key={role} className="flex gap-2 text-[var(--muted)]">
517
+ <span className="text-green-400 w-16 shrink-0">{role}:</span>
518
+ <code className="truncate opacity-70">{sel}</code>
519
+ </div>
520
+ ))}
521
+ </div>
522
+ )}
523
+ </div>
524
+ )}
525
+ {cookieTestResult && (
526
+ <div className={`text-xs p-2 rounded ${cookieTestResult.ok ? 'bg-green-900/20 text-green-400' : 'bg-red-900/20 text-red-400'}`}>
527
+ {cookieTestResult.ok ? t('providers.webConfigure.testSuccess') : t('providers.webConfigure.testFailed', { error: cookieTestResult.error })}
528
+ </div>
529
+ )}
530
+ <div className="flex gap-2">
531
+ <button className="btn-secondary flex-1" onClick={() => setWebConfigTarget(null)}>{t('common.cancel')}</button>
532
+ {webConfigTarget.enabled && (
533
+ <button
534
+ className="btn-danger flex-1"
535
+ onClick={async () => { await configureProvider(webConfigTarget.id, ''); setWebConfigTarget(null); }}
536
+ >
537
+ {t('common.disable')}
538
+ </button>
539
+ )}
540
+ </div>
541
+ </div>
542
+ </div>
543
+ )}
544
+
545
+ {/* Talent Market modal */}
546
+ {showTalentMarket && (
547
+ <TalentMarket asModal onClose={() => setShowTalentMarket(false)} />
548
+ )}
549
+ </>
550
+ );
551
+ }
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import ProviderGrid from './ProviderGrid';
4
+
5
+ export default function ProvidersBoard() {
6
+ return (
7
+ <div className="p-6 space-y-6 animate-fade-in">
8
+ <ProviderGrid
9
+ showHeader
10
+ showDescription
11
+ showSecretary
12
+ showStatusDot
13
+ />
14
+ </div>
15
+ );
16
+ }