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,926 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { getAvatarUrl } from '@/lib/avatar';
6
+ import AgentDetailModal from './AgentDetailModal';
7
+ import { useI18n } from '@/lib/i18n';
8
+ import GroupChatView from './GroupChatView';
9
+ import { MessageBubble, ChatInput, TaskStatusPanel, formatTime } from './ChatShared';
10
+ import CachedAvatar from './CachedAvatar';
11
+
12
+ /**
13
+ * IM chat interface - Lark style
14
+ * Left: conversation list (secretary pinned on top), Right: chat bubbles
15
+ */
16
+ export default function Mailbox() {
17
+ const { t } = useI18n();
18
+ const {
19
+ company, fetchCompany,
20
+ chatWithSecretary, chatOpen, setChatOpen,
21
+ navigateToRequirement, fetchRequirements, fetchRequirementDetail,
22
+ chatWithAgent, fetchAgentChatHistory, markAgentChatRead,
23
+ sendGroupChatMessage,
24
+ sendDeptGroupChatMessage, fetchDeptGroupChat,
25
+ } = useStore();
26
+
27
+ const [activeChat, setActiveChat] = useState(null); // { type: 'secretary' } | { type: 'agent-chat', agentId, ... }
28
+ const [inputText, setInputText] = useState('');
29
+ const [sending, setSending] = useState(false);
30
+ const [sendingTargetId, setSendingTargetId] = useState(null); // 追踪正在发送消息的目标ID,防止typing状态串台
31
+ const [secretaryHistory, setSecretaryHistory] = useState([]);
32
+ const [selectedAgent, setSelectedAgent] = useState(null); // View employee detail
33
+ const [chatFilter, setChatFilter] = useState('all'); // Chat filter: all | group | private | important
34
+ const [requirements, setRequirements] = useState([]); // Requirements list (for group chat sessions)
35
+ const [activeReqChat, setActiveReqChat] = useState(null); // Current active requirement group chat
36
+ const [reqChatDetail, setReqChatDetail] = useState(null); // Requirement group chat detail
37
+ const [activeDeptChat, setActiveDeptChat] = useState(null); // Current active department group chat
38
+ const [deptChatDetail, setDeptChatDetail] = useState(null); // Department group chat detail
39
+ const [agentChatMessages, setAgentChatMessages] = useState([]); // Agent 1-on-1 chat messages
40
+ const [agentChatLoading, setAgentChatLoading] = useState(false); // Agent chat loading state
41
+ const [showGroupMembers, setShowGroupMembers] = useState(false); // 群聊成员弹窗
42
+ const reqChatPollRef = useRef(null);
43
+ const agentChatPollRef = useRef(null);
44
+ const messagesEndRef = useRef(null);
45
+ const activeChatRef = useRef(null); // 追踪当前活跃的聊天对象,防止异步消息串台
46
+
47
+ if (!company) return null;
48
+
49
+ const secretary = company.secretary;
50
+ const agentChatSessions = company.agentChatSessions || [];
51
+
52
+ // 构建 agentId -> agentName 映射(用于 @[id] 渲染)
53
+ const agentMap = {};
54
+ if (company?.departments) {
55
+ for (const dept of company.departments) {
56
+ for (const agent of (dept.members || dept.agents || [])) {
57
+ agentMap[agent.id] = agent.name;
58
+ }
59
+ }
60
+ }
61
+
62
+ // 定时刷新 company 状态,确保新的 agentChatSessions(如 onboarding 打招呼)能及时出现
63
+ const companyPollRef = useRef(null);
64
+ useEffect(() => {
65
+ companyPollRef.current = setInterval(() => {
66
+ fetchCompany();
67
+ }, 5000);
68
+ return () => clearInterval(companyPollRef.current);
69
+ }, []);
70
+
71
+ // Load requirements list (for group chat sessions)
72
+ useEffect(() => {
73
+ fetchRequirements().then(setRequirements);
74
+ const timer = setInterval(() => {
75
+ fetchRequirements().then(setRequirements);
76
+ }, 10000);
77
+ return () => clearInterval(timer);
78
+ }, [company]);
79
+
80
+ // Requirement group chat polling
81
+ useEffect(() => {
82
+ if (reqChatPollRef.current) clearInterval(reqChatPollRef.current);
83
+ if (activeReqChat) {
84
+ const loadDetail = () => {
85
+ fetchRequirementDetail(activeReqChat).then(detail => {
86
+ if (detail) setReqChatDetail(detail);
87
+ });
88
+ };
89
+ loadDetail();
90
+ reqChatPollRef.current = setInterval(loadDetail, 3000);
91
+ } else {
92
+ setReqChatDetail(null);
93
+ }
94
+ return () => {
95
+ if (reqChatPollRef.current) clearInterval(reqChatPollRef.current);
96
+ };
97
+ }, [activeReqChat]);
98
+
99
+ // Department group chat polling
100
+ const deptChatPollRef = useRef(null);
101
+ useEffect(() => {
102
+ if (deptChatPollRef.current) clearInterval(deptChatPollRef.current);
103
+ if (activeDeptChat) {
104
+ const loadDetail = () => {
105
+ fetchDeptGroupChat(activeDeptChat).then(data => {
106
+ if (data) {
107
+ // 合并部门基础信息
108
+ const dept = (company?.departments || []).find(d => d.id === activeDeptChat);
109
+ setDeptChatDetail({
110
+ ...data,
111
+ id: activeDeptChat,
112
+ name: dept?.name || '',
113
+ members: dept?.members || [],
114
+ leader: dept?.leader,
115
+ });
116
+ }
117
+ });
118
+ };
119
+ loadDetail();
120
+ deptChatPollRef.current = setInterval(loadDetail, 3000);
121
+ } else {
122
+ setDeptChatDetail(null);
123
+ }
124
+ return () => {
125
+ if (deptChatPollRef.current) clearInterval(deptChatPollRef.current);
126
+ };
127
+ }, [activeDeptChat]);
128
+
129
+ // Agent 1-on-1 chat polling(和群聊一样,3秒轮询)
130
+ useEffect(() => {
131
+ if (agentChatPollRef.current) clearInterval(agentChatPollRef.current);
132
+ if (activeChat?.type === 'agent-chat' && activeChat.agentId) {
133
+ const agentId = activeChat.agentId;
134
+ agentChatPollRef.current = setInterval(() => {
135
+ // 只在不是正在发送消息时才轮询,避免和发送逻辑冲突
136
+ if (!sending) {
137
+ fetchAgentChatHistory(agentId).then(msgs => {
138
+ // 只有当前仍在同一会话时才更新
139
+ if (activeChatRef.current?.agentId === agentId && msgs.length > 0) {
140
+ setAgentChatMessages(prev => {
141
+ // 只有消息数量变化时才更新,避免无谓的 re-render
142
+ if (prev.length !== msgs.length) {
143
+ return msgs;
144
+ }
145
+ return prev;
146
+ });
147
+ }
148
+ });
149
+ }
150
+ }, 3000);
151
+ }
152
+ return () => {
153
+ if (agentChatPollRef.current) clearInterval(agentChatPollRef.current);
154
+ };
155
+ }, [activeChat?.type === 'agent-chat' ? activeChat?.agentId : null, sending]);
156
+
157
+ // Sync secretary chat history
158
+ useEffect(() => {
159
+ if (company?.chatHistory) {
160
+ setSecretaryHistory(company.chatHistory);
161
+ }
162
+ }, [company?.chatHistory]);
163
+
164
+ // Track whether we should auto-scroll (only on initial load / conversation switch)
165
+ const shouldAutoScrollRef = useRef(true);
166
+
167
+ // Auto scroll to bottom: ONLY on conversation switch or initial load
168
+ useEffect(() => {
169
+ shouldAutoScrollRef.current = true;
170
+ }, [activeChat]);
171
+
172
+ useEffect(() => {
173
+ if (shouldAutoScrollRef.current) {
174
+ setTimeout(() => {
175
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
176
+ }, 50);
177
+ shouldAutoScrollRef.current = false;
178
+ }
179
+ }, [activeChat, secretaryHistory, reqChatDetail, agentChatMessages]);
180
+
181
+ // Close ChatPanel when secretary is selected to avoid conflict
182
+ useEffect(() => {
183
+ if (activeChat?.type === 'secretary' && chatOpen) {
184
+ setChatOpen(false);
185
+ }
186
+ }, [activeChat]);
187
+
188
+ // Build conversation list: sorted by latest message time
189
+ const allConversations = buildConversations(secretary, secretaryHistory, requirements, t, agentChatSessions, company?.departments || []);
190
+
191
+ // Filter conversations by category
192
+ const conversations = allConversations.filter(conv => {
193
+ if (chatFilter === 'all') return true;
194
+ if (chatFilter === 'group') return conv.type === 'requirement' || conv.type === 'department';
195
+ if (chatFilter === 'private') return conv.type === 'secretary' || conv.type === 'agent-chat';
196
+ if (chatFilter === 'important') return conv.type === 'secretary' || conv.type === 'requirement';
197
+ return true;
198
+ });
199
+
200
+ // Send message
201
+ const handleSend = async () => {
202
+ if (!inputText.trim() || sending) return;
203
+ const text = inputText.trim();
204
+ setInputText('');
205
+ setSending(true);
206
+ // 记录当前发送目标,防止切换聊天后typing状态串台
207
+ const currentTargetId = activeChat?.type === 'secretary' ? 'secretary'
208
+ : activeChat?.type === 'agent-chat' ? activeChat.agentId
209
+ : activeChat?.type === 'requirement' ? activeChat.id : null;
210
+ setSendingTargetId(currentTargetId);
211
+
212
+ // 用户发消息后立即滚动到底部
213
+ setTimeout(() => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 50);
214
+
215
+ try {
216
+ if (activeChat?.type === 'secretary') {
217
+ // Optimistic update for boss message
218
+ setSecretaryHistory(prev => [...prev, {
219
+ role: 'boss', content: text, time: new Date().toISOString(),
220
+ }]);
221
+ await chatWithSecretary(text);
222
+ // Secretary replies sync via useEffect
223
+ } else if (activeChat?.type === 'requirement') {
224
+ // 群聊发送由 GroupChatView 组件内部管理,此处不再处理
225
+ } else if (activeChat?.type === 'agent-chat') {
226
+ // Agent 1-on-1 chat
227
+ const targetAgentId = activeChat.agentId; // 捕获当前发送目标
228
+ const optimisticMsg = { role: 'boss', content: text, time: new Date().toISOString() };
229
+ setAgentChatMessages(prev => [...prev, optimisticMsg]);
230
+ try {
231
+ const data = await chatWithAgent(targetAgentId, text);
232
+ // 只有当前仍在同一会话时才更新消息
233
+ if (activeChatRef.current?.agentId === targetAgentId) {
234
+ if (data.chatHistory) {
235
+ setAgentChatMessages(data.chatHistory);
236
+ } else if (data.reply) {
237
+ setAgentChatMessages(prev => [...prev, {
238
+ role: 'agent', content: data.reply.reply, time: data.reply.time,
239
+ }]);
240
+ }
241
+ }
242
+ } catch (err) {
243
+ if (activeChatRef.current?.agentId === targetAgentId) {
244
+ setAgentChatMessages(prev => [...prev, {
245
+ role: 'agent', content: `😵 ${t('agentChat.error')}: ${err.message}`, time: new Date().toISOString(),
246
+ }]);
247
+ }
248
+ }
249
+ }
250
+ } catch (e) {
251
+ if (activeChat?.type === 'secretary') {
252
+ setSecretaryHistory(prev => [...prev, {
253
+ role: 'secretary',
254
+ content: `${t('chat.errorPrefix')}${e.message}`,
255
+ time: new Date().toISOString(),
256
+ }]);
257
+ }
258
+ }
259
+ setSending(false);
260
+ setSendingTargetId(null);
261
+ };
262
+
263
+ const handleKeyDown = (e) => {
264
+ if (e.key === 'Enter' && !e.shiftKey) {
265
+ e.preventDefault();
266
+ handleSend();
267
+ }
268
+ };
269
+
270
+ const handleSelectConversation = (conv) => {
271
+ if (conv.type === 'secretary') {
272
+ setActiveChat({ type: 'secretary' });
273
+ setActiveReqChat(null);
274
+ setActiveDeptChat(null);
275
+ } else if (conv.type === 'requirement') {
276
+ setActiveChat({ type: 'requirement', id: conv.requirementId });
277
+ setActiveReqChat(conv.requirementId);
278
+ setActiveDeptChat(null);
279
+ } else if (conv.type === 'department') {
280
+ setActiveChat({ type: 'department', id: conv.departmentId, name: conv.name });
281
+ setActiveDeptChat(conv.departmentId);
282
+ setActiveReqChat(null);
283
+ } else if (conv.type === 'agent-chat') {
284
+ setActiveChat({
285
+ type: 'agent-chat',
286
+ agentId: conv.agentId,
287
+ agentName: conv.name,
288
+ agentAvatar: conv.avatar,
289
+ agentRole: conv.role,
290
+ agentSignature: conv.agentSignature,
291
+ agentDepartment: conv.departmentName,
292
+ });
293
+ setActiveReqChat(null);
294
+ setActiveDeptChat(null);
295
+ // 标记为已读(持久化到后端)
296
+ markAgentChatRead(conv.agentId);
297
+ // 加载聊天历史
298
+ setAgentChatLoading(true);
299
+ fetchAgentChatHistory(conv.agentId).then(msgs => {
300
+ setAgentChatMessages(msgs);
301
+ setAgentChatLoading(false);
302
+ }).catch(() => setAgentChatLoading(false));
303
+ }
304
+ setInputText('');
305
+ };
306
+
307
+ // 同步 activeChatRef
308
+ useEffect(() => {
309
+ activeChatRef.current = activeChat;
310
+ }, [activeChat]);
311
+
312
+ return (
313
+ <div className="flex h-full animate-fade-in">
314
+ {/* Left: conversation list */}
315
+ <div className="w-80 shrink-0 border-r border-[var(--border)] flex flex-col bg-[#0d0d0d]">
316
+ {/* Search bar */}
317
+ <div className="border-b border-white/[0.06]">
318
+ <div className="flex items-center justify-between px-3 py-2.5">
319
+ <h2 className="text-sm font-semibold leading-none">{t('mailbox.title')}</h2> </div>
320
+ {/* Category tabs */}
321
+ <div className="flex px-3 pb-2 gap-1">
322
+ {[
323
+ { key: 'all', label: t('mailbox.tabs.all') },
324
+ { key: 'group', label: t('mailbox.tabs.group') },
325
+ { key: 'private', label: t('mailbox.tabs.private') },
326
+ { key: 'important', label: t('mailbox.tabs.important') },
327
+ ].map(tab => (
328
+ <button
329
+ key={tab.key}
330
+ onClick={() => setChatFilter(tab.key)}
331
+ className={`text-[11px] px-2.5 py-1 rounded-full transition-all ${
332
+ chatFilter === tab.key
333
+ ? 'bg-[var(--accent)] text-white font-medium'
334
+ : 'bg-white/5 text-[var(--muted)] hover:bg-white/10 hover:text-white'
335
+ }`}
336
+ >
337
+ {tab.label}
338
+ </button>
339
+ ))}
340
+ </div>
341
+ </div>
342
+
343
+ {/* Conversation list */}
344
+ <div className="flex-1 overflow-auto">
345
+ {conversations.map((conv) => {
346
+ const isActive = activeChat?.type === conv.type &&
347
+ (conv.type === 'secretary' || (conv.type === 'requirement' && activeChat?.id === conv.requirementId) || (conv.type === 'department' && activeChat?.id === conv.departmentId) || (conv.type === 'agent-chat' && activeChat?.agentId === conv.agentId));
348
+
349
+ return (
350
+ <div
351
+ key={conv.key}
352
+ onClick={() => handleSelectConversation(conv)}
353
+ className={`flex items-center gap-3 px-3 py-3 cursor-pointer transition-all border-b border-white/[0.04] ${
354
+ isActive
355
+ ? 'bg-[var(--accent)]/10 border-l-2 border-l-[var(--accent)]'
356
+ : 'hover:bg-white/5 border-l-2 border-l-transparent'
357
+ }`}
358
+ >
359
+ {/* Avatar */}
360
+ <div className="relative shrink-0">
361
+ {(conv.isRequirement || conv.isDepartment) && conv.memberAvatars?.length > 0 ? (
362
+ <div className="w-10 h-10 rounded-xl bg-[var(--border)] overflow-hidden grid gap-[1px] p-[1px]" style={{
363
+ gridTemplateColumns: `repeat(${conv.memberAvatars.length <= 4 ? 2 : 3}, 1fr)`,
364
+ }}>
365
+ {conv.memberAvatars.slice(0, conv.memberAvatars.length <= 4 ? 4 : 9).map((av, i) => (
366
+ <img key={i} src={av} alt="" className="w-full h-full object-cover rounded-sm bg-[var(--card)]" />
367
+ ))}
368
+ </div>
369
+ ) : conv.isRequirement ? (
370
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500/80 to-cyan-600/80 flex items-center justify-center text-sm font-bold">
371
+ 📋
372
+ </div>
373
+ ) : conv.isDepartment ? (
374
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-500/80 to-teal-600/80 flex items-center justify-center text-lg">
375
+ 🏢
376
+ </div>
377
+ ) : conv.avatar ? (
378
+ <img
379
+ src={conv.avatar}
380
+ alt={conv.name}
381
+ className="w-10 h-10 rounded-full bg-[var(--border)]"
382
+ />
383
+ ) : (
384
+ <div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-600 to-blue-700 flex items-center justify-center text-sm">
385
+ {(conv.name || '?').charAt(0)}
386
+ </div>
387
+ )}
388
+ {conv.type === 'secretary' && (
389
+ <span className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-green-500 rounded-full border-2 border-[#0d0d0d]" />
390
+ )}
391
+ {conv.unread && conv.type !== 'secretary' && (
392
+ <span className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-red-500 rounded-full border-2 border-[#0d0d0d]" />
393
+ )}
394
+ </div>
395
+
396
+ {/* Info */}
397
+ <div className="flex-1 min-w-0">
398
+ <div className="flex items-center justify-between">
399
+ <span className={`text-sm truncate ${conv.unread ? 'font-semibold' : 'font-medium'}`}>
400
+ {conv.name}
401
+ {conv.pinned && <span className="ml-1 text-[10px] text-yellow-400">📌</span>}
402
+ </span>
403
+ <span className="text-[10px] text-[var(--muted)] shrink-0 ml-2">
404
+ {formatTime(conv.lastTime, t)}
405
+ </span>
406
+ </div>
407
+ <div className="flex items-center gap-1 mt-0.5">
408
+ {conv.role && (
409
+ <span className="text-[10px] text-[var(--muted)] bg-white/5 px-1 py-0.5 rounded shrink-0">{conv.role}</span>
410
+ )}
411
+ <span className={`text-xs truncate ${conv.unread ? 'text-[var(--foreground)]' : 'text-[var(--muted)]'}`}>
412
+ {conv.lastMessage}
413
+ </span>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ );
418
+ })}
419
+
420
+ {conversations.length <= 1 && (
421
+ <div className="text-center py-8 text-[var(--muted)]">
422
+ <div className="text-3xl mb-2">🤫</div>
423
+ <p className="text-xs">{t('mailbox.noMessages')}</p>
424
+ <p className="text-[10px] mt-1">{t('mailbox.noMessagesHint')}</p>
425
+ </div>
426
+ )}
427
+ </div>
428
+ </div>
429
+
430
+ {/* Right: chat area */}
431
+ <div className="flex-1 flex flex-col bg-[var(--background)] min-w-0 overflow-hidden">
432
+ {!activeChat ? (
433
+ /* No conversation selected */
434
+ <div className="flex-1 flex items-center justify-center text-[var(--muted)]">
435
+ <div className="text-center">
436
+ <div className="text-6xl mb-4">💬</div>
437
+ <p className="text-lg font-medium">{t('mailbox.selectChat')}</p>
438
+ <p className="text-sm mt-1">{t('mailbox.selectChatHint')}</p>
439
+ </div>
440
+ </div>
441
+ ) : activeChat.type === 'secretary' ? (
442
+ /* Secretary chat */
443
+ <>
444
+ {/* Secretary chat header */}
445
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] bg-[var(--card)]">
446
+ <img
447
+ src={secretary?.avatar || getAvatarUrl('secretary')}
448
+ alt="secretary"
449
+ className="w-9 h-9 rounded-full bg-[var(--border)]"
450
+ />
451
+ <div className="flex-1 min-w-0">
452
+ <div className="text-sm font-semibold flex items-center gap-2">
453
+ {secretary?.name || t('setup.defaultSecretary')}
454
+ <span className="w-2 h-2 bg-green-500 rounded-full" />
455
+ <span className="text-[10px] text-[var(--muted)] font-normal">{t('mailbox.personalSecretary')}</span>
456
+ </div>
457
+ {secretary?.signature && (
458
+ <div className="text-[10px] text-[var(--muted)] italic truncate" title={secretary.signature}>"{secretary.signature}"</div>
459
+ )}
460
+ </div>
461
+ </div>
462
+
463
+ {/* Secretary messages area */}
464
+ <div className="flex-1 overflow-auto px-4 py-3 space-y-3">
465
+ {secretaryHistory.length === 0 && (
466
+ <div className="text-center py-12">
467
+ <div className="text-4xl mb-2">💬</div>
468
+ <p className="text-sm text-[var(--muted)]">{t('mailbox.chatHint', { name: secretary?.name || t('setup.defaultSecretary') })}</p>
469
+ <div className="mt-3 space-y-1 max-w-xs mx-auto">
470
+ {t('chat.suggestions').map((q, i) => (
471
+ <button
472
+ key={i}
473
+ className="block w-full text-xs text-left px-3 py-2 rounded-lg bg-white/5 hover:bg-white/10 text-[var(--muted)] hover:text-white transition-all"
474
+ onClick={() => setInputText(q)}
475
+ >
476
+ 💡 {q}
477
+ </button>
478
+ ))}
479
+ </div>
480
+ </div>
481
+ )}
482
+
483
+ {secretaryHistory.map((msg, i) => (
484
+ <MessageBubble
485
+ key={i}
486
+ isMe={msg.role === 'boss'}
487
+ avatar={msg.role === 'secretary' ? (secretary?.avatar || getAvatarUrl('secretary')) : null}
488
+ name={msg.role === 'boss' ? company.boss : (secretary?.name || t('setup.defaultSecretary'))}
489
+ content={msg.content}
490
+ time={msg.time}
491
+ action={msg.action}
492
+ agentId={null}
493
+ onClickAvatar={null}
494
+ bossAvatar={company?.bossAvatar}
495
+ />
496
+ ))}
497
+
498
+ {sending && sendingTargetId === 'secretary' && (
499
+ <div className="flex gap-2">
500
+ <img
501
+ src={secretary?.avatar || getAvatarUrl('secretary')}
502
+ alt="secretary"
503
+ className="w-8 h-8 rounded-full bg-[var(--border)] shrink-0"
504
+ />
505
+ <div className="bg-[var(--card)] border border-[var(--border)] rounded-2xl rounded-bl-sm px-3 py-2 text-sm">
506
+ <span className="animate-pulse text-[var(--muted)]">{t('chat.typing')}</span>
507
+ </div>
508
+ </div>
509
+ )}
510
+
511
+ <div ref={messagesEndRef} />
512
+ </div>
513
+
514
+ {/* Task status panel */}
515
+ <TaskStatusPanel />
516
+
517
+ {/* Secretary input box */}
518
+ <ChatInput
519
+ value={inputText}
520
+ onChange={setInputText}
521
+ onSend={handleSend}
522
+ onKeyDown={handleKeyDown}
523
+ sending={sending}
524
+ placeholder={t('chat.inputPlaceholder', { name: secretary?.name || t('setup.defaultSecretary') })}
525
+ />
526
+ </>
527
+ ) : activeChat?.type === 'agent-chat' ? (
528
+ /* Agent 1-on-1 private chat */
529
+ <>
530
+ {/* Agent chat header */}
531
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] bg-[var(--card)]">
532
+ {activeChat.agentAvatar ? (
533
+ <img
534
+ src={activeChat.agentAvatar}
535
+ alt={activeChat.agentName}
536
+ className="w-9 h-9 rounded-full bg-[var(--border)] cursor-pointer hover:ring-2 hover:ring-[var(--accent)] transition-all"
537
+ onClick={() => setSelectedAgent(activeChat.agentId)}
538
+ title={t('mailbox.viewAgentDetail')}
539
+ />
540
+ ) : (
541
+ <div className="w-9 h-9 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-base shrink-0">💬</div>
542
+ )}
543
+ <div className="flex-1 min-w-0">
544
+ <div className="text-sm font-semibold flex items-center gap-2">
545
+ {activeChat.agentName}
546
+ <span className="w-2 h-2 bg-green-500 rounded-full" />
547
+ {(activeChat.agentRole || activeChat.agentDepartment) && (
548
+ <span className="text-[10px] text-[var(--muted)] font-normal">
549
+ {activeChat.agentRole}{activeChat.agentDepartment ? ` · ${activeChat.agentDepartment}` : ''}
550
+ </span>
551
+ )}
552
+ </div>
553
+ {activeChat.agentSignature && (
554
+ <div className="text-[10px] text-[var(--muted)] italic truncate" title={activeChat.agentSignature}>"{activeChat.agentSignature}"</div>
555
+ )}
556
+ </div>
557
+ </div>
558
+
559
+ {/* Agent chat messages */}
560
+ <div className="flex-1 overflow-auto px-4 py-3 space-y-3">
561
+ {agentChatLoading ? (
562
+ <div className="text-center text-[var(--muted)] py-8">
563
+ <div className="text-2xl animate-pulse">💬</div>
564
+ <p className="text-xs mt-2">{t('common.loading')}</p>
565
+ </div>
566
+ ) : agentChatMessages.length === 0 ? (
567
+ <div className="text-center text-[var(--muted)] py-8">
568
+ <div className="text-3xl">👋</div>
569
+ <p className="text-sm mt-2">{t('agentChat.empty', { name: activeChat.agentName })}</p>
570
+ </div>
571
+ ) : (
572
+ agentChatMessages.map((msg, i) => (
573
+ <MessageBubble
574
+ key={i}
575
+ isMe={msg.role === 'boss'}
576
+ avatar={msg.role !== 'boss' ? activeChat.agentAvatar : null}
577
+ name={msg.role === 'boss' ? company.boss : activeChat.agentName}
578
+ content={msg.content}
579
+ time={msg.time}
580
+ agentId={msg.role !== 'boss' ? activeChat.agentId : null}
581
+ onClickAvatar={setSelectedAgent}
582
+ bossAvatar={company?.bossAvatar}
583
+ />
584
+ ))
585
+ )}
586
+
587
+ {sending && sendingTargetId === activeChat.agentId && (
588
+ <div className="flex gap-2">
589
+ {activeChat.agentAvatar ? (
590
+ <CachedAvatar src={activeChat.agentAvatar} alt="" className="w-8 h-8 rounded-full bg-[var(--border)] shrink-0" />
591
+ ) : (
592
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-xs shrink-0">💬</div>
593
+ )}
594
+ <div className="bg-[var(--card)] border border-[var(--border)] rounded-2xl rounded-bl-sm px-3 py-2 text-sm">
595
+ <span className="animate-pulse text-[var(--muted)]">{t('agentChat.typing')}</span>
596
+ </div>
597
+ </div>
598
+ )}
599
+
600
+ <div ref={messagesEndRef} />
601
+ </div>
602
+
603
+ {/* Task status panel */}
604
+ <TaskStatusPanel />
605
+
606
+ {/* Agent chat input */}
607
+ <ChatInput
608
+ value={inputText}
609
+ onChange={setInputText}
610
+ onSend={handleSend}
611
+ onKeyDown={handleKeyDown}
612
+ sending={sending}
613
+ placeholder={t('agentChat.inputPlaceholder', { name: activeChat.agentName })}
614
+ />
615
+ </>
616
+ ) : activeChat?.type === 'requirement' && reqChatDetail ? (
617
+ /* Requirement group chat */
618
+ <>
619
+ {/* Requirement group chat header */}
620
+ <div className="px-4 py-3 border-b border-white/[0.06] bg-[var(--card)]">
621
+ <div className="flex items-center justify-between">
622
+ <div className="flex items-center gap-3 flex-1 min-w-0">
623
+ {(() => {
624
+ const reqDept = (company?.departments || []).find(d => d.id === reqChatDetail.departmentId);
625
+ const avatars = reqDept ? (reqDept.members || []).slice(0, 9).map(m => m.avatar).filter(Boolean) : [];
626
+ return avatars.length > 0 ? (
627
+ <div className="w-9 h-9 rounded-xl bg-[var(--border)] overflow-hidden grid gap-[1px] p-[1px] shrink-0" style={{
628
+ gridTemplateColumns: `repeat(${avatars.length <= 4 ? 2 : 3}, 1fr)`,
629
+ }}>
630
+ {avatars.map((av, i) => (
631
+ <img key={i} src={av} alt="" className="w-full h-full object-cover rounded-sm bg-[var(--card)]" />
632
+ ))}
633
+ </div>
634
+ ) : (
635
+ <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500/80 to-cyan-600/80 flex items-center justify-center text-sm font-bold shrink-0">
636
+ 📋
637
+ </div>
638
+ );
639
+ })()}
640
+ <div className="flex-1 min-w-0">
641
+ <div className="text-sm font-semibold flex items-center gap-2">
642
+ {reqChatDetail.title}
643
+ {(() => {
644
+ const stCfg = {
645
+ pending: { label: t('requirements.status.pending'), color: 'text-gray-400', bg: 'bg-gray-900/30' },
646
+ planning: { label: t('requirements.status.planning'), color: 'text-blue-400', bg: 'bg-blue-900/30' },
647
+ in_progress: { label: t('requirements.status.in_progress'), color: 'text-yellow-400', bg: 'bg-yellow-900/30' },
648
+ pending_approval: { label: t('requirements.status.pending_approval'), color: 'text-orange-400', bg: 'bg-orange-900/30' },
649
+ completed: { label: t('requirements.stats.completed'), color: 'text-green-400', bg: 'bg-green-900/30' },
650
+ failed: { label: t('requirements.status.failed'), color: 'text-red-400', bg: 'bg-red-900/30' },
651
+ };
652
+ const s = stCfg[reqChatDetail.status] || stCfg.pending;
653
+ return <span className={`text-[10px] px-1.5 py-0.5 rounded ${s.bg} ${s.color}`}>{s.label}</span>;
654
+ })()}
655
+ </div>
656
+ <div className="text-[10px] text-[var(--muted)] truncate flex items-center gap-2">
657
+ <span>{t('mailbox.groupChatCount', { dept: reqChatDetail.departmentName, n: reqChatDetail.groupChat?.length || 0 })}</span>
658
+ {(() => {
659
+ // 从群聊消息中提取唯一参与者
660
+ const memberMap = {};
661
+ (reqChatDetail.groupChat || []).forEach(m => {
662
+ if (m.from?.id && m.from.id !== 'system') {
663
+ memberMap[m.from.id] = m.from;
664
+ }
665
+ });
666
+ const memberCount = Object.keys(memberMap).length;
667
+ if (memberCount === 0) return null;
668
+ return (
669
+ <button
670
+ onClick={(e) => { e.stopPropagation(); setShowGroupMembers(true); }}
671
+ className="text-[10px] text-blue-400 hover:text-blue-300 bg-blue-500/10 hover:bg-blue-500/20 px-1.5 py-0.5 rounded transition-colors"
672
+ >
673
+ {t('mailbox.membersCount', { n: memberCount })}
674
+ </button>
675
+ );
676
+ })()}
677
+ </div>
678
+ </div>
679
+ </div>
680
+ <button
681
+ onClick={() => navigateToRequirement(activeReqChat)}
682
+ className="text-xs text-[var(--accent)] hover:text-white bg-[var(--accent)]/10 hover:bg-[var(--accent)]/20 px-3 py-1.5 rounded-lg transition-all flex items-center gap-1.5 shrink-0"
683
+ >{t('mailbox.viewRequirement')}</button>
684
+ </div>
685
+ </div>
686
+
687
+ {/* 群聊成员弹窗 */}
688
+ {showGroupMembers && (() => {
689
+ const memberMap = {};
690
+ (reqChatDetail.groupChat || []).forEach(m => {
691
+ if (m.from?.id && m.from.id !== 'system') {
692
+ memberMap[m.from.id] = m.from;
693
+ }
694
+ });
695
+ const members = Object.values(memberMap);
696
+ return (
697
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[60] !m-0" onClick={() => setShowGroupMembers(false)}>
698
+ <div className="bg-[var(--card)] border border-[var(--border)] rounded-xl max-w-sm w-full mx-4 overflow-hidden shadow-2xl" onClick={e => e.stopPropagation()}>
699
+ <div className="px-4 py-3 border-b border-[var(--border)] flex items-center justify-between">
700
+ <span className="text-sm font-semibold">{t('mailbox.groupMembers', { n: members.length })}</span>
701
+ <button onClick={() => setShowGroupMembers(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
702
+ </div>
703
+ <div className="max-h-[50vh] overflow-auto py-2">
704
+ {members.map(m => (
705
+ <div
706
+ key={m.id}
707
+ className="flex items-center gap-3 px-4 py-2 hover:bg-white/5 cursor-pointer transition-colors"
708
+ onClick={() => { setShowGroupMembers(false); setSelectedAgent(m.id); }}
709
+ >
710
+ {m.avatar ? (
711
+ <CachedAvatar src={m.avatar} alt="" className="w-8 h-8 rounded-full bg-[var(--border)]" />
712
+ ) : (
713
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-indigo-600 to-blue-700 flex items-center justify-center text-xs">🤖</div>
714
+ )}
715
+ <div className="flex-1 min-w-0">
716
+ <div className="text-sm font-medium truncate">{m.name}</div>
717
+ {m.role && <div className="text-[10px] text-[var(--muted)]">{m.role}</div>}
718
+ </div>
719
+ </div>
720
+ ))}
721
+ </div>
722
+ </div>
723
+ </div>
724
+ );
725
+ })()}
726
+
727
+ {/* Requirement group chat - using shared GroupChatView */}
728
+ <GroupChatView
729
+ groupChat={reqChatDetail.groupChat || []}
730
+ agentMap={agentMap}
731
+ bossAvatar={company?.bossAvatar}
732
+ bossName={company?.boss || 'Boss'}
733
+ requirementId={activeReqChat}
734
+ onSendMessage={sendGroupChatMessage}
735
+ fetchDetail={async (reqId) => {
736
+ const detail = await fetchRequirementDetail(reqId);
737
+ if (detail) setReqChatDetail(detail);
738
+ return detail;
739
+ }}
740
+ leaderInfo={(() => {
741
+ const leaderMsg = (reqChatDetail.groupChat || []).find(m => m.from?.id !== 'boss' && m.from?.role !== 'system' && m.type !== 'system');
742
+ return leaderMsg ? { name: leaderMsg.from?.name, avatar: leaderMsg.from?.avatar } : null;
743
+ })()}
744
+ chatEndRef={messagesEndRef}
745
+ embedded
746
+ />
747
+ </>
748
+ ) : activeChat?.type === 'department' && deptChatDetail ? (
749
+ /* Department group chat */
750
+ <>
751
+ {/* Department group chat header */}
752
+ <div className="px-4 py-3 border-b border-white/[0.06] bg-[var(--card)]">
753
+ <div className="flex items-center gap-3 flex-1 min-w-0">
754
+ {(deptChatDetail.members || []).length > 0 ? (
755
+ <div className="w-9 h-9 rounded-xl bg-[var(--border)] overflow-hidden grid gap-[1px] p-[1px] shrink-0" style={{
756
+ gridTemplateColumns: `repeat(${(deptChatDetail.members || []).length <= 4 ? 2 : 3}, 1fr)`,
757
+ }}>
758
+ {(deptChatDetail.members || []).slice(0, 9).map((m, i) => (
759
+ <img key={i} src={m.avatar} alt="" className="w-full h-full object-cover rounded-sm bg-[var(--card)]" />
760
+ ))}
761
+ </div>
762
+ ) : (
763
+ <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-emerald-500/80 to-teal-600/80 flex items-center justify-center text-sm font-bold shrink-0">
764
+ 🏢
765
+ </div>
766
+ )}
767
+ <div className="flex-1 min-w-0">
768
+ <div className="text-sm font-semibold">{t('mailbox.deptGroup', { name: deptChatDetail.name })}</div>
769
+ <div className="text-[10px] text-[var(--muted)] truncate">
770
+ {t('mailbox.deptGroupInfo', { count: (deptChatDetail.members || []).length, msgs: (deptChatDetail.groupChat || []).filter(m => m.visibility !== 'flow').length })}
771
+ </div>
772
+ </div>
773
+ </div>
774
+ </div>
775
+
776
+ {/* Department group chat - using shared GroupChatView */}
777
+ <GroupChatView
778
+ groupChat={deptChatDetail.groupChat || []}
779
+ agentMap={agentMap}
780
+ bossAvatar={company?.bossAvatar}
781
+ bossName={company?.boss || 'Boss'}
782
+ requirementId={`dept-${activeDeptChat}`}
783
+ onSendMessage={async (_id, msg) => {
784
+ await sendDeptGroupChatMessage(activeDeptChat, msg);
785
+ // 刷新部门群聊
786
+ const data = await fetchDeptGroupChat(activeDeptChat);
787
+ if (data) {
788
+ const dept = (company?.departments || []).find(d => d.id === activeDeptChat);
789
+ setDeptChatDetail({ ...data, id: activeDeptChat, name: dept?.name || '', members: dept?.members || [], leader: dept?.leader });
790
+ }
791
+ }}
792
+ fetchDetail={async () => {
793
+ const data = await fetchDeptGroupChat(activeDeptChat);
794
+ if (data) {
795
+ const dept = (company?.departments || []).find(d => d.id === activeDeptChat);
796
+ setDeptChatDetail({ ...data, id: activeDeptChat, name: dept?.name || '', members: dept?.members || [], leader: dept?.leader });
797
+ }
798
+ }}
799
+ leaderInfo={null}
800
+ chatEndRef={messagesEndRef}
801
+ embedded
802
+ />
803
+ </>
804
+ ) : (
805
+ <div className="flex-1 flex items-center justify-center text-[var(--muted)]">
806
+ <p>{t('mailbox.chatNotExist')}</p>
807
+ </div>
808
+ )}
809
+ </div>
810
+
811
+ {/* Employee detail modal */}
812
+ {selectedAgent && (
813
+ <AgentDetailModal agentId={selectedAgent} onClose={() => setSelectedAgent(null)} />
814
+ )}
815
+ </div>
816
+ );
817
+ }
818
+
819
+ // ============ Utility functions ============
820
+
821
+ /**
822
+ * Build conversation list: secretary pinned on top + employees sorted by latest message desc
823
+ */
824
+ function buildConversations(secretary, secretaryHistory, requirements = [], t = (k) => k, agentChatSessions = [], departments = []) {
825
+ const convs = [];
826
+
827
+ // Pin secretary on top
828
+ const lastSecMsg = secretaryHistory.length > 0 ? secretaryHistory[secretaryHistory.length - 1] : null;
829
+ convs.push({
830
+ key: 'secretary',
831
+ type: 'secretary',
832
+ name: secretary?.name || t('setup.defaultSecretary'),
833
+ avatar: secretary?.avatar || getAvatarUrl('secretary'),
834
+ role: t('mailbox.personalSecretary'),
835
+ lastMessage: lastSecMsg ? lastSecMsg.content?.slice(0, 40) : t('mailbox.clickToChat'),
836
+ lastTime: lastSecMsg?.time || null,
837
+ unread: false,
838
+ pinned: true,
839
+ });
840
+
841
+ // Department group chat sessions (every active department gets one)
842
+ for (const dept of departments) {
843
+ if (dept.status === 'disbanded') continue;
844
+ const deptChat = dept.groupChat || [];
845
+ const lastMsg = deptChat.length > 0 ? deptChat[deptChat.length - 1] : null;
846
+ const visibleMsgs = deptChat.filter(m => m.visibility !== 'flow');
847
+ convs.push({
848
+ key: `dept-${dept.id}`,
849
+ type: 'department',
850
+ departmentId: dept.id,
851
+ name: `🏢 ${dept.name}`,
852
+ avatar: null,
853
+ memberAvatars: (dept.members || []).slice(0, 9).map(m => m.avatar).filter(Boolean),
854
+ role: t('mailbox.membersCount', { n: (dept.members || []).length }),
855
+ lastMessage: lastMsg ? `${lastMsg.from?.name || ''}: ${(lastMsg.content || '').slice(0, 30)}` : t('mailbox.deptGroupChat'),
856
+ lastTime: lastMsg?.time || dept.createdAt,
857
+ unread: visibleMsgs.length > 0,
858
+ pinned: true,
859
+ isDepartment: true,
860
+ });
861
+ }
862
+
863
+ // Requirement group chat sessions (pinned below secretary)
864
+ const activeReqs = requirements.filter(r =>
865
+ r.status === 'in_progress' || r.status === 'planning' || r.status === 'pending_approval' || r.status === 'completed' || r.status === 'failed'
866
+ );
867
+ for (const req of activeReqs) {
868
+ const statusEmoji = {
869
+ planning: '📝',
870
+ in_progress: '⚙️',
871
+ pending_approval: '🔍',
872
+ completed: '✅',
873
+ failed: '❌',
874
+ };
875
+ const reqDept = departments.find(d => d.id === req.departmentId);
876
+ const reqMemberAvatars = reqDept ? (reqDept.members || []).slice(0, 9).map(m => m.avatar).filter(Boolean) : [];
877
+ convs.push({
878
+ key: `req-${req.id}`,
879
+ type: 'requirement',
880
+ requirementId: req.id,
881
+ name: `📋 ${req.title}`,
882
+ avatar: null,
883
+ memberAvatars: reqMemberAvatars,
884
+ role: req.departmentName,
885
+ lastMessage: `${statusEmoji[req.status] || '⏳'} ${req.chatCount || 0} group msgs · ${req.workflow?.completedCount || 0}/${req.workflow?.nodeCount || 0} tasks`,
886
+ lastTime: req.createdAt,
887
+ unread: req.status === 'in_progress',
888
+ pinned: true,
889
+ isRequirement: true,
890
+ });
891
+ }
892
+
893
+ // Boss-Agent 1-on-1 private chat sessions (from chatStore)
894
+ const agentChatConvs = [];
895
+ for (const session of agentChatSessions) {
896
+ const lastMsgPreview = session.lastMessageRole === 'boss'
897
+ ? `${t('mailbox.you')}${session.lastMessage || '...'}`
898
+ : session.lastMessage || '...';
899
+ agentChatConvs.push({
900
+ key: `agent-chat-${session.agentId}`,
901
+ type: 'agent-chat',
902
+ agentId: session.agentId,
903
+ name: session.agentName,
904
+ avatar: session.agentAvatar,
905
+ role: session.agentRole,
906
+ agentSignature: session.agentSignature,
907
+ departmentName: session.departmentName,
908
+ lastMessage: lastMsgPreview,
909
+ lastTime: session.lastTime,
910
+ unread: !!session.unread,
911
+ pinned: false,
912
+ totalMessages: session.totalMessages,
913
+ });
914
+ }
915
+
916
+ // 按时间排序
917
+ agentChatConvs.sort((a, b) => {
918
+ if (!a.lastTime) return 1;
919
+ if (!b.lastTime) return -1;
920
+ return new Date(b.lastTime) - new Date(a.lastTime);
921
+ });
922
+
923
+ return [...convs, ...agentChatConvs];
924
+ }
925
+
926
+