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,481 @@
1
+
2
+ 'use client';
3
+
4
+ import { useState, useEffect, useRef } from 'react';
5
+ import { useStore } from '@/lib/client-store';
6
+ import { useI18n } from '@/lib/i18n';
7
+ import AgentDetailModal from './AgentDetailModal';
8
+ import ReactMarkdown from 'react-markdown';
9
+ import remarkGfm from 'remark-gfm';
10
+ import CachedAvatar from './CachedAvatar';
11
+
12
+ // 复用 Mailbox 的 Markdown 渲染组件映射
13
+ const chatMarkdownComponents = {
14
+ p: ({ children }) => <p className="mb-1 last:mb-0">{children}</p>,
15
+ ul: ({ children }) => <ul className="list-disc list-inside mb-1 space-y-0.5">{children}</ul>,
16
+ ol: ({ children }) => <ol className="list-decimal list-inside mb-1 space-y-0.5">{children}</ol>,
17
+ li: ({ children }) => <li className="text-sm">{children}</li>,
18
+ strong: ({ children }) => <strong className="font-bold">{children}</strong>,
19
+ em: ({ children }) => <em className="italic">{children}</em>,
20
+ code: ({ inline, children }) => {
21
+ if (inline) {
22
+ return <code className="bg-white/10 px-1 py-0.5 rounded text-xs font-mono">{children}</code>;
23
+ }
24
+ return (
25
+ <pre className="bg-black/30 rounded-lg p-2 my-1 overflow-x-auto">
26
+ <code className="text-xs font-mono">{children}</code>
27
+ </pre>
28
+ );
29
+ },
30
+ a: ({ href, children }) => (
31
+ <a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-400 underline hover:text-blue-300">
32
+ {children}
33
+ </a>
34
+ ),
35
+ h1: ({ children }) => <h1 className="text-base font-bold mb-1">{children}</h1>,
36
+ h2: ({ children }) => <h2 className="text-sm font-bold mb-1">{children}</h2>,
37
+ h3: ({ children }) => <h3 className="text-sm font-semibold mb-0.5">{children}</h3>,
38
+ blockquote: ({ children }) => (
39
+ <blockquote className="border-l-2 border-white/30 pl-2 my-1 text-[var(--muted)]">{children}</blockquote>
40
+ ),
41
+ hr: () => <hr className="my-2 border-white/10" />,
42
+ table: ({ children }) => (
43
+ <div className="overflow-x-auto my-1">
44
+ <table className="text-xs border-collapse">{children}</table>
45
+ </div>
46
+ ),
47
+ th: ({ children }) => <th className="border border-white/20 px-2 py-1 bg-white/5 font-semibold">{children}</th>,
48
+ td: ({ children }) => <td className="border border-white/20 px-2 py-1">{children}</td>,
49
+ };
50
+
51
+ // 清理消息内容中的内部标签
52
+ function cleanMessageContent(content) {
53
+ if (!content || typeof content !== 'string') return content;
54
+ let cleaned = content.replace(/<[||]DSML[||][^>]*>[\s\S]*/g, '').trim();
55
+ cleaned = cleaned.replace(/<\|DSML\|[^>]*>[\s\S]*/g, '').trim();
56
+ cleaned = cleaned.replace(/<\|(?:im_start|im_end|endoftext)\|>/g, '').trim();
57
+ return cleaned || content;
58
+ }
59
+
60
+ /**
61
+ * 渲染 @[id] 或 @Name mention 高亮
62
+ * 同时兼容新格式 @[agentId] 和旧格式 @AgentName
63
+ */
64
+ function renderMentions(text, agentMap, onClickMention) {
65
+ if (!text || typeof text !== 'string') return null;
66
+
67
+ const nameToId = {};
68
+ if (agentMap) {
69
+ for (const [id, name] of Object.entries(agentMap)) {
70
+ nameToId[name] = id;
71
+ }
72
+ }
73
+
74
+ const hasNewFormat = /@\[[^\]]+\]/.test(text);
75
+ const names = Object.keys(nameToId).sort((a, b) => b.length - a.length);
76
+ const hasOldFormat = names.length > 0 && names.some(n => text.includes(`@${n}`));
77
+
78
+ if (!hasNewFormat && !hasOldFormat) return null;
79
+
80
+ const regexParts = ['(@\\[[^\\]]+\\])'];
81
+ if (names.length > 0) {
82
+ const escapedNames = names.map(n => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
83
+ regexParts.push(`(@(?:${escapedNames.join('|')}))`);
84
+ }
85
+ const regex = new RegExp(regexParts.join('|'), 'g');
86
+ const parts = text.split(regex).filter(p => p !== undefined);
87
+
88
+ if (parts.length <= 1) return null;
89
+
90
+ const renderTag = (key, displayName, agentId) => (
91
+ <span
92
+ key={key}
93
+ className={`inline-flex items-center bg-blue-500/30 text-blue-200 px-1.5 py-0.5 rounded text-xs font-semibold mx-0.5 border border-blue-500/20 ${onClickMention ? 'cursor-pointer hover:bg-blue-500/40 transition-colors' : ''}`}
94
+ onClick={() => agentId && onClickMention?.(agentId)}
95
+ >
96
+ @{displayName}
97
+ </span>
98
+ );
99
+
100
+ return parts.map((part, i) => {
101
+ const newMatch = part.match(/^@\[([^\]]+)\]$/);
102
+ if (newMatch) {
103
+ const id = newMatch[1];
104
+ const name = agentMap?.[id] || id;
105
+ return renderTag(i, name, id);
106
+ }
107
+ const oldMatch = part.match(/^@(.+)$/);
108
+ if (oldMatch && nameToId[oldMatch[1]]) {
109
+ const name = oldMatch[1];
110
+ const id = nameToId[name];
111
+ return renderTag(i, name, id);
112
+ }
113
+ return part;
114
+ });
115
+ }
116
+
117
+ /**
118
+ * 消息分组:将同一发送者连续的短消息合并到一组
119
+ */
120
+ function groupConsecutiveMessages(messages, getSenderId) {
121
+ if (!messages?.length) return [];
122
+ const groups = [];
123
+ let currentGroup = null;
124
+
125
+ for (const msg of messages) {
126
+ const senderId = getSenderId(msg);
127
+ const isShort = (msg.content?.length || 0) <= 120;
128
+ const timeDiff = currentGroup
129
+ ? Math.abs(new Date(msg.time) - new Date(currentGroup.messages[currentGroup.messages.length - 1].time)) / 1000
130
+ : Infinity;
131
+
132
+ if (currentGroup && currentGroup.senderId === senderId && isShort && timeDiff <= 60 && currentGroup.isShort) {
133
+ currentGroup.messages.push(msg);
134
+ } else {
135
+ currentGroup = { senderId, messages: [msg], isShort };
136
+ groups.push(currentGroup);
137
+ }
138
+ }
139
+ return groups;
140
+ }
141
+
142
+ /**
143
+ * AgentSpyModal — 🕵️ 偷窥某员工的IM界面
144
+ * 左边:同事头像列表(会话列表)
145
+ * 右边:聊天窗口
146
+ */
147
+ export default function AgentSpyModal({ agentId, agentName, agentAvatar, onClose }) {
148
+ const { t } = useI18n();
149
+ const { fetchAgentConversations, fetchAgentConversationHistory } = useStore();
150
+ const [conversations, setConversations] = useState([]);
151
+ const [selectedConv, setSelectedConv] = useState(null);
152
+ const [messages, setMessages] = useState([]);
153
+ const [sessionParticipants, setSessionParticipants] = useState([]);
154
+ const [loading, setLoading] = useState(true);
155
+ const [msgLoading, setMsgLoading] = useState(false);
156
+ const [selectedAgentId, setSelectedAgentId] = useState(null); // 点击头像/消息弹出员工详情
157
+ const messagesEndRef = useRef(null);
158
+
159
+ // 构建 agentId -> agentName 映射(用于 @[id] 渲染)
160
+ const { company } = useStore();
161
+ const agentMap = {};
162
+ if (company?.departments) {
163
+ for (const dept of company.departments) {
164
+ for (const agent of (dept.members || dept.agents || [])) {
165
+ agentMap[agent.id] = agent.name;
166
+ }
167
+ }
168
+ }
169
+
170
+ // 加载会话列表
171
+ useEffect(() => {
172
+ setLoading(true);
173
+ fetchAgentConversations(agentId).then(data => {
174
+ setConversations(data);
175
+ // 自动选中第一个会话
176
+ if (data.length > 0) {
177
+ setSelectedConv(data[0]);
178
+ }
179
+ setLoading(false);
180
+ }).catch(() => setLoading(false));
181
+ }, [agentId]);
182
+
183
+ // 选中会话后加载消息
184
+ useEffect(() => {
185
+ if (selectedConv) {
186
+ setMsgLoading(true);
187
+ fetchAgentConversationHistory(agentId, selectedConv.sessionId).then(result => {
188
+ setMessages(result.messages || []);
189
+ setSessionParticipants(result.participants || []);
190
+ setMsgLoading(false);
191
+ }).catch(() => setMsgLoading(false));
192
+ }
193
+ }, [selectedConv]);
194
+
195
+ // 自动滚动到底部
196
+ useEffect(() => {
197
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
198
+ }, [messages]);
199
+
200
+ return (
201
+ <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-[70] !m-0" onClick={onClose}>
202
+ <div
203
+ className="bg-[var(--card)] border border-[var(--border)] rounded-2xl max-w-3xl w-full mx-4 h-[75vh] flex flex-col overflow-hidden shadow-2xl"
204
+ onClick={e => e.stopPropagation()}
205
+ >
206
+ {/* Header */}
207
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-[var(--border)] shrink-0 bg-[var(--card)]">
208
+ <div className="text-lg">🕵️</div>
209
+ <div className="flex-1 min-w-0">
210
+ <div className="text-sm font-semibold flex items-center gap-2">
211
+ {t('agent.spyTitle', { name: agentName })}
212
+ <span className="text-[10px] text-[var(--muted)] font-normal bg-purple-500/10 text-purple-400 px-1.5 py-0.5 rounded-full">
213
+ {t('agent.spyMode')}
214
+ </span>
215
+ </div>
216
+ <div className="text-[10px] text-[var(--muted)]">{t('agent.spyHint')}</div>
217
+ </div>
218
+ <button
219
+ onClick={onClose}
220
+ className="text-[var(--muted)] hover:text-white text-xl w-7 h-7 flex items-center justify-center rounded-lg hover:bg-white/10 transition-all"
221
+ >
222
+
223
+ </button>
224
+ </div>
225
+
226
+ {/* Body: 左边联系人列表 + 右边聊天区 */}
227
+ <div className="flex-1 flex overflow-hidden">
228
+ {/* 左侧:联系人列表 */}
229
+ <div className="w-[200px] shrink-0 border-r border-[var(--border)] overflow-auto bg-[var(--background)]/50">
230
+ {loading ? (
231
+ <div className="flex items-center justify-center h-full">
232
+ <div className="text-2xl animate-pulse">🔍</div>
233
+ </div>
234
+ ) : conversations.length === 0 ? (
235
+ <div className="flex flex-col items-center justify-center h-full px-4 text-center">
236
+ <div className="text-3xl mb-2">🤫</div>
237
+ <p className="text-xs text-[var(--muted)]">{t('agent.noConversations')}</p>
238
+ <p className="text-[10px] text-[var(--muted)] mt-1">{t('agent.noConversationsHint')}</p>
239
+ </div>
240
+ ) : (
241
+ <div className="py-1">
242
+ {conversations.map(conv => {
243
+ const isActive = selectedConv?.sessionId === conv.sessionId;
244
+ return (
245
+ <div
246
+ key={conv.sessionId}
247
+ onClick={() => setSelectedConv(conv)}
248
+ className={`flex items-center gap-2.5 px-3 py-2.5 cursor-pointer transition-all ${
249
+ isActive
250
+ ? 'bg-[var(--accent)]/10 border-l-2 border-[var(--accent)]'
251
+ : 'hover:bg-white/5 border-l-2 border-transparent'
252
+ }`}
253
+ >
254
+ {/* 头像 */}
255
+ {conv.peerAvatar ? (
256
+ <img
257
+ src={conv.peerAvatar}
258
+ alt=""
259
+ className={`w-9 h-9 rounded-full bg-[var(--border)] shrink-0 ${isActive ? 'ring-2 ring-[var(--accent)]/50' : ''}`}
260
+ />
261
+ ) : (
262
+ <div className={`w-9 h-9 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-sm shrink-0 ${isActive ? 'ring-2 ring-[var(--accent)]/50' : ''}`}>
263
+ {conv.type === 'boss-agent' ? '👤' : '🤖'}
264
+ </div>
265
+ )}
266
+ {/* 名字和简介 */}
267
+ <div className="flex-1 min-w-0">
268
+ <div className="text-xs font-medium truncate">{conv.peerName}</div>
269
+ {conv.peerRole && (
270
+ <div className="text-[10px] text-[var(--muted)] truncate">{conv.peerRole}</div>
271
+ )}
272
+ {conv.lastMessage && (
273
+ <div className="text-[10px] text-[var(--muted)] truncate mt-0.5 italic">{conv.lastMessage}</div>
274
+ )}
275
+ </div>
276
+ {/* 消息数气泡 */}
277
+ {conv.totalMessages > 0 && (
278
+ <span className="text-[9px] bg-white/10 text-[var(--muted)] px-1.5 py-0.5 rounded-full shrink-0">
279
+ {conv.totalMessages}
280
+ </span>
281
+ )}
282
+ </div>
283
+ );
284
+ })}
285
+ </div>
286
+ )}
287
+ </div>
288
+
289
+ {/* 右侧:聊天窗口 */}
290
+ <div className="flex-1 flex flex-col overflow-hidden">
291
+ {!selectedConv ? (
292
+ /* 未选中会话 */
293
+ <div className="flex-1 flex flex-col items-center justify-center text-center">
294
+ <div className="text-5xl mb-3 opacity-30">🕵️</div>
295
+ <p className="text-sm text-[var(--muted)]">{t('agent.spySelectHint')}</p>
296
+ </div>
297
+ ) : (
298
+ <>
299
+ {/* 聊天对象信息 header */}
300
+ <div className="flex items-center gap-3 px-4 py-2.5 border-b border-[var(--border)] shrink-0 bg-[var(--background)]/30">
301
+ {selectedConv.peerAvatar ? (
302
+ <CachedAvatar src={selectedConv.peerAvatar} alt="" className="w-8 h-8 rounded-full bg-[var(--border)]" />
303
+ ) : (
304
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-sm">
305
+ {selectedConv.type === 'boss-agent' ? '👤' : '🤖'}
306
+ </div>
307
+ )}
308
+ <div className="flex-1 min-w-0">
309
+ <div className="text-sm font-medium flex items-center gap-2">
310
+ <span className="truncate">{selectedConv.peerName}</span>
311
+ </div>
312
+ <div className="flex items-center gap-2">
313
+ {selectedConv.peerRole && (
314
+ <span className="text-[10px] text-[var(--muted)] bg-white/5 px-1.5 py-0.5 rounded">{selectedConv.peerRole}</span>
315
+ )}
316
+ {selectedConv.peerDepartment && (
317
+ <span className="text-[10px] text-[var(--muted)]">· {selectedConv.peerDepartment}</span>
318
+ )}
319
+ <span className="text-[10px] text-[var(--muted)]">· {selectedConv.totalMessages} {t('agent.spyMsgCount')}</span>
320
+ </div>
321
+ </div>
322
+ </div>
323
+
324
+ {/* 消息列表 */}
325
+ <div className="flex-1 overflow-auto px-4 py-3 space-y-3">
326
+ {msgLoading ? (
327
+ <div className="flex items-center justify-center h-full">
328
+ <div className="text-center">
329
+ <div className="text-2xl animate-pulse">🔍</div>
330
+ <p className="text-xs text-[var(--muted)] mt-2">{t('agent.spyLoading')}</p>
331
+ </div>
332
+ </div>
333
+ ) : messages.length === 0 ? (
334
+ <div className="flex items-center justify-center h-full">
335
+ <div className="text-center">
336
+ <div className="text-3xl mb-2 opacity-30">💬</div>
337
+ <p className="text-sm text-[var(--muted)]">{t('agent.noConversationMessages')}</p>
338
+ </div>
339
+ </div>
340
+ ) : (
341
+ groupConsecutiveMessages(messages, m => {
342
+ // 判断消息发送者ID
343
+ if (m.fromAgentId) return m.fromAgentId;
344
+ // 旧数据 fallback: boss-agent 会话根据 role 判断
345
+ if (selectedConv.type === 'boss-agent') {
346
+ return m.role === 'boss' ? 'boss' : agentId;
347
+ }
348
+ // 旧数据 fallback: agent-agent 会话,如果消息内容 @了对方,说明是目标员工发的
349
+ const peerId = selectedConv.peerId;
350
+ if (peerId && m.content) {
351
+ const peerName = agentMap[peerId];
352
+ if (peerName && (m.content.includes(`@${peerName}`) || m.content.includes(`@[${peerId}]`))) {
353
+ return agentId; // 目标员工 @ 了对方 → 是目标员工发的
354
+ }
355
+ const agentName_ = agentMap[agentId];
356
+ if (agentName_ && (m.content.includes(`@${agentName_}`) || m.content.includes(`@[${agentId}]`))) {
357
+ return peerId; // 对方 @ 了目标员工 → 是对方发的
358
+ }
359
+ }
360
+ // 最终 fallback: 交替分配
361
+ return '__unknown__';
362
+ }).map((group, gi) => {
363
+ const firstMsg = group.messages[0];
364
+ // 判断消息是否由目标员工发出
365
+ const isFromAgent = firstMsg.fromAgentId === agentId
366
+ || (selectedConv.type === 'boss-agent' && firstMsg.role !== 'boss')
367
+ || (!firstMsg.fromAgentId && selectedConv.type !== 'boss-agent' && (() => {
368
+ // 旧数据 fallback 判断
369
+ const peerId = selectedConv.peerId;
370
+ const peerName = agentMap[peerId];
371
+ if (peerName && firstMsg.content && (firstMsg.content.includes(`@${peerName}`) || firstMsg.content.includes(`@[${peerId}]`))) {
372
+ return true;
373
+ }
374
+ return false;
375
+ })());
376
+ const isBoss = firstMsg.role === 'boss';
377
+ const isRight = isFromAgent && !isBoss;
378
+ const isMerged = group.messages.length > 1;
379
+
380
+ return (
381
+ <div key={`group-${gi}`} className={`flex gap-2 ${isRight ? 'flex-row-reverse' : ''}`}>
382
+ {/* Avatar */}
383
+ {isRight ? (
384
+ agentAvatar ? (
385
+ <CachedAvatar src={agentAvatar} alt="" className="w-7 h-7 rounded-full bg-[var(--border)] shrink-0 mt-0.5 cursor-pointer hover:ring-2 hover:ring-[var(--accent)] transition-all" onClick={() => setSelectedAgentId(agentId)} />
386
+ ) : (
387
+ <div className="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-xs shrink-0 mt-0.5">🤖</div>
388
+ )
389
+ ) : (
390
+ selectedConv.peerAvatar ? (
391
+ <img
392
+ src={selectedConv.peerAvatar}
393
+ alt=""
394
+ className="w-7 h-7 rounded-full bg-[var(--border)] shrink-0 mt-0.5 cursor-pointer hover:ring-2 hover:ring-[var(--accent)] transition-all"
395
+ onClick={() => {
396
+ const peerId = selectedConv.peerId;
397
+ if (peerId) setSelectedAgentId(peerId);
398
+ }}
399
+ />
400
+ ) : (
401
+ <div className="w-7 h-7 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-xs shrink-0 mt-0.5">
402
+ {isBoss ? '👤' : '🤖'}
403
+ </div>
404
+ )
405
+ )}
406
+
407
+ <div className={`max-w-[70%] ${isRight ? 'text-right' : ''}`}>
408
+ {/* Name + Time */}
409
+ <div className={`flex items-center gap-2 mb-0.5 ${isRight ? 'flex-row-reverse' : ''}`}>
410
+ <span className="text-[10px] text-[var(--muted)]">
411
+ {isRight ? agentName : (firstMsg.fromAgentName || selectedConv.peerName)}
412
+ </span>
413
+ {firstMsg.time && (
414
+ <span className="text-[10px] text-[var(--muted)]/60">
415
+ {new Date(firstMsg.time).toLocaleTimeString()}
416
+ </span>
417
+ )}
418
+ </div>
419
+ {/* Bubble */}
420
+ {isMerged ? (
421
+ <div className={`inline-block rounded-2xl px-3 py-2 text-sm leading-relaxed text-left ${
422
+ isRight
423
+ ? 'bg-indigo-500/15 border border-indigo-500/20 rounded-br-sm'
424
+ : 'bg-[var(--card)] border border-[var(--border)] rounded-bl-sm'
425
+ }`}>
426
+ {group.messages.map((msg, mi) => (
427
+ <div key={mi}>
428
+ {mi > 0 && <div className="border-t border-white/[0.06] my-1.5" />}
429
+ <div className="break-words chat-markdown">
430
+ {renderMentions(cleanMessageContent(msg.content), agentMap, setSelectedAgentId) || (
431
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={chatMarkdownComponents}>
432
+ {cleanMessageContent(msg.content)}
433
+ </ReactMarkdown>
434
+ )}
435
+ </div>
436
+ </div>
437
+ ))}
438
+ </div>
439
+ ) : (
440
+ <div className={`inline-block rounded-2xl px-3 py-2 text-sm leading-relaxed text-left ${isRight
441
+ ? 'bg-indigo-500/15 border border-indigo-500/20 rounded-br-sm'
442
+ : 'bg-[var(--card)] border border-[var(--border)] rounded-bl-sm'
443
+ }`}>
444
+ <div className="break-words chat-markdown">
445
+ {renderMentions(cleanMessageContent(firstMsg.content), agentMap, setSelectedAgentId) || (
446
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={chatMarkdownComponents}>
447
+ {cleanMessageContent(firstMsg.content)}
448
+ </ReactMarkdown>
449
+ )}
450
+ </div>
451
+ </div>
452
+ )}
453
+ </div>
454
+ </div>
455
+ );
456
+ })
457
+ )}
458
+ <div ref={messagesEndRef} />
459
+ </div>
460
+
461
+ {/* 底部提示 */}
462
+ <div className="px-4 py-2 border-t border-[var(--border)] shrink-0 bg-[var(--background)]/30">
463
+ <div className="text-[10px] text-[var(--muted)] text-center flex items-center justify-center gap-1">
464
+ 🔒 {t('agent.spyReadonly')}
465
+ </div>
466
+ </div>
467
+ </>
468
+ )}
469
+ </div>
470
+ </div>
471
+
472
+ {/* 员工详情弹窗 */}
473
+ {selectedAgentId && (
474
+ <AgentDetailModal agentId={selectedAgentId} onClose={() => setSelectedAgentId(null)} />
475
+ )}
476
+ </div>
477
+ </div>
478
+ );
479
+ }
480
+
481
+
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * AvatarGrid - 通用头像选择网格组件
5
+ * 方形网格视图,选中后主色调高亮背景,点击即选
6
+ */
7
+ export default function AvatarGrid({ choices = [], selectedId, onSelect, cols = 8, gap = 'gap-1.5' }) {
8
+ return (
9
+ <div className={`grid ${gap} overflow-auto pr-1`} style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}>
10
+ {choices.map((choice) => (
11
+ <button
12
+ key={choice.id}
13
+ onClick={() => onSelect(choice)}
14
+ className={`relative aspect-square rounded-lg transition-all overflow-hidden ${
15
+ selectedId === choice.id
16
+ ? 'bg-[var(--accent)] p-1 scale-[1.02]'
17
+ : 'bg-[var(--border)] hover:bg-[var(--accent)]/30 hover:scale-[1.03]'
18
+ }`}
19
+ >
20
+ <img
21
+ src={choice.url}
22
+ alt="avatar"
23
+ className="w-full h-full object-cover rounded-md"
24
+ />
25
+ </button>
26
+ ))}
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,162 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { getAvatarChoices } from '@/lib/avatar';
6
+ import { useI18n } from '@/lib/i18n';
7
+ import CachedAvatar from './CachedAvatar';
8
+ import AvatarGrid from './AvatarGrid';
9
+
10
+ export default function BossProfileModal({ onClose }) {
11
+ const { t } = useI18n();
12
+ const { company, updateBossProfile } = useStore();
13
+ const [selectedAvatar, setSelectedAvatar] = useState(null);
14
+ const [avatarChoices, setAvatarChoices] = useState([]);
15
+ const [saving, setSaving] = useState(false);
16
+ const [saved, setSaved] = useState(false);
17
+
18
+ // 生成头像选项 (老板默认使用 male, 35 岁参数)
19
+ const [gender, setGender] = useState('male');
20
+ const [age, setAge] = useState(35);
21
+
22
+ const debounceTimer = useRef(null);
23
+ const refreshAvatarDebounced = useCallback((g, a) => {
24
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
25
+ debounceTimer.current = setTimeout(() => {
26
+ const choices = getAvatarChoices(20, g, a);
27
+ setAvatarChoices(choices);
28
+ }, 300);
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ refreshAvatarDebounced(gender, age);
33
+ return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current); };
34
+ }, [gender, age, refreshAvatarDebounced]);
35
+
36
+ const refreshChoices = () => {
37
+ const choices = getAvatarChoices(20, gender, age);
38
+ setAvatarChoices(choices);
39
+ };
40
+
41
+ const previewAvatar = selectedAvatar?.url || company?.bossAvatar;
42
+
43
+ const handleSave = async () => {
44
+ if (!selectedAvatar) return;
45
+ setSaving(true);
46
+ setSaved(false);
47
+ try {
48
+ await updateBossProfile({ avatar: selectedAvatar.url });
49
+ setSaved(true);
50
+ setTimeout(() => setSaved(false), 2000);
51
+ } catch (e) { /* handled */ }
52
+ setSaving(false);
53
+ };
54
+
55
+ return (
56
+ <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 !m-0" onClick={onClose}>
57
+ <div className="card max-w-lg w-full mx-4 max-h-[85vh] overflow-hidden flex flex-col" onClick={e => e.stopPropagation()}>
58
+ {/* Header */}
59
+ <div className="flex items-center justify-between pb-4 border-b border-[var(--border)]">
60
+ <div className="flex items-center gap-3">
61
+ {previewAvatar ? (
62
+ <CachedAvatar src={previewAvatar} alt="boss" className="w-12 h-12 rounded-full bg-[var(--border)]" />
63
+ ) : (
64
+ <div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-xl font-bold">
65
+ 👑
66
+ </div>
67
+ )}
68
+ <div>
69
+ <h2 className="text-lg font-bold">{t('bossProfile.title')}</h2>
70
+ <p className="text-xs text-[var(--muted)]">{t('bossProfile.subtitle')}</p>
71
+ </div>
72
+ </div>
73
+ <button onClick={onClose} className="text-[var(--muted)] hover:text-white text-xl">✕</button>
74
+ </div>
75
+
76
+ {/* Content */}
77
+ <div className="flex-1 overflow-auto py-4 space-y-5">
78
+ {/* Gender & Age */}
79
+ <div className="grid grid-cols-2 gap-4">
80
+ <div>
81
+ <label className="block text-sm font-medium mb-1.5 text-[var(--muted)]">{t('setup.gender')}</label>
82
+ <div className="flex gap-2">
83
+ <button
84
+ onClick={() => { setGender('female'); setSelectedAvatar(null); }}
85
+ className={`flex-1 py-2 px-3 rounded-lg border text-sm transition-all ${
86
+ gender === 'female'
87
+ ? 'border-pink-400 bg-pink-400/10 text-pink-300'
88
+ : 'border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)]/40'
89
+ }`}
90
+ >{t('setup.female')}</button>
91
+ <button
92
+ onClick={() => { setGender('male'); setSelectedAvatar(null); }}
93
+ className={`flex-1 py-2 px-3 rounded-lg border text-sm transition-all ${
94
+ gender === 'male'
95
+ ? 'border-blue-400 bg-blue-400/10 text-blue-300'
96
+ : 'border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)]/40'
97
+ }`}
98
+ >{t('setup.male')}</button>
99
+ </div>
100
+ </div>
101
+ <div>
102
+ <label className="block text-sm font-medium mb-1.5 text-[var(--muted)]">{t('setup.age', { n: age })}</label>
103
+ <div className="relative flex items-center gap-3">
104
+ <button
105
+ onClick={() => setAge(a => Math.max(18, a - 1))}
106
+ className="w-7 h-7 rounded-full border border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-all flex items-center justify-center text-sm font-bold shrink-0"
107
+ >−</button>
108
+ <div className="flex-1 relative h-5 flex items-center">
109
+ <div className="absolute left-0 right-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-[var(--border)] pointer-events-none" />
110
+ <div
111
+ className="absolute left-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-gradient-to-r from-[var(--accent)] to-purple-400 pointer-events-none"
112
+ style={{ width: `${((age - 18) / 42) * 100}%` }}
113
+ />
114
+ <input
115
+ type="range"
116
+ min="18"
117
+ max="60"
118
+ value={age}
119
+ onChange={e => { setAge(Number(e.target.value)); setSelectedAvatar(null); }}
120
+ className="absolute inset-0 z-10 w-full appearance-none cursor-pointer bg-transparent [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[var(--accent)] [&::-webkit-slider-thumb]:shadow-[0_0_6px_rgba(99,102,241,0.5)] [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:hover:scale-125 [&::-webkit-slider-thumb]:-mt-[5px]"
121
+ />
122
+ </div>
123
+ <button
124
+ onClick={() => setAge(a => Math.min(60, a + 1))}
125
+ className="w-7 h-7 rounded-full border border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-all flex items-center justify-center text-sm font-bold shrink-0"
126
+ >+</button>
127
+ </div>
128
+ <div className="flex justify-between text-[10px] text-[var(--muted)] mt-1 px-10">
129
+ <span>18</span><span>30</span><span>45</span><span>60</span>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ {/* Avatar selection */}
135
+ <div>
136
+ <div className="flex items-center justify-between mb-1.5">
137
+ <label className="text-sm font-medium text-[var(--muted)]">{t('bossProfile.avatarTitle')}</label>
138
+ <button
139
+ className="text-xs text-[var(--accent)] hover:underline flex items-center gap-1"
140
+ onClick={refreshChoices}
141
+ >{t('bossProfile.refreshAvatar')}</button>
142
+ </div>
143
+ <AvatarGrid
144
+ choices={avatarChoices}
145
+ selectedId={selectedAvatar?.id}
146
+ onSelect={setSelectedAvatar}
147
+ />
148
+ </div>
149
+ </div>
150
+
151
+ {/* Footer */}
152
+ <div className="pt-4 border-t border-[var(--border)] flex items-center justify-end gap-2">
153
+ {saved && <span className="text-xs text-green-400 animate-fade-in">{t('bossProfile.saved')}</span>}
154
+ <button className="btn-secondary" onClick={onClose}>{t('common.cancel')}</button>
155
+ <button className="btn-primary" disabled={saving || !selectedAvatar} onClick={handleSave}>
156
+ {saving ? t('bossProfile.saving') : t('bossProfile.saveBtn')}
157
+ </button>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ );
162
+ }