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