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,799 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { useI18n } from '@/lib/i18n';
|
|
5
|
+
import ReactMarkdown from 'react-markdown';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
7
|
+
import { parseFileReferences, FileRefList } from './FileReference';
|
|
8
|
+
import AgentDetailModal from './AgentDetailModal';
|
|
9
|
+
import CachedAvatar from './CachedAvatar';
|
|
10
|
+
|
|
11
|
+
// ============ 共享工具函数 ============
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 根据名字生成唯一hash深色背景色
|
|
15
|
+
*/
|
|
16
|
+
export function nameToColor(name) {
|
|
17
|
+
if (!name) return 'bg-gradient-to-br from-indigo-600 to-blue-700';
|
|
18
|
+
let hash = 0;
|
|
19
|
+
for (let i = 0; i < name.length; i++) {
|
|
20
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
21
|
+
hash = hash & hash;
|
|
22
|
+
}
|
|
23
|
+
const darkColors = [
|
|
24
|
+
'bg-gradient-to-br from-rose-700 to-pink-900',
|
|
25
|
+
'bg-gradient-to-br from-violet-700 to-purple-900',
|
|
26
|
+
'bg-gradient-to-br from-indigo-700 to-blue-900',
|
|
27
|
+
'bg-gradient-to-br from-cyan-700 to-teal-900',
|
|
28
|
+
'bg-gradient-to-br from-emerald-700 to-green-900',
|
|
29
|
+
'bg-gradient-to-br from-amber-700 to-orange-900',
|
|
30
|
+
'bg-gradient-to-br from-red-700 to-rose-900',
|
|
31
|
+
'bg-gradient-to-br from-fuchsia-700 to-pink-900',
|
|
32
|
+
'bg-gradient-to-br from-blue-700 to-indigo-900',
|
|
33
|
+
'bg-gradient-to-br from-teal-700 to-cyan-900',
|
|
34
|
+
'bg-gradient-to-br from-lime-700 to-green-900',
|
|
35
|
+
'bg-gradient-to-br from-orange-700 to-red-900',
|
|
36
|
+
];
|
|
37
|
+
return darkColors[Math.abs(hash) % darkColors.length];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Clean message content: filter out leaked LLM internal tags
|
|
42
|
+
*/
|
|
43
|
+
export function cleanMessageContent(content) {
|
|
44
|
+
if (!content || typeof content !== 'string') return content;
|
|
45
|
+
let cleaned = content.replace(/<[||]DSML[||][^>]*>[\s\S]*/g, '').trim();
|
|
46
|
+
cleaned = cleaned.replace(/<\|DSML\|[^>]*>[\s\S]*/g, '').trim();
|
|
47
|
+
cleaned = cleaned.replace(/<\|(?:im_start|im_end|endoftext)\|>/g, '').trim();
|
|
48
|
+
return cleaned || content;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Render @[id] or @Name mention as highlighted tag
|
|
53
|
+
*/
|
|
54
|
+
export function renderMentions(text, agentMap, onClickMention) {
|
|
55
|
+
if (!text || typeof text !== 'string') return null;
|
|
56
|
+
|
|
57
|
+
const nameToId = {};
|
|
58
|
+
if (agentMap) {
|
|
59
|
+
for (const [id, name] of Object.entries(agentMap)) {
|
|
60
|
+
nameToId[name] = id;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const hasNewFormat = /@\[[^\]]+\]/.test(text);
|
|
65
|
+
const names = Object.keys(nameToId).sort((a, b) => b.length - a.length);
|
|
66
|
+
const hasOldFormat = names.length > 0 && names.some(n => text.includes(`@${n}`));
|
|
67
|
+
|
|
68
|
+
if (!hasNewFormat && !hasOldFormat) return null;
|
|
69
|
+
|
|
70
|
+
const regexParts = ['(@\\[[^\\]]+\\])'];
|
|
71
|
+
if (names.length > 0) {
|
|
72
|
+
const escapedNames = names.map(n => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
73
|
+
regexParts.push(`(@(?:${escapedNames.join('|')}))`);
|
|
74
|
+
}
|
|
75
|
+
const regex = new RegExp(regexParts.join('|'), 'g');
|
|
76
|
+
const parts = text.split(regex).filter(p => p !== undefined);
|
|
77
|
+
|
|
78
|
+
if (parts.length <= 1) return null;
|
|
79
|
+
|
|
80
|
+
const renderTag = (key, displayName, agentId) => (
|
|
81
|
+
<span
|
|
82
|
+
key={key}
|
|
83
|
+
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' : ''}`}
|
|
84
|
+
onClick={() => agentId && onClickMention?.(agentId)}
|
|
85
|
+
>
|
|
86
|
+
@{displayName}
|
|
87
|
+
</span>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return parts.map((part, i) => {
|
|
91
|
+
const newMatch = part.match(/^@\[([^\]]+)\]$/);
|
|
92
|
+
if (newMatch) {
|
|
93
|
+
const id = newMatch[1];
|
|
94
|
+
const name = agentMap?.[id] || id;
|
|
95
|
+
return renderTag(i, name, id);
|
|
96
|
+
}
|
|
97
|
+
const oldMatch = part.match(/^@(.+)$/);
|
|
98
|
+
if (oldMatch && nameToId[oldMatch[1]]) {
|
|
99
|
+
const name = oldMatch[1];
|
|
100
|
+
const id = nameToId[name];
|
|
101
|
+
return renderTag(i, name, id);
|
|
102
|
+
}
|
|
103
|
+
return part;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 消息分组:将同一发送者连续的短消息合并到一组
|
|
109
|
+
*/
|
|
110
|
+
export function groupConsecutiveMessages(messages, getSenderId) {
|
|
111
|
+
if (!messages?.length) return [];
|
|
112
|
+
const groups = [];
|
|
113
|
+
let currentGroup = null;
|
|
114
|
+
|
|
115
|
+
for (const msg of messages) {
|
|
116
|
+
const senderId = getSenderId(msg);
|
|
117
|
+
const isShort = (msg.content?.length || 0) <= 120;
|
|
118
|
+
const timeDiff = currentGroup
|
|
119
|
+
? Math.abs(new Date(msg.time) - new Date(currentGroup.messages[currentGroup.messages.length - 1].time)) / 1000
|
|
120
|
+
: Infinity;
|
|
121
|
+
|
|
122
|
+
// monologue(内心独白)消息永远不合并,始终独立展示
|
|
123
|
+
const isMonologue = msg.type === 'monologue';
|
|
124
|
+
const prevIsMonologue = currentGroup?.messages[0]?.type === 'monologue';
|
|
125
|
+
|
|
126
|
+
if (currentGroup && currentGroup.senderId === senderId && isShort && timeDiff <= 60 && currentGroup.isShort && !isMonologue && !prevIsMonologue) {
|
|
127
|
+
currentGroup.messages.push(msg);
|
|
128
|
+
} else {
|
|
129
|
+
currentGroup = { senderId, messages: [msg], isShort };
|
|
130
|
+
groups.push(currentGroup);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return groups;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Markdown render component mapping for chat bubbles
|
|
137
|
+
const chatMarkdownComponents = {
|
|
138
|
+
p: ({ children }) => <p className="mb-1 last:mb-0">{children}</p>,
|
|
139
|
+
ul: ({ children }) => <ul className="list-disc list-inside mb-1 space-y-0.5">{children}</ul>,
|
|
140
|
+
ol: ({ children }) => <ol className="list-decimal list-inside mb-1 space-y-0.5">{children}</ol>,
|
|
141
|
+
li: ({ children }) => <li className="text-sm">{children}</li>,
|
|
142
|
+
strong: ({ children }) => <strong className="font-bold">{children}</strong>,
|
|
143
|
+
em: ({ children }) => <em className="italic">{children}</em>,
|
|
144
|
+
code: ({ inline, children }) => {
|
|
145
|
+
if (inline) {
|
|
146
|
+
return <code className="bg-white/10 px-1 py-0.5 rounded text-xs font-mono">{children}</code>;
|
|
147
|
+
}
|
|
148
|
+
return (
|
|
149
|
+
<pre className="bg-black/30 rounded-lg p-2 my-1 overflow-x-auto">
|
|
150
|
+
<code className="text-xs font-mono">{children}</code>
|
|
151
|
+
</pre>
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
a: ({ href, children }) => (
|
|
155
|
+
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-400 underline hover:text-blue-300">
|
|
156
|
+
{children}
|
|
157
|
+
</a>
|
|
158
|
+
),
|
|
159
|
+
h1: ({ children }) => <h1 className="text-base font-bold mb-1">{children}</h1>,
|
|
160
|
+
h2: ({ children }) => <h2 className="text-sm font-bold mb-1">{children}</h2>,
|
|
161
|
+
h3: ({ children }) => <h3 className="text-sm font-semibold mb-0.5">{children}</h3>,
|
|
162
|
+
blockquote: ({ children }) => (
|
|
163
|
+
<blockquote className="border-l-2 border-white/30 pl-2 my-1 text-[var(--muted)]">{children}</blockquote>
|
|
164
|
+
),
|
|
165
|
+
hr: () => <hr className="my-2 border-white/10" />,
|
|
166
|
+
table: ({ children }) => (
|
|
167
|
+
<div className="overflow-x-auto my-1">
|
|
168
|
+
<table className="text-xs border-collapse">{children}</table>
|
|
169
|
+
</div>
|
|
170
|
+
),
|
|
171
|
+
th: ({ children }) => <th className="border border-white/20 px-2 py-1 bg-white/5 font-semibold">{children}</th>,
|
|
172
|
+
td: ({ children }) => <td className="border border-white/20 px-2 py-1">{children}</td>,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 共享的群聊消息渲染组件
|
|
177
|
+
*
|
|
178
|
+
* Props:
|
|
179
|
+
* - groupChat: 群聊消息数组
|
|
180
|
+
* - agentMap: agentId -> agentName 映射
|
|
181
|
+
* - bossAvatar: Boss头像URL
|
|
182
|
+
* - bossName: Boss名字
|
|
183
|
+
* - requirementId: 需求ID(用于发送消息)
|
|
184
|
+
* - onSendMessage: 发送消息函数 (reqId, text) => Promise
|
|
185
|
+
* - fetchDetail: 刷新详情函数 (reqId) => Promise
|
|
186
|
+
* - sendingPlaceholder: 发送中的占位提示
|
|
187
|
+
* - inputPlaceholder: 输入框占位提示
|
|
188
|
+
* - typingLabel: "正在思考回复..." 前面显示的负责人名称
|
|
189
|
+
* - leaderInfo: { name, avatar } 负责人信息,用于typing提示
|
|
190
|
+
* - chatEndRef: 外部传入的滚动ref(可选,不传则内部创建)
|
|
191
|
+
* - embedded: 是否是嵌入模式(如RequirementDetail中,true则不显示sticky input)
|
|
192
|
+
*/
|
|
193
|
+
export default function GroupChatView({
|
|
194
|
+
groupChat = [],
|
|
195
|
+
agentMap = {},
|
|
196
|
+
bossAvatar,
|
|
197
|
+
bossName = 'Boss',
|
|
198
|
+
requirementId,
|
|
199
|
+
onSendMessage,
|
|
200
|
+
fetchDetail,
|
|
201
|
+
inputPlaceholder,
|
|
202
|
+
leaderInfo,
|
|
203
|
+
chatEndRef: externalChatEndRef,
|
|
204
|
+
embedded = false,
|
|
205
|
+
}) {
|
|
206
|
+
const { t } = useI18n();
|
|
207
|
+
const [selectedAgentId, setSelectedAgentId] = useState(null);
|
|
208
|
+
const [inputText, setInputText] = useState('');
|
|
209
|
+
const [sending, setSending] = useState(false);
|
|
210
|
+
const [optimisticMessages, setOptimisticMessages] = useState([]);
|
|
211
|
+
const internalChatEndRef = useRef(null);
|
|
212
|
+
const chatEndRef = externalChatEndRef || internalChatEndRef;
|
|
213
|
+
|
|
214
|
+
// 自动滚动到底部:仅首次进入时
|
|
215
|
+
const hasInitialScrolled = useRef(false);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (!hasInitialScrolled.current && groupChat.length > 0) {
|
|
218
|
+
hasInitialScrolled.current = true;
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
chatEndRef.current?.scrollIntoView({ behavior: 'auto' });
|
|
221
|
+
}, 80);
|
|
222
|
+
}
|
|
223
|
+
}, [groupChat.length]);
|
|
224
|
+
|
|
225
|
+
// 心流偷看相关状态
|
|
226
|
+
const [monologueAgentId, setMonologueAgentId] = useState(null);
|
|
227
|
+
const [monologueData, setMonologueData] = useState(null);
|
|
228
|
+
const [monologueFlowMsgs, setMonologueFlowMsgs] = useState([]); // 工作日志(flow 消息,排除 monologue)
|
|
229
|
+
const [monologueHistory, setMonologueHistory] = useState([]); // 心流历史(含 decision/thoughts,仅本次运行)
|
|
230
|
+
const [monologueThoughtMsgs, setMonologueThoughtMsgs] = useState([]); // 持久化心流消息(群聊中 type=monologue)
|
|
231
|
+
const [monologueLoading, setMonologueLoading] = useState(false);
|
|
232
|
+
const [monologueTab, setMonologueTab] = useState('thoughts'); // thoughts | flow | history
|
|
233
|
+
const [activeThinking, setActiveThinking] = useState([]);
|
|
234
|
+
|
|
235
|
+
// SSE 实时订阅心流事件
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (!requirementId) return;
|
|
238
|
+
|
|
239
|
+
const es = new EventSource('/api/group-chat-loop/events');
|
|
240
|
+
// 用 Map 维护当前 thinking 状态:agentId → { agentId, agentName, groupId, status }
|
|
241
|
+
const thinkingMap = new Map();
|
|
242
|
+
|
|
243
|
+
const syncState = () => {
|
|
244
|
+
setActiveThinking(
|
|
245
|
+
Array.from(thinkingMap.values()).filter(a => a.groupId === requirementId)
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
es.addEventListener('snapshot', (e) => {
|
|
250
|
+
try {
|
|
251
|
+
const list = JSON.parse(e.data);
|
|
252
|
+
thinkingMap.clear();
|
|
253
|
+
for (const a of list) thinkingMap.set(a.agentId, { ...a, status: a.status || 'thinking' });
|
|
254
|
+
syncState();
|
|
255
|
+
} catch {}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
es.addEventListener('monologue:start', (e) => {
|
|
259
|
+
try {
|
|
260
|
+
const data = JSON.parse(e.data);
|
|
261
|
+
thinkingMap.set(data.agentId, { ...data, status: 'thinking' });
|
|
262
|
+
syncState();
|
|
263
|
+
} catch {}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
es.addEventListener('monologue:end', (e) => {
|
|
267
|
+
try {
|
|
268
|
+
const data = JSON.parse(e.data);
|
|
269
|
+
// 标记为 done,保留 30 秒后移除
|
|
270
|
+
thinkingMap.set(data.agentId, { ...data, status: 'done' });
|
|
271
|
+
syncState();
|
|
272
|
+
setTimeout(() => {
|
|
273
|
+
if (thinkingMap.get(data.agentId)?.status === 'done') {
|
|
274
|
+
thinkingMap.delete(data.agentId);
|
|
275
|
+
syncState();
|
|
276
|
+
}
|
|
277
|
+
}, 30000);
|
|
278
|
+
} catch {}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
es.onerror = () => {
|
|
282
|
+
// EventSource 会自动重连
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return () => es.close();
|
|
286
|
+
}, [requirementId]);
|
|
287
|
+
|
|
288
|
+
// 偷看心流
|
|
289
|
+
const peekMonologue = async (agentId) => {
|
|
290
|
+
setMonologueAgentId(agentId);
|
|
291
|
+
setMonologueLoading(true);
|
|
292
|
+
setMonologueTab('thoughts');
|
|
293
|
+
try {
|
|
294
|
+
// 同时获取当前心流、历史心流(含 decision)、工作日志、持久化心流消息
|
|
295
|
+
const [currentRes, historyRes, flowRes, thoughtRes] = await Promise.all([
|
|
296
|
+
fetch(`/api/group-chat-loop?agentId=${agentId}&groupId=${requirementId}`),
|
|
297
|
+
fetch(`/api/group-chat-loop?agentId=${agentId}&groupId=${requirementId}&history=1`),
|
|
298
|
+
fetch(`/api/group-chat-loop?agentId=${agentId}&groupId=${requirementId}&flowMessages=1`),
|
|
299
|
+
fetch(`/api/group-chat-loop?agentId=${agentId}&groupId=${requirementId}&monologueMessages=1`),
|
|
300
|
+
]);
|
|
301
|
+
const currentData = await currentRes.json();
|
|
302
|
+
const historyData = await historyRes.json();
|
|
303
|
+
const flowData = await flowRes.json();
|
|
304
|
+
const thoughtData = await thoughtRes.json();
|
|
305
|
+
setMonologueData(currentData.data);
|
|
306
|
+
setMonologueHistory(historyData.data || []);
|
|
307
|
+
setMonologueFlowMsgs(flowData.data || []);
|
|
308
|
+
setMonologueThoughtMsgs(thoughtData.data || []);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
console.error('Failed to peek monologue:', err);
|
|
311
|
+
} finally {
|
|
312
|
+
setMonologueLoading(false);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// 自动刷新当前偷看的心流和工作日志
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (!monologueAgentId || !requirementId) return;
|
|
319
|
+
const timer = setInterval(async () => {
|
|
320
|
+
try {
|
|
321
|
+
const [res, historyRes, flowRes, thoughtRes] = await Promise.all([
|
|
322
|
+
fetch(`/api/group-chat-loop?agentId=${monologueAgentId}&groupId=${requirementId}`),
|
|
323
|
+
fetch(`/api/group-chat-loop?agentId=${monologueAgentId}&groupId=${requirementId}&history=1`),
|
|
324
|
+
fetch(`/api/group-chat-loop?agentId=${monologueAgentId}&groupId=${requirementId}&flowMessages=1`),
|
|
325
|
+
fetch(`/api/group-chat-loop?agentId=${monologueAgentId}&groupId=${requirementId}&monologueMessages=1`),
|
|
326
|
+
]);
|
|
327
|
+
const data = await res.json();
|
|
328
|
+
const historyData = await historyRes.json();
|
|
329
|
+
const flowData = await flowRes.json();
|
|
330
|
+
const thoughtData = await thoughtRes.json();
|
|
331
|
+
if (data.data) setMonologueData(data.data);
|
|
332
|
+
if (historyData.data) setMonologueHistory(historyData.data);
|
|
333
|
+
if (flowData.data) setMonologueFlowMsgs(flowData.data);
|
|
334
|
+
if (thoughtData.data) setMonologueThoughtMsgs(thoughtData.data);
|
|
335
|
+
} catch {}
|
|
336
|
+
}, 3000);
|
|
337
|
+
return () => clearInterval(timer);
|
|
338
|
+
}, [monologueAgentId, requirementId]);
|
|
339
|
+
|
|
340
|
+
// 合并真实消息和乐观消息(去重:如果真实消息中已包含乐观消息的内容,则移除乐观消息)
|
|
341
|
+
// 内心独白不在群聊消息流中展示,通过点击头像入口查看
|
|
342
|
+
const realMessages = groupChat.filter(m => m.visibility !== 'flow');
|
|
343
|
+
const dedupedOptimistic = optimisticMessages.filter(opt => {
|
|
344
|
+
// 如果真实消息中已存在相同内容+相近时间的 boss 消息,说明已入库,去掉乐观消息
|
|
345
|
+
return !realMessages.some(real =>
|
|
346
|
+
real.from?.id === 'boss' &&
|
|
347
|
+
real.content === opt.content &&
|
|
348
|
+
Math.abs(new Date(real.time) - new Date(opt.time)) < 10000
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
const allMessages = [...realMessages, ...dedupedOptimistic];
|
|
352
|
+
|
|
353
|
+
// 发送消息的通用逻辑
|
|
354
|
+
const doSend = async () => {
|
|
355
|
+
if (!inputText.trim() || sending || !requirementId || !onSendMessage) return;
|
|
356
|
+
const msg = inputText.trim();
|
|
357
|
+
setInputText('');
|
|
358
|
+
setSending(true);
|
|
359
|
+
|
|
360
|
+
// 乐观更新:立即添加Boss消息
|
|
361
|
+
const optimisticMsg = {
|
|
362
|
+
id: `boss-opt-${Date.now()}`,
|
|
363
|
+
from: { id: 'boss', name: bossName, avatar: bossAvatar, role: 'Boss' },
|
|
364
|
+
content: msg,
|
|
365
|
+
type: 'message',
|
|
366
|
+
time: new Date().toISOString(),
|
|
367
|
+
};
|
|
368
|
+
setOptimisticMessages(prev => [...prev, optimisticMsg]);
|
|
369
|
+
|
|
370
|
+
// 立即滚动到底部
|
|
371
|
+
setTimeout(() => chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 50);
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
await onSendMessage(requirementId, msg);
|
|
375
|
+
if (fetchDetail) await fetchDetail(requirementId);
|
|
376
|
+
// API返回后清除乐观消息(真实数据已通过去重机制自动覆盖)
|
|
377
|
+
setOptimisticMessages([]);
|
|
378
|
+
// 再次滚动(leader回复可能已加入)
|
|
379
|
+
setTimeout(() => chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
console.error('Send message failed:', err);
|
|
382
|
+
setOptimisticMessages(prev => [...prev, {
|
|
383
|
+
id: `err-${Date.now()}`,
|
|
384
|
+
from: { id: 'system', name: 'System' },
|
|
385
|
+
content: `⚠️ ${t('mailbox.sendFailed')}: ${err.message}`,
|
|
386
|
+
type: 'system',
|
|
387
|
+
time: new Date().toISOString(),
|
|
388
|
+
}]);
|
|
389
|
+
} finally {
|
|
390
|
+
setSending(false);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const isEmpty = allMessages.length === 0 && !sending;
|
|
395
|
+
|
|
396
|
+
// 渲染单条消息气泡内容(支持Markdown + @提及 + 文件引用)
|
|
397
|
+
const renderMessageContent = (content) => {
|
|
398
|
+
const { cleanContent, fileRefs } = parseFileReferences(content);
|
|
399
|
+
const cleaned = cleanMessageContent(cleanContent);
|
|
400
|
+
const mentionRendered = renderMentions(cleaned, agentMap, setSelectedAgentId);
|
|
401
|
+
return (
|
|
402
|
+
<>
|
|
403
|
+
<div className="break-words text-sm leading-relaxed chat-markdown">
|
|
404
|
+
{mentionRendered || (
|
|
405
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={chatMarkdownComponents}>
|
|
406
|
+
{cleaned}
|
|
407
|
+
</ReactMarkdown>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
<FileRefList fileRefs={fileRefs} />
|
|
411
|
+
</>
|
|
412
|
+
);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// Boss消息内容渲染(纯文本 + 文件引用)
|
|
416
|
+
const renderBossContent = (content) => {
|
|
417
|
+
const { cleanContent, fileRefs } = parseFileReferences(content);
|
|
418
|
+
return (
|
|
419
|
+
<>
|
|
420
|
+
<div className="whitespace-pre-wrap break-words text-sm leading-relaxed">
|
|
421
|
+
{cleanMessageContent(cleanContent)}
|
|
422
|
+
</div>
|
|
423
|
+
<FileRefList fileRefs={fileRefs} />
|
|
424
|
+
</>
|
|
425
|
+
);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<div className={`flex flex-col ${embedded ? 'flex-1 min-h-0' : ''}`}>
|
|
430
|
+
<div className={`${embedded ? 'flex-1 overflow-auto' : ''} p-4 space-y-3`}>
|
|
431
|
+
{isEmpty ? (
|
|
432
|
+
<div className="flex items-center justify-center py-16 text-[var(--muted)]">
|
|
433
|
+
<div className="text-center">
|
|
434
|
+
<div className="text-4xl mb-2">💬</div>
|
|
435
|
+
<p>{t('reqDetail.chat.noMessages')}</p>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
) : (
|
|
439
|
+
<>
|
|
440
|
+
{groupConsecutiveMessages(
|
|
441
|
+
allMessages,
|
|
442
|
+
m => m.type === 'system' ? '__system__' : (m.from?.id || m.from?.name || '__unknown__')
|
|
443
|
+
).map((group, gi) => {
|
|
444
|
+
const firstMsg = group.messages[0];
|
|
445
|
+
if (firstMsg.type === 'system') {
|
|
446
|
+
return group.messages.map(msg => (
|
|
447
|
+
<div key={msg.id} className="text-center">
|
|
448
|
+
<span className="text-[10px] text-[var(--muted)] bg-white/5 px-3 py-1 rounded-full">
|
|
449
|
+
{msg.content}
|
|
450
|
+
</span>
|
|
451
|
+
</div>
|
|
452
|
+
));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const isMerged = group.messages.length > 1;
|
|
456
|
+
const isBoss = firstMsg.from?.id === 'boss';
|
|
457
|
+
|
|
458
|
+
// Boss 消息右对齐渲染
|
|
459
|
+
if (isBoss) {
|
|
460
|
+
return (
|
|
461
|
+
<div key={`group-${gi}`} className="flex gap-2 flex-row-reverse">
|
|
462
|
+
{bossAvatar ? (
|
|
463
|
+
<CachedAvatar src={bossAvatar} alt="boss" className="w-8 h-8 rounded-lg bg-[var(--border)] shrink-0 mt-0.5" />
|
|
464
|
+
) : (
|
|
465
|
+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-xs font-bold shrink-0 mt-0.5">👤</div>
|
|
466
|
+
)}
|
|
467
|
+
<div className="flex flex-col items-end min-w-0 flex-1">
|
|
468
|
+
<div className="flex items-center gap-2 mb-0.5 flex-row-reverse">
|
|
469
|
+
<span className="text-xs font-medium">{firstMsg.from?.name || bossName}</span>
|
|
470
|
+
<span className="text-[10px] text-[var(--muted)]">
|
|
471
|
+
{new Date(firstMsg.time).toLocaleTimeString('zh', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
|
472
|
+
</span>
|
|
473
|
+
</div>
|
|
474
|
+
{group.messages.map((msg) => (
|
|
475
|
+
<div key={msg.id} className="rounded-2xl rounded-br-sm px-3 py-2 text-sm max-w-[min(85%,600px)] w-fit bg-[var(--accent)] text-white mb-1">
|
|
476
|
+
{renderBossContent(msg.content)}
|
|
477
|
+
</div>
|
|
478
|
+
))}
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<div key={`group-${gi}`} className="flex gap-2">
|
|
486
|
+
<div className="relative group/avatar shrink-0">
|
|
487
|
+
{firstMsg.from?.avatar ? (
|
|
488
|
+
<img
|
|
489
|
+
src={firstMsg.from.avatar}
|
|
490
|
+
alt=""
|
|
491
|
+
className="w-8 h-8 rounded-lg bg-[var(--border)] mt-0.5 cursor-pointer hover:ring-2 hover:ring-[var(--accent)] transition-all"
|
|
492
|
+
onClick={() => firstMsg.from?.id && firstMsg.from.id !== 'system' && setSelectedAgentId(firstMsg.from.id)}
|
|
493
|
+
/>
|
|
494
|
+
) : (
|
|
495
|
+
<div className={`w-8 h-8 rounded-lg ${nameToColor(firstMsg.from?.name)} flex items-center justify-center text-xs mt-0.5`}>
|
|
496
|
+
{firstMsg.from?.name?.charAt(0) || '🤖'}
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
{/* 偷看心流按钮 - 悬浮在头像上方 */}
|
|
500
|
+
{firstMsg.from?.id && firstMsg.from.id !== 'system' && firstMsg.from.id !== 'boss' && (
|
|
501
|
+
<button
|
|
502
|
+
onClick={(e) => { e.stopPropagation(); peekMonologue(firstMsg.from.id); }}
|
|
503
|
+
className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-purple-600 text-[8px] flex items-center justify-center opacity-0 group-hover/avatar:opacity-100 transition-opacity hover:bg-purple-500 shadow-lg"
|
|
504
|
+
title={t('reqDetail.members.peekFlow')}
|
|
505
|
+
>🧠</button>
|
|
506
|
+
)}
|
|
507
|
+
</div>
|
|
508
|
+
<div className="flex-1 min-w-0">
|
|
509
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
510
|
+
<span
|
|
511
|
+
className={`text-xs font-medium ${firstMsg.from?.id && firstMsg.from.id !== 'system' ? 'cursor-pointer hover:text-[var(--accent)] transition-colors' : ''}`}
|
|
512
|
+
onClick={() => firstMsg.from?.id && firstMsg.from.id !== 'system' && setSelectedAgentId(firstMsg.from.id)}
|
|
513
|
+
>{firstMsg.from?.name}</span>
|
|
514
|
+
{firstMsg.from?.role && (
|
|
515
|
+
<span className="text-[10px] text-[var(--muted)] bg-white/5 px-1 py-0.5 rounded">{firstMsg.from.role}</span>
|
|
516
|
+
)}
|
|
517
|
+
{firstMsg.auto && (
|
|
518
|
+
<span className="text-[9px] text-yellow-500/70 bg-yellow-900/20 px-1 py-0.5 rounded">{t('systemSettings.autoSent')}</span>
|
|
519
|
+
)}
|
|
520
|
+
<span className="text-[10px] text-[var(--muted)]">
|
|
521
|
+
{new Date(firstMsg.time).toLocaleTimeString('zh', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
|
522
|
+
</span>
|
|
523
|
+
</div>
|
|
524
|
+
{isMerged ? (
|
|
525
|
+
<div className="rounded-2xl rounded-tl-sm px-3 py-2 text-sm inline-block max-w-[min(85%,600px)] bg-[var(--card)] border border-[var(--border)]">
|
|
526
|
+
{group.messages.map((msg, mi) => (
|
|
527
|
+
<div key={msg.id}>
|
|
528
|
+
{mi > 0 && <div className="border-t border-white/[0.06] my-1.5" />}
|
|
529
|
+
{renderMessageContent(msg.content)}
|
|
530
|
+
</div>
|
|
531
|
+
))}
|
|
532
|
+
</div>
|
|
533
|
+
) : (
|
|
534
|
+
firstMsg.type === 'monologue' ? (
|
|
535
|
+
<div className="rounded-2xl rounded-tl-sm px-3 py-2 text-sm inline-block max-w-[min(85%,600px)] bg-purple-900/10 border border-purple-500/15 border-dashed opacity-75">
|
|
536
|
+
<div className="flex items-center gap-1 mb-1">
|
|
537
|
+
<span className="text-[10px] text-purple-400">{t('systemSettings.monologue')}</span>
|
|
538
|
+
</div>
|
|
539
|
+
<div className="break-words text-sm leading-relaxed italic text-purple-200/80 whitespace-pre-wrap">
|
|
540
|
+
{cleanMessageContent(firstMsg.content)}
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
) : (
|
|
544
|
+
<div className={`rounded-2xl rounded-tl-sm px-3 py-2 text-sm inline-block max-w-[min(85%,600px)] ${
|
|
545
|
+
firstMsg.type === 'output'
|
|
546
|
+
? 'bg-green-900/20 border border-green-500/20'
|
|
547
|
+
: firstMsg.type === 'tool_call'
|
|
548
|
+
? 'bg-purple-900/20 border border-purple-500/20'
|
|
549
|
+
: 'bg-[var(--card)] border border-[var(--border)]'
|
|
550
|
+
}`}>
|
|
551
|
+
{renderMessageContent(firstMsg.content)}
|
|
552
|
+
</div>
|
|
553
|
+
)
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
);
|
|
558
|
+
})}
|
|
559
|
+
|
|
560
|
+
{/* Typing indicator */}
|
|
561
|
+
{sending && leaderInfo && (
|
|
562
|
+
<div className="flex gap-2">
|
|
563
|
+
{leaderInfo.avatar ? (
|
|
564
|
+
<CachedAvatar src={leaderInfo.avatar} alt={leaderInfo.name} className="w-8 h-8 rounded-lg bg-[var(--border)] shrink-0 mt-0.5" />
|
|
565
|
+
) : (
|
|
566
|
+
<div className={`w-8 h-8 rounded-lg ${nameToColor(leaderInfo.name)} flex items-center justify-center text-xs shrink-0 mt-0.5`}>
|
|
567
|
+
{leaderInfo.name?.charAt(0) || '🤖'}
|
|
568
|
+
</div>
|
|
569
|
+
)}
|
|
570
|
+
<div className="bg-[var(--card)] border border-[var(--border)] rounded-2xl rounded-bl-sm px-3 py-2 text-sm">
|
|
571
|
+
<span className="animate-pulse text-[var(--muted)]">{leaderInfo.name} {t('mailbox.thinkingReply')}</span>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
)}
|
|
575
|
+
</>
|
|
576
|
+
)}
|
|
577
|
+
|
|
578
|
+
<div ref={chatEndRef} />
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
{/* 心流中的员工提示条 — 轻量:只显示谁在思考/谁刚思考完,点名字看详情 */}
|
|
582
|
+
{activeThinking.length > 0 && (() => {
|
|
583
|
+
const thinking = activeThinking.filter(a => a.status === 'thinking');
|
|
584
|
+
const done = activeThinking.filter(a => a.status === 'done');
|
|
585
|
+
if (thinking.length === 0 && done.length === 0) return null;
|
|
586
|
+
return (
|
|
587
|
+
<div className="px-4 py-1.5 bg-purple-900/15 border-t border-purple-500/20 shrink-0 flex items-center flex-wrap gap-x-3 gap-y-1 text-xs">
|
|
588
|
+
{thinking.length > 0 && (
|
|
589
|
+
<span className="flex items-center gap-1 text-purple-300 animate-pulse">
|
|
590
|
+
🧠{' '}
|
|
591
|
+
{thinking.map((a, i) => (
|
|
592
|
+
<span key={a.agentId}>
|
|
593
|
+
{i > 0 && '、'}
|
|
594
|
+
<span
|
|
595
|
+
className="text-purple-200 cursor-pointer hover:underline"
|
|
596
|
+
onClick={() => peekMonologue(a.agentId)}
|
|
597
|
+
>{a.agentName}</span>
|
|
598
|
+
</span>
|
|
599
|
+
))}
|
|
600
|
+
{' '}{t('reqDetail.flowPeek.thinking')}
|
|
601
|
+
</span>
|
|
602
|
+
)}
|
|
603
|
+
{done.map(a => (
|
|
604
|
+
<span key={a.agentId} className="flex items-center gap-1 text-[var(--muted)]">
|
|
605
|
+
<span
|
|
606
|
+
className="text-purple-200/70 cursor-pointer hover:underline"
|
|
607
|
+
onClick={() => peekMonologue(a.agentId)}
|
|
608
|
+
>{a.agentName}</span>
|
|
609
|
+
<span className={`text-[10px] ${a.decision === 'spoke' ? 'text-green-400' : 'text-gray-500'}`}>
|
|
610
|
+
{a.decision === 'spoke' ? '💬' : '🤫'}
|
|
611
|
+
</span>
|
|
612
|
+
</span>
|
|
613
|
+
))}
|
|
614
|
+
</div>
|
|
615
|
+
);
|
|
616
|
+
})()}
|
|
617
|
+
|
|
618
|
+
{/* 发消息输入框 */}
|
|
619
|
+
{requirementId && onSendMessage && (
|
|
620
|
+
<div className={`${embedded ? 'sticky bottom-0' : ''} pt-3 px-4 pb-4`}>
|
|
621
|
+
<div className="flex items-center gap-2 bg-[var(--card)] border border-[var(--border)] rounded-xl px-3 py-2">
|
|
622
|
+
<input
|
|
623
|
+
type="text"
|
|
624
|
+
value={inputText}
|
|
625
|
+
onChange={(e) => setInputText(e.target.value)}
|
|
626
|
+
onKeyDown={(e) => {
|
|
627
|
+
if (e.key === 'Enter' && !e.shiftKey && inputText.trim() && !sending) {
|
|
628
|
+
e.preventDefault();
|
|
629
|
+
doSend();
|
|
630
|
+
}
|
|
631
|
+
}}
|
|
632
|
+
placeholder={inputPlaceholder || t('mailbox.groupChatInput')}
|
|
633
|
+
disabled={sending}
|
|
634
|
+
className="flex-1 bg-transparent text-sm outline-none placeholder:text-[var(--muted)] disabled:opacity-50"
|
|
635
|
+
/>
|
|
636
|
+
<button
|
|
637
|
+
onClick={doSend}
|
|
638
|
+
disabled={!inputText.trim() || sending}
|
|
639
|
+
className="text-sm px-3 py-1 rounded-lg bg-[var(--accent)] text-white disabled:opacity-40 hover:opacity-90 transition-opacity"
|
|
640
|
+
>
|
|
641
|
+
{sending ? '✉️' : t('mailbox.sendBtn')}
|
|
642
|
+
</button>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
)}
|
|
646
|
+
|
|
647
|
+
{/* 心流偷看弹窗 */}
|
|
648
|
+
{monologueAgentId && (
|
|
649
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" onClick={() => setMonologueAgentId(null)}>
|
|
650
|
+
<div className="w-full max-w-lg mx-4 bg-[var(--card)] border border-[var(--border)] rounded-2xl shadow-2xl max-h-[80vh] overflow-hidden flex flex-col" onClick={e => e.stopPropagation()}>
|
|
651
|
+
{/* 头部 */}
|
|
652
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)] bg-purple-900/20">
|
|
653
|
+
<div className="flex items-center gap-2">
|
|
654
|
+
<span className="text-lg">🧠</span>
|
|
655
|
+
<span className="font-medium text-sm">{t('reqDetail.flowPeek.title', { name: agentMap[monologueAgentId] || monologueAgentId })}</span>
|
|
656
|
+
</div>
|
|
657
|
+
<button onClick={() => setMonologueAgentId(null)} className="text-[var(--muted)] hover:text-white transition-colors text-lg">✕</button>
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
{/* Tab 切换 */}
|
|
661
|
+
<div className="flex border-b border-[var(--border)] px-4 bg-[var(--card)]">
|
|
662
|
+
{[
|
|
663
|
+
{ id: 'thoughts', label: t('reqDetail.flowPeek.tabThoughts'), badge: monologueHistory.length || monologueThoughtMsgs.length },
|
|
664
|
+
{ id: 'flow', label: t('reqDetail.flowPeek.tabFlow'), badge: monologueFlowMsgs.length },
|
|
665
|
+
].map(tab => (
|
|
666
|
+
<button
|
|
667
|
+
key={tab.id}
|
|
668
|
+
onClick={() => setMonologueTab(tab.id)}
|
|
669
|
+
className={`px-3 py-2 text-xs font-medium transition-all border-b-2 ${
|
|
670
|
+
monologueTab === tab.id
|
|
671
|
+
? 'border-purple-500 text-purple-300'
|
|
672
|
+
: 'border-transparent text-[var(--muted)] hover:text-white'
|
|
673
|
+
}`}
|
|
674
|
+
>
|
|
675
|
+
{tab.label}
|
|
676
|
+
{tab.badge > 0 && (
|
|
677
|
+
<span className="ml-1 text-[10px] bg-white/10 px-1.5 py-0.5 rounded-full">{tab.badge}</span>
|
|
678
|
+
)}
|
|
679
|
+
</button>
|
|
680
|
+
))}
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
{/* 内容 */}
|
|
684
|
+
<div className="flex-1 overflow-auto p-4 space-y-3">
|
|
685
|
+
{monologueLoading ? (
|
|
686
|
+
<div className="flex items-center justify-center py-8">
|
|
687
|
+
<span className="animate-spin text-2xl">🧠</span>
|
|
688
|
+
<span className="ml-2 text-sm text-[var(--muted)]">{t('reqDetail.flowPeek.loading')}</span>
|
|
689
|
+
</div>
|
|
690
|
+
) : monologueTab === 'thoughts' ? (
|
|
691
|
+
// 心流列表 — 优先用 monologueHistory(含 decision),fallback 到持久化消息
|
|
692
|
+
(() => {
|
|
693
|
+
const agentState = activeThinking.find(a => a.agentId === monologueAgentId);
|
|
694
|
+
const isThinking = agentState?.status === 'thinking' || monologueData?.status === 'thinking';
|
|
695
|
+
const hasHistory = monologueHistory.length > 0;
|
|
696
|
+
const hasThoughts = monologueThoughtMsgs.length > 0;
|
|
697
|
+
return hasHistory || hasThoughts || isThinking ? (
|
|
698
|
+
<div className="space-y-3">
|
|
699
|
+
{/* 正在进行中的思考 */}
|
|
700
|
+
{isThinking && (
|
|
701
|
+
<div className="flex items-center gap-2 p-2 rounded-lg bg-purple-500/10 border border-purple-500/20">
|
|
702
|
+
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-500/20 text-purple-300 animate-pulse">
|
|
703
|
+
{t('reqDetail.flowPeek.thinking')}
|
|
704
|
+
</span>
|
|
705
|
+
</div>
|
|
706
|
+
)}
|
|
707
|
+
{hasHistory ? (
|
|
708
|
+
/* 有 monologueHistory(本次运行的心流,含 decision) */
|
|
709
|
+
monologueHistory.slice().reverse().map((m, i) => (
|
|
710
|
+
<div key={m.id || i} className="bg-white/5 rounded-xl p-3 space-y-2">
|
|
711
|
+
<div className="flex items-center justify-between">
|
|
712
|
+
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
|
713
|
+
m.decision === 'spoke'
|
|
714
|
+
? 'bg-green-500/20 text-green-400'
|
|
715
|
+
: 'bg-gray-500/20 text-gray-400'
|
|
716
|
+
}`}>
|
|
717
|
+
{m.decision === 'spoke' ? t('reqDetail.flowPeek.spoke') : t('reqDetail.flowPeek.keptSilent')}
|
|
718
|
+
</span>
|
|
719
|
+
<span className="text-xs text-[var(--muted)]">
|
|
720
|
+
{new Date(m.startedAt).toLocaleTimeString('zh', { hour: '2-digit', minute: '2-digit' })}
|
|
721
|
+
</span>
|
|
722
|
+
</div>
|
|
723
|
+
{m.thoughts?.map((thought, ti) => (
|
|
724
|
+
<div key={thought.id || ti} className="text-sm text-[var(--muted)] bg-black/20 rounded-lg p-2">
|
|
725
|
+
{thought.content.startsWith('[Sent to group]') || thought.content.startsWith('[发送到群聊]') || thought.content.startsWith('[Self-regulation]') || thought.content.startsWith(t('systemSettings.sendToGroupChat'))
|
|
726
|
+
? <span className="text-green-400">{thought.content}</span>
|
|
727
|
+
: <span className="italic">{thought.content}</span>
|
|
728
|
+
}
|
|
729
|
+
</div>
|
|
730
|
+
))}
|
|
731
|
+
</div>
|
|
732
|
+
))
|
|
733
|
+
) : (
|
|
734
|
+
/* Fallback: 持久化的心流消息(无 decision,服务重启后仍可用) */
|
|
735
|
+
monologueThoughtMsgs.slice().reverse().map((msg, i) => (
|
|
736
|
+
<div key={msg.id || i} className="bg-purple-900/20 border border-purple-500/10 rounded-xl p-3">
|
|
737
|
+
<div className="flex items-center justify-between mb-1">
|
|
738
|
+
<span className="text-xs text-purple-400">{t('systemSettings.monologue')}</span>
|
|
739
|
+
<span className="text-xs text-[var(--muted)]">
|
|
740
|
+
{new Date(msg.time).toLocaleTimeString('zh')}
|
|
741
|
+
</span>
|
|
742
|
+
</div>
|
|
743
|
+
<div className="text-sm italic text-purple-200 whitespace-pre-wrap">
|
|
744
|
+
{cleanMessageContent(msg.content)}
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
))
|
|
748
|
+
)}
|
|
749
|
+
</div>
|
|
750
|
+
) : (
|
|
751
|
+
<div className="text-center py-8">
|
|
752
|
+
<div className="text-3xl mb-2">😴</div>
|
|
753
|
+
<p className="text-sm text-[var(--muted)]">{t('reqDetail.flowPeek.noMonologue')}</p>
|
|
754
|
+
<p className="text-xs text-[var(--muted)] mt-1">{t('systemSettings.noMonologueYet')}</p>
|
|
755
|
+
</div>
|
|
756
|
+
);
|
|
757
|
+
})()
|
|
758
|
+
) : monologueTab === 'flow' ? (
|
|
759
|
+
// 工作日志 — 该员工的 flow 可见性消息
|
|
760
|
+
monologueFlowMsgs.length === 0 ? (
|
|
761
|
+
<div className="text-center py-8 text-[var(--muted)] text-sm">
|
|
762
|
+
<div className="text-3xl mb-2">📋</div>
|
|
763
|
+
<p>{t('reqDetail.flowPeek.noFlowLogs')}</p>
|
|
764
|
+
<p className="text-xs mt-1">{t('reqDetail.flowPeek.noFlowLogsHint')}</p>
|
|
765
|
+
</div>
|
|
766
|
+
) : (
|
|
767
|
+
monologueFlowMsgs.map((msg, i) => (
|
|
768
|
+
<div key={msg.id || i} className={`rounded-xl p-3 text-sm ${
|
|
769
|
+
msg.type === 'tool_call'
|
|
770
|
+
? 'bg-purple-900/20 border border-purple-500/10'
|
|
771
|
+
: msg.type === 'output'
|
|
772
|
+
? 'bg-green-900/20 border border-green-500/10'
|
|
773
|
+
: 'bg-white/5 border border-white/10'
|
|
774
|
+
}`}>
|
|
775
|
+
<div className="flex items-center justify-between mb-1">
|
|
776
|
+
<span className="text-xs text-[var(--muted)]">
|
|
777
|
+
{msg.type === 'tool_call' ? '🔧' : msg.type === 'output' ? '📄' : '💬'}{' '}
|
|
778
|
+
{new Date(msg.time).toLocaleTimeString('zh')}
|
|
779
|
+
</span>
|
|
780
|
+
</div>
|
|
781
|
+
<div className="text-sm break-words">
|
|
782
|
+
{cleanMessageContent(msg.content)}
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
))
|
|
786
|
+
)
|
|
787
|
+
) : null}
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
)}
|
|
792
|
+
|
|
793
|
+
{/* 员工详情弹窗 */}
|
|
794
|
+
{selectedAgentId && (
|
|
795
|
+
<AgentDetailModal agentId={selectedAgentId} onClose={() => setSelectedAgentId(null)} />
|
|
796
|
+
)}
|
|
797
|
+
</div>
|
|
798
|
+
);
|
|
799
|
+
}
|