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,697 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import AgentDetailModal from './AgentDetailModal';
6
+ import AgentChatModal from './AgentChatModal';
7
+ import { useI18n } from '@/lib/i18n';
8
+ import CachedAvatar from './CachedAvatar';
9
+ import FilesView from './FilesView';
10
+ import GroupChatView from './GroupChatView';
11
+ import ReactMarkdown from 'react-markdown';
12
+ import remarkGfm from 'remark-gfm';
13
+
14
+ export default function TeamDetail() {
15
+ const { t } = useI18n();
16
+ const {
17
+ company, loading, activeTeamId, activeSprintId, setActiveSprintId,
18
+ navigateBackFromTeam, fetchTeamDetail, updateTeam, deleteTeam,
19
+ createSprint, discussSprint, approveSprint, fetchSprintDetail,
20
+ sendSprintMessage, deleteSprint,
21
+ fetchWorkspaceFile, navigateToRequirement,
22
+ } = useStore();
23
+
24
+ const [team, setTeam] = useState(null);
25
+ const [activeTab, setActiveTab] = useState('overview'); // overview | sprints | files
26
+ const [selectedAgent, setSelectedAgent] = useState(null);
27
+ const [chatAgent, setChatAgent] = useState(null);
28
+
29
+ // Sprint form
30
+ const [showNewSprint, setShowNewSprint] = useState(false);
31
+ const [sprintTitle, setSprintTitle] = useState('');
32
+ const [sprintGoal, setSprintGoal] = useState('');
33
+
34
+ // Sprint detail
35
+ const [sprintDetail, setSprintDetail] = useState(null);
36
+ const [chatInput, setChatInput] = useState('');
37
+ const chatEndRef = useRef(null);
38
+
39
+ // Skills editing
40
+ const [editingSkills, setEditingSkills] = useState(false);
41
+ const [skillsInput, setSkillsInput] = useState('');
42
+
43
+ // Workspace
44
+ const [showWorkspaceSelector, setShowWorkspaceSelector] = useState(false);
45
+ const [workspaceInput, setWorkspaceInput] = useState('');
46
+ const [browseDirs, setBrowseDirs] = useState([]);
47
+ const [browseCurrentPath, setBrowseCurrentPath] = useState('');
48
+ const [browseParentPath, setBrowseParentPath] = useState(null);
49
+ const [browseLoading, setBrowseLoading] = useState(false);
50
+
51
+ // Files
52
+ const [previewFile, setPreviewFile] = useState(null); // { path, content, loading }
53
+
54
+ const fetchDirs = async (dirPath) => {
55
+ setBrowseLoading(true);
56
+ try {
57
+ const url = dirPath ? `/api/browse-dir?path=${encodeURIComponent(dirPath)}` : '/api/browse-dir';
58
+ const res = await fetch(url);
59
+ const data = await res.json();
60
+ if (!data.error) {
61
+ setBrowseDirs(data.dirs || []);
62
+ setBrowseCurrentPath(data.current || '');
63
+ setBrowseParentPath(data.parent || null);
64
+ }
65
+ } catch (e) { /* handled */ }
66
+ setBrowseLoading(false);
67
+ };
68
+
69
+ // Fetch team detail
70
+ const loadTeam = async () => {
71
+ if (!activeTeamId) return;
72
+ const data = await fetchTeamDetail(activeTeamId);
73
+ if (data) setTeam(data);
74
+ };
75
+
76
+ useEffect(() => {
77
+ loadTeam();
78
+ }, [activeTeamId]);
79
+
80
+ // Auto-refresh sprint detail
81
+ useEffect(() => {
82
+ if (!activeSprintId || !activeTeamId) { setSprintDetail(null); return; }
83
+ let running = true;
84
+ const load = async () => {
85
+ const data = await fetchSprintDetail(activeTeamId, activeSprintId);
86
+ if (data && running) setSprintDetail(data);
87
+ };
88
+ load();
89
+ const interval = setInterval(load, 5000);
90
+ return () => { running = false; clearInterval(interval); };
91
+ }, [activeSprintId, activeTeamId]);
92
+
93
+ // Scroll chat to bottom
94
+ useEffect(() => {
95
+ setTimeout(() => {
96
+ chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
97
+ }, 80);
98
+ }, [sprintDetail?.groupChat?.length]);
99
+
100
+ // File preview
101
+ const loadFilePreview = useCallback(async (filePath) => {
102
+ if (!team?.departmentId) return;
103
+ setPreviewFile({ path: filePath, content: null, loading: true });
104
+ try {
105
+ const content = await fetchWorkspaceFile(team.departmentId, filePath);
106
+ setPreviewFile({ path: filePath, content: content?.content || content || '', loading: false });
107
+ } catch {
108
+ setPreviewFile({ path: filePath, content: 'Failed to read file', loading: false });
109
+ }
110
+ }, [team?.departmentId, fetchWorkspaceFile]);
111
+
112
+ if (!team) {
113
+ return (
114
+ <div className="p-6 text-center text-[var(--muted)]">
115
+ <div className="animate-pulse text-4xl mb-4">👥</div>
116
+ <p>{t('common.loading')}</p>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ const handleCreateSprint = async () => {
122
+ if (!sprintTitle || !sprintGoal) return;
123
+ const result = await createSprint(activeTeamId, sprintTitle, sprintGoal);
124
+ if (result?.id) {
125
+ setShowNewSprint(false);
126
+ setSprintTitle('');
127
+ setSprintGoal('');
128
+ loadTeam();
129
+ setActiveSprintId(result.id);
130
+ }
131
+ };
132
+
133
+ const handleDiscuss = async (sprintId) => {
134
+ await discussSprint(activeTeamId, sprintId);
135
+ loadTeam();
136
+ const data = await fetchSprintDetail(activeTeamId, sprintId);
137
+ if (data) setSprintDetail(data);
138
+ };
139
+
140
+ const handleApprove = async (sprintId) => {
141
+ await approveSprint(activeTeamId, sprintId);
142
+ loadTeam();
143
+ const data = await fetchSprintDetail(activeTeamId, sprintId);
144
+ if (data) setSprintDetail(data);
145
+ };
146
+
147
+ const handleSendChat = async () => {
148
+ if (!chatInput.trim() || !activeSprintId) return;
149
+ const msg = chatInput;
150
+ setChatInput('');
151
+ await sendSprintMessage(activeTeamId, activeSprintId, msg);
152
+ const data = await fetchSprintDetail(activeTeamId, activeSprintId);
153
+ if (data) setSprintDetail(data);
154
+ };
155
+
156
+ const handleSaveSkills = async () => {
157
+ const skills = skillsInput.split(',').map(s => s.trim()).filter(Boolean);
158
+ await updateTeam(activeTeamId, { skills });
159
+ setEditingSkills(false);
160
+ loadTeam();
161
+ };
162
+
163
+ const handleSetWorkspace = async (path) => {
164
+ await updateTeam(activeTeamId, { workspacePath: path });
165
+ setShowWorkspaceSelector(false);
166
+ loadTeam();
167
+ };
168
+
169
+ const statusCfg = {
170
+ draft: { label: t('team.sprint.draft'), color: 'text-gray-400', bg: 'bg-gray-900/30', icon: '📝' },
171
+ discussing: { label: t('team.sprint.discussing'), color: 'text-blue-400', bg: 'bg-blue-900/30', icon: '💬' },
172
+ pending_approval: { label: t('team.sprint.pendingApproval'), color: 'text-yellow-400', bg: 'bg-yellow-900/30', icon: '⏳' },
173
+ in_progress: { label: t('team.sprint.inProgress'), color: 'text-green-400', bg: 'bg-green-900/30', icon: '⚙️' },
174
+ completed: { label: t('team.sprint.completed'), color: 'text-emerald-400', bg: 'bg-emerald-900/30', icon: '✅' },
175
+ failed: { label: t('team.sprint.failed'), color: 'text-red-400', bg: 'bg-red-900/30', icon: '❌' },
176
+ };
177
+
178
+ // Sprint detail view (inline) — two-column layout like RequirementDetail
179
+ const renderSprintDetail = () => {
180
+ if (!sprintDetail) return null;
181
+ const st = statusCfg[sprintDetail.status] || statusCfg.draft;
182
+ const groupChat = (sprintDetail.groupChat || []).filter(m => m.visibility !== 'flow');
183
+
184
+ // Build agentMap for GroupChatView
185
+ const chatAgentMap = {};
186
+ if (company?.departments) {
187
+ for (const dept of company.departments) {
188
+ for (const agent of (dept.members || dept.agents || [])) {
189
+ chatAgentMap[agent.id] = agent.name;
190
+ }
191
+ }
192
+ }
193
+
194
+ // Adapt sendSprintMessage to GroupChatView's (id, msg) signature
195
+ const handleSprintChatSend = async (_id, msg) => {
196
+ await sendSprintMessage(activeTeamId, sprintDetail.id, msg);
197
+ };
198
+ const handleSprintChatRefresh = async () => {
199
+ const data = await fetchSprintDetail(activeTeamId, sprintDetail.id);
200
+ if (data) setSprintDetail(data);
201
+ };
202
+
203
+ // Find leader info for typing indicator
204
+ const leaderMsg = groupChat.find(m => m.from?.id !== 'boss' && m.from?.role !== 'system' && m.type !== 'system');
205
+ const chatLeaderInfo = leaderMsg ? { name: leaderMsg.from?.name, avatar: leaderMsg.from?.avatar } : null;
206
+
207
+ return (
208
+ <div className="flex-1 min-h-0 flex flex-col">
209
+ {/* Sprint header */}
210
+ <div className="shrink-0 border-b border-[var(--border)] bg-[var(--card)]/50 px-6 py-3">
211
+ <div className="flex items-center justify-between">
212
+ <div className="flex items-center gap-3">
213
+ <button
214
+ onClick={() => { setActiveSprintId(null); setSprintDetail(null); }}
215
+ className="text-[var(--muted)] hover:text-[var(--foreground)] text-sm"
216
+ >
217
+ ← {t('team.sprint.backToList')}
218
+ </button>
219
+ <div className="w-px h-5 bg-[var(--border)]" />
220
+ <span className="text-lg font-bold">{sprintDetail.title}</span>
221
+ <span className={`text-xs px-2 py-0.5 rounded-full ${st.bg} ${st.color}`}>{st.icon} {st.label}</span>
222
+ </div>
223
+ <div className="flex items-center gap-2">
224
+ {sprintDetail.status === 'draft' && (
225
+ <button className="btn-primary text-xs" disabled={loading} onClick={() => handleDiscuss(sprintDetail.id)}>
226
+ {loading ? t('team.sprint.discussing') : t('team.sprint.startDiscussion')}
227
+ </button>
228
+ )}
229
+ {sprintDetail.status === 'pending_approval' && (
230
+ <button className="btn-primary text-xs" disabled={loading} onClick={() => handleApprove(sprintDetail.id)}>
231
+ {loading ? '...' : t('team.sprint.approve')}
232
+ </button>
233
+ )}
234
+ {sprintDetail.requirementId && (
235
+ <button
236
+ className="text-xs px-3 py-1 rounded bg-blue-600/20 text-blue-400 hover:bg-blue-600/30 font-medium"
237
+ onClick={() => navigateToRequirement(sprintDetail.requirementId)}
238
+ >
239
+ {t('team.sprint.viewRequirement')}
240
+ </button>
241
+ )}
242
+ <button
243
+ className="text-xs px-2 py-1 rounded bg-red-600/15 text-red-400 hover:bg-red-600/25"
244
+ onClick={async () => {
245
+ if (confirm(t('team.sprint.confirmDelete'))) {
246
+ await deleteSprint(activeTeamId, sprintDetail.id);
247
+ setActiveSprintId(null);
248
+ setSprintDetail(null);
249
+ loadTeam();
250
+ }
251
+ }}
252
+ >
253
+ 🗑️
254
+ </button>
255
+ </div>
256
+ </div>
257
+ {/* Goal & Plan summary */}
258
+ <div className="flex items-center gap-4 mt-2 text-xs text-[var(--muted)]">
259
+ <span>🎯 {sprintDetail.goal}</span>
260
+ {sprintDetail.workflow?.nodes?.length > 0 && (
261
+ <span>📊 {sprintDetail.workflow.nodes.filter(n => n.status === 'completed').length}/{sprintDetail.workflow.nodes.length}</span>
262
+ )}
263
+ </div>
264
+ </div>
265
+
266
+ {/* Two-column body */}
267
+ <div className="flex-1 min-h-0 flex">
268
+ {/* Left: Group Chat */}
269
+ <div className="w-[380px] shrink-0 border-r border-white/[0.06] flex flex-col bg-[var(--background)]">
270
+ <div className="px-4 py-2.5 border-b border-white/[0.06] bg-[var(--card)] flex items-center justify-between">
271
+ <span className="text-sm font-medium">💬 {t('team.sprint.chat')}</span>
272
+ <span className="text-[10px] bg-white/10 px-1.5 py-0.5 rounded-full text-[var(--muted)]">{groupChat.length}</span>
273
+ </div>
274
+ <GroupChatView
275
+ groupChat={groupChat}
276
+ agentMap={chatAgentMap}
277
+ bossAvatar={company?.bossAvatar}
278
+ bossName={company?.boss || 'Boss'}
279
+ requirementId={sprintDetail.id}
280
+ onSendMessage={handleSprintChatSend}
281
+ fetchDetail={handleSprintChatRefresh}
282
+ leaderInfo={chatLeaderInfo}
283
+ chatEndRef={chatEndRef}
284
+ embedded
285
+ />
286
+ </div>
287
+
288
+ {/* Right: Sprint Plan */}
289
+ <div className="flex-1 min-w-0 flex flex-col">
290
+ <div className="flex items-center gap-2 px-6 py-2.5 border-b border-white/[0.06] bg-[var(--card)]">
291
+ <span className="text-sm font-medium">📋 {t('team.sprint.plan')}</span>
292
+ </div>
293
+
294
+ <div className="flex-1 min-h-0 overflow-auto p-4">
295
+ {sprintDetail.plan ? (
296
+ <div className="prose prose-invert prose-xs max-w-none font-mono text-xs leading-relaxed">
297
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{sprintDetail.plan}</ReactMarkdown>
298
+ </div>
299
+ ) : (
300
+ <div className="text-center py-8 text-[var(--muted)] text-sm">
301
+ <div className="text-3xl mb-2">📋</div>
302
+ <p>{t('team.sprint.noPlan')}</p>
303
+ </div>
304
+ )}
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ );
310
+ };
311
+
312
+ return (
313
+ <div className="h-full flex flex-col animate-fade-in">
314
+ {/* Top bar */}
315
+ <div className="shrink-0 border-b border-[var(--border)] bg-[var(--card)]/50 backdrop-blur-sm px-6 py-4">
316
+ <div className="flex items-center justify-between">
317
+ <div className="flex items-center gap-4 min-w-0">
318
+ <button
319
+ onClick={navigateBackFromTeam}
320
+ className="text-[var(--muted)] hover:text-[var(--foreground)] transition-colors text-sm flex items-center gap-1 shrink-0"
321
+ >
322
+ ← {t('team.back')}
323
+ </button>
324
+ <div className="w-px h-6 bg-[var(--border)]" />
325
+ <div className="flex items-center gap-3 min-w-0">
326
+ <div className="w-10 h-10 bg-gradient-to-br from-violet-600 to-purple-700 rounded-lg flex items-center justify-center text-lg shrink-0">
327
+ 👥
328
+ </div>
329
+ <div className="min-w-0">
330
+ <h1 className="text-lg font-bold flex items-center gap-2 truncate">
331
+ {team.name}
332
+ <span className="text-xs px-2 py-0.5 rounded-full bg-purple-900/30 text-purple-400">
333
+ {team.departmentName}
334
+ </span>
335
+ </h1>
336
+ <p className="text-xs text-[var(--muted)] truncate">{team.description || t('team.noDescription')}</p>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ <div className="flex items-center gap-2 shrink-0">
341
+ <button
342
+ className="btn-primary flex items-center gap-1.5 text-sm"
343
+ onClick={() => { setShowNewSprint(true); setSprintTitle(''); setSprintGoal(''); }}
344
+ >
345
+ 🚀 {t('team.newSprint')}
346
+ </button>
347
+ </div>
348
+ </div>
349
+ {/* Stats */}
350
+ <div className="flex items-center gap-4 mt-2 text-xs text-[var(--muted)]">
351
+ <span>👥 {team.memberIds?.length || 0} {t('team.members')}</span>
352
+ <span>👔 {team.leaderName}</span>
353
+ <span>🔄 {team.sprints?.length || 0} {t('team.sprints')}</span>
354
+ {team.workspacePath && <span>📁 {team.workspacePath}</span>}
355
+ </div>
356
+ {/* Tabs */}
357
+ <div className="flex gap-1 mt-3">
358
+ {[
359
+ { key: 'overview', label: t('team.tab.overview') },
360
+ { key: 'sprints', label: t('team.tab.sprints') },
361
+ { key: 'files', label: t('team.tab.files') },
362
+ ].map(tab => (
363
+ <button
364
+ key={tab.key}
365
+ onClick={() => { setActiveTab(tab.key); setActiveSprintId(null); setSprintDetail(null); }}
366
+ className={`text-xs px-3 py-1.5 rounded-lg transition-colors ${
367
+ activeTab === tab.key
368
+ ? 'bg-[var(--accent)] text-white'
369
+ : 'text-[var(--muted)] hover:bg-white/5'
370
+ }`}
371
+ >
372
+ {tab.label}
373
+ </button>
374
+ ))}
375
+ </div>
376
+ </div>
377
+
378
+ {/* Main content */}
379
+ <div className={`flex-1 min-h-0 flex flex-col ${
380
+ activeSprintId
381
+ ? 'overflow-hidden'
382
+ : `p-6 space-y-6 ${activeTab === 'files' ? 'overflow-hidden' : 'overflow-auto'}`
383
+ }`}>
384
+
385
+ {/* New Sprint Form */}
386
+ {showNewSprint && (
387
+ <div className="card border-[var(--accent)]/30 animate-fade-in space-y-4">
388
+ <div className="flex items-center justify-between">
389
+ <h3 className="text-base font-semibold">🚀 {t('team.newSprint')}</h3>
390
+ <button onClick={() => setShowNewSprint(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
391
+ </div>
392
+ <div>
393
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('team.sprint.titleLabel')}</label>
394
+ <input
395
+ className="input w-full"
396
+ placeholder={t('team.sprint.titlePlaceholder')}
397
+ value={sprintTitle}
398
+ onChange={e => setSprintTitle(e.target.value)}
399
+ autoFocus
400
+ />
401
+ </div>
402
+ <div>
403
+ <label className="block text-sm mb-1 text-[var(--muted)]">{t('team.sprint.goalLabel')}</label>
404
+ <textarea
405
+ className="input w-full h-24 resize-none"
406
+ placeholder={t('team.sprint.goalPlaceholder')}
407
+ value={sprintGoal}
408
+ onChange={e => setSprintGoal(e.target.value)}
409
+ />
410
+ </div>
411
+ <div className="flex justify-end gap-2">
412
+ <button className="btn-secondary" onClick={() => setShowNewSprint(false)}>{t('common.cancel')}</button>
413
+ <button
414
+ className="btn-primary"
415
+ disabled={!sprintTitle || !sprintGoal || loading}
416
+ onClick={handleCreateSprint}
417
+ >
418
+ {loading ? t('common.loading') : t('team.sprint.createBtn')}
419
+ </button>
420
+ </div>
421
+ </div>
422
+ )}
423
+
424
+ {/* Overview tab */}
425
+ {activeTab === 'overview' && !activeSprintId && (
426
+ <>
427
+ {/* Members */}
428
+ <div>
429
+ <h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-3">{t('team.membersTitle')}</h2>
430
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
431
+ {(team.membersDetail || []).map(member => (
432
+ <div
433
+ key={member.id}
434
+ className="card hover:border-[var(--accent)]/30 transition-all cursor-pointer group"
435
+ onClick={() => setSelectedAgent(member.id)}
436
+ >
437
+ <div className="flex items-start gap-3">
438
+ <CachedAvatar src={member.avatar} alt={member.name} className="w-12 h-12 rounded-full bg-[var(--border)] shrink-0" />
439
+ <div className="flex-1 min-w-0">
440
+ <div className="flex items-center gap-2">
441
+ <span className="font-medium text-sm truncate">{member.name}</span>
442
+ {team.leaderId === member.id && (
443
+ <span className="text-[10px] bg-yellow-900/30 text-yellow-400 px-1.5 py-0.5 rounded">{t('team.leader')}</span>
444
+ )}
445
+ <span className={`status-dot ${member.status}`} />
446
+ </div>
447
+ <div className="text-xs text-[var(--muted)]">{member.role}</div>
448
+ {member.signature && <div className="text-[10px] text-[var(--muted)] italic mt-1 truncate">"{member.signature}"</div>}
449
+ </div>
450
+ <button
451
+ className="opacity-0 group-hover:opacity-100 text-blue-400 hover:text-blue-300 text-sm transition-opacity"
452
+ onClick={(e) => {
453
+ e.stopPropagation();
454
+ setChatAgent({ id: member.id, name: member.name, avatar: member.avatar, role: member.role, signature: member.signature, department: team.departmentName });
455
+ }}
456
+ >
457
+ 💬
458
+ </button>
459
+ </div>
460
+ <div className="flex gap-1 mt-2 flex-wrap">
461
+ {member.skills?.slice(0, 4).map((s, i) => (
462
+ <span key={i} className="text-[10px] text-[var(--muted)] bg-white/5 px-1.5 py-0.5 rounded">{s}</span>
463
+ ))}
464
+ </div>
465
+ </div>
466
+ ))}
467
+ </div>
468
+ </div>
469
+
470
+ {/* Skills */}
471
+ <div>
472
+ <div className="flex items-center justify-between mb-3">
473
+ <h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">{t('team.skillsTitle')}</h2>
474
+ <button
475
+ className="text-xs text-[var(--accent)] hover:underline"
476
+ onClick={() => { setEditingSkills(true); setSkillsInput((team.skills || []).join(', ')); }}
477
+ >
478
+ ✏️ {t('team.editSkills')}
479
+ </button>
480
+ </div>
481
+ {editingSkills ? (
482
+ <div className="card space-y-3">
483
+ <textarea
484
+ className="input w-full h-16 resize-none text-sm"
485
+ placeholder={t('team.skillsPlaceholder')}
486
+ value={skillsInput}
487
+ onChange={e => setSkillsInput(e.target.value)}
488
+ />
489
+ <div className="flex justify-end gap-2">
490
+ <button className="btn-secondary text-xs" onClick={() => setEditingSkills(false)}>{t('common.cancel')}</button>
491
+ <button className="btn-primary text-xs" onClick={handleSaveSkills}>{t('common.save')}</button>
492
+ </div>
493
+ </div>
494
+ ) : (
495
+ <div className="flex gap-2 flex-wrap">
496
+ {(team.skills || []).length > 0 ? (
497
+ team.skills.map((s, i) => (
498
+ <span key={i} className="text-xs bg-purple-900/30 text-purple-400 px-2 py-1 rounded">{s}</span>
499
+ ))
500
+ ) : (
501
+ <span className="text-xs text-[var(--muted)]">{t('team.noSkills')}</span>
502
+ )}
503
+ </div>
504
+ )}
505
+ </div>
506
+
507
+ {/* Workspace */}
508
+ <div>
509
+ <div className="flex items-center justify-between mb-3">
510
+ <h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">{t('team.workspaceTitle')}</h2>
511
+ <button
512
+ className="text-xs text-[var(--accent)] hover:underline"
513
+ onClick={() => { setShowWorkspaceSelector(true); setWorkspaceInput(team.workspacePath || ''); fetchDirs(team.workspacePath || ''); }}
514
+ >
515
+ 📁 {t('team.selectWorkspace')}
516
+ </button>
517
+ </div>
518
+ {team.workspacePath ? (
519
+ <div className="card text-sm font-mono">{team.workspacePath}</div>
520
+ ) : (
521
+ <div className="card text-sm text-[var(--muted)]">{t('team.noWorkspace')}</div>
522
+ )}
523
+ </div>
524
+
525
+ {/* Recent sprints */}
526
+ <div>
527
+ <h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-3">{t('team.recentSprints')}</h2>
528
+ {(team.sprints || []).length > 0 ? (
529
+ <div className="space-y-2">
530
+ {team.sprints.slice(0, 5).map(sprint => {
531
+ const st = statusCfg[sprint.status] || statusCfg.draft;
532
+ return (
533
+ <div
534
+ key={sprint.id}
535
+ className="card cursor-pointer hover:border-[var(--accent)]/30 transition-all"
536
+ onClick={() => { setActiveTab('sprints'); setActiveSprintId(sprint.id); }}
537
+ >
538
+ <div className="flex items-center justify-between">
539
+ <div className="flex items-center gap-2">
540
+ <span>{st.icon}</span>
541
+ <span className="text-sm font-medium">{sprint.title}</span>
542
+ </div>
543
+ <div className="flex items-center gap-2">
544
+ <span className={`text-[10px] px-1.5 py-0.5 rounded ${st.bg} ${st.color}`}>{st.label}</span>
545
+ {sprint.workflow && <span className="text-[10px] text-[var(--muted)]">📊 {sprint.workflow.completedCount || 0}/{sprint.workflow.nodeCount || 0}</span>}
546
+ </div>
547
+ </div>
548
+ </div>
549
+ );
550
+ })}
551
+ </div>
552
+ ) : (
553
+ <div className="card text-center py-6 text-[var(--muted)]">
554
+ <div className="text-2xl mb-2">🚀</div>
555
+ <p className="text-sm">{t('team.noSprints')}</p>
556
+ </div>
557
+ )}
558
+ </div>
559
+ </>
560
+ )}
561
+
562
+ {/* Sprints tab / Sprint detail */}
563
+ {activeTab === 'sprints' && (
564
+ activeSprintId ? renderSprintDetail() : (
565
+ <div>
566
+ <div className="flex items-center justify-between mb-4">
567
+ <h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">{t('team.tab.sprints')}</h2>
568
+ <button
569
+ className="text-xs text-[var(--accent)] hover:underline"
570
+ onClick={() => { setShowNewSprint(true); setSprintTitle(''); setSprintGoal(''); }}
571
+ >
572
+ + {t('team.newSprint')}
573
+ </button>
574
+ </div>
575
+ {(team.sprints || []).length > 0 ? (
576
+ <div className="space-y-2">
577
+ {team.sprints.map(sprint => {
578
+ const st = statusCfg[sprint.status] || statusCfg.draft;
579
+ return (
580
+ <div
581
+ key={sprint.id}
582
+ className="card cursor-pointer hover:border-[var(--accent)]/30 transition-all"
583
+ onClick={() => setActiveSprintId(sprint.id)}
584
+ >
585
+ <div className="flex items-center justify-between">
586
+ <div className="flex items-center gap-2">
587
+ <span>{st.icon}</span>
588
+ <span className="text-sm font-medium">{sprint.title}</span>
589
+ </div>
590
+ <div className="flex items-center gap-2">
591
+ <span className={`text-[10px] px-1.5 py-0.5 rounded ${st.bg} ${st.color}`}>{st.label}</span>
592
+ {sprint.chatCount > 0 && <span className="text-[10px] text-[var(--muted)]">💬 {sprint.chatCount}</span>}
593
+ {sprint.workflow && <span className="text-[10px] text-[var(--muted)]">📊 {sprint.workflow.completedCount || 0}/{sprint.workflow.nodeCount || 0}</span>}
594
+ </div>
595
+ </div>
596
+ <p className="text-xs text-[var(--muted)] mt-1 truncate">🎯 {sprint.goal}</p>
597
+ </div>
598
+ );
599
+ })}
600
+ </div>
601
+ ) : (
602
+ <div className="card text-center py-8 text-[var(--muted)]">
603
+ <div className="text-3xl mb-2">🚀</div>
604
+ <p className="text-sm">{t('team.noSprints')}</p>
605
+ </div>
606
+ )}
607
+ </div>
608
+ )
609
+ )}
610
+
611
+ {/* Overview + active sprint */}
612
+ {activeTab === 'overview' && activeSprintId && renderSprintDetail()}
613
+
614
+ {/* Files tab */}
615
+ {activeTab === 'files' && (
616
+ <div className="flex-1 min-h-0">
617
+ {team.workspacePath ? (
618
+ <div className="h-full">
619
+ <FilesView
620
+ departmentId={team.departmentId}
621
+ previewFile={previewFile}
622
+ onPreview={loadFilePreview}
623
+ onClosePreview={() => setPreviewFile(null)}
624
+ />
625
+ </div>
626
+ ) : (
627
+ <div className="card text-center py-8 text-[var(--muted)]">
628
+ <div className="text-3xl mb-2">📁</div>
629
+ <p className="text-sm">{t('team.noWorkspace')}</p>
630
+ <button
631
+ className="btn-secondary mt-3 text-sm"
632
+ onClick={() => { setShowWorkspaceSelector(true); fetchDirs(''); }}
633
+ >
634
+ {t('team.selectWorkspace')}
635
+ </button>
636
+ </div>
637
+ )}
638
+ </div>
639
+ )}
640
+ </div>
641
+
642
+ {/* Modals */}
643
+ {selectedAgent && <AgentDetailModal agentId={selectedAgent} onClose={() => setSelectedAgent(null)} />}
644
+ {chatAgent && (
645
+ <AgentChatModal
646
+ agentId={chatAgent.id}
647
+ agentName={chatAgent.name}
648
+ agentAvatar={chatAgent.avatar}
649
+ agentRole={chatAgent.role}
650
+ agentSignature={chatAgent.signature}
651
+ agentDepartment={chatAgent.department}
652
+ onClose={() => setChatAgent(null)}
653
+ />
654
+ )}
655
+
656
+ {/* Workspace selector modal */}
657
+ {showWorkspaceSelector && (
658
+ <div className="fixed inset-0 z-[70] bg-black/70 flex items-center justify-center !m-0" onClick={() => setShowWorkspaceSelector(false)}>
659
+ <div className="card max-w-lg w-full mx-4 max-h-[70vh] flex flex-col" onClick={e => e.stopPropagation()}>
660
+ <div className="flex items-center justify-between pb-3 border-b border-[var(--border)]">
661
+ <h3 className="text-base font-semibold">📁 {t('team.selectWorkspace')}</h3>
662
+ <button onClick={() => setShowWorkspaceSelector(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
663
+ </div>
664
+ <div className="flex items-center gap-2 py-2 px-1 bg-[var(--background)] rounded-lg mt-3 mb-2">
665
+ <span className="text-xs text-[var(--muted)] shrink-0">📍</span>
666
+ <span className="text-xs font-mono text-[var(--foreground)] truncate">{browseCurrentPath}</span>
667
+ </div>
668
+ <div className="flex-1 overflow-auto space-y-0.5 min-h-[200px]">
669
+ {browseLoading ? (
670
+ <div className="text-center py-8 text-[var(--muted)] text-sm animate-pulse">{t('common.loading')}</div>
671
+ ) : (
672
+ <>
673
+ {browseParentPath !== null && (
674
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-white/5 cursor-pointer text-sm" onClick={() => fetchDirs(browseParentPath)}>
675
+ <span>📂</span><span className="text-[var(--muted)]">..</span>
676
+ </div>
677
+ )}
678
+ {browseDirs.map(dir => (
679
+ <div key={dir.path} className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-white/5 cursor-pointer text-sm" onClick={() => fetchDirs(dir.path)}>
680
+ <span>📁</span><span className="truncate">{dir.name}</span>
681
+ </div>
682
+ ))}
683
+ </>
684
+ )}
685
+ </div>
686
+ <div className="flex gap-2 pt-3 border-t border-[var(--border)] mt-2">
687
+ <button className="btn-secondary flex-1" onClick={() => setShowWorkspaceSelector(false)}>{t('common.cancel')}</button>
688
+ <button className="btn-primary flex-1" onClick={() => handleSetWorkspace(browseCurrentPath)}>
689
+ {t('dept.newReq.selectDir')}
690
+ </button>
691
+ </div>
692
+ </div>
693
+ </div>
694
+ )}
695
+ </div>
696
+ );
697
+ }