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,842 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import AgentDetailModal from './AgentDetailModal';
6
+ import AgentChatModal from './AgentChatModal';
7
+ import RequirementDetail from './RequirementDetail';
8
+ import { useI18n } from '@/lib/i18n';
9
+ import CachedAvatar from './CachedAvatar';
10
+
11
+ export default function DepartmentDetail() {
12
+ const { t } = useI18n();
13
+ const {
14
+ company, loading, dismissAgent,
15
+ fetchDepartmentRequirements, createRequirement,
16
+ navigateToRequirement, navigateBackFromDepartment,
17
+ activeDepartmentId, planAdjustment, confirmAdjustment,
18
+ disbandDepartment, pendingPlan, setPendingPlan,
19
+ deleteRequirement, restartRequirement,
20
+ createTeam, fetchTeams, navigateToTeam,
21
+ } = useStore();
22
+
23
+ // Sub-modals
24
+ const [selectedAgent, setSelectedAgent] = useState(null);
25
+ const [chatAgent, setChatAgent] = useState(null);
26
+ const [activeReqId, setActiveReqId] = useState(null);
27
+ const [dismissTarget, setDismissTarget] = useState(null);
28
+ const [dismissReason, setDismissReason] = useState('');
29
+ const [showAdjust, setShowAdjust] = useState(false);
30
+ const [adjustGoal, setAdjustGoal] = useState('');
31
+ const [showDisband, setShowDisband] = useState(false);
32
+ const [disbandReason, setDisbandReason] = useState('');
33
+
34
+ // New requirement form (inline)
35
+ const [showNewReq, setShowNewReq] = useState(false);
36
+ const [newReqTitle, setNewReqTitle] = useState('');
37
+ const [newReqDesc, setNewReqDesc] = useState('');
38
+ const [newReqWorkspaceDir, setNewReqWorkspaceDir] = useState('');
39
+
40
+ const [deptRequirements, setDeptRequirements] = useState([]);
41
+
42
+ // Folder browser state
43
+ const [showFolderBrowser, setShowFolderBrowser] = useState(false);
44
+ const [browseDirs, setBrowseDirs] = useState([]);
45
+ const [browseCurrentPath, setBrowseCurrentPath] = useState('');
46
+ const [browseParentPath, setBrowseParentPath] = useState(null);
47
+ const [browseLoading, setBrowseLoading] = useState(false);
48
+
49
+ // Team creation form
50
+ const [showNewTeam, setShowNewTeam] = useState(false);
51
+ const [teamName, setTeamName] = useState('');
52
+ const [teamDesc, setTeamDesc] = useState('');
53
+ const [selectedMembers, setSelectedMembers] = useState([]);
54
+ const [selectedLeader, setSelectedLeader] = useState('');
55
+ const [deptTeams, setDeptTeams] = useState([]);
56
+
57
+ const fetchDirs = async (dirPath) => {
58
+ setBrowseLoading(true);
59
+ try {
60
+ const url = dirPath ? `/api/browse-dir?path=${encodeURIComponent(dirPath)}` : '/api/browse-dir';
61
+ const res = await fetch(url);
62
+ const data = await res.json();
63
+ if (data.error) return;
64
+ setBrowseDirs(data.dirs || []);
65
+ setBrowseCurrentPath(data.current || '');
66
+ setBrowseParentPath(data.parent || null);
67
+ } catch (e) { /* handled */ }
68
+ setBrowseLoading(false);
69
+ };
70
+
71
+ const dept = company?.departments?.find(d => d.id === activeDepartmentId);
72
+
73
+ useEffect(() => {
74
+ if (activeDepartmentId) {
75
+ fetchDepartmentRequirements(activeDepartmentId).then(setDeptRequirements);
76
+ fetchTeams(activeDepartmentId).then(teams => setDeptTeams(teams || []));
77
+ }
78
+ }, [activeDepartmentId]);
79
+
80
+ if (!dept) {
81
+ return (
82
+ <div className="p-6 text-center text-[var(--muted)]">
83
+ <p>{t('dept.empty')}</p>
84
+ <button className="btn-secondary mt-4" onClick={navigateBackFromDepartment}>{t('dept.detail.back')}</button>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ const handleDismiss = async () => {
90
+ if (!dismissTarget) return;
91
+ try {
92
+ await dismissAgent(dismissTarget.deptId, dismissTarget.agentId, dismissReason || 'Boss decision');
93
+ setDismissTarget(null);
94
+ setDismissReason('');
95
+ } catch (e) { /* handled */ }
96
+ };
97
+
98
+ const handleCreateRequirement = async () => {
99
+ if (!newReqTitle) return;
100
+ try {
101
+ const result = await createRequirement(activeDepartmentId, newReqTitle, newReqDesc, newReqWorkspaceDir || undefined);
102
+ setShowNewReq(false);
103
+ setNewReqTitle('');
104
+ setNewReqDesc('');
105
+ setNewReqWorkspaceDir('');
106
+ if (result?.id) {
107
+ navigateToRequirement(result.id);
108
+ }
109
+ fetchDepartmentRequirements(activeDepartmentId).then(setDeptRequirements);
110
+ } catch (e) { /* handled */ }
111
+ };
112
+
113
+ const handleCreateTeam = async () => {
114
+ if (!teamName || selectedMembers.length === 0 || !selectedLeader) return;
115
+ try {
116
+ const result = await createTeam(activeDepartmentId, teamName, selectedMembers, selectedLeader, teamDesc);
117
+ setShowNewTeam(false);
118
+ setTeamName('');
119
+ setTeamDesc('');
120
+ setSelectedMembers([]);
121
+ setSelectedLeader('');
122
+ if (result?.id) {
123
+ navigateToTeam(result.id);
124
+ }
125
+ fetchTeams(activeDepartmentId).then(teams => setDeptTeams(teams || []));
126
+ } catch (e) { /* handled */ }
127
+ };
128
+
129
+ const handleAdjustPlan = async () => {
130
+ if (!adjustGoal) return;
131
+ try {
132
+ await planAdjustment(activeDepartmentId, adjustGoal);
133
+ } catch (e) { /* handled */ }
134
+ };
135
+
136
+ const handleDisband = async () => {
137
+ try {
138
+ await disbandDepartment(activeDepartmentId, disbandReason || 'Organization restructuring');
139
+ setShowDisband(false);
140
+ setDisbandReason('');
141
+ navigateBackFromDepartment();
142
+ } catch (e) { /* handled */ }
143
+ };
144
+
145
+ const deptReports = (company.progressReports || [])
146
+ .slice().reverse()
147
+ .filter(pr => pr.reports.some(r => r.department === dept.name))
148
+ .slice(0, 5);
149
+
150
+ return (
151
+ <div className="h-full flex flex-col animate-fade-in">
152
+ {/* ===== Top navigation bar ===== */}
153
+ <div className="shrink-0 border-b border-[var(--border)] bg-[var(--card)]/50 backdrop-blur-sm px-6 py-4">
154
+ <div className="flex items-center justify-between">
155
+ <div className="flex items-center gap-4 min-w-0">
156
+ <button
157
+ onClick={navigateBackFromDepartment}
158
+ className="text-[var(--muted)] hover:text-[var(--foreground)] transition-colors text-sm flex items-center gap-1 shrink-0"
159
+ >
160
+ ← {t('dept.detail.back')}
161
+ </button>
162
+ <div className="w-px h-6 bg-[var(--border)]" />
163
+ <div className="flex items-center gap-3 min-w-0">
164
+ <div className="w-10 h-10 bg-gradient-to-br from-indigo-600 to-blue-700 rounded-lg flex items-center justify-center text-lg shrink-0">
165
+ 🏢
166
+ </div>
167
+ <div className="min-w-0">
168
+ <h1 className="text-lg font-bold flex items-center gap-2 truncate">
169
+ {dept.name}
170
+ <span className={`text-xs px-2 py-0.5 rounded-full ${
171
+ dept.status === 'completed' ? 'bg-green-900/30 text-green-400' :
172
+ dept.status === 'active' ? 'bg-yellow-900/30 text-yellow-400' :
173
+ 'bg-blue-900/30 text-blue-400'
174
+ }`}>
175
+ {dept.status}
176
+ </span>
177
+ </h1>
178
+ <p className="text-xs text-[var(--muted)] truncate">{dept.mission}</p>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ <div className="flex items-center gap-2 shrink-0">
183
+ <button
184
+ className="btn-primary flex items-center gap-1.5"
185
+ onClick={() => { setShowNewReq(true); setNewReqTitle(''); setNewReqDesc(''); setNewReqWorkspaceDir(''); }}
186
+ >
187
+ {t('dept.newReq.btn')}
188
+ </button>
189
+ <button
190
+ className="text-xs bg-purple-900/20 text-purple-400 hover:bg-purple-900/40 px-3 py-1.5 rounded-lg transition-colors"
191
+ onClick={() => { setShowNewTeam(true); setTeamName(''); setTeamDesc(''); setSelectedMembers([]); setSelectedLeader(''); }}
192
+ >{t('team.newTeamBtn')}</button>
193
+ <button
194
+ className="text-xs bg-blue-900/20 text-blue-400 hover:bg-blue-900/40 px-3 py-1.5 rounded-lg transition-colors"
195
+ onClick={() => { setShowAdjust(true); setAdjustGoal(''); setPendingPlan(null); }}
196
+ >{t('dept.detail.adjustBtn')}</button>
197
+ <button
198
+ className="text-xs bg-red-900/20 text-red-400 hover:bg-red-900/40 px-3 py-1.5 rounded-lg transition-colors"
199
+ onClick={() => { setShowDisband(true); setDisbandReason(''); }}
200
+ >{t('dept.detail.disbandBtn')}</button>
201
+ </div>
202
+ </div>
203
+ {/* Stats bar */}
204
+ <div className="flex items-center gap-4 mt-2 text-xs text-[var(--muted)]">
205
+ <span>👥 {t('dept.members', { n: dept.members.length })}</span>
206
+ <span>💰 ${(dept.tokenUsage?.totalCost || 0).toFixed(4)}</span>
207
+ <span>🔢 {(dept.tokenUsage?.totalTokens || 0).toLocaleString()} tokens</span>
208
+ <span>📋 {deptRequirements.length} {t('dept.detail.requirements')}</span>
209
+ <span>👥 {deptTeams.length} {t('team.teamsCount')}</span>
210
+ </div>
211
+ </div>
212
+
213
+ {/* ===== Main content (scrollable) ===== */}
214
+ <div className="flex-1 overflow-auto p-6 space-y-6">
215
+
216
+ {/* New requirement form (expandable card) */}
217
+ {showNewReq && (
218
+ <div className="card border-[var(--accent)]/30 animate-fade-in space-y-4">
219
+ <div className="flex items-center justify-between">
220
+ <h3 className="text-base font-semibold">{t('dept.newReq.title')}</h3>
221
+ <button onClick={() => setShowNewReq(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
222
+ </div>
223
+ <div>
224
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.newReq.nameLabel')}</label>
225
+ <input
226
+ className="input w-full"
227
+ placeholder={t('dept.newReq.namePlaceholder')}
228
+ value={newReqTitle}
229
+ onChange={e => setNewReqTitle(e.target.value)}
230
+ autoFocus
231
+ />
232
+ </div>
233
+ <div>
234
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.newReq.descLabel')}</label>
235
+ <textarea
236
+ className="input w-full h-20 resize-none"
237
+ placeholder={t('dept.newReq.descPlaceholder')}
238
+ value={newReqDesc}
239
+ onChange={e => setNewReqDesc(e.target.value)}
240
+ />
241
+ </div>
242
+ <div>
243
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.newReq.workspaceDirLabel')}</label>
244
+ <div className="flex gap-2">
245
+ <input
246
+ className="input w-full font-mono text-xs min-h-[36px]"
247
+ value={newReqWorkspaceDir}
248
+ onChange={e => setNewReqWorkspaceDir(e.target.value)}
249
+ placeholder={t('dept.newReq.workspaceDirPlaceholder')}
250
+ />
251
+ <button
252
+ className="btn-secondary shrink-0 text-sm px-3"
253
+ onClick={() => { setShowFolderBrowser(true); fetchDirs(newReqWorkspaceDir || ''); }}
254
+ title={t('dept.newReq.browseTitle')}
255
+ >📁</button>
256
+ {newReqWorkspaceDir && (
257
+ <button
258
+ className="text-[var(--muted)] hover:text-red-400 text-sm px-1 shrink-0"
259
+ onClick={() => setNewReqWorkspaceDir('')}
260
+ title={t('common.delete')}
261
+ >✕</button>
262
+ )}
263
+ </div>
264
+ <p className="text-[10px] text-[var(--muted)] mt-1">{t('dept.newReq.workspaceDirHint')}</p>
265
+ </div>
266
+ <div className="flex justify-end gap-2">
267
+ <button className="btn-secondary" onClick={() => setShowNewReq(false)}>{t('common.cancel')}</button>
268
+ <button
269
+ className="btn-primary"
270
+ disabled={!newReqTitle || loading}
271
+ onClick={handleCreateRequirement}
272
+ >
273
+ {loading ? t('dept.newReq.creating') : t('dept.newReq.submitBtn')}
274
+ </button>
275
+ </div>
276
+ </div>
277
+ )}
278
+
279
+ {/* New team form (expandable card) */}
280
+ {showNewTeam && (
281
+ <div className="card border-purple-500/30 animate-fade-in space-y-4">
282
+ <div className="flex items-center justify-between">
283
+ <h3 className="text-base font-semibold">👥 {t('team.newTeamBtn')}</h3>
284
+ <button onClick={() => setShowNewTeam(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
285
+ </div>
286
+ <div>
287
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('team.nameLabel')}</label>
288
+ <input
289
+ className="input w-full"
290
+ placeholder={t('team.namePlaceholder')}
291
+ value={teamName}
292
+ onChange={e => setTeamName(e.target.value)}
293
+ autoFocus
294
+ />
295
+ </div>
296
+ <div>
297
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('team.descLabel')}</label>
298
+ <textarea
299
+ className="input w-full h-16 resize-none"
300
+ placeholder={t('team.descPlaceholder')}
301
+ value={teamDesc}
302
+ onChange={e => setTeamDesc(e.target.value)}
303
+ />
304
+ </div>
305
+ <div>
306
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('team.selectMembers')}</label>
307
+ <div className="flex flex-wrap gap-1.5 max-h-40 overflow-auto">
308
+ {dept.members.map(member => {
309
+ const selected = selectedMembers.includes(member.id);
310
+ return (
311
+ <label
312
+ key={member.id}
313
+ className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-full cursor-pointer text-xs transition-all border ${
314
+ selected
315
+ ? 'bg-purple-900/30 border-purple-500/50 text-purple-300'
316
+ : 'bg-white/5 border-transparent hover:bg-white/10 text-[var(--foreground)]'
317
+ }`}
318
+ >
319
+ <input
320
+ type="checkbox"
321
+ checked={selected}
322
+ onChange={e => {
323
+ if (e.target.checked) {
324
+ setSelectedMembers(prev => [...prev, member.id]);
325
+ } else {
326
+ setSelectedMembers(prev => prev.filter(id => id !== member.id));
327
+ if (selectedLeader === member.id) setSelectedLeader('');
328
+ }
329
+ }}
330
+ className="hidden"
331
+ />
332
+ <CachedAvatar src={member.avatar} alt={member.name} className="w-5 h-5 rounded-full" />
333
+ <span>{member.name}</span>
334
+ {selected && <span className="text-purple-400">✓</span>}
335
+ </label>
336
+ );
337
+ })}
338
+ </div>
339
+ </div>
340
+ {selectedMembers.length > 0 && (
341
+ <div>
342
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('team.selectLeader')}</label>
343
+ <select
344
+ className="input w-full"
345
+ value={selectedLeader}
346
+ onChange={e => setSelectedLeader(e.target.value)}
347
+ >
348
+ <option value="">{t('team.selectLeaderPlaceholder')}</option>
349
+ {dept.members.filter(m => selectedMembers.includes(m.id)).map(m => (
350
+ <option key={m.id} value={m.id}>{m.name} - {m.role}</option>
351
+ ))}
352
+ </select>
353
+ </div>
354
+ )}
355
+ <div className="flex justify-end gap-2">
356
+ <button className="btn-secondary" onClick={() => setShowNewTeam(false)}>{t('common.cancel')}</button>
357
+ <button
358
+ className="btn-primary"
359
+ disabled={!teamName || selectedMembers.length === 0 || !selectedLeader || loading}
360
+ onClick={handleCreateTeam}
361
+ >
362
+ {loading ? t('common.loading') : t('team.createBtn')}
363
+ </button>
364
+ </div>
365
+ </div>
366
+ )}
367
+
368
+ {/* Requirements list */}
369
+ <div>
370
+ <div className="flex items-center justify-between mb-3">
371
+ <h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">{t('dept.detail.requirements')}</h2>
372
+ {!showNewReq && (
373
+ <button
374
+ className="text-xs text-[var(--accent)] hover:underline"
375
+ onClick={() => { setShowNewReq(true); setNewReqTitle(''); setNewReqDesc(''); setNewReqWorkspaceDir(''); }}
376
+ >
377
+ + {t('dept.newReq.btn')}
378
+ </button>
379
+ )}
380
+ </div>
381
+ {deptRequirements.length > 0 ? (
382
+ <div className="space-y-2">
383
+ {deptRequirements.map((req) => {
384
+ const statusCfg = {
385
+ pending: { label: t('requirements.status.pending'), color: 'text-gray-400', bg: 'bg-gray-900/30', icon: '⏳' },
386
+ planning: { label: t('requirements.status.planning'), color: 'text-blue-400', bg: 'bg-blue-900/30', icon: '📝' },
387
+ in_progress: { label: t('requirements.status.in_progress'), color: 'text-yellow-400', bg: 'bg-yellow-900/30', icon: '⚙️' },
388
+ pending_approval: { label: t('requirements.status.pending_approval'), color: 'text-orange-400', bg: 'bg-orange-900/30', icon: '🔍' },
389
+ completed: { label: t('requirements.stats.completed'), color: 'text-green-400', bg: 'bg-green-900/30', icon: '✅' },
390
+ failed: { label: t('requirements.status.failed'), color: 'text-red-400', bg: 'bg-red-900/30', icon: '❌' },
391
+ };
392
+ const st = statusCfg[req.status] || statusCfg.pending;
393
+ return (
394
+ <div
395
+ key={req.id}
396
+ className="card cursor-pointer hover:border-[var(--accent)]/30 transition-all"
397
+ onClick={() => navigateToRequirement(req.id)}
398
+ >
399
+ <div className="flex items-center justify-between">
400
+ <div className="flex items-center gap-2">
401
+ <span>{st.icon}</span>
402
+ <span className="text-sm font-medium">{req.title}</span>
403
+ </div>
404
+ <div className="flex items-center gap-2">
405
+ <span className={`text-[10px] px-1.5 py-0.5 rounded ${st.bg} ${st.color}`}>{st.label}</span>
406
+ {req.workflow && <span className="text-[10px] text-[var(--muted)]">📊 {req.workflow.completedCount || 0}/{req.workflow.nodeCount || 0}</span>}
407
+ {req.chatCount > 0 && <span className="text-[10px] text-[var(--muted)]">💬 {req.chatCount}</span>}
408
+ {req.outputCount > 0 && <span className="text-[10px] text-[var(--muted)]">📦 {req.outputCount}</span>}
409
+ <button
410
+ onClick={(e) => { e.stopPropagation(); restartRequirement(req.id); }}
411
+ className="text-[10px] px-1.5 py-0.5 rounded bg-blue-600/15 hover:bg-blue-600/25 text-blue-400 transition-colors"
412
+ title={t('reqDetail.live.restart')}
413
+ >🔄</button>
414
+ <button
415
+ onClick={(e) => { e.stopPropagation(); if (confirm(t('reqDetail.live.confirmDelete'))) deleteRequirement(req.id); }}
416
+ className="text-[10px] px-1.5 py-0.5 rounded bg-red-600/15 hover:bg-red-600/25 text-red-400 transition-colors"
417
+ title={t('reqDetail.live.deleteReq')}
418
+ >🗑️</button>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ );
423
+ })}
424
+ </div>
425
+ ) : (
426
+ <div className="card text-center py-8 text-[var(--muted)]">
427
+ <div className="text-3xl mb-2">📋</div>
428
+ <p className="text-sm">{t('requirements.empty')}</p>
429
+ {!showNewReq && (
430
+ <button
431
+ className="btn-secondary mt-3 text-sm"
432
+ onClick={() => { setShowNewReq(true); setNewReqTitle(''); setNewReqDesc(''); setNewReqWorkspaceDir(''); }}
433
+ >
434
+ {t('dept.newReq.btn')}
435
+ </button>
436
+ )}
437
+ </div>
438
+ )}
439
+ </div>
440
+
441
+ {/* Teams list */}
442
+ <div>
443
+ <div className="flex items-center justify-between mb-3">
444
+ <h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">👥 {t('team.teamsTitle')}</h2>
445
+ {!showNewTeam && (
446
+ <button
447
+ className="text-xs text-purple-400 hover:underline"
448
+ onClick={() => { setShowNewTeam(true); setTeamName(''); setTeamDesc(''); setSelectedMembers([]); setSelectedLeader(''); }}
449
+ >
450
+ + {t('team.newTeamBtn')}
451
+ </button>
452
+ )}
453
+ </div>
454
+ {deptTeams.length > 0 ? (
455
+ <div className="space-y-2">
456
+ {deptTeams.map(team => {
457
+ const teamMembers = (team.memberIds || [])
458
+ .map(mid => dept.members.find(m => m.id === mid))
459
+ .filter(Boolean);
460
+ return (
461
+ <div
462
+ key={team.id}
463
+ className="card cursor-pointer hover:border-purple-500/30 transition-all"
464
+ onClick={() => navigateToTeam(team.id)}
465
+ >
466
+ <div className="flex items-center justify-between">
467
+ <div className="flex items-center gap-2">
468
+ <span className="text-lg">👥</span>
469
+ <span className="text-sm font-medium">{team.name}</span>
470
+ {team.leaderName && (
471
+ <span className="text-[10px] bg-yellow-900/30 text-yellow-400 px-1.5 py-0.5 rounded">👔 {team.leaderName}</span>
472
+ )}
473
+ </div>
474
+ <div className="flex items-center gap-2">
475
+ <div className="flex items-center -space-x-1.5">
476
+ {teamMembers.slice(0, 5).map(m => (
477
+ <CachedAvatar key={m.id} src={m.avatar} alt={m.name} className="w-5 h-5 rounded-full ring-1 ring-[var(--card)]" />
478
+ ))}
479
+ {teamMembers.length > 5 && (
480
+ <span className="w-5 h-5 rounded-full bg-white/10 ring-1 ring-[var(--card)] flex items-center justify-center text-[8px] text-[var(--muted)]">+{teamMembers.length - 5}</span>
481
+ )}
482
+ </div>
483
+ <span className="text-[10px] text-[var(--muted)]">🔄 {team.sprintCount || 0}</span>
484
+ </div>
485
+ </div>
486
+ {team.description && <p className="text-xs text-[var(--muted)] mt-1 truncate">{team.description}</p>}
487
+ </div>
488
+ );
489
+ })}
490
+ </div>
491
+ ) : (
492
+ <div className="card text-center py-6 text-[var(--muted)]">
493
+ <div className="text-2xl mb-2">👥</div>
494
+ <p className="text-sm">{t('team.noTeams')}</p>
495
+ {!showNewTeam && (
496
+ <button
497
+ className="btn-secondary mt-3 text-sm"
498
+ onClick={() => { setShowNewTeam(true); setTeamName(''); setTeamDesc(''); setSelectedMembers([]); setSelectedLeader(''); }}
499
+ >
500
+ {t('team.newTeamBtn')}
501
+ </button>
502
+ )}
503
+ </div>
504
+ )}
505
+ </div>
506
+
507
+ {/* Member list */}
508
+ <div>
509
+ <h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-3">{t('dept.detail.members')}</h2>
510
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
511
+ {dept.members.map((member) => (
512
+ <div
513
+ key={member.id}
514
+ className="card hover:border-[var(--accent)]/30 transition-all cursor-pointer group"
515
+ onClick={() => setSelectedAgent(member.id)}
516
+ >
517
+ <div className="flex items-start gap-3">
518
+ <div className="relative shrink-0">
519
+ <CachedAvatar src={member.avatar} alt={member.name} className="w-12 h-12 rounded-full bg-[var(--border)]" />
520
+ {member.avgScore >= 80 && (
521
+ <span className="absolute -top-1 -right-1 text-xs animate-pulse drop-shadow-lg">🌸</span>
522
+ )}
523
+ </div>
524
+ <div className="flex-1 min-w-0">
525
+ <div className="flex items-center gap-2">
526
+ <span className="font-medium text-sm truncate">{member.name}</span>
527
+ {dept.leader === member.id && (
528
+ <span className="text-[10px] bg-yellow-900/30 text-yellow-400 px-1.5 py-0.5 rounded">{t('dept.detail.leader')}</span>
529
+ )}
530
+ <span className={`status-dot ${member.status}`} />
531
+ </div>
532
+ <div className="text-xs text-[var(--muted)]">
533
+ {member.gender === 'female' ? '👩' : '👨'}{member.age ? ` ${t('display.ageYears', { n: member.age })}` : ''} · {member.role}
534
+ </div>
535
+ <div className="text-[10px] text-[var(--muted)] italic mt-1 truncate">"{member.signature}"</div>
536
+ </div>
537
+ <button
538
+ className="opacity-0 group-hover:opacity-100 text-blue-400 hover:text-blue-300 text-sm transition-opacity"
539
+ title={t('agentChat.chatBtn')}
540
+ onClick={(e) => {
541
+ e.stopPropagation();
542
+ setChatAgent({ id: member.id, name: member.name, avatar: member.avatar, role: member.role, signature: member.signature, department: dept.name });
543
+ }}
544
+ >
545
+ 💬
546
+ </button>
547
+ <button
548
+ className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 text-sm transition-opacity"
549
+ title={t('dept.dismiss.title')}
550
+ onClick={(e) => {
551
+ e.stopPropagation();
552
+ setDismissTarget({ deptId: dept.id, agentId: member.id, name: member.name });
553
+ }}
554
+ >
555
+ 🔥
556
+ </button>
557
+ </div>
558
+ {/* Tags */}
559
+ <div className="flex items-center gap-2 mt-2 flex-wrap">
560
+ <span className="text-[10px] bg-blue-900/30 text-blue-400 px-1.5 py-0.5 rounded">{member.provider.name}</span>
561
+ {member.avgScore && (
562
+ <span className={`text-[10px] px-1.5 py-0.5 rounded ${
563
+ member.avgScore >= 80 ? 'bg-green-900/30 text-green-400' :
564
+ member.avgScore >= 60 ? 'bg-yellow-900/30 text-yellow-400' :
565
+ 'bg-red-900/30 text-red-400'
566
+ }`}>
567
+ {t('dept.detail.performance', { score: member.avgScore })}
568
+ </span>
569
+ )}
570
+ <span className="text-[10px] bg-purple-900/30 text-purple-400 px-1.5 py-0.5 rounded">
571
+ {t('dept.detail.memory', { n: (member.memory?.shortTermCount || 0) + (member.memory?.longTermCount || 0) })}
572
+ </span>
573
+ {member.taskCount > 0 && (
574
+ <span className="text-[10px] bg-orange-900/30 text-orange-400 px-1.5 py-0.5 rounded">
575
+ {t('dept.detail.tasks', { n: member.taskCount })}
576
+ </span>
577
+ )}
578
+ {member.tokenUsage?.totalTokens > 0 && (
579
+ <span className="text-[10px] bg-green-900/30 text-green-400 px-1.5 py-0.5 rounded">
580
+ ${(member.tokenUsage.totalCost || 0).toFixed(4)}
581
+ </span>
582
+ )}
583
+ </div>
584
+ {/* Skills */}
585
+ <div className="flex gap-1 mt-2 flex-wrap">
586
+ {member.skills.slice(0, 3).map((s, i) => (
587
+ <span key={i} className="text-[10px] text-[var(--muted)] bg-white/5 px-1.5 py-0.5 rounded">{s}</span>
588
+ ))}
589
+ {member.skills.length > 3 && (
590
+ <span className="text-[10px] text-[var(--muted)]">+{member.skills.length - 3}</span>
591
+ )}
592
+ </div>
593
+ </div>
594
+ ))}
595
+ </div>
596
+ </div>
597
+
598
+ {/* Project reports */}
599
+ {deptReports.length > 0 && (
600
+ <div>
601
+ <h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-3">{t('dept.detail.reports')}</h2>
602
+ <div className="space-y-2">
603
+ {deptReports.map((pr, i) => {
604
+ const r = pr.reports.find(r => r.department === dept.name);
605
+ if (!r) return null;
606
+ return (
607
+ <div key={i} className="card text-sm flex items-center justify-between">
608
+ <div className="flex items-center gap-2">
609
+ <span className="text-[10px] text-[var(--muted)]">{new Date(pr.time).toLocaleString()}</span>
610
+ <span className={`text-xs px-1.5 py-0.5 rounded ${
611
+ r.status === 'completed' ? 'bg-green-900/30 text-green-400' : 'bg-blue-900/30 text-blue-400'
612
+ }`}>{r.status}</span>
613
+ </div>
614
+ <div className="flex items-center gap-3 text-xs text-[var(--muted)]">
615
+ <span>🤖 {r.memberCount}</span>
616
+ <span>📝 {r.completedTasks}</span>
617
+ {r.avgScore && <span className="text-yellow-400">⭐ {r.avgScore}</span>}
618
+ </div>
619
+ </div>
620
+ );
621
+ })}
622
+ </div>
623
+ </div>
624
+ )}
625
+ </div>
626
+
627
+ {/* ========== Sub-modals ========== */}
628
+
629
+ {/* Requirement detail modal */}
630
+ {activeReqId && (
631
+ <RequirementDetail
632
+ requirementId={activeReqId}
633
+ onClose={() => setActiveReqId(null)}
634
+ />
635
+ )}
636
+
637
+ {/* Agent detail modal */}
638
+ {selectedAgent && (
639
+ <AgentDetailModal agentId={selectedAgent} onClose={() => setSelectedAgent(null)} />
640
+ )}
641
+
642
+ {/* Agent chat modal */}
643
+ {chatAgent && (
644
+ <AgentChatModal
645
+ agentId={chatAgent.id}
646
+ agentName={chatAgent.name}
647
+ agentAvatar={chatAgent.avatar}
648
+ agentRole={chatAgent.role}
649
+ agentSignature={chatAgent.signature}
650
+ agentDepartment={chatAgent.department}
651
+ onClose={() => setChatAgent(null)}
652
+ />
653
+ )}
654
+
655
+ {/* Dismiss confirm modal */}
656
+ {dismissTarget && (
657
+ <div className="fixed inset-0 z-[60] bg-black/70 flex items-center justify-center !m-0" onClick={() => setDismissTarget(null)}>
658
+ <div className="card max-w-sm w-full mx-4 space-y-4" onClick={e => e.stopPropagation()}>
659
+ <h3 className="text-lg font-semibold text-red-400">{t('dept.dismiss.title')}</h3>
660
+ <p className="text-sm">{t('dept.dismiss.desc', { name: '' })}<strong>{dismissTarget.name}</strong></p>
661
+ <div>
662
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.dismiss.reasonLabel')}</label>
663
+ <input className="input w-full" placeholder={t('dept.dismiss.reasonPlaceholder')} value={dismissReason} onChange={e => setDismissReason(e.target.value)} />
664
+ </div>
665
+ <div className="flex gap-2">
666
+ <button className="btn-secondary flex-1" onClick={() => setDismissTarget(null)}>{t('common.cancel')}</button>
667
+ <button className="btn-danger flex-1" onClick={handleDismiss}>{t('dept.dismiss.confirmBtn')}</button>
668
+ </div>
669
+ </div>
670
+ </div>
671
+ )}
672
+
673
+ {/* Disband department confirm modal */}
674
+ {showDisband && (
675
+ <div className="fixed inset-0 z-[60] bg-black/70 flex items-center justify-center !m-0" onClick={() => setShowDisband(false)}>
676
+ <div className="card max-w-sm w-full mx-4 space-y-4" onClick={e => e.stopPropagation()}>
677
+ <h3 className="text-lg font-semibold text-red-400">{t('dept.detail.disbandBtn')}</h3>
678
+ <p className="text-sm">
679
+ {t('dept.disband.desc', { name: '' })}<strong>{dept.name}</strong>
680
+ <br />{t('dept.disband.descSuffix')}
681
+ </p>
682
+ <div>
683
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.disband.reasonLabel')}</label>
684
+ <input className="input w-full" placeholder={t('dept.disband.reasonPlaceholder')} value={disbandReason} onChange={e => setDisbandReason(e.target.value)} />
685
+ </div>
686
+ <div className="flex gap-2">
687
+ <button className="btn-secondary flex-1" onClick={() => setShowDisband(false)}>{t('common.cancel')}</button>
688
+ <button className="btn-danger flex-1" onClick={handleDisband} disabled={loading}>
689
+ {loading ? t('dept.disband.disbanding') : t('dept.disband.confirmBtn')}
690
+ </button>
691
+ </div>
692
+ </div>
693
+ </div>
694
+ )}
695
+
696
+ {/* Adjust workforce modal */}
697
+ {showAdjust && (
698
+ <div className="fixed inset-0 z-[60] bg-black/70 flex items-center justify-center !m-0" onClick={() => { setShowAdjust(false); setPendingPlan(null); }}>
699
+ <div className="card max-w-lg w-full mx-4 space-y-4 max-h-[80vh] overflow-auto" onClick={e => e.stopPropagation()}>
700
+ {!pendingPlan || pendingPlan.type !== 'adjustment' ? (
701
+ <>
702
+ <h3 className="text-lg font-semibold">{t('dept.detail.adjustBtn')}</h3>
703
+ <p className="text-sm text-[var(--muted)]">{t('dept.adjust.desc')}</p>
704
+ <div className="bg-[var(--background)] border border-[var(--border)] rounded-lg p-3">
705
+ <div className="text-xs text-[var(--muted)] mb-1">{t('dept.adjust.currentDept')}</div>
706
+ <div className="font-medium">{dept.name}</div>
707
+ <div className="text-xs text-[var(--muted)] mt-1">
708
+ {t('dept.adjust.currentMembers', { n: dept.members.length })}
709
+ </div>
710
+ </div>
711
+ <div>
712
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('dept.adjust.goalLabel')}</label>
713
+ <textarea
714
+ className="input w-full h-20 resize-none"
715
+ placeholder={t('dept.adjust.goalPlaceholder')}
716
+ value={adjustGoal}
717
+ onChange={e => setAdjustGoal(e.target.value)}
718
+ />
719
+ </div>
720
+ <div className="flex gap-2">
721
+ <button className="btn-secondary flex-1" onClick={() => setShowAdjust(false)}>{t('common.cancel')}</button>
722
+ <button className="btn-primary flex-1" disabled={!adjustGoal || loading} onClick={handleAdjustPlan}>
723
+ {loading ? t('dept.adjust.planning') : t('dept.adjust.planBtn')}
724
+ </button>
725
+ </div>
726
+ </>
727
+ ) : (
728
+ <>
729
+ <h3 className="text-lg font-semibold">{t('dept.adjust.reviewTitle')}</h3>
730
+ <p className="text-sm text-[var(--muted)]">
731
+ {t('dept.adjust.reviewDesc', { dept: pendingPlan.departmentName })}
732
+ </p>
733
+ {pendingPlan.reasoning && (
734
+ <div className="bg-blue-900/10 border border-blue-500/20 rounded-lg p-3">
735
+ <div className="text-xs font-medium text-blue-400 mb-1">{t('overview.planReview.analysis')}</div>
736
+ <div className="text-sm text-[var(--muted)]">{pendingPlan.reasoning}</div>
737
+ </div>
738
+ )}
739
+ {pendingPlan.fires?.length > 0 && (
740
+ <div className="bg-red-900/10 border border-red-500/20 rounded-lg p-3 space-y-2">
741
+ <div className="text-xs font-medium text-red-400 mb-1">{t('dept.adjust.firesTitle', { n: pendingPlan.fires.length })}</div>
742
+ {pendingPlan.fires.map((f, i) => (
743
+ <div key={i} className="flex items-center gap-2 text-sm">
744
+ <span className="text-red-400">✕</span>
745
+ <span className="font-medium">{f.name}</span>
746
+ <span className="text-xs text-[var(--muted)]">- {f.reason}</span>
747
+ </div>
748
+ ))}
749
+ </div>
750
+ )}
751
+ {pendingPlan.hires?.length > 0 && (
752
+ <div className="bg-green-900/10 border border-green-500/20 rounded-lg p-3 space-y-2">
753
+ <div className="text-xs font-medium text-green-400 mb-1">{t('dept.adjust.hiresTitle', { n: pendingPlan.hires.length })}</div>
754
+ {pendingPlan.hires.map((h, i) => (
755
+ <div key={i} className="flex items-center gap-3 p-2 rounded-lg bg-white/5">
756
+ <span>🤖</span>
757
+ <div className="flex-1">
758
+ <div className="text-sm font-medium">{h.name}</div>
759
+ <div className="text-xs text-[var(--muted)]">{h.templateTitle || h.templateId}</div>
760
+ {h.providerName && <div className="text-[10px] text-purple-400/80 mt-0.5">⚡ {h.providerName}</div>}
761
+ {h.reason && <div className="text-[10px] text-blue-400/70 mt-0.5">💡 {h.reason}</div>}
762
+ </div>
763
+ </div>
764
+ ))}
765
+ </div>
766
+ )}
767
+ {pendingPlan.fires?.length === 0 && pendingPlan.hires?.length === 0 && (
768
+ <div className="text-center text-[var(--muted)] py-4">{t('dept.adjust.noChanges')}</div>
769
+ )}
770
+ <div className="flex gap-2">
771
+ <button className="btn-secondary flex-1" onClick={() => setPendingPlan(null)}>{t('overview.planReview.rejectBtn')}</button>
772
+ <button
773
+ className="btn-primary flex-1"
774
+ disabled={loading || (pendingPlan.fires?.length === 0 && pendingPlan.hires?.length === 0)}
775
+ onClick={async () => { await confirmAdjustment(pendingPlan.planId); setShowAdjust(false); }}
776
+ >
777
+ {loading ? t('dept.adjust.executing') : t('dept.adjust.approveBtn')}
778
+ </button>
779
+ </div>
780
+ </>
781
+ )}
782
+ </div>
783
+ </div>
784
+ )}
785
+
786
+ {/* Folder browser modal */}
787
+ {showFolderBrowser && (
788
+ <div className="fixed inset-0 z-[70] bg-black/70 flex items-center justify-center !m-0" onClick={() => setShowFolderBrowser(false)}>
789
+ <div className="card max-w-lg w-full mx-4 max-h-[70vh] flex flex-col" onClick={e => e.stopPropagation()}>
790
+ <div className="flex items-center justify-between pb-3 border-b border-[var(--border)]">
791
+ <h3 className="text-base font-semibold">📁 {t('dept.newReq.browseTitle')}</h3>
792
+ <button onClick={() => setShowFolderBrowser(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
793
+ </div>
794
+ <div className="flex items-center gap-2 py-2 px-1 bg-[var(--background)] rounded-lg mt-3 mb-2">
795
+ <span className="text-xs text-[var(--muted)] shrink-0">📍</span>
796
+ <span className="text-xs font-mono text-[var(--foreground)] truncate">{browseCurrentPath}</span>
797
+ </div>
798
+ <div className="flex-1 overflow-auto space-y-0.5 min-h-[200px]">
799
+ {browseLoading ? (
800
+ <div className="text-center py-8 text-[var(--muted)] text-sm animate-pulse">{t('common.loading')}</div>
801
+ ) : (
802
+ <>
803
+ {browseParentPath !== null && (
804
+ <div
805
+ className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors text-sm"
806
+ onClick={() => fetchDirs(browseParentPath)}
807
+ >
808
+ <span>📂</span>
809
+ <span className="text-[var(--muted)]">..</span>
810
+ </div>
811
+ )}
812
+ {browseDirs.length === 0 && !browseLoading && (
813
+ <div className="text-center py-6 text-xs text-[var(--muted)]">{t('dept.newReq.emptyDir')}</div>
814
+ )}
815
+ {browseDirs.map((dir) => (
816
+ <div
817
+ key={dir.path}
818
+ className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors text-sm group"
819
+ onClick={() => fetchDirs(dir.path)}
820
+ >
821
+ <span>📁</span>
822
+ <span className="flex-1 truncate">{dir.name}</span>
823
+ </div>
824
+ ))}
825
+ </>
826
+ )}
827
+ </div>
828
+ <div className="flex gap-2 pt-3 border-t border-[var(--border)] mt-2">
829
+ <button className="btn-secondary flex-1" onClick={() => setShowFolderBrowser(false)}>{t('common.cancel')}</button>
830
+ <button
831
+ className="btn-primary flex-1"
832
+ onClick={() => { setNewReqWorkspaceDir(browseCurrentPath); setShowFolderBrowser(false); }}
833
+ >
834
+ {t('dept.newReq.selectDir')}
835
+ </button>
836
+ </div>
837
+ </div>
838
+ </div>
839
+ )}
840
+ </div>
841
+ );
842
+ }