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,77 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+
5
+ /**
6
+ * 全局内存缓存:url → dataURL / blob URL
7
+ * 避免组件重新挂载时重复请求头像,消除闪动
8
+ */
9
+ const avatarCache = new Map();
10
+ const pendingRequests = new Map();
11
+
12
+ function loadAvatar(src) {
13
+ if (avatarCache.has(src)) return Promise.resolve(avatarCache.get(src));
14
+ if (pendingRequests.has(src)) return pendingRequests.get(src);
15
+
16
+ const promise = fetch(src)
17
+ .then(res => res.blob())
18
+ .then(blob => {
19
+ const objectUrl = URL.createObjectURL(blob);
20
+ avatarCache.set(src, objectUrl);
21
+ pendingRequests.delete(src);
22
+ return objectUrl;
23
+ })
24
+ .catch(() => {
25
+ pendingRequests.delete(src);
26
+ return src; // fallback to original URL
27
+ });
28
+
29
+ pendingRequests.set(src, promise);
30
+ return promise;
31
+ }
32
+
33
+ /**
34
+ * CachedAvatar - 带内存缓存的头像组件
35
+ * 首次加载后缓存为 blob URL,后续渲染零闪动
36
+ */
37
+ export default function CachedAvatar({ src, alt, className, title, onClick, style }) {
38
+ const [cachedSrc, setCachedSrc] = useState(() => avatarCache.get(src) || null);
39
+ const mountedRef = useRef(true);
40
+
41
+ useEffect(() => {
42
+ mountedRef.current = true;
43
+ if (!src) return;
44
+
45
+ // 已缓存,直接使用
46
+ const cached = avatarCache.get(src);
47
+ if (cached) {
48
+ setCachedSrc(cached);
49
+ return;
50
+ }
51
+
52
+ // 加载并缓存
53
+ loadAvatar(src).then(url => {
54
+ if (mountedRef.current) setCachedSrc(url);
55
+ });
56
+
57
+ return () => { mountedRef.current = false; };
58
+ }, [src]);
59
+
60
+ // 缓存命中时直接渲染,无闪动
61
+ if (cachedSrc) {
62
+ return <img src={cachedSrc} alt={alt || ''} className={className} title={title} onClick={onClick} style={style} />;
63
+ }
64
+
65
+ // 首次加载:先显示背景占位,img 隐藏加载完后淡入
66
+ return (
67
+ <img
68
+ src={src}
69
+ alt={alt || ''}
70
+ className={className}
71
+ title={title}
72
+ onClick={onClick}
73
+ style={{ ...style, opacity: 0, transition: 'opacity 0.2s ease-in' }}
74
+ onLoad={e => { e.target.style.opacity = 1; }}
75
+ />
76
+ );
77
+ }
@@ -0,0 +1,219 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, useMemo } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { getAvatarUrl } from '@/lib/avatar';
6
+ import { useI18n } from '@/lib/i18n';
7
+ import { MessageBubble, ChatInput, TaskStatusPanel } from './ChatShared';
8
+ import ProvidersBoard from './ProvidersBoard';
9
+
10
+ export default function ChatPanel() {
11
+ const { company, chatWithSecretary, chatOpen, setChatOpen, chatMinimized, setChatMinimized } = useStore();
12
+ const { t } = useI18n();
13
+ const [message, setMessage] = useState('');
14
+ const [sending, setSending] = useState(false);
15
+ const [localHistory, setLocalHistory] = useState([]);
16
+ const [showProviders, setShowProviders] = useState(false);
17
+ const messagesEndRef = useRef(null);
18
+
19
+ // Check if any provider is enabled across all categories
20
+ const hasAnyProvider = useMemo(() => {
21
+ const dashboard = company?.providerDashboard;
22
+ if (!dashboard) return false;
23
+ return Object.values(dashboard).some(cat => cat.enabled > 0);
24
+ }, [company?.providerDashboard]);
25
+
26
+ useEffect(() => {
27
+ if (company?.chatHistory) {
28
+ setLocalHistory(company.chatHistory);
29
+ }
30
+ }, [company?.chatHistory]);
31
+
32
+ useEffect(() => {
33
+ if (chatOpen && !chatMinimized) {
34
+ setTimeout(() => {
35
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
36
+ }, 50);
37
+ }
38
+ }, [localHistory, chatOpen, chatMinimized]);
39
+
40
+ if (!company || !chatOpen) return null;
41
+
42
+ const secretary = company.secretary;
43
+
44
+ const handleSend = async () => {
45
+ if (!message.trim() || sending) return;
46
+ const msg = message.trim();
47
+ setMessage('');
48
+ setSending(true);
49
+
50
+ setLocalHistory(prev => [...prev, { role: 'boss', content: msg, time: new Date().toISOString() }]);
51
+
52
+ try {
53
+ await chatWithSecretary(msg);
54
+ } catch (e) {
55
+ setLocalHistory(prev => [...prev, {
56
+ role: 'secretary',
57
+ content: `${t('chat.errorPrefix')}${e.message}`,
58
+ time: new Date().toISOString(),
59
+ }]);
60
+ }
61
+ setSending(false);
62
+ };
63
+
64
+ const handleKeyDown = (e) => {
65
+ if (e.key === 'Enter' && !e.shiftKey) {
66
+ e.preventDefault();
67
+ handleSend();
68
+ }
69
+ };
70
+
71
+ if (chatMinimized) {
72
+ return (
73
+ <button
74
+ onClick={() => setChatMinimized(false)}
75
+ className="fixed bottom-6 right-6 w-14 h-14 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 shadow-2xl flex items-center justify-center z-[70] hover:scale-110 transition-all animate-fade-in group"
76
+ title={t('chat.openChat', { name: secretary?.name || t('setup.defaultSecretary') })}
77
+ >
78
+ <img
79
+ src={secretary?.avatar || getAvatarUrl('secretary')}
80
+ alt={t('chat.secretary')}
81
+ className="w-11 h-11 rounded-full border-2 border-white/20"
82
+ />
83
+ <span className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-[#0d0d0d] animate-pulse" />
84
+ </button>
85
+ );
86
+ }
87
+
88
+ return (
89
+ <div className="fixed bottom-6 right-6 w-[440px] h-[600px] bg-[var(--card)] border border-[var(--border)] rounded-2xl shadow-2xl flex flex-col z-[70] animate-fade-in overflow-hidden">
90
+ {/* Header */}
91
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-[var(--border)] bg-gradient-to-r from-blue-900/30 to-purple-900/30 shrink-0">
92
+ <img
93
+ src={secretary?.avatar || getAvatarUrl('secretary')}
94
+ alt={t('chat.secretary')}
95
+ className="w-10 h-10 rounded-full bg-[var(--border)]"
96
+ />
97
+ <div className="flex-1 min-w-0">
98
+ <div className="text-sm font-semibold flex items-center gap-2">
99
+ {secretary?.name || t('setup.defaultSecretary')}
100
+ <span className="w-2 h-2 bg-green-500 rounded-full" />
101
+ </div>
102
+ {secretary?.signature ? (
103
+ <div className="text-[10px] text-[var(--muted)] italic truncate" title={secretary.signature}>"{secretary.signature}"</div>
104
+ ) : (
105
+ <div className="text-[10px] text-[var(--muted)]">{t('chat.online')}</div>
106
+ )}
107
+ </div>
108
+ <div className="flex items-center gap-1">
109
+ <button
110
+ onClick={() => setChatMinimized(true)}
111
+ className="text-[var(--muted)] hover:text-white text-lg w-7 h-7 flex items-center justify-center rounded-lg hover:bg-white/10 transition-all"
112
+ title={t('common.minimize')}
113
+ >
114
+
115
+ </button>
116
+ <button
117
+ onClick={() => setChatOpen(false)}
118
+ className="text-[var(--muted)] hover:text-white text-lg w-7 h-7 flex items-center justify-center rounded-lg hover:bg-white/10 transition-all"
119
+ >
120
+
121
+ </button>
122
+ </div>
123
+ </div>
124
+
125
+ {/* No provider configured — block chat and show setup prompt */}
126
+ {!hasAnyProvider ? (
127
+ <div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
128
+ <div className="text-5xl mb-4">🧠</div>
129
+ <h3 className="text-base font-semibold text-yellow-400 mb-2">{t('chat.noProviderTitle')}</h3>
130
+ <p className="text-xs text-[var(--muted)] mb-5 max-w-xs leading-relaxed">{t('chat.noProviderDesc')}</p>
131
+ <button
132
+ onClick={() => setShowProviders(true)}
133
+ className="px-5 py-2.5 rounded-lg bg-[var(--accent)] text-white text-sm font-medium hover:opacity-90 transition-all"
134
+ >
135
+ {t('chat.noProviderBtn')}
136
+ </button>
137
+
138
+ {/* Inline ProvidersBoard modal */}
139
+ {showProviders && (
140
+ <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-[80] !m-0" onClick={() => setShowProviders(false)}>
141
+ <div className="bg-[var(--card)] border border-[var(--border)] rounded-2xl max-w-4xl w-full mx-4 max-h-[85vh] overflow-auto" onClick={e => e.stopPropagation()}>
142
+ <div className="flex justify-end p-3 pb-0">
143
+ <button onClick={() => setShowProviders(false)} className="text-[var(--muted)] hover:text-white text-lg">✕</button>
144
+ </div>
145
+ <ProvidersBoard />
146
+ </div>
147
+ </div>
148
+ )}
149
+ </div>
150
+ ) : (<>
151
+ {/* Messages area - reuse MessageBubble from ChatShared */}
152
+ <div className="flex-1 overflow-auto p-3 space-y-3">
153
+ {localHistory.length === 0 && (
154
+ <div className="text-center py-8">
155
+ <div className="text-4xl mb-2">💬</div>
156
+ <p className="text-sm text-[var(--muted)]">
157
+ {t('chat.welcome', { name: secretary?.name || t('setup.defaultSecretary') })}
158
+ </p>
159
+ <div className="mt-3 space-y-1">
160
+ {t('chat.suggestions').map((q, i) => (
161
+ <button
162
+ key={i}
163
+ 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"
164
+ onClick={() => { setMessage(q); }}
165
+ >
166
+ 💡 {q}
167
+ </button>
168
+ ))}
169
+ </div>
170
+ </div>
171
+ )}
172
+
173
+ {localHistory.map((msg, i) => (
174
+ <MessageBubble
175
+ key={i}
176
+ isMe={msg.role === 'boss'}
177
+ avatar={msg.role === 'secretary' ? (secretary?.avatar || getAvatarUrl('secretary')) : null}
178
+ name={msg.role === 'boss' ? company.boss : (secretary?.name || t('setup.defaultSecretary'))}
179
+ content={msg.content}
180
+ time={msg.time}
181
+ action={msg.action}
182
+ agentId={null}
183
+ onClickAvatar={null}
184
+ bossAvatar={company?.bossAvatar}
185
+ />
186
+ ))}
187
+
188
+ {sending && (
189
+ <div className="flex gap-2">
190
+ <img
191
+ src={secretary?.avatar || getAvatarUrl('secretary')}
192
+ alt={t('chat.secretary')}
193
+ className="w-7 h-7 rounded-full bg-[var(--border)] shrink-0"
194
+ />
195
+ <div className="bg-[var(--card)] border border-[var(--border)] rounded-2xl rounded-bl-sm px-3 py-2 text-sm">
196
+ <span className="animate-pulse text-[var(--muted)]">{t('chat.typing')}</span>
197
+ </div>
198
+ </div>
199
+ )}
200
+
201
+ <div ref={messagesEndRef} />
202
+ </div>
203
+
204
+ {/* Task status panel - shared component */}
205
+ <TaskStatusPanel />
206
+
207
+ {/* Input area - reuse ChatInput from ChatShared */}
208
+ <ChatInput
209
+ value={message}
210
+ onChange={setMessage}
211
+ onSend={handleSend}
212
+ onKeyDown={handleKeyDown}
213
+ sending={sending}
214
+ placeholder={t('chat.inputPlaceholder', { name: secretary?.name || t('setup.defaultSecretary') })}
215
+ />
216
+ </>)}
217
+ </div>
218
+ );
219
+ }
@@ -0,0 +1,255 @@
1
+ 'use client';
2
+
3
+ import ReactMarkdown from 'react-markdown';
4
+ import remarkGfm from 'remark-gfm';
5
+ import { useI18n } from '@/lib/i18n';
6
+ import { useStore } from '@/lib/client-store';
7
+ import { parseFileReferences, FileRefList } from './FileReference';
8
+ import { cleanMessageContent } from './GroupChatView';
9
+ import CachedAvatar from './CachedAvatar';
10
+
11
+ // ============ Markdown 渲染组件 ============
12
+
13
+ export 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
+
53
+ export function formatTime(time, t = (k) => k) {
54
+ if (!time) return '';
55
+ const d = new Date(time);
56
+ const now = new Date();
57
+ const diff = now - d;
58
+
59
+ if (diff < 60 * 1000) return t('time.justNow');
60
+ if (diff < 60 * 60 * 1000) return t('time.minutesAgo', { n: Math.floor(diff / 60000) });
61
+ if (diff < 24 * 60 * 60 * 1000) {
62
+ return d.toLocaleTimeString('zh', { hour: '2-digit', minute: '2-digit' });
63
+ }
64
+ if (diff < 7 * 24 * 60 * 60 * 1000) {
65
+ const days = [t('time.sun'), t('time.mon'), t('time.tue'), t('time.wed'), t('time.thu'), t('time.fri'), t('time.sat')];
66
+ return days[d.getDay()];
67
+ }
68
+ return d.toLocaleDateString('zh', { month: 'short', day: 'numeric' });
69
+ }
70
+
71
+ // ============ 消息气泡组件 ============
72
+
73
+ /**
74
+ * 共享消息气泡组件
75
+ * 支持 Markdown 渲染、文件引用、action 标签
76
+ */
77
+ export function MessageBubble({ isMe, avatar, name, content, time, action, subject, agentId, onClickAvatar, bossAvatar }) {
78
+ const { t } = useI18n();
79
+ const { cleanContent, fileRefs } = parseFileReferences(content);
80
+ return (
81
+ <div className={`flex gap-2 ${isMe ? 'flex-row-reverse' : ''}`}>
82
+ {!isMe ? (
83
+ <CachedAvatar
84
+ src={avatar}
85
+ alt=""
86
+ className={`w-8 h-8 rounded-full bg-[var(--border)] shrink-0 mt-0.5 ${
87
+ agentId ? 'cursor-pointer hover:ring-2 hover:ring-[var(--accent)] transition-all' : ''
88
+ }`}
89
+ onClick={() => agentId && onClickAvatar?.(agentId)}
90
+ />
91
+ ) : (
92
+ bossAvatar ? (
93
+ <CachedAvatar src={bossAvatar} alt="boss" className="w-8 h-8 rounded-full bg-[var(--border)] shrink-0 mt-0.5" />
94
+ ) : (
95
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-xs font-bold shrink-0 mt-0.5">
96
+ 👤
97
+ </div>
98
+ )
99
+ )}
100
+ <div className={`max-w-[min(70%,560px)] ${isMe ? 'items-end' : 'items-start'}`}>
101
+ {/* Name + time */}
102
+ <div className={`flex items-center gap-2 mb-0.5 ${isMe ? 'flex-row-reverse' : ''}`}>
103
+ <span className="text-[10px] text-[var(--muted)]">{name}</span>
104
+ {time && (
105
+ <span className="text-[10px] text-[var(--muted)]/60">
106
+ {formatTime(time, t)}
107
+ </span>
108
+ )}
109
+ </div>
110
+ {/* Subject tag */}
111
+ {subject && (
112
+ <div className="text-[10px] text-[var(--accent)] bg-[var(--accent)]/10 px-2 py-0.5 rounded mb-1 inline-block">
113
+ 📌 {subject}
114
+ </div>
115
+ )}
116
+ {/* Bubble */}
117
+ <div className={`rounded-2xl px-3 py-2 text-sm leading-relaxed ${
118
+ isMe
119
+ ? 'bg-[var(--accent)] text-white rounded-br-sm'
120
+ : 'bg-[var(--card)] border border-[var(--border)] rounded-bl-sm'
121
+ }`}>
122
+ <div className="break-words chat-markdown">
123
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={chatMarkdownComponents}>
124
+ {cleanMessageContent(cleanContent)}
125
+ </ReactMarkdown>
126
+ </div>
127
+ <FileRefList fileRefs={fileRefs} />
128
+ </div>
129
+ {/* Action tag */}
130
+ {action && (
131
+ <div className="mt-1 text-[10px] text-blue-400 bg-blue-900/10 px-2 py-0.5 rounded inline-block">
132
+ {action.type === 'task_assigned' && (
133
+ <>
134
+ {t('chat.taskAssigned', { dept: action.departmentName })}
135
+ {action.taskStatus === 'running' && <span className="ml-1 animate-pulse">{t('chat.running')}</span>}
136
+ </>
137
+ )}
138
+ {action.type === 'need_new_department' && t('chat.needNewDept')}
139
+ {action.type === 'create_department' && (
140
+ <>
141
+ {t('chat.creatingDept', { dept: action.departmentName })}
142
+ {action.taskStatus === 'running' && <span className="ml-1 animate-pulse">{t('chat.planningHiring')}</span>}
143
+ </>
144
+ )}
145
+ {action.type === 'department_created' && t('chat.deptCreated', { dept: action.departmentName })}
146
+ {action.type === 'progress_report' && t('chat.progressReport')}
147
+ </div>
148
+ )}
149
+ </div>
150
+ </div>
151
+ );
152
+ }
153
+
154
+ // ============ 输入框组件 ============
155
+
156
+ /**
157
+ * 共享聊天输入框组件
158
+ */
159
+ export function ChatInput({ value, onChange, onSend, onKeyDown, sending, placeholder, inputRef }) {
160
+ const { t } = useI18n();
161
+ return (
162
+ <div className="px-4 py-3 border-t border-white/[0.06] bg-[var(--card)]">
163
+ <div className="flex gap-2 items-end">
164
+ <textarea
165
+ ref={inputRef}
166
+ className="input flex-1 text-sm resize-none min-h-[40px] max-h-[120px]"
167
+ placeholder={placeholder}
168
+ value={value}
169
+ onChange={e => onChange(e.target.value)}
170
+ onKeyDown={onKeyDown}
171
+ disabled={sending}
172
+ rows={1}
173
+ style={{ height: 'auto' }}
174
+ onInput={e => {
175
+ e.target.style.height = 'auto';
176
+ e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';
177
+ }}
178
+ />
179
+ <button
180
+ className="btn-primary px-4 py-2 text-sm shrink-0"
181
+ disabled={!value.trim() || sending}
182
+ onClick={onSend}
183
+ >
184
+ {sending ? '⏳' : t('mailbox.sendBtn')}
185
+ </button>
186
+ </div>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ // ============ 任务状态面板 ============
192
+
193
+ /**
194
+ * 共享任务进度/结果面板
195
+ * 当有 runningTaskId 或 taskResult 时显示
196
+ */
197
+ export function TaskStatusPanel() {
198
+ const { t } = useI18n();
199
+ const { runningTaskId, taskResult, clearTaskResult } = useStore();
200
+
201
+ if (!runningTaskId && !taskResult) return null;
202
+
203
+ return (
204
+ <div className="border-t border-[var(--border)] bg-gradient-to-r from-blue-950/40 to-indigo-950/40 shrink-0">
205
+ {runningTaskId && !taskResult && (
206
+ <div className="px-4 py-3 flex items-center gap-3">
207
+ <div className="relative w-5 h-5 shrink-0">
208
+ <div className="absolute inset-0 rounded-full border-2 border-blue-400/30" />
209
+ <div className="absolute inset-0 rounded-full border-2 border-blue-400 border-t-transparent animate-spin" />
210
+ </div>
211
+ <div className="flex-1 min-w-0">
212
+ <div className="text-xs font-medium text-blue-300">{t('chat.running')}</div>
213
+ <div className="text-[10px] text-[var(--muted)] mt-0.5 truncate">{t('chat.taskAssigned', { dept: '...' })}</div>
214
+ </div>
215
+ <div className="flex gap-1">
216
+ <span className="w-1 h-1 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: '0ms' }} />
217
+ <span className="w-1 h-1 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: '150ms' }} />
218
+ <span className="w-1 h-1 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: '300ms' }} />
219
+ </div>
220
+ </div>
221
+ )}
222
+ {taskResult && (
223
+ <div className="px-4 py-3">
224
+ <div className="flex items-center justify-between mb-2">
225
+ <div className="flex items-center gap-2">
226
+ <span className="text-sm">{taskResult.error ? '❌' : '✅'}</span>
227
+ <span className="text-xs font-medium text-white">{taskResult.error ? t('chat.errorPrefix') : t('chat.progressReport')}</span>
228
+ </div>
229
+ <button
230
+ onClick={clearTaskResult}
231
+ className="text-[10px] text-[var(--muted)] hover:text-white px-2 py-0.5 rounded hover:bg-white/10 transition-all"
232
+ >
233
+
234
+ </button>
235
+ </div>
236
+ {taskResult.error ? (
237
+ <div className="text-xs text-red-300 bg-red-900/20 rounded-lg px-3 py-2 border border-red-500/20">
238
+ {taskResult.error}
239
+ </div>
240
+ ) : (
241
+ <div className="text-xs text-[var(--foreground)] bg-white/5 rounded-lg px-3 py-2 border border-white/10 max-h-32 overflow-auto">
242
+ {typeof taskResult === 'string' ? (
243
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={{
244
+ p: ({ children }) => <p className="mb-1 last:mb-0">{children}</p>,
245
+ }}>{taskResult}</ReactMarkdown>
246
+ ) : (
247
+ <pre className="whitespace-pre-wrap text-[10px]">{JSON.stringify(taskResult, null, 2)}</pre>
248
+ )}
249
+ </div>
250
+ )}
251
+ </div>
252
+ )}
253
+ </div>
254
+ );
255
+ }