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,388 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { getAvatarChoices } from '@/lib/avatar';
6
+ import { useI18n, LanguageSelector } from '@/lib/i18n';
7
+ import AvatarGrid from './AvatarGrid';
8
+
9
+ const AVATAR_CHOICES_COUNT = 16;
10
+
11
+ export default function SetupWizard() {
12
+ const { createCompany, loading } = useStore();
13
+ const { t, lang } = useI18n();
14
+ const [step, setStep] = useState(1);
15
+ const [companyName, setCompanyName] = useState(t('setup.defaultCompany'));
16
+ const [bossName, setBossName] = useState('');
17
+
18
+ // Boss avatar state
19
+ const [bossGender, setBossGender] = useState('male');
20
+ const [bossAge, setBossAge] = useState(35);
21
+ const [bossSelectedAvatar, setBossSelectedAvatar] = useState(null);
22
+ const [bossAvatarChoices, setBossAvatarChoices] = useState([]);
23
+
24
+ const [secretaryName, setSecretaryName] = useState(t('setup.defaultSecretary'));
25
+
26
+ // Track previous language to auto-update defaults on language switch
27
+ const prevLangRef = useRef(lang);
28
+ const prevDefaultCompanyRef = useRef(t('setup.defaultCompany'));
29
+ const prevDefaultSecretaryRef = useRef(t('setup.defaultSecretary'));
30
+ useEffect(() => {
31
+ if (prevLangRef.current !== lang) {
32
+ if (companyName === prevDefaultCompanyRef.current) {
33
+ setCompanyName(t('setup.defaultCompany'));
34
+ }
35
+ if (secretaryName === prevDefaultSecretaryRef.current) {
36
+ setSecretaryName(t('setup.defaultSecretary'));
37
+ }
38
+ prevLangRef.current = lang;
39
+ prevDefaultCompanyRef.current = t('setup.defaultCompany');
40
+ prevDefaultSecretaryRef.current = t('setup.defaultSecretary');
41
+ }
42
+ }, [lang, t, companyName, secretaryName]);
43
+
44
+ const [secretaryGender, setSecretaryGender] = useState('female');
45
+ const [secretaryAge, setSecretaryAge] = useState(18);
46
+ const [selectedAvatar, setSelectedAvatar] = useState(null);
47
+ const [avatarChoices, setAvatarChoices] = useState([]);
48
+
49
+ // Boss avatar debounced refresh
50
+ const bossDebounceTimer = useRef(null);
51
+ const refreshBossAvatarChoices = useCallback((g, a) => {
52
+ if (bossDebounceTimer.current) clearTimeout(bossDebounceTimer.current);
53
+ bossDebounceTimer.current = setTimeout(() => {
54
+ const choices = getAvatarChoices(AVATAR_CHOICES_COUNT, g, a);
55
+ setBossAvatarChoices(choices);
56
+ if (choices.length > 0) setBossSelectedAvatar(choices[0]);
57
+ }, 300);
58
+ }, []);
59
+
60
+ useEffect(() => {
61
+ refreshBossAvatarChoices(bossGender, bossAge);
62
+ return () => { if (bossDebounceTimer.current) clearTimeout(bossDebounceTimer.current); };
63
+ }, [bossGender, bossAge, refreshBossAvatarChoices]);
64
+
65
+ const bossAvatar = bossSelectedAvatar?.url || '';
66
+
67
+ const refreshBossChoices = () => {
68
+ const choices = getAvatarChoices(AVATAR_CHOICES_COUNT, bossGender, bossAge);
69
+ setBossAvatarChoices(choices);
70
+ };
71
+
72
+ // Secretary avatar debounced refresh
73
+ const debounceTimer = useRef(null);
74
+ const refreshAvatarChoices = useCallback((g, a) => {
75
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
76
+ debounceTimer.current = setTimeout(() => {
77
+ const choices = getAvatarChoices(AVATAR_CHOICES_COUNT, g, a);
78
+ setAvatarChoices(choices);
79
+ if (choices.length > 0) {
80
+ setSelectedAvatar(choices[0]);
81
+ }
82
+ }, 300);
83
+ }, []);
84
+
85
+ useEffect(() => {
86
+ refreshAvatarChoices(secretaryGender, secretaryAge);
87
+ return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current); };
88
+ }, [secretaryGender, secretaryAge, refreshAvatarChoices]);
89
+
90
+ const secretaryAvatar = selectedAvatar?.url || '';
91
+
92
+ const refreshChoices = () => {
93
+ const choices = getAvatarChoices(AVATAR_CHOICES_COUNT, secretaryGender, secretaryAge);
94
+ setAvatarChoices(choices);
95
+ };
96
+
97
+ const handleCreate = async () => {
98
+ try {
99
+ await createCompany(companyName, bossName, {
100
+ secretaryName: secretaryName || t('setup.defaultSecretary'),
101
+ secretaryAvatar: secretaryAvatar,
102
+ secretaryGender,
103
+ secretaryAge,
104
+ bossAvatar: bossAvatar,
105
+ });
106
+ } catch (e) {
107
+ // error handled by store
108
+ }
109
+ };
110
+
111
+ return (
112
+ <div className="min-h-screen flex items-center justify-center p-4">
113
+ <div className="max-w-lg w-full">
114
+ {/* Logo & Title */}
115
+ <div className="text-center mb-8 animate-fade-in">
116
+ <div className="flex justify-end mb-2">
117
+ <LanguageSelector direction="down" />
118
+ </div>
119
+ <div className="text-6xl mb-4">🏢</div>
120
+ <h1 className="text-3xl font-bold bg-gradient-to-r from-red-400 to-purple-500 bg-clip-text text-transparent">
121
+ {t('setup.title')}
122
+ </h1>
123
+ <p className="text-[var(--muted)] mt-2">{t('setup.subtitle')}</p>
124
+ </div>
125
+
126
+ {/* Step 1 */}
127
+ {step === 1 && (
128
+ <div className="card animate-fade-in space-y-4">
129
+ <h2 className="text-xl font-semibold">{t('setup.step1Title')}</h2>
130
+ <p className="text-sm text-[var(--muted)]">{t('setup.step1Desc')}</p>
131
+
132
+ <div>
133
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('setup.companyName')}</label>
134
+ <input
135
+ className="input w-full"
136
+ placeholder={t('setup.companyPlaceholder')}
137
+ value={companyName}
138
+ onChange={(e) => setCompanyName(e.target.value)}
139
+ />
140
+ </div>
141
+
142
+ <div className="flex items-center gap-4">
143
+ <div className="shrink-0">
144
+ {bossAvatar ? (
145
+ <img
146
+ src={bossAvatar}
147
+ alt="boss"
148
+ className="w-16 h-16 rounded-full bg-[var(--border)] border-2 border-[var(--accent)]/30"
149
+ />
150
+ ) : (
151
+ <div className="w-16 h-16 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-2xl font-bold">
152
+ 👑
153
+ </div>
154
+ )}
155
+ </div>
156
+ <div className="flex-1">
157
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('setup.bossTitle')}</label>
158
+ <input
159
+ className="input w-full"
160
+ placeholder={t('setup.bossPlaceholder')}
161
+ value={bossName}
162
+ onChange={(e) => setBossName(e.target.value)}
163
+ />
164
+ </div>
165
+ </div>
166
+
167
+ {/* Boss avatar selection */}
168
+ <div className="space-y-3 pt-1">
169
+ <div className="flex items-center justify-between">
170
+ <label className="text-sm text-[var(--muted)]">{t('setup.bossAvatarTitle')}</label>
171
+ <span className="text-xs text-[var(--muted)]">{t('setup.bossAvatarDesc')}</span>
172
+ </div>
173
+
174
+ {/* Gender & Age */}
175
+ <div className="grid grid-cols-2 gap-3">
176
+ <div>
177
+ <label className="block text-xs mb-1 text-[var(--muted)]">{t('setup.gender')}</label>
178
+ <div className="flex gap-2">
179
+ <button
180
+ onClick={() => { setBossGender('male'); setBossSelectedAvatar(null); }}
181
+ className={`flex-1 py-1.5 px-2 rounded-lg border text-xs transition-all ${
182
+ bossGender === 'male'
183
+ ? 'border-blue-400 bg-blue-400/10 text-blue-300'
184
+ : 'border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)]/40'
185
+ }`}
186
+ >{t('setup.male')}</button>
187
+ <button
188
+ onClick={() => { setBossGender('female'); setBossSelectedAvatar(null); }}
189
+ className={`flex-1 py-1.5 px-2 rounded-lg border text-xs transition-all ${
190
+ bossGender === 'female'
191
+ ? 'border-pink-400 bg-pink-400/10 text-pink-300'
192
+ : 'border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)]/40'
193
+ }`}
194
+ >{t('setup.female')}</button>
195
+ </div>
196
+ </div>
197
+ <div>
198
+ <label className="block text-xs mb-1 text-[var(--muted)]">{t('setup.age', { n: bossAge })}</label>
199
+ <div className="relative flex items-center gap-2">
200
+ <button
201
+ onClick={() => setBossAge(a => Math.max(18, a - 1))}
202
+ className="w-6 h-6 rounded-full border border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-all flex items-center justify-center text-xs font-bold shrink-0"
203
+ >−</button>
204
+ <div className="flex-1 relative h-4 flex items-center">
205
+ <div className="absolute left-0 right-0 top-1/2 -translate-y-1/2 h-1 rounded-full bg-[var(--border)] pointer-events-none" />
206
+ <div
207
+ className="absolute left-0 top-1/2 -translate-y-1/2 h-1 rounded-full bg-gradient-to-r from-[var(--accent)] to-purple-400 pointer-events-none"
208
+ style={{ width: `${((bossAge - 18) / 42) * 100}%` }}
209
+ />
210
+ <input
211
+ type="range"
212
+ min="18"
213
+ max="60"
214
+ value={bossAge}
215
+ onChange={e => { setBossAge(Number(e.target.value)); setBossSelectedAvatar(null); }}
216
+ className="absolute inset-0 z-10 w-full appearance-none cursor-pointer bg-transparent [&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[var(--accent)] [&::-webkit-slider-thumb]:shadow-[0_0_6px_rgba(99,102,241,0.5)] [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:hover:scale-125 [&::-webkit-slider-thumb]:-mt-[5px]"
217
+ />
218
+ </div>
219
+ <button
220
+ onClick={() => setBossAge(a => Math.min(60, a + 1))}
221
+ className="w-6 h-6 rounded-full border border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-all flex items-center justify-center text-xs font-bold shrink-0"
222
+ >+</button>
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ {/* Avatar grid */}
228
+ <div>
229
+ <div className="flex items-center justify-between mb-1.5">
230
+ <label className="text-xs text-[var(--muted)]">{t('setup.avatarStyle')}</label>
231
+ <button
232
+ className="text-xs text-[var(--accent)] hover:underline"
233
+ onClick={refreshBossChoices}
234
+ >{t('setup.refreshBatch')}</button>
235
+ </div>
236
+ <AvatarGrid
237
+ choices={bossAvatarChoices}
238
+ selectedId={bossSelectedAvatar?.id}
239
+ onSelect={setBossSelectedAvatar}
240
+ />
241
+ </div>
242
+ </div>
243
+
244
+ <button
245
+ className="btn-primary w-full"
246
+ disabled={!companyName}
247
+ onClick={() => setStep(2)}
248
+ >
249
+ {t('common.next')}
250
+ </button>
251
+ </div>
252
+ )}
253
+
254
+ {/* Step 2: Secretary + Create */}
255
+ {step === 2 && (
256
+ <div className="card animate-fade-in space-y-4">
257
+ <h2 className="text-xl font-semibold">{t('setup.step2Title')}</h2>
258
+ <p className="text-sm text-[var(--muted)]">{t('setup.step2Desc')}</p>
259
+
260
+ <div className="flex items-center gap-4">
261
+ <div className="shrink-0">
262
+ <img
263
+ src={secretaryAvatar}
264
+ alt={t('chat.secretary')}
265
+ className="w-20 h-20 rounded-full bg-[var(--border)] border-2 border-[var(--accent)]/30"
266
+ />
267
+ </div>
268
+ <div className="flex-1 space-y-2">
269
+ <div>
270
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('setup.secretaryName')}</label>
271
+ <input
272
+ className="input w-full"
273
+ placeholder={t('setup.secretaryPlaceholder')}
274
+ value={secretaryName}
275
+ onChange={(e) => {
276
+ setSecretaryName(e.target.value);
277
+ }}
278
+ />
279
+ </div>
280
+ </div>
281
+ </div>
282
+
283
+ {/* Gender & Age */}
284
+ <div className="grid grid-cols-2 gap-4">
285
+ <div>
286
+ <label className="block text-sm mb-1.5 text-[var(--muted)]">{t('setup.gender')}</label>
287
+ <div className="flex gap-2">
288
+ <button
289
+ onClick={() => { setSecretaryGender('female'); setSelectedAvatar(null); }}
290
+ className={`flex-1 py-2 px-3 rounded-lg border text-sm transition-all ${
291
+ secretaryGender === 'female'
292
+ ? 'border-pink-400 bg-pink-400/10 text-pink-300'
293
+ : 'border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)]/40'
294
+ }`}
295
+ >{t('setup.female')}</button>
296
+ <button
297
+ onClick={() => { setSecretaryGender('male'); setSelectedAvatar(null); }}
298
+ className={`flex-1 py-2 px-3 rounded-lg border text-sm transition-all ${
299
+ secretaryGender === 'male'
300
+ ? 'border-blue-400 bg-blue-400/10 text-blue-300'
301
+ : 'border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)]/40'
302
+ }`}
303
+ >{t('setup.male')}</button>
304
+ </div>
305
+ </div>
306
+ <div>
307
+ <label className="block text-sm mb-1.5 text-[var(--muted)]">{t('setup.age', { n: secretaryAge })}</label>
308
+ <div className="relative flex items-center gap-3">
309
+ <button
310
+ onClick={() => setSecretaryAge(a => Math.max(18, a - 1))}
311
+ className="w-7 h-7 rounded-full border border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-all flex items-center justify-center text-sm font-bold shrink-0"
312
+ >−</button>
313
+ <div className="flex-1 relative h-5 flex items-center">
314
+ <div className="absolute left-0 right-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-[var(--border)] pointer-events-none" />
315
+ <div
316
+ className="absolute left-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-gradient-to-r from-[var(--accent)] to-purple-400 pointer-events-none"
317
+ style={{ width: `${((secretaryAge - 18) / 42) * 100}%` }}
318
+ />
319
+ <input
320
+ type="range"
321
+ min="18"
322
+ max="60"
323
+ value={secretaryAge}
324
+ onChange={e => { setSecretaryAge(Number(e.target.value)); setSelectedAvatar(null); }}
325
+ className="absolute inset-0 z-10 w-full appearance-none cursor-pointer bg-transparent [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[var(--accent)] [&::-webkit-slider-thumb]:shadow-[0_0_6px_rgba(99,102,241,0.5)] [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:hover:scale-125 [&::-webkit-slider-thumb]:-mt-[5px]"
326
+ />
327
+ </div>
328
+ <button
329
+ onClick={() => setSecretaryAge(a => Math.min(60, a + 1))}
330
+ className="w-7 h-7 rounded-full border border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-all flex items-center justify-center text-sm font-bold shrink-0"
331
+ >+</button>
332
+ </div>
333
+ <div className="flex justify-between text-[10px] text-[var(--muted)] mt-1 px-10">
334
+ <span>18</span><span>30</span><span>45</span><span>60</span>
335
+ </div>
336
+ </div>
337
+ </div>
338
+
339
+ {/* Avatar */}
340
+ <div>
341
+ <div className="flex items-center justify-between mb-2">
342
+ <label className="text-sm text-[var(--muted)]">{t('setup.avatarStyle')}</label>
343
+ <button
344
+ className="text-xs text-[var(--accent)] hover:underline"
345
+ onClick={refreshChoices}
346
+ >{t('setup.refreshBatch')}</button>
347
+ </div>
348
+ <AvatarGrid
349
+ choices={avatarChoices}
350
+ selectedId={selectedAvatar?.id}
351
+ onSelect={setSelectedAvatar}
352
+ />
353
+ </div>
354
+
355
+ <div className="flex gap-2">
356
+ <button className="btn-secondary flex-1" onClick={() => setStep(1)}>
357
+ {t('common.prev')}
358
+ </button>
359
+ <button
360
+ className="btn-primary flex-1"
361
+ disabled={loading}
362
+ onClick={handleCreate}
363
+ >
364
+ {loading ? t('setup.creating') : t('setup.createBtn')}
365
+ </button>
366
+ </div>
367
+ </div>
368
+ )}
369
+
370
+ {/* Step indicator */}
371
+ <div className="flex justify-center mt-6 gap-2">
372
+ {[1, 2].map((s) => (
373
+ <div
374
+ key={s}
375
+ className={`w-2 h-2 rounded-full transition-all ${
376
+ step === s ? 'bg-[var(--accent)] w-6' : 'bg-[var(--border)]'
377
+ }`}
378
+ />
379
+ ))}
380
+ </div>
381
+
382
+ <div className="text-center mt-4 text-xs text-[var(--muted)]">
383
+ {t('setup.footer')}
384
+ </div>
385
+ </div>
386
+ </div>
387
+ );
388
+ }
@@ -0,0 +1,169 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { getAvatarUrl } from '@/lib/avatar';
6
+ import { useI18n, LanguageSelector } from '@/lib/i18n';
7
+ import SecretarySettings from './SecretarySettings';
8
+ import BossProfileModal from './BossProfileModal';
9
+ import CachedAvatar from './CachedAvatar';
10
+
11
+ export default function Sidebar() {
12
+ const { company, activeTab, setActiveTab, setChatOpen, chatOpen, chatMinimized, setChatMinimized } = useStore();
13
+ const { t } = useI18n();
14
+ const [showSettings, setShowSettings] = useState(false);
15
+ const [showBossProfile, setShowBossProfile] = useState(false);
16
+
17
+ if (!company) return null;
18
+
19
+ const NAV_ITEMS = [
20
+ { id: 'overview', label: t('sidebar.nav.overview'), icon: '📊' },
21
+ { id: 'office', label: t('sidebar.nav.office'), icon: '🎮' },
22
+ { id: 'mailbox', label: t('sidebar.nav.mailbox'), icon: '💬' },
23
+ { id: 'requirements', label: t('sidebar.nav.requirements'), icon: '📋' },
24
+ { id: 'departments', label: t('sidebar.nav.departments'), icon: '🏢' },
25
+ ];
26
+
27
+ const deptCount = company.departments?.length || 0;
28
+ const agentCount = company.departments?.reduce((s, d) => s + d.members.length, 0) || 0;
29
+ const reqCount = company.requirements?.length || 0;
30
+ const budget = company.budget || {};
31
+ const chatSessionCount = company.agentChatSessions?.length || 0;
32
+
33
+ return (
34
+ <aside className="w-64 bg-[#0d0d0d] border-r border-[var(--border)] flex flex-col h-screen">
35
+ {/* Company name - top left */}
36
+ <div className="p-4 border-b border-[var(--border)]" style={{ paddingTop: 'calc(1rem + var(--titlebar-height))' }}>
37
+ <div className="flex items-center gap-3">
38
+ <button
39
+ onClick={() => setShowBossProfile(true)}
40
+ className="w-10 h-10 rounded-lg flex items-center justify-center text-lg font-bold shrink-0 transition-all hover:scale-105 hover:ring-2 hover:ring-[var(--accent)]/40 overflow-hidden"
41
+ title={t('bossProfile.editAvatar')}
42
+ >
43
+ {company.bossAvatar ? (
44
+ <CachedAvatar src={company.bossAvatar} alt="boss" className="w-10 h-10 rounded-lg" />
45
+ ) : (
46
+ <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center text-lg font-bold">
47
+ {company.name.charAt(0)}
48
+ </div>
49
+ )}
50
+ </button>
51
+ <div>
52
+ <div className="font-semibold text-sm truncate">{company.name}</div>
53
+ <div className="text-xs text-[var(--muted)]">👤 {company.boss}</div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ {/* Secretary info - clickable to open chat */}
59
+ <div className="mx-3 mt-3 rounded-lg bg-gradient-to-r from-blue-900/20 to-purple-900/20 border border-blue-500/20">
60
+ <button
61
+ onClick={() => {
62
+ if (!chatOpen) {
63
+ setChatOpen(true);
64
+ } else if (chatMinimized) {
65
+ setChatMinimized(false);
66
+ }
67
+ }}
68
+ className="w-full p-3 hover:bg-white/5 transition-all text-left rounded-t-lg"
69
+ >
70
+ <div className="flex items-center gap-2">
71
+ <img
72
+ src={company.secretary?.avatar || getAvatarUrl('secretary')}
73
+ alt={t('chat.secretary')}
74
+ className="w-8 h-8 rounded-full bg-[var(--card)]"
75
+ />
76
+ <div className="flex-1 min-w-0">
77
+ <div className="text-xs font-medium flex items-center gap-1">
78
+ {company.secretary?.name || t('setup.defaultSecretary')}
79
+ <span className="w-1.5 h-1.5 bg-green-500 rounded-full inline-block" />
80
+ </div>
81
+ <div className="text-[10px] text-[var(--muted)] truncate">
82
+ {t('sidebar.clickToChat', { provider: company.secretary?.provider })}
83
+ </div>
84
+ </div>
85
+ <span className="text-sm">💬</span>
86
+ </div>
87
+ </button>
88
+ <button
89
+ onClick={() => setShowSettings(true)}
90
+ className="w-full text-[10px] text-[var(--muted)] hover:text-[var(--accent)] py-1.5 border-t border-white/[0.06] transition-all hover:bg-white/5 rounded-b-lg"
91
+ >
92
+ {t('sidebar.secretarySettings')}
93
+ </button>
94
+ </div>
95
+
96
+ {/* Budget overview */}
97
+ <div className="px-3 mt-3">
98
+ <div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-2.5">
99
+ <div className="text-[10px] text-[var(--muted)] mb-1.5">{t('sidebar.budgetUsage')}</div>
100
+ <div className="flex items-baseline gap-1">
101
+ <span className="text-lg font-bold text-green-400">${budget.totalCost?.toFixed(4) || '0.00'}</span>
102
+ </div>
103
+ <div className="text-[10px] text-[var(--muted)] mt-1">
104
+ {t('sidebar.tokenLabel')}: {(budget.totalTokens || 0).toLocaleString()}
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ {/* Navigation menu */}
110
+ <nav className="flex-1 p-3 space-y-1">
111
+ {NAV_ITEMS.map((item) => {
112
+ const isActive = activeTab === item.id;
113
+ let badge = null;
114
+ if (item.id === 'departments') badge = deptCount;
115
+ if (item.id === 'requirements') badge = company.requirements?.length || 0;
116
+ if (item.id === 'mailbox') badge = chatSessionCount;
117
+
118
+ return (
119
+ <button
120
+ key={item.id}
121
+ onClick={() => setActiveTab(item.id)}
122
+ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-all ${
123
+ isActive || (item.id === 'requirements' && activeTab === 'requirement-detail') || (item.id === 'departments' && activeTab === 'department-detail')
124
+ ? 'bg-[var(--accent)]/10 text-[var(--accent)] font-medium'
125
+ : 'text-[var(--muted)] hover:text-[var(--foreground)] hover:bg-white/5'
126
+ }`}
127
+ >
128
+ <span>{item.icon}</span>
129
+ <span className="flex-1 text-left">{item.label}</span>
130
+ {badge > 0 && (
131
+ <span className={`text-xs px-1.5 py-0.5 rounded-full ${
132
+ isActive ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'bg-white/10 text-[var(--muted)]'
133
+ }`}>
134
+ {badge}
135
+ </span>
136
+ )}
137
+ </button>
138
+ );
139
+ })}
140
+ </nav>
141
+
142
+ {/* Bottom statistics */}
143
+ <div className="p-4 border-t border-[var(--border)]">
144
+ <div className="grid grid-cols-3 gap-2 text-center">
145
+ <div>
146
+ <div className="text-lg font-bold text-[var(--accent)]">{deptCount}</div>
147
+ <div className="text-[10px] text-[var(--muted)]">{t('sidebar.stats.departments')}</div>
148
+ </div>
149
+ <div>
150
+ <div className="text-lg font-bold text-green-400">{agentCount}</div>
151
+ <div className="text-[10px] text-[var(--muted)]">{t('sidebar.stats.workers')}</div>
152
+ </div>
153
+ <div>
154
+ <div className="text-lg font-bold text-amber-400">{reqCount}</div>
155
+ <div className="text-[10px] text-[var(--muted)]">{t('sidebar.stats.requirements')}</div>
156
+ </div>
157
+ </div>
158
+ {/* Language switch */}
159
+ <div className="mt-3 flex justify-center">
160
+ <LanguageSelector />
161
+ </div>
162
+ </div>
163
+
164
+ {/* Secretary settings modal */}
165
+ {showSettings && <SecretarySettings onClose={() => setShowSettings(false)} />}
166
+ {showBossProfile && <BossProfileModal onClose={() => setShowBossProfile(false)} />}
167
+ </aside>
168
+ );
169
+ }