@amaster.ai/components-templates 1.4.11 → 1.6.0
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/README.md +12 -8
- package/components/ai-assistant/amaster.config.json +3 -0
- package/components/ai-assistant/package.json +3 -3
- package/components/ai-assistant/template/components/chat-assistant-message.tsx +1 -1
- package/components/ai-assistant/template/components/chat-banner.tsx +1 -1
- package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +6 -6
- package/components/ai-assistant/template/components/chat-floating-button.tsx +4 -5
- package/components/ai-assistant/template/components/chat-floating-card.tsx +3 -3
- package/components/ai-assistant/template/components/chat-header.tsx +5 -5
- package/components/ai-assistant/template/components/chat-input.tsx +21 -22
- package/components/ai-assistant/template/components/chat-messages.tsx +10 -2
- package/components/ai-assistant/template/components/chat-recommends.tsx +12 -14
- package/components/ai-assistant/template/components/chat-user-message.tsx +1 -1
- package/components/ai-assistant/template/components/ui-renderer.tsx +1 -1
- package/components/ai-assistant/template/components/voice-input.tsx +1 -1
- package/components/ai-assistant/template/hooks/useVoiceInput.ts +18 -20
- package/components/ai-assistant/template/inline-ai-assistant.tsx +1 -1
- package/components/ai-assistant-taro/amaster.config.json +3 -0
- package/components/ai-assistant-taro/package.json +94 -0
- package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +154 -0
- package/components/ai-assistant-taro/template/components/ChatHeader.tsx +27 -0
- package/components/ai-assistant-taro/template/components/ChatInput.tsx +204 -0
- package/components/ai-assistant-taro/template/components/ChatMessages.tsx +126 -0
- package/components/ai-assistant-taro/template/components/ChatUserMessage.tsx +25 -0
- package/components/ai-assistant-taro/template/components/VoiceInput.tsx +169 -0
- package/components/ai-assistant-taro/template/components/markdown.tsx +156 -0
- package/components/ai-assistant-taro/template/hooks/useConversation.ts +787 -0
- package/components/ai-assistant-taro/template/hooks/useSafeArea.ts +20 -0
- package/components/ai-assistant-taro/template/hooks/useVoiceInput.ts +204 -0
- package/components/ai-assistant-taro/template/i18n.ts +157 -0
- package/components/ai-assistant-taro/template/index.config.ts +10 -0
- package/components/ai-assistant-taro/template/index.tsx +83 -0
- package/components/ai-assistant-taro/template/types.ts +58 -0
- package/package.json +5 -2
- package/packages/cli/dist/index.js +14 -3
- package/packages/cli/dist/index.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/components/ai-assistant/example.md +0 -34
- package/components/ai-assistant/others.md +0 -16
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SendStreamingMessageResponse,
|
|
3
|
+
SendStreamingMessageSuccessResponse,
|
|
4
|
+
} from "@a2a-js/sdk";
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { client } from "../../../lib/client";
|
|
7
|
+
import { useAiAssistantI18n } from "../i18n";
|
|
8
|
+
import type {
|
|
9
|
+
Conversation,
|
|
10
|
+
ErrorMessage,
|
|
11
|
+
MessagesItem,
|
|
12
|
+
Role,
|
|
13
|
+
TextMessage,
|
|
14
|
+
ThoughtMessage,
|
|
15
|
+
ToolMessage,
|
|
16
|
+
} from "../types";
|
|
17
|
+
|
|
18
|
+
class SimpleAbortController {
|
|
19
|
+
aborted = false;
|
|
20
|
+
private listeners: (() => void)[] = [];
|
|
21
|
+
|
|
22
|
+
signal = {
|
|
23
|
+
aborted: false,
|
|
24
|
+
addEventListener: (_: "abort", cb: () => void) => {
|
|
25
|
+
this.listeners.push(cb);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
abort() {
|
|
30
|
+
if (this.aborted) return;
|
|
31
|
+
this.aborted = true;
|
|
32
|
+
(this.signal as { aborted: boolean }).aborted = true;
|
|
33
|
+
this.listeners.forEach((cb) => {
|
|
34
|
+
cb();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createAbortController(): AbortController | SimpleAbortController {
|
|
40
|
+
if (typeof AbortController !== "undefined") {
|
|
41
|
+
return new AbortController();
|
|
42
|
+
}
|
|
43
|
+
return new SimpleAbortController();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UpdateMessageInput {
|
|
47
|
+
taskId: string;
|
|
48
|
+
messageId: string;
|
|
49
|
+
content?: string;
|
|
50
|
+
partial?: boolean;
|
|
51
|
+
status?: string;
|
|
52
|
+
response?: any;
|
|
53
|
+
metadata?: Record<string, any>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ================= 消息处理器 =================
|
|
57
|
+
|
|
58
|
+
type MessageHandler = (
|
|
59
|
+
conv: Conversation,
|
|
60
|
+
part: any,
|
|
61
|
+
id: string,
|
|
62
|
+
role: Role,
|
|
63
|
+
status?: any,
|
|
64
|
+
) => Conversation;
|
|
65
|
+
|
|
66
|
+
const handlers: Record<string, MessageHandler> = {
|
|
67
|
+
"text-content": (conv, part, id, role, status) => {
|
|
68
|
+
if (!id) return conv;
|
|
69
|
+
const existing = conv.messages.find((m) => m.messageId === id);
|
|
70
|
+
const newText = part.text || "";
|
|
71
|
+
if (existing && "content" in existing) {
|
|
72
|
+
return {
|
|
73
|
+
...conv,
|
|
74
|
+
messages: conv.messages.map((m) =>
|
|
75
|
+
m.messageId === id
|
|
76
|
+
? { ...m, content: (m as TextMessage).content + newText }
|
|
77
|
+
: m,
|
|
78
|
+
),
|
|
79
|
+
lastUpdated: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...conv,
|
|
85
|
+
messages: [
|
|
86
|
+
...conv.messages,
|
|
87
|
+
{
|
|
88
|
+
messageId: id,
|
|
89
|
+
role,
|
|
90
|
+
kind: "text-content",
|
|
91
|
+
content: newText,
|
|
92
|
+
timestamp: status.timestamp || new Date().toISOString(),
|
|
93
|
+
} as TextMessage,
|
|
94
|
+
],
|
|
95
|
+
lastUpdated: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// thought 类型(逐步追加 description)
|
|
100
|
+
thought: (conv, part, id, role, status) => {
|
|
101
|
+
if (!id) return conv;
|
|
102
|
+
const newChunk = part?.data?.description || "";
|
|
103
|
+
const existing = conv.messages.find((m) => m.messageId === id);
|
|
104
|
+
if (existing && "thought" in existing) {
|
|
105
|
+
return {
|
|
106
|
+
...conv,
|
|
107
|
+
messages: conv.messages.map((m) =>
|
|
108
|
+
m.messageId === id
|
|
109
|
+
? { ...m, thought: (m as ThoughtMessage).thought + newChunk }
|
|
110
|
+
: m,
|
|
111
|
+
),
|
|
112
|
+
lastUpdated: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...conv,
|
|
118
|
+
messages: [
|
|
119
|
+
...conv.messages,
|
|
120
|
+
{
|
|
121
|
+
messageId: id,
|
|
122
|
+
role,
|
|
123
|
+
kind: "thought",
|
|
124
|
+
thought: newChunk,
|
|
125
|
+
timestamp: status?.timestamp || new Date().toISOString(),
|
|
126
|
+
} as ThoughtMessage,
|
|
127
|
+
],
|
|
128
|
+
lastUpdated: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// 工具调用(状态变更 + 最终结果)
|
|
133
|
+
tool: (conv, part, _id, role, status) => {
|
|
134
|
+
const partData = part?.data || {};
|
|
135
|
+
const tool = partData?.tool;
|
|
136
|
+
const name = tool?.displayName || tool?.name || "";
|
|
137
|
+
const request = partData?.request;
|
|
138
|
+
const id = request?.callId;
|
|
139
|
+
if (!id || !name) return conv; // 必须有 callId 来识别消息
|
|
140
|
+
|
|
141
|
+
const toolStatus = partData?.status;
|
|
142
|
+
const existing = conv.messages.find((m) => m.messageId === id);
|
|
143
|
+
|
|
144
|
+
if (existing && "toolStatus" in existing) {
|
|
145
|
+
return {
|
|
146
|
+
...conv,
|
|
147
|
+
messages: conv.messages.map((m) =>
|
|
148
|
+
m.messageId === id
|
|
149
|
+
? {
|
|
150
|
+
...m,
|
|
151
|
+
toolStatus: toolStatus || (m as ToolMessage).toolStatus,
|
|
152
|
+
}
|
|
153
|
+
: m,
|
|
154
|
+
),
|
|
155
|
+
lastUpdated: new Date().toISOString(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...conv,
|
|
161
|
+
messages: [
|
|
162
|
+
...conv.messages,
|
|
163
|
+
{
|
|
164
|
+
messageId: id,
|
|
165
|
+
role,
|
|
166
|
+
kind: "tool",
|
|
167
|
+
toolName: name,
|
|
168
|
+
toolDescription: request?.args?.query || "",
|
|
169
|
+
toolStatus: toolStatus || "pending",
|
|
170
|
+
timestamp: status?.timestamp || new Date().toISOString(),
|
|
171
|
+
} as ToolMessage,
|
|
172
|
+
],
|
|
173
|
+
lastUpdated: new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
error: (conv, part, id, role) => {
|
|
178
|
+
const errorMsg = part?.text || part?.data?.error || "发生错误";
|
|
179
|
+
return {
|
|
180
|
+
...conv,
|
|
181
|
+
messages: [
|
|
182
|
+
...conv.messages,
|
|
183
|
+
{
|
|
184
|
+
messageId: id || `error-${Date.now()}`,
|
|
185
|
+
role,
|
|
186
|
+
kind: "error",
|
|
187
|
+
content: errorMsg,
|
|
188
|
+
timestamp: new Date().toISOString(),
|
|
189
|
+
} as ErrorMessage,
|
|
190
|
+
],
|
|
191
|
+
status: "error",
|
|
192
|
+
lastUpdated: new Date().toISOString(),
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// 用于生成唯一 assistant message id
|
|
198
|
+
const generateId = () =>
|
|
199
|
+
`msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
200
|
+
|
|
201
|
+
// ================= 历史消息处理 =================
|
|
202
|
+
|
|
203
|
+
interface HistoryState {
|
|
204
|
+
next: string | null;
|
|
205
|
+
hasMore: boolean;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface HistoryMessage {
|
|
209
|
+
messageId: string;
|
|
210
|
+
historyId?: string;
|
|
211
|
+
role: string;
|
|
212
|
+
parts?: any[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ================= 主 Hook =================
|
|
216
|
+
|
|
217
|
+
export function useConversationProcessor() {
|
|
218
|
+
const [conversationsMap, setConversationsMap] = useState<
|
|
219
|
+
Map<string, Conversation>
|
|
220
|
+
>(new Map());
|
|
221
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
222
|
+
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
|
223
|
+
const [historyState, setHistoryState] = useState<HistoryState>({
|
|
224
|
+
next: null,
|
|
225
|
+
hasMore: false,
|
|
226
|
+
});
|
|
227
|
+
const [starting, setStarting] = useState(true);
|
|
228
|
+
const { t } = useAiAssistantI18n();
|
|
229
|
+
const abortControllerRef = useRef<
|
|
230
|
+
AbortController | SimpleAbortController | null
|
|
231
|
+
>(null);
|
|
232
|
+
const forceStopRef = useRef(false);
|
|
233
|
+
const lastMessageTypeRef = useRef<string>("");
|
|
234
|
+
const lastMessageIdRef = useRef<string>("");
|
|
235
|
+
const currentStreamingTaskIdRef = useRef<string | null>(null);
|
|
236
|
+
const loadingRef = useRef(false);
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
loadingRef.current = isLoading;
|
|
240
|
+
}, [isLoading]);
|
|
241
|
+
|
|
242
|
+
const handleEnd = useCallback(() => {
|
|
243
|
+
setIsLoading(false);
|
|
244
|
+
lastMessageTypeRef.current = "";
|
|
245
|
+
lastMessageIdRef.current = "";
|
|
246
|
+
currentStreamingTaskIdRef.current = null;
|
|
247
|
+
}, []);
|
|
248
|
+
|
|
249
|
+
const abort = useCallback(() => {
|
|
250
|
+
if (abortControllerRef.current) {
|
|
251
|
+
abortControllerRef.current.abort();
|
|
252
|
+
abortControllerRef.current = null;
|
|
253
|
+
}
|
|
254
|
+
forceStopRef.current = true;
|
|
255
|
+
setIsLoading(false);
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
// 处理历史消息数据
|
|
259
|
+
const processHistoryData = useCallback(
|
|
260
|
+
(data: HistoryMessage, taskId: string) => {
|
|
261
|
+
const { messageId, historyId } = data || {};
|
|
262
|
+
if (!messageId) return;
|
|
263
|
+
|
|
264
|
+
const parts = data.parts || [];
|
|
265
|
+
const firstPart = parts?.[0];
|
|
266
|
+
|
|
267
|
+
let kind = "unknown";
|
|
268
|
+
if (firstPart?.data?.tool) {
|
|
269
|
+
kind = "tool";
|
|
270
|
+
} else if (firstPart?.data?.type === "ui") {
|
|
271
|
+
kind = "ui-render";
|
|
272
|
+
} else if (firstPart?.kind === "text") {
|
|
273
|
+
kind = "text-content";
|
|
274
|
+
} else if (firstPart?.data?.type === "though") {
|
|
275
|
+
kind = "thought";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const handler = handlers[kind];
|
|
279
|
+
if (!handler) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 对于历史消息,我们需要用不同的方式处理
|
|
284
|
+
setConversationsMap((prev) => {
|
|
285
|
+
const prevConv = prev.get(taskId) ?? {
|
|
286
|
+
taskId,
|
|
287
|
+
status: "completed" as const,
|
|
288
|
+
messages: [],
|
|
289
|
+
historyId,
|
|
290
|
+
lastUpdated: new Date().toISOString(),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const role = data.role === "agent" ? "assistant" : "user";
|
|
294
|
+
|
|
295
|
+
const nextConv = handler(prevConv, firstPart, messageId, role, {
|
|
296
|
+
message: data,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const nextMap = new Map(prev);
|
|
300
|
+
nextMap.set(taskId, {
|
|
301
|
+
...nextConv,
|
|
302
|
+
lastUpdated: new Date().toISOString(),
|
|
303
|
+
});
|
|
304
|
+
return nextMap;
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
[],
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const processDataLine = useCallback((data: SendStreamingMessageResponse) => {
|
|
311
|
+
if (!(data as any)?.result) return;
|
|
312
|
+
const result = (data as SendStreamingMessageSuccessResponse).result;
|
|
313
|
+
|
|
314
|
+
const { taskId, status, metadata, final } = result as any;
|
|
315
|
+
|
|
316
|
+
if (final) {
|
|
317
|
+
handleEnd();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!taskId) return;
|
|
322
|
+
|
|
323
|
+
const parts = status?.message?.parts || [];
|
|
324
|
+
const part = parts?.[0];
|
|
325
|
+
|
|
326
|
+
let kind = "unknown";
|
|
327
|
+
if (part?.data?.tool) {
|
|
328
|
+
kind = "tool";
|
|
329
|
+
} else if (part?.data?.type === "ui") {
|
|
330
|
+
kind = "ui-render";
|
|
331
|
+
} else if (part?.kind === "text") {
|
|
332
|
+
kind = "text-content";
|
|
333
|
+
} else if (
|
|
334
|
+
part?.data?.type === "though" ||
|
|
335
|
+
metadata?.coderAgent?.kind === "thought"
|
|
336
|
+
) {
|
|
337
|
+
kind = "thought";
|
|
338
|
+
} else if (metadata?.error) {
|
|
339
|
+
kind = "error";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const handler = handlers[kind];
|
|
343
|
+
if (!handler) {
|
|
344
|
+
console.warn("Unhandled message type", result);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (kind && kind !== lastMessageTypeRef.current) {
|
|
349
|
+
lastMessageTypeRef.current = kind;
|
|
350
|
+
lastMessageIdRef.current = generateId();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
removeLoadingPlaceholder();
|
|
354
|
+
|
|
355
|
+
setConversationsMap((prev) => {
|
|
356
|
+
const prevConv = prev.get(taskId) ?? {
|
|
357
|
+
taskId,
|
|
358
|
+
status: "submitted" as const,
|
|
359
|
+
messages: [],
|
|
360
|
+
lastUpdated: new Date().toISOString(),
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const nextConv = handler(
|
|
364
|
+
prevConv,
|
|
365
|
+
part,
|
|
366
|
+
lastMessageIdRef.current,
|
|
367
|
+
(status?.message?.role === "agent" ? "assistant" : "") || "assistant",
|
|
368
|
+
status,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const nextMap = new Map(prev);
|
|
372
|
+
|
|
373
|
+
nextMap.set(taskId, {
|
|
374
|
+
...nextConv,
|
|
375
|
+
status: status?.state || prevConv.status,
|
|
376
|
+
lastUpdated: new Date().toISOString(),
|
|
377
|
+
});
|
|
378
|
+
return nextMap;
|
|
379
|
+
});
|
|
380
|
+
}, []);
|
|
381
|
+
|
|
382
|
+
const conversations = useMemo(() => {
|
|
383
|
+
return Array.from(conversationsMap.values());
|
|
384
|
+
}, [conversationsMap]);
|
|
385
|
+
|
|
386
|
+
const getConversation = useCallback(
|
|
387
|
+
(taskId: string) => {
|
|
388
|
+
return conversationsMap.get(taskId);
|
|
389
|
+
},
|
|
390
|
+
[conversationsMap],
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const clearConversation = useCallback((taskId: string) => {
|
|
394
|
+
setConversationsMap((prev) => {
|
|
395
|
+
if (!prev.has(taskId)) return prev;
|
|
396
|
+
const next = new Map(prev);
|
|
397
|
+
next.delete(taskId);
|
|
398
|
+
return next;
|
|
399
|
+
});
|
|
400
|
+
}, []);
|
|
401
|
+
|
|
402
|
+
const removeMessage = useCallback((taskId: string, messageId: string) => {
|
|
403
|
+
setConversationsMap((prev) => {
|
|
404
|
+
const conv = prev.get(taskId);
|
|
405
|
+
if (!conv) return prev;
|
|
406
|
+
|
|
407
|
+
const nextConv = {
|
|
408
|
+
...conv,
|
|
409
|
+
messages: conv.messages.filter((m) => m.messageId !== messageId),
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const nextMap = new Map(prev);
|
|
413
|
+
nextMap.set(taskId, nextConv);
|
|
414
|
+
return nextMap;
|
|
415
|
+
});
|
|
416
|
+
}, []);
|
|
417
|
+
|
|
418
|
+
// 新增:更新已有消息
|
|
419
|
+
const updateMessage = useCallback((input: UpdateMessageInput) => {
|
|
420
|
+
const {
|
|
421
|
+
taskId,
|
|
422
|
+
messageId,
|
|
423
|
+
content,
|
|
424
|
+
partial = false,
|
|
425
|
+
status,
|
|
426
|
+
response,
|
|
427
|
+
metadata = {},
|
|
428
|
+
} = input;
|
|
429
|
+
|
|
430
|
+
setConversationsMap((prev) => {
|
|
431
|
+
const conv = prev.get(taskId);
|
|
432
|
+
if (!conv) return prev;
|
|
433
|
+
|
|
434
|
+
const msgIndex = conv.messages.findIndex(
|
|
435
|
+
(m) => m.messageId === messageId,
|
|
436
|
+
);
|
|
437
|
+
if (msgIndex === -1) {
|
|
438
|
+
console.warn(
|
|
439
|
+
`Message ${messageId} not found in conversation ${taskId}`,
|
|
440
|
+
);
|
|
441
|
+
return prev;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const oldMsg = conv.messages[msgIndex];
|
|
445
|
+
const updatedMsg: MessagesItem = { ...oldMsg, ...metadata };
|
|
446
|
+
|
|
447
|
+
// 处理文本 / thought 内容更新
|
|
448
|
+
if (content !== undefined) {
|
|
449
|
+
if ("content" in updatedMsg) {
|
|
450
|
+
updatedMsg.content = partial
|
|
451
|
+
? (updatedMsg as TextMessage).content + content
|
|
452
|
+
: content;
|
|
453
|
+
} else if ("thought" in updatedMsg) {
|
|
454
|
+
updatedMsg.thought = partial
|
|
455
|
+
? (updatedMsg as ThoughtMessage).thought + content
|
|
456
|
+
: content;
|
|
457
|
+
} else if (oldMsg.kind === "text-content") {
|
|
458
|
+
// 兼容旧消息类型
|
|
459
|
+
(updatedMsg as any).content = partial
|
|
460
|
+
? ((oldMsg as any).content || "") + content
|
|
461
|
+
: content;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 处理 tool 消息的特殊字段
|
|
466
|
+
if ("toolStatus" in updatedMsg || status) {
|
|
467
|
+
(updatedMsg as any).toolStatus =
|
|
468
|
+
status || (updatedMsg as any).toolStatus;
|
|
469
|
+
}
|
|
470
|
+
if (response !== undefined) {
|
|
471
|
+
(updatedMsg as any).response = response;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const nextMessages = [...conv.messages];
|
|
475
|
+
nextMessages[msgIndex] = updatedMsg;
|
|
476
|
+
|
|
477
|
+
const nextConv: Conversation = {
|
|
478
|
+
...conv,
|
|
479
|
+
messages: nextMessages,
|
|
480
|
+
lastUpdated: new Date().toISOString(),
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const nextMap = new Map(prev);
|
|
484
|
+
nextMap.set(taskId, nextConv);
|
|
485
|
+
return nextMap;
|
|
486
|
+
});
|
|
487
|
+
}, []);
|
|
488
|
+
|
|
489
|
+
const addConversation = useCallback((conversation: Conversation) => {
|
|
490
|
+
setConversationsMap((prev) => {
|
|
491
|
+
const nextMap = new Map(prev);
|
|
492
|
+
const id = conversation.taskId;
|
|
493
|
+
nextMap.set(id, conversation);
|
|
494
|
+
return nextMap;
|
|
495
|
+
});
|
|
496
|
+
}, []);
|
|
497
|
+
|
|
498
|
+
const addLoadingPlaceholder = useCallback(() => {
|
|
499
|
+
addConversation({
|
|
500
|
+
taskId: "loading-placeholder",
|
|
501
|
+
status: "working",
|
|
502
|
+
messages: [
|
|
503
|
+
{
|
|
504
|
+
messageId: "loading-placeholder",
|
|
505
|
+
role: "assistant",
|
|
506
|
+
kind: "text-content",
|
|
507
|
+
content: t.thinking,
|
|
508
|
+
timestamp: new Date().toISOString(),
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
lastUpdated: new Date().toISOString(),
|
|
512
|
+
});
|
|
513
|
+
}, [t.thinking]);
|
|
514
|
+
|
|
515
|
+
useEffect(() => {
|
|
516
|
+
if (!isLoading) {
|
|
517
|
+
removeLoadingPlaceholder();
|
|
518
|
+
}
|
|
519
|
+
}, [isLoading]);
|
|
520
|
+
|
|
521
|
+
const removeLoadingPlaceholder = useCallback(() => {
|
|
522
|
+
clearConversation("loading-placeholder");
|
|
523
|
+
}, []);
|
|
524
|
+
|
|
525
|
+
// 发送用户消息,启动流式对话
|
|
526
|
+
const sendMessage = useCallback(
|
|
527
|
+
async (userContent: string, _placeholderText?: string) => {
|
|
528
|
+
if (!userContent.trim()) return;
|
|
529
|
+
|
|
530
|
+
const taskId = `conv-${Date.now()}`;
|
|
531
|
+
|
|
532
|
+
// 添加用户消息
|
|
533
|
+
const userMsgId = generateId();
|
|
534
|
+
|
|
535
|
+
addConversation({
|
|
536
|
+
taskId,
|
|
537
|
+
status: "submitted",
|
|
538
|
+
messages: [
|
|
539
|
+
{
|
|
540
|
+
messageId: userMsgId,
|
|
541
|
+
role: "user",
|
|
542
|
+
kind: "text-content",
|
|
543
|
+
content: userContent,
|
|
544
|
+
timestamp: new Date().toISOString(),
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
lastUpdated: new Date().toISOString(),
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
addLoadingPlaceholder();
|
|
551
|
+
|
|
552
|
+
setIsLoading(true);
|
|
553
|
+
|
|
554
|
+
if (abortControllerRef.current) {
|
|
555
|
+
abortControllerRef.current.abort();
|
|
556
|
+
}
|
|
557
|
+
abortControllerRef.current = createAbortController();
|
|
558
|
+
const controller = abortControllerRef.current;
|
|
559
|
+
|
|
560
|
+
forceStopRef.current = false;
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const stream = client.copilot.chat([
|
|
564
|
+
{ role: "user", content: userContent },
|
|
565
|
+
]);
|
|
566
|
+
|
|
567
|
+
for await (const chunk of stream) {
|
|
568
|
+
if (controller?.signal.aborted || forceStopRef.current) break;
|
|
569
|
+
|
|
570
|
+
// 根据你的实际 stream 返回格式调整
|
|
571
|
+
processDataLine(chunk);
|
|
572
|
+
|
|
573
|
+
// 检查是否结束
|
|
574
|
+
if ((chunk as any)?.isFinal || (chunk as any)?.final) {
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} catch (err: any) {
|
|
579
|
+
if (err.name === "AbortError" || forceStopRef.current) {
|
|
580
|
+
console.log("[Chat] Request aborted");
|
|
581
|
+
} else {
|
|
582
|
+
console.error("[Chat] Stream error:", err);
|
|
583
|
+
const id = generateId();
|
|
584
|
+
addConversation({
|
|
585
|
+
taskId: id,
|
|
586
|
+
status: "error",
|
|
587
|
+
messages: [
|
|
588
|
+
{
|
|
589
|
+
messageId: `error-${Date.now()}`,
|
|
590
|
+
role: "assistant",
|
|
591
|
+
kind: "error",
|
|
592
|
+
content: t.errorMessage,
|
|
593
|
+
timestamp: new Date().toISOString(),
|
|
594
|
+
} as ErrorMessage,
|
|
595
|
+
],
|
|
596
|
+
lastUpdated: new Date().toISOString(),
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
} finally {
|
|
600
|
+
setIsLoading(false);
|
|
601
|
+
if (abortControllerRef.current === controller) {
|
|
602
|
+
abortControllerRef.current = null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
[processDataLine],
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const resetConversation = useCallback(
|
|
610
|
+
(welcome?: string) => {
|
|
611
|
+
abort();
|
|
612
|
+
|
|
613
|
+
setConversationsMap(new Map());
|
|
614
|
+
setIsLoading(false);
|
|
615
|
+
setHistoryState({ next: null, hasMore: false });
|
|
616
|
+
|
|
617
|
+
if (welcome) {
|
|
618
|
+
const taskId = `conv-${Date.now()}`;
|
|
619
|
+
setConversationsMap(
|
|
620
|
+
new Map([
|
|
621
|
+
[
|
|
622
|
+
taskId,
|
|
623
|
+
{
|
|
624
|
+
taskId,
|
|
625
|
+
status: "submitted",
|
|
626
|
+
messages: [
|
|
627
|
+
{
|
|
628
|
+
messageId: `greeting-${Date.now()}`,
|
|
629
|
+
role: "assistant",
|
|
630
|
+
kind: "text-content",
|
|
631
|
+
content: welcome,
|
|
632
|
+
timestamp: new Date().toISOString(),
|
|
633
|
+
} as TextMessage,
|
|
634
|
+
],
|
|
635
|
+
lastUpdated: new Date().toISOString(),
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
]),
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
[abort],
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
const loadHistory = useCallback(
|
|
646
|
+
async (limit = 20, next?: string) => {
|
|
647
|
+
setIsLoadingHistory(true);
|
|
648
|
+
try {
|
|
649
|
+
const copilot = client.copilot as any;
|
|
650
|
+
const result = await copilot.getHistory?.(limit, next);
|
|
651
|
+
const messages: HistoryMessage[] = result?.messages || [];
|
|
652
|
+
|
|
653
|
+
let taskId = "";
|
|
654
|
+
let lastRole = "";
|
|
655
|
+
const taskIds: string[] = [];
|
|
656
|
+
|
|
657
|
+
messages.forEach((msg) => {
|
|
658
|
+
const role = msg.role === "agent" ? "assistant" : "user";
|
|
659
|
+
if (role !== lastRole) {
|
|
660
|
+
taskId = `history-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
661
|
+
taskIds.push(taskId);
|
|
662
|
+
lastRole = role;
|
|
663
|
+
}
|
|
664
|
+
processHistoryData(msg, taskId);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
setHistoryState({
|
|
668
|
+
next: result?.next || null,
|
|
669
|
+
hasMore: result?.hasMore || false,
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
return { taskIds, hasMore: result?.hasMore || false };
|
|
673
|
+
} catch (error) {
|
|
674
|
+
console.error("[Chat] Failed to load history:", error);
|
|
675
|
+
return { taskIds: [], hasMore: false };
|
|
676
|
+
} finally {
|
|
677
|
+
setIsLoadingHistory(false);
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
[processHistoryData],
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const loadMoreHistory = useCallback(async () => {
|
|
684
|
+
if (!historyState.hasMore || isLoadingHistory || !historyState.next) return;
|
|
685
|
+
await loadHistory(20, historyState.next || undefined);
|
|
686
|
+
}, [historyState.hasMore, historyState.next, isLoadingHistory, loadHistory]);
|
|
687
|
+
|
|
688
|
+
const checkActiveTask = useCallback(async () => {
|
|
689
|
+
try {
|
|
690
|
+
const result = await client.copilot.getChatStatus();
|
|
691
|
+
const taskId = result.taskId;
|
|
692
|
+
if (taskId && result.working) {
|
|
693
|
+
currentStreamingTaskIdRef.current = taskId;
|
|
694
|
+
setIsLoading(true);
|
|
695
|
+
|
|
696
|
+
const stream = await client.copilot.chat([], { taskId });
|
|
697
|
+
for await (const chunk of stream) {
|
|
698
|
+
if (forceStopRef.current) break;
|
|
699
|
+
processDataLine(chunk);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.error("[Chat] Failed to check active task:", error);
|
|
704
|
+
}
|
|
705
|
+
}, [processDataLine]);
|
|
706
|
+
|
|
707
|
+
const cancelChat = useCallback(async () => {
|
|
708
|
+
abort();
|
|
709
|
+
|
|
710
|
+
const targetTaskId = currentStreamingTaskIdRef.current;
|
|
711
|
+
if (!targetTaskId) return;
|
|
712
|
+
|
|
713
|
+
addConversation({
|
|
714
|
+
taskId: generateId(),
|
|
715
|
+
status: "completed",
|
|
716
|
+
messages: [
|
|
717
|
+
{
|
|
718
|
+
messageId: `cancel-${Date.now()}`,
|
|
719
|
+
role: "assistant",
|
|
720
|
+
kind: "text-content",
|
|
721
|
+
content: t.cancelled,
|
|
722
|
+
timestamp: new Date().toISOString(),
|
|
723
|
+
} as TextMessage,
|
|
724
|
+
],
|
|
725
|
+
lastUpdated: new Date().toISOString(),
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
await client.copilot.cancelChat(targetTaskId);
|
|
730
|
+
currentStreamingTaskIdRef.current = null;
|
|
731
|
+
} catch (error) {
|
|
732
|
+
console.error("[Chat] Failed to cancel chat:", error);
|
|
733
|
+
}
|
|
734
|
+
}, [abort, addConversation, t.cancelled]);
|
|
735
|
+
|
|
736
|
+
const startNewConversation = useCallback(async () => {
|
|
737
|
+
addConversation({
|
|
738
|
+
taskId: generateId(),
|
|
739
|
+
status: "submitted",
|
|
740
|
+
messages: [],
|
|
741
|
+
lastUpdated: new Date().toISOString(),
|
|
742
|
+
system: { level: "newConversation" },
|
|
743
|
+
});
|
|
744
|
+
try {
|
|
745
|
+
const copilot = client.copilot as any;
|
|
746
|
+
await copilot.newConversation?.();
|
|
747
|
+
} catch (error) {
|
|
748
|
+
console.error("[Chat] Failed to create new conversation:", error);
|
|
749
|
+
}
|
|
750
|
+
}, [addConversation]);
|
|
751
|
+
|
|
752
|
+
useEffect(() => {
|
|
753
|
+
if (starting) {
|
|
754
|
+
const init = async () => {
|
|
755
|
+
console.log("[Chat] Initializing conversation...");
|
|
756
|
+
try {
|
|
757
|
+
await loadHistory();
|
|
758
|
+
await checkActiveTask();
|
|
759
|
+
} catch (error) {
|
|
760
|
+
console.log("[Chat] Initialization error:", error);
|
|
761
|
+
}
|
|
762
|
+
setStarting(false);
|
|
763
|
+
};
|
|
764
|
+
init();
|
|
765
|
+
}
|
|
766
|
+
}, [starting]);
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
starting,
|
|
770
|
+
isLoading,
|
|
771
|
+
isLoadingHistory,
|
|
772
|
+
historyState,
|
|
773
|
+
conversations,
|
|
774
|
+
sendMessage,
|
|
775
|
+
abort,
|
|
776
|
+
cancelChat,
|
|
777
|
+
resetConversation,
|
|
778
|
+
startNewConversation,
|
|
779
|
+
loadHistory,
|
|
780
|
+
loadMoreHistory,
|
|
781
|
+
processDataLine,
|
|
782
|
+
getConversation,
|
|
783
|
+
clearConversation,
|
|
784
|
+
removeMessage,
|
|
785
|
+
updateMessage,
|
|
786
|
+
};
|
|
787
|
+
}
|