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.
- package/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- 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
|
+
}
|