@amaster.ai/components-templates 1.3.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.
Files changed (40) hide show
  1. package/README.md +193 -0
  2. package/bin/amaster.js +2 -0
  3. package/components/ai-assistant/example.md +34 -0
  4. package/components/ai-assistant/package.json +34 -0
  5. package/components/ai-assistant/template/ai-assistant.tsx +88 -0
  6. package/components/ai-assistant/template/components/Markdown.tsx +70 -0
  7. package/components/ai-assistant/template/components/chat-assistant-message.tsx +190 -0
  8. package/components/ai-assistant/template/components/chat-banner.tsx +17 -0
  9. package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +70 -0
  10. package/components/ai-assistant/template/components/chat-floating-button.tsx +56 -0
  11. package/components/ai-assistant/template/components/chat-floating-card.tsx +43 -0
  12. package/components/ai-assistant/template/components/chat-header.tsx +66 -0
  13. package/components/ai-assistant/template/components/chat-input.tsx +143 -0
  14. package/components/ai-assistant/template/components/chat-messages.tsx +81 -0
  15. package/components/ai-assistant/template/components/chat-recommends.tsx +36 -0
  16. package/components/ai-assistant/template/components/chat-speech-button.tsx +43 -0
  17. package/components/ai-assistant/template/components/chat-user-message.tsx +26 -0
  18. package/components/ai-assistant/template/components/ui-renderer-lazy.tsx +307 -0
  19. package/components/ai-assistant/template/components/ui-renderer.tsx +34 -0
  20. package/components/ai-assistant/template/components/voice-input.tsx +43 -0
  21. package/components/ai-assistant/template/hooks/useAssistantStore.tsx +36 -0
  22. package/components/ai-assistant/template/hooks/useAutoScroll.ts +90 -0
  23. package/components/ai-assistant/template/hooks/useConversationProcessor.ts +649 -0
  24. package/components/ai-assistant/template/hooks/useDisplayMode.tsx +74 -0
  25. package/components/ai-assistant/template/hooks/useDraggable.ts +125 -0
  26. package/components/ai-assistant/template/hooks/usePosition.ts +206 -0
  27. package/components/ai-assistant/template/hooks/useSpeak.ts +50 -0
  28. package/components/ai-assistant/template/hooks/useVoiceInput.ts +172 -0
  29. package/components/ai-assistant/template/i18n.ts +114 -0
  30. package/components/ai-assistant/template/index.ts +6 -0
  31. package/components/ai-assistant/template/inline-ai-assistant.tsx +78 -0
  32. package/components/ai-assistant/template/mock/mock-data.ts +643 -0
  33. package/components/ai-assistant/template/types.ts +72 -0
  34. package/index.js +13 -0
  35. package/package.json +67 -0
  36. package/packages/cli/dist/index.d.ts +3 -0
  37. package/packages/cli/dist/index.d.ts.map +1 -0
  38. package/packages/cli/dist/index.js +335 -0
  39. package/packages/cli/dist/index.js.map +1 -0
  40. package/packages/cli/package.json +35 -0
@@ -0,0 +1,649 @@
1
+ import { useState, useCallback, useMemo, useRef, useEffect } from "react";
2
+ import { produce } from "immer";
3
+ import type {
4
+ MessagesItem,
5
+ Conversation,
6
+ Role,
7
+ TextMessage,
8
+ ThoughtMessage,
9
+ ToolMessage,
10
+ ErrorMessage,
11
+ UIRenderMessage,
12
+ } from "../types";
13
+ import type {
14
+ SendStreamingMessageResponse,
15
+ SendStreamingMessageSuccessResponse,
16
+ } from "@a2a-js/sdk";
17
+ import { client } from "@/lib/client";
18
+ import { getText } from "../i18n";
19
+
20
+ export interface UpdateMessageInput {
21
+ taskId: string;
22
+ messageId: string;
23
+ content?: string;
24
+ partial?: boolean;
25
+ status?: string;
26
+ response?: any;
27
+ metadata?: Record<string, any>;
28
+ }
29
+
30
+ // ================= 消息处理器 =================
31
+
32
+ type MessageHandler = (
33
+ conv: Conversation,
34
+ part: any,
35
+ id: string,
36
+ role: Role,
37
+ status?: any,
38
+ ) => Conversation;
39
+
40
+ const handlers: Record<string, MessageHandler> = {
41
+ // 文本内容(逐步追加 content)
42
+ // {"jsonrpc":"2.0","id":"6d90729b-dd32-46c7-8f48-502850dd33a5","result":{"kind":"status-update","taskId":"b7f71d08-3d35-4cca-88f8-75864632b787","contextId":"9436a03e-ae32-43b7-97cf-309667580efc:anonymous-ef04dcc5-c314-4494-8fd6-3d342bc07dfc","status":{"state":"working","message":{"kind":"message","role":"agent","parts":[{"kind":"text","text":"自动回复"}],"messageId":"e7f45bfa-8273-4c3d-b635-125b10a32a63","taskId":"b7f71d08-3d35-4cca-88f8-75864632b787","contextId":"9436a03e-ae32-43b7-97cf-309667580efc:anonymous-ef04dcc5-c314-4494-8fd6-3d342bc07dfc"},"timestamp":"2026-03-11T11:29:50.614Z"},"final":false,"metadata":{"coderAgent":{"kind":"text-content"},"model":"MiniMax-M2.5","traceId":"msg_36e36c20-f7c7-4b05-b884-c9b21337d2ed"}}}
43
+ "text-content": (conv, part, id, role, status) => {
44
+ if (!id) return conv;
45
+ const existing = conv.messages.find((m) => m.messageId === id);
46
+ const newText = part.text || "";
47
+ if (existing && "content" in existing) {
48
+ return {
49
+ ...conv,
50
+ messages: conv.messages.map((m) =>
51
+ m.messageId === id
52
+ ? { ...m, content: (m as TextMessage).content + newText }
53
+ : m,
54
+ ),
55
+ lastUpdated: new Date().toISOString(),
56
+ };
57
+ }
58
+
59
+ return {
60
+ ...conv,
61
+ messages: [
62
+ ...conv.messages,
63
+ {
64
+ messageId: id,
65
+ role,
66
+ kind: "text-content",
67
+ content: newText,
68
+ timestamp: status.timestamp || new Date().toISOString(),
69
+ } as TextMessage,
70
+ ],
71
+ lastUpdated: new Date().toISOString(),
72
+ };
73
+ },
74
+
75
+ // thought 类型(逐步追加 description)
76
+ // {"jsonrpc":"2.0","id":"6d90729b-dd32-46c7-8f48-502850dd33a5","result":{"kind":"status-update","taskId":"b7f71d08-3d35-4cca-88f8-75864632b787","contextId":"9436a03e-ae32-43b7-97cf-309667580efc:anonymous-ef04dcc5-c314-4494-8fd6-3d342bc07dfc","status":{"state":"working","message":{"kind":"message","role":"agent","parts":[{"kind":"data","data":{"subject":"","description":"技能"}}],"messageId":"2b5db798-052c-4616-8299-722af26d13ed","taskId":"b7f71d08-3d35-4cca-88f8-75864632b787","contextId":"9436a03e-ae32-43b7-97cf-309667580efc:anonymous-ef04dcc5-c314-4494-8fd6-3d342bc07dfc"},"timestamp":"2026-03-11T11:29:44.295Z"},"final":false,"metadata":{"coderAgent":{"kind":"thought"},"model":"MiniMax-M2.5","traceId":"msg_36e36c20-f7c7-4b05-b884-c9b21337d2ed"}}}
77
+ thought: (conv, part, id, role, status) => {
78
+ if (!id) return conv;
79
+ const newChunk = part?.data?.description || "";
80
+ const existing = conv.messages.find((m) => m.messageId === id);
81
+ if (existing && "thought" in existing) {
82
+ return {
83
+ ...conv,
84
+ messages: conv.messages.map((m) =>
85
+ m.messageId === id
86
+ ? { ...m, thought: (m as ThoughtMessage).thought + newChunk }
87
+ : m,
88
+ ),
89
+ lastUpdated: new Date().toISOString(),
90
+ };
91
+ }
92
+
93
+ return {
94
+ ...conv,
95
+ messages: [
96
+ ...conv.messages,
97
+ {
98
+ messageId: id,
99
+ role,
100
+ kind: "thought",
101
+ thought: newChunk,
102
+ timestamp: status?.timestamp || new Date().toISOString(),
103
+ } as ThoughtMessage,
104
+ ],
105
+ lastUpdated: new Date().toISOString(),
106
+ };
107
+ },
108
+
109
+ // 工具调用(状态变更 + 最终结果)
110
+ // {"jsonrpc":"2.0","id":"e76855a5-db73-4fa9-ac09-a795512d3579","result":{"kind":"status-update","taskId":"0d9abf02-b15d-4ef3-92e6-b8e8e68eda34","contextId":"9436a03e-ae32-43b7-97cf-309667580efc:anonymous-ef04dcc5-c314-4494-8fd6-3d342bc07dfc","status":{"state":"working","message":{"kind":"message","role":"agent","parts":[{"kind":"data","data":{"request":{"callId":"toolu_0048e8fad3514a358b031792","name":"WebSearch","args":{"query":"北京天气 今天 2026年3月11日"},"isClientInitiated":false,"prompt_id":"9436a03e-ae32-43b7-97cf-309667580efc:anonymous-ef04dcc5-c314-4494-8fd6-3d342bc07dfc########0","traceId":"msg_1183218d-0e0f-48fe-98c9-817f2992c742"},"status":"error","response":{"callId":"toolu_0048e8fad3514a358b031792","error":{},"responseParts":[{"functionResponse":{"id":"toolu_0048e8fad3514a358b031792","name":"WebSearch","response":{"error":"Tool \"WebSearch\" not found in registry. Tools must use the exact names that are registered. Did you mean \"activate_skill\"?"}}}],"resultDisplay":"Tool \"WebSearch\" not found in registry. Tools must use the exact names that are registered. Did you mean \"activate_skill\"?","errorType":"tool_not_registered","contentLength":122}}}],"messageId":"89431f8f-277b-4e78-87f8-4fb6cc29ffd5","taskId":"0d9abf02-b15d-4ef3-92e6-b8e8e68eda34","contextId":"9436a03e-ae32-43b7-97cf-309667580efc:anonymous-ef04dcc5-c314-4494-8fd6-3d342bc07dfc"},"timestamp":"2026-03-11T11:31:56.090Z"},"final":false,"metadata":{"coderAgent":{"kind":"tool-call-update"},"model":"MiniMax-M2.5"}}}
111
+ tool: (conv, part, _id, role, status) => {
112
+ const partData = part?.data || {};
113
+ const tool = partData?.tool;
114
+ const name = tool?.displayName || tool?.name || "";
115
+ const request = partData?.request;
116
+ const id = request?.callId;
117
+ if (!id || !name) return conv; // 必须有 callId 来识别消息
118
+
119
+ const toolStatus = partData?.status;
120
+ const existing = conv.messages.find((m) => m.messageId === id);
121
+
122
+ if (existing && "toolStatus" in existing) {
123
+ return {
124
+ ...conv,
125
+ messages: conv.messages.map((m) =>
126
+ m.messageId === id
127
+ ? {
128
+ ...m,
129
+ toolStatus: toolStatus || (m as ToolMessage).toolStatus,
130
+ }
131
+ : m,
132
+ ),
133
+ lastUpdated: new Date().toISOString(),
134
+ };
135
+ }
136
+
137
+ return {
138
+ ...conv,
139
+ messages: [
140
+ ...conv.messages,
141
+ {
142
+ messageId: id,
143
+ role,
144
+ kind: "tool",
145
+ toolName: name,
146
+ toolDescription: request?.args?.query || "",
147
+ toolStatus: toolStatus || "pending",
148
+ timestamp: status?.timestamp || new Date().toISOString(),
149
+ } as ToolMessage,
150
+ ],
151
+ lastUpdated: new Date().toISOString(),
152
+ };
153
+ },
154
+
155
+ error: (conv, part, id, role, status) => {
156
+ const errorMsg = part?.text || part?.data?.error || "发生错误";
157
+ return {
158
+ ...conv,
159
+ messages: [
160
+ ...conv.messages,
161
+ {
162
+ messageId: id || `error-${Date.now()}`,
163
+ role,
164
+ kind: "error",
165
+ content: errorMsg,
166
+ timestamp: new Date().toISOString(),
167
+ } as ErrorMessage,
168
+ ],
169
+ status: "error",
170
+ lastUpdated: new Date().toISOString(),
171
+ };
172
+ },
173
+
174
+ "ui-render": (conv, part, id, role, status) => {
175
+ if (!id) return conv;
176
+ const existing = conv.messages.find((m) => m.messageId === id);
177
+ const partData = part?.data || {};
178
+ const spec = partData.spec || { root: "", elements: {} };
179
+
180
+ if (existing && existing.kind === "ui-render") {
181
+ return {
182
+ ...conv,
183
+ messages: conv.messages.map((m) =>
184
+ m.messageId === id ? { ...(m as UIRenderMessage), spec } : m,
185
+ ),
186
+ lastUpdated: new Date().toISOString(),
187
+ };
188
+ }
189
+
190
+ return {
191
+ ...conv,
192
+ messages: [
193
+ ...conv.messages,
194
+ {
195
+ messageId: id,
196
+ role,
197
+ kind: "ui-render",
198
+ spec,
199
+ timestamp: status?.timestamp || new Date().toISOString(),
200
+ } as UIRenderMessage,
201
+ ],
202
+ lastUpdated: new Date().toISOString(),
203
+ };
204
+ },
205
+ };
206
+
207
+ // 用于生成唯一 assistant message id
208
+ const generateId = () =>
209
+ `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
210
+
211
+ // ================= 主 Hook =================
212
+
213
+ type ConversationsState = {
214
+ byId: Record<string, Conversation>;
215
+ order: string[]; // taskId 数组,按所需顺序(最近活跃的在前面)
216
+ };
217
+
218
+ export function useConversationProcessor() {
219
+ const [state, setState] = useState<ConversationsState>({
220
+ byId: {},
221
+ order: [],
222
+ });
223
+ const [isLoading, setIsLoading] = useState(false);
224
+ const loadingRef = useRef(false);
225
+ const abortControllerRef = useRef<AbortController | null>(null);
226
+ const forceStopRef = useRef(false);
227
+ const lastMessageTypeRef = useRef<string>("unknown");
228
+ const lastMessageIdRef = useRef<string>("");
229
+
230
+ useEffect(() => {
231
+ loadingRef.current = isLoading;
232
+ }, [isLoading]);
233
+
234
+ const handleEnd = useCallback(() => {
235
+ abort();
236
+ lastMessageTypeRef.current = "";
237
+ lastMessageIdRef.current = "";
238
+ }, []);
239
+
240
+ const processDataLine = useCallback((data: SendStreamingMessageResponse) => {
241
+ if (!(data as any)?.result) return;
242
+ const result = (data as SendStreamingMessageSuccessResponse).result;
243
+
244
+ const { taskId, status, metadata, final } = result as any;
245
+
246
+ if (final) {
247
+ handleEnd();
248
+ return;
249
+ }
250
+
251
+ if (!taskId) return;
252
+
253
+ const parts = status?.message?.parts || [];
254
+ const firstPart = parts?.[0];
255
+
256
+ let kind = "unknown";
257
+ if (firstPart?.data?.tool) {
258
+ kind = "tool";
259
+ } else if (firstPart?.data?.type === "ui") {
260
+ kind = "ui-render";
261
+ } else if (firstPart?.kind === "text") {
262
+ kind = "text-content";
263
+ } else if (metadata?.coderAgent?.kind === "thought") {
264
+ kind = "thought";
265
+ } else if (metadata?.error) {
266
+ kind = "error";
267
+ }
268
+
269
+ const handler = handlers[kind];
270
+ if (!handler) {
271
+ return;
272
+ }
273
+ removeLoadingPlaceholder();
274
+
275
+ setState(
276
+ produce((draft) => {
277
+ let id = lastMessageIdRef.current;
278
+ if (
279
+ kind === "ui-render" ||
280
+ (kind !== "unknown" && kind !== lastMessageTypeRef.current)
281
+ ) {
282
+ lastMessageTypeRef.current = kind;
283
+ id = generateId();
284
+ lastMessageIdRef.current = id;
285
+ }
286
+
287
+ const prevConv = draft.byId[taskId] ?? {
288
+ taskId,
289
+ status: "submitted" as const,
290
+ messages: [],
291
+ lastUpdated: new Date().toISOString(),
292
+ };
293
+
294
+ const nextConv = handler(
295
+ prevConv,
296
+ firstPart,
297
+ id,
298
+ (status?.message?.role === "agent" ? "assistant" : "") || "assistant",
299
+ status,
300
+ );
301
+
302
+ draft.byId[taskId] = {
303
+ ...nextConv,
304
+ status: status?.state || prevConv.status,
305
+ lastUpdated: new Date().toISOString(),
306
+ };
307
+
308
+ // 调整 order:将更新的 taskId 移到最前面(最近活跃置顶)
309
+ const index = draft.order.indexOf(taskId);
310
+ if (index !== -1) {
311
+ draft.order.splice(index, 1);
312
+ } else {
313
+ // 如果是新会话,添加到最前面
314
+ }
315
+ draft.order.push(taskId);
316
+ }),
317
+ );
318
+ }, []);
319
+
320
+ const conversations = useMemo(() => {
321
+ return state.order
322
+ .map((id) => state.byId[id])
323
+ .filter(Boolean) as Conversation[];
324
+ }, [state]);
325
+
326
+ const getConversation = useCallback(
327
+ (taskId: string) => {
328
+ return state.byId[taskId];
329
+ },
330
+ [state],
331
+ );
332
+
333
+ const clearConversation = useCallback((taskId: string) => {
334
+ setState(
335
+ produce((draft) => {
336
+ if (!draft.byId[taskId]) return;
337
+ delete draft.byId[taskId];
338
+ draft.order = draft.order.filter((id) => id !== taskId);
339
+ }),
340
+ );
341
+ }, []);
342
+
343
+ const removeMessage = useCallback((taskId: string, messageId: string) => {
344
+ setState(
345
+ produce((draft) => {
346
+ const conv = draft.byId[taskId];
347
+ if (!conv) return;
348
+
349
+ const nextMessages = conv.messages.filter(
350
+ (m) => m.messageId !== messageId,
351
+ );
352
+
353
+ draft.byId[taskId] = {
354
+ ...conv,
355
+ messages: nextMessages,
356
+ lastUpdated: new Date().toISOString(),
357
+ };
358
+
359
+ // 调整 order:更新后置顶
360
+ const index = draft.order.indexOf(taskId);
361
+ if (index !== -1) {
362
+ draft.order.splice(index, 1);
363
+ }
364
+ draft.order.push(taskId);
365
+ }),
366
+ );
367
+ }, []);
368
+
369
+ // 新增:更新已有消息
370
+ const updateMessage = useCallback((input: UpdateMessageInput) => {
371
+ const {
372
+ taskId,
373
+ messageId,
374
+ content,
375
+ partial = false,
376
+ status,
377
+ response,
378
+ metadata = {},
379
+ } = input;
380
+
381
+ setState(
382
+ produce((draft) => {
383
+ const conv = draft.byId[taskId];
384
+ if (!conv) return;
385
+
386
+ const msgIndex = conv.messages.findIndex(
387
+ (m) => m.messageId === messageId,
388
+ );
389
+ if (msgIndex === -1) {
390
+ return;
391
+ }
392
+
393
+ const oldMsg = conv.messages[msgIndex];
394
+ let updatedMsg: MessagesItem = { ...oldMsg, ...metadata };
395
+
396
+ // 处理文本 / thought 内容更新
397
+ if (content !== undefined) {
398
+ if ("content" in updatedMsg) {
399
+ updatedMsg.content = partial
400
+ ? (updatedMsg as TextMessage).content + content
401
+ : content;
402
+ } else if ("thought" in updatedMsg) {
403
+ updatedMsg.thought = partial
404
+ ? (updatedMsg as ThoughtMessage).thought + content
405
+ : content;
406
+ } else if (oldMsg.kind === "text-content") {
407
+ // 兼容旧消息类型
408
+ (updatedMsg as any).content = partial
409
+ ? ((oldMsg as any).content || "") + content
410
+ : content;
411
+ }
412
+ }
413
+
414
+ // 处理 tool 消息的特殊字段
415
+ if ("toolStatus" in updatedMsg || status) {
416
+ (updatedMsg as any).toolStatus =
417
+ status || (updatedMsg as any).toolStatus;
418
+ }
419
+ if (response !== undefined) {
420
+ (updatedMsg as any).response = response;
421
+ }
422
+
423
+ const nextMessages = [...conv.messages];
424
+ nextMessages[msgIndex] = updatedMsg;
425
+
426
+ draft.byId[taskId] = {
427
+ ...conv,
428
+ messages: nextMessages,
429
+ lastUpdated: new Date().toISOString(),
430
+ };
431
+
432
+ // 调整 order:更新后置顶
433
+ const index = draft.order.indexOf(taskId);
434
+ if (index !== -1) {
435
+ draft.order.splice(index, 1);
436
+ }
437
+ draft.order.push(taskId);
438
+ }),
439
+ );
440
+ }, []);
441
+
442
+ const addConversation = useCallback((conversation: Conversation) => {
443
+ setState(
444
+ produce((draft) => {
445
+ const id = conversation.taskId;
446
+ draft.byId[id] = conversation;
447
+ // 新会话添加到最前面
448
+ const index = draft.order.indexOf(id);
449
+ if (index !== -1) {
450
+ draft.order.splice(index, 1);
451
+ }
452
+ draft.order.push(id);
453
+ }),
454
+ );
455
+ }, []);
456
+
457
+ const addLoadingPlaceholder = useCallback(() => {
458
+ addConversation({
459
+ taskId: "loading-placeholder",
460
+ status: "working",
461
+ messages: [
462
+ {
463
+ messageId: "loading-placeholder",
464
+ role: "assistant",
465
+ kind: "text-content",
466
+ content: getText().thinking,
467
+ timestamp: new Date().toISOString(),
468
+ },
469
+ ],
470
+ lastUpdated: new Date().toISOString(),
471
+ });
472
+ }, []);
473
+
474
+ useEffect(() => {
475
+ if (!isLoading) {
476
+ removeLoadingPlaceholder();
477
+ }
478
+ }, [isLoading]);
479
+
480
+ const removeLoadingPlaceholder = useCallback(() => {
481
+ clearConversation("loading-placeholder");
482
+ }, []);
483
+
484
+ const onSendError = useCallback(() => {
485
+ addConversation({
486
+ taskId: generateId(),
487
+ status: "error",
488
+ messages: [
489
+ {
490
+ messageId: `error-${Date.now()}`,
491
+ role: "assistant",
492
+ kind: "error",
493
+ content: getText().errorMessage,
494
+ timestamp: new Date().toISOString(),
495
+ } as ErrorMessage,
496
+ ],
497
+ lastUpdated: new Date().toISOString(),
498
+ });
499
+ setIsLoading(false);
500
+ }, []);
501
+
502
+ // 发送用户消息,启动流式对话
503
+ const sendMessage = useCallback(
504
+ async (userContent: string, enableMock = false) => {
505
+ if (loadingRef.current) {
506
+ return;
507
+ }
508
+ if (!userContent.trim()) return;
509
+
510
+ const taskId = `conv-${Date.now()}`;
511
+
512
+ // 添加用户消息
513
+ const userMsgId = generateId();
514
+
515
+ addConversation({
516
+ taskId,
517
+ status: "submitted",
518
+ messages: [
519
+ {
520
+ messageId: userMsgId,
521
+ role: "user",
522
+ kind: "text-content",
523
+ content: userContent,
524
+ timestamp: new Date().toISOString(),
525
+ },
526
+ ],
527
+ lastUpdated: new Date().toISOString(),
528
+ });
529
+
530
+ addLoadingPlaceholder();
531
+
532
+ setIsLoading(true);
533
+ let hasResponse = false;
534
+
535
+ if (abortControllerRef.current) {
536
+ abortControllerRef.current.abort();
537
+ }
538
+ if (typeof AbortController !== "undefined") {
539
+ abortControllerRef.current = new AbortController();
540
+ }
541
+ const controller = abortControllerRef.current;
542
+
543
+ forceStopRef.current = false;
544
+
545
+ if (enableMock || userContent === "MAGA") {
546
+ const { generateMockUIStream } = await import("../mock/mock-data");
547
+ const cancelMock = generateMockUIStream(taskId, (chunk) => {
548
+ if (forceStopRef.current) return;
549
+ hasResponse = true;
550
+ processDataLine(chunk as any);
551
+ if (chunk.result.final) {
552
+ setIsLoading(false);
553
+ }
554
+ });
555
+ abortControllerRef.current?.signal.addEventListener(
556
+ "abort",
557
+ cancelMock,
558
+ );
559
+ return;
560
+ }
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
+ hasResponse = true;
570
+
571
+ // 根据你的实际 stream 返回格式调整
572
+ processDataLine(chunk);
573
+
574
+ // 检查是否结束
575
+ if ((chunk as any)?.isFinal || (chunk as any)?.final) {
576
+ break;
577
+ }
578
+ }
579
+ } catch (err: any) {
580
+ if (err.name === "AbortError" || forceStopRef.current) {
581
+ console.error("[Chat] Request aborted", err);
582
+ } else {
583
+ console.error("[Chat] Stream error:", err);
584
+ onSendError();
585
+ }
586
+ } finally {
587
+ abort();
588
+ if (!hasResponse) {
589
+ // 如果没有任何响应,移除加载占位
590
+ onSendError();
591
+ }
592
+ }
593
+ },
594
+ [processDataLine],
595
+ );
596
+
597
+ // 中止当前请求
598
+ const abort = useCallback(() => {
599
+ if (abortControllerRef.current) {
600
+ abortControllerRef.current.abort();
601
+ abortControllerRef.current = null;
602
+ }
603
+ forceStopRef.current = true;
604
+ setIsLoading(false);
605
+ console.debug("[Chat] Conversation aborted");
606
+ }, []);
607
+
608
+ // 重置会话
609
+ const resetConversation = useCallback((greeting?: string) => {
610
+ abort();
611
+ setState({ byId: {}, order: [] });
612
+
613
+ if (greeting) {
614
+ const taskId = `conv-${Date.now()}`;
615
+ setState({
616
+ byId: {
617
+ [taskId]: {
618
+ taskId,
619
+ status: "submitted",
620
+ messages: [
621
+ {
622
+ messageId: `greeting-${Date.now()}`,
623
+ role: "assistant",
624
+ kind: "text-content",
625
+ content: greeting,
626
+ timestamp: new Date().toISOString(),
627
+ } as TextMessage,
628
+ ],
629
+ lastUpdated: new Date().toISOString(),
630
+ },
631
+ },
632
+ order: [taskId],
633
+ });
634
+ }
635
+ }, []);
636
+
637
+ return {
638
+ isLoading,
639
+ conversations,
640
+ sendMessage,
641
+ abort,
642
+ resetConversation,
643
+ processDataLine,
644
+ getConversation,
645
+ clearConversation,
646
+ removeMessage,
647
+ updateMessage,
648
+ };
649
+ }