@amaster.ai/components-templates 1.6.0 → 1.8.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.
@@ -1,9 +1,12 @@
1
1
  import type {
2
+ Message1,
2
3
  SendStreamingMessageResponse,
3
4
  SendStreamingMessageSuccessResponse,
5
+ TextPart,
4
6
  } from "@a2a-js/sdk";
7
+ import { produce } from "immer";
5
8
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
- import { client } from "../../../lib/client";
9
+ import { client } from "@/lib/client";
7
10
  import { useAiAssistantI18n } from "../i18n";
8
11
  import type {
9
12
  Conversation,
@@ -13,8 +16,32 @@ import type {
13
16
  TextMessage,
14
17
  ThoughtMessage,
15
18
  ToolMessage,
19
+ UIRenderMessage,
16
20
  } from "../types";
17
21
 
22
+ interface HistoryState {
23
+ next: string | null;
24
+ hasMore: boolean;
25
+ }
26
+
27
+ export interface UpdateMessageInput {
28
+ taskId: string;
29
+ messageId: string;
30
+ content?: string;
31
+ partial?: boolean;
32
+ status?: string;
33
+ response?: any;
34
+ metadata?: Record<string, any>;
35
+ }
36
+
37
+ type MessageHandler = (
38
+ conv: Conversation,
39
+ part: any,
40
+ id: string,
41
+ role: Role,
42
+ status?: any,
43
+ ) => Conversation;
44
+
18
45
  class SimpleAbortController {
19
46
  aborted = false;
20
47
  private listeners: (() => void)[] = [];
@@ -43,26 +70,6 @@ function createAbortController(): AbortController | SimpleAbortController {
43
70
  return new SimpleAbortController();
44
71
  }
45
72
 
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
73
  const handlers: Record<string, MessageHandler> = {
67
74
  "text-content": (conv, part, id, role, status) => {
68
75
  if (!id) return conv;
@@ -96,7 +103,6 @@ const handlers: Record<string, MessageHandler> = {
96
103
  };
97
104
  },
98
105
 
99
- // thought 类型(逐步追加 description)
100
106
  thought: (conv, part, id, role, status) => {
101
107
  if (!id) return conv;
102
108
  const newChunk = part?.data?.description || "";
@@ -129,14 +135,13 @@ const handlers: Record<string, MessageHandler> = {
129
135
  };
130
136
  },
131
137
 
132
- // 工具调用(状态变更 + 最终结果)
133
138
  tool: (conv, part, _id, role, status) => {
134
139
  const partData = part?.data || {};
135
140
  const tool = partData?.tool;
136
141
  const name = tool?.displayName || tool?.name || "";
137
142
  const request = partData?.request;
138
143
  const id = request?.callId;
139
- if (!id || !name) return conv; // 必须有 callId 来识别消息
144
+ if (!id || !name) return conv;
140
145
 
141
146
  const toolStatus = partData?.status;
142
147
  const existing = conv.messages.find((m) => m.messageId === id);
@@ -174,7 +179,7 @@ const handlers: Record<string, MessageHandler> = {
174
179
  };
175
180
  },
176
181
 
177
- error: (conv, part, id, role) => {
182
+ error: (conv, part, id, role, _status) => {
178
183
  const errorMsg = part?.text || part?.data?.error || "发生错误";
179
184
  return {
180
185
  ...conv,
@@ -192,60 +197,92 @@ const handlers: Record<string, MessageHandler> = {
192
197
  lastUpdated: new Date().toISOString(),
193
198
  };
194
199
  },
195
- };
196
200
 
197
- // 用于生成唯一 assistant message id
198
- const generateId = () =>
199
- `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
201
+ "ui-render": (conv, part, id, role, status) => {
202
+ if (!id) return conv;
203
+ const existing = conv.messages.find((m) => m.messageId === id);
204
+ const partData = part?.data || {};
205
+ const spec = partData.spec || { root: "", elements: {} };
200
206
 
201
- // ================= 历史消息处理 =================
207
+ if (existing && existing.kind === "ui-render") {
208
+ return {
209
+ ...conv,
210
+ messages: conv.messages.map((m) =>
211
+ m.messageId === id ? { ...(m as UIRenderMessage), spec } : m,
212
+ ),
213
+ lastUpdated: new Date().toISOString(),
214
+ };
215
+ }
202
216
 
203
- interface HistoryState {
204
- next: string | null;
205
- hasMore: boolean;
206
- }
217
+ return {
218
+ ...conv,
219
+ messages: [
220
+ ...conv.messages,
221
+ {
222
+ messageId: id,
223
+ role,
224
+ kind: "ui-render",
225
+ spec,
226
+ timestamp: status?.timestamp || new Date().toISOString(),
227
+ } as UIRenderMessage,
228
+ ],
229
+ lastUpdated: new Date().toISOString(),
230
+ };
231
+ },
232
+ };
207
233
 
208
- interface HistoryMessage {
209
- messageId: string;
210
- historyId?: string;
211
- role: string;
212
- parts?: any[];
213
- }
234
+ const getKind = (part: any, metadata?: any) => {
235
+ let kind = "unknown";
236
+ if (part?.data?.tool) {
237
+ kind = "tool";
238
+ } else if (part?.data?.type === "ui") {
239
+ kind = "ui-render";
240
+ } else if (part?.kind === "text") {
241
+ kind = "text-content";
242
+ } else if (
243
+ part?.data?.type === "though" ||
244
+ metadata?.coderAgent?.kind === "thought"
245
+ ) {
246
+ kind = "thought";
247
+ } else if (metadata?.error) {
248
+ kind = "error";
249
+ }
250
+ return kind;
251
+ };
252
+
253
+ const generateId = () =>
254
+ `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
214
255
 
215
- // ================= Hook =================
256
+ type ConversationsState = {
257
+ byId: Record<string, Conversation>;
258
+ order: string[];
259
+ };
216
260
 
217
- export function useConversationProcessor() {
218
- const [conversationsMap, setConversationsMap] = useState<
219
- Map<string, Conversation>
220
- >(new Map());
261
+ export function useConversation() {
262
+ const [state, setState] = useState<ConversationsState>({
263
+ byId: {},
264
+ order: [],
265
+ });
221
266
  const [isLoading, setIsLoading] = useState(false);
222
267
  const [isLoadingHistory, setIsLoadingHistory] = useState(false);
223
268
  const [historyState, setHistoryState] = useState<HistoryState>({
224
269
  next: null,
225
270
  hasMore: false,
226
271
  });
227
- const [starting, setStarting] = useState(true);
228
- const { t } = useAiAssistantI18n();
229
- const abortControllerRef = useRef<
230
- AbortController | SimpleAbortController | null
231
- >(null);
272
+ const loadingRef = useRef(false);
273
+ const abortControllerRef = useRef<any>(null);
232
274
  const forceStopRef = useRef(false);
233
- const lastMessageTypeRef = useRef<string>("");
275
+ const lastMessageTypeRef = useRef<string>("unknown");
234
276
  const lastMessageIdRef = useRef<string>("");
277
+ const lastMessageRoleRef = useRef<Role | string>("");
235
278
  const currentStreamingTaskIdRef = useRef<string | null>(null);
236
- const loadingRef = useRef(false);
279
+ const [starting, setStarting] = useState(true);
280
+ const {t} = useAiAssistantI18n();
237
281
 
238
282
  useEffect(() => {
239
283
  loadingRef.current = isLoading;
240
284
  }, [isLoading]);
241
285
 
242
- const handleEnd = useCallback(() => {
243
- setIsLoading(false);
244
- lastMessageTypeRef.current = "";
245
- lastMessageIdRef.current = "";
246
- currentStreamingTaskIdRef.current = null;
247
- }, []);
248
-
249
286
  const abort = useCallback(() => {
250
287
  if (abortControllerRef.current) {
251
288
  abortControllerRef.current.abort();
@@ -255,167 +292,218 @@ export function useConversationProcessor() {
255
292
  setIsLoading(false);
256
293
  }, []);
257
294
 
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
- }
295
+ const handleEnd = useCallback(() => {
296
+ abort();
297
+ lastMessageTypeRef.current = "";
298
+ lastMessageIdRef.current = "";
299
+ lastMessageRoleRef.current = "";
300
+ currentStreamingTaskIdRef.current = null;
301
+ }, [abort]);
302
+
303
+ // 填充数据到 conversation 中
304
+ const fillData = ({
305
+ kind,
306
+ taskId,
307
+ part,
308
+ status,
309
+ handler,
310
+ isHistory,
311
+ historyId,
312
+ messageId,
313
+ }: {
314
+ kind: string;
315
+ taskId: string;
316
+ part: any;
317
+ status: any;
318
+ handler: MessageHandler;
319
+ isHistory?: boolean;
320
+ historyId?: string;
321
+ messageId?: string;
322
+ }) => {
323
+ setState(
324
+ produce((draft) => {
325
+ let id = messageId || lastMessageIdRef.current;
326
+ const role = status?.message?.role === "agent" ? "assistant" : "user";
327
+
328
+ if (
329
+ !messageId &&
330
+ (kind === "ui-render" ||
331
+ (kind !== "unknown" && kind !== lastMessageTypeRef.current))
332
+ ) {
333
+ lastMessageTypeRef.current = kind;
334
+ id = generateId();
335
+ lastMessageIdRef.current = id;
336
+ }
282
337
 
283
- // 对于历史消息,我们需要用不同的方式处理
284
- setConversationsMap((prev) => {
285
- const prevConv = prev.get(taskId) ?? {
338
+ const prevConv = draft.byId[taskId] ?? {
286
339
  taskId,
287
- status: "completed" as const,
340
+ status: isHistory ? "completed" : "submitted",
288
341
  messages: [],
342
+ role,
289
343
  historyId,
290
344
  lastUpdated: new Date().toISOString(),
291
345
  };
292
346
 
293
- const role = data.role === "agent" ? "assistant" : "user";
294
-
295
- const nextConv = handler(prevConv, firstPart, messageId, role, {
296
- message: data,
297
- });
347
+ const nextConv = handler(prevConv, part, id, role, status);
298
348
 
299
- const nextMap = new Map(prev);
300
- nextMap.set(taskId, {
349
+ draft.byId[taskId] = {
301
350
  ...nextConv,
351
+ status: status?.state || prevConv.status,
302
352
  lastUpdated: new Date().toISOString(),
303
- });
304
- return nextMap;
305
- });
306
- },
307
- [],
308
- );
353
+ };
309
354
 
310
- const processDataLine = useCallback((data: SendStreamingMessageResponse) => {
311
- if (!(data as any)?.result) return;
312
- const result = (data as SendStreamingMessageSuccessResponse).result;
355
+ const index = draft.order.indexOf(taskId);
356
+ if (index !== -1) {
357
+ draft.order.splice(index, 1);
358
+ }
313
359
 
314
- const { taskId, status, metadata, final } = result as any;
360
+ if (!isHistory) {
361
+ draft.order.push(taskId);
362
+ }
363
+ }),
364
+ );
365
+ };
315
366
 
316
- if (final) {
317
- handleEnd();
318
- return;
319
- }
367
+ // 处理实时数据
368
+ const processLiveData = useCallback(
369
+ (data: SendStreamingMessageResponse) => {
370
+ if (!(data as any)?.result) return;
371
+ const result = (data as SendStreamingMessageSuccessResponse).result;
320
372
 
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
- }
373
+ const { taskId, status, metadata, final } = result as any;
341
374
 
342
- const handler = handlers[kind];
343
- if (!handler) {
344
- console.warn("Unhandled message type", result);
345
- return;
346
- }
375
+ if (final) {
376
+ handleEnd();
377
+ return;
378
+ }
347
379
 
348
- if (kind && kind !== lastMessageTypeRef.current) {
349
- lastMessageTypeRef.current = kind;
350
- lastMessageIdRef.current = generateId();
351
- }
380
+ if (!taskId) return;
352
381
 
353
- removeLoadingPlaceholder();
382
+ currentStreamingTaskIdRef.current = taskId;
354
383
 
355
- setConversationsMap((prev) => {
356
- const prevConv = prev.get(taskId) ?? {
357
- taskId,
358
- status: "submitted" as const,
359
- messages: [],
360
- lastUpdated: new Date().toISOString(),
361
- };
384
+ const parts = status?.message?.parts || [];
385
+ const firstPart = parts?.[0];
386
+ const kind = getKind(firstPart, metadata);
387
+
388
+ const handler = handlers[kind];
389
+ if (!handler) {
390
+ return;
391
+ }
392
+ removeLoadingPlaceholder();
362
393
 
363
- const nextConv = handler(
364
- prevConv,
365
- part,
366
- lastMessageIdRef.current,
367
- (status?.message?.role === "agent" ? "assistant" : "") || "assistant",
394
+ fillData({
395
+ kind,
396
+ taskId,
397
+ part: firstPart,
368
398
  status,
369
- );
399
+ handler,
400
+ });
401
+ },
402
+ [handleEnd],
403
+ );
370
404
 
371
- const nextMap = new Map(prev);
405
+ const processHistoryData = useCallback(
406
+ (data: Message1, taskId: string) => {
407
+ const { messageId, historyId } = (data || {}) as any;
408
+ if (!messageId) return;
372
409
 
373
- nextMap.set(taskId, {
374
- ...nextConv,
375
- status: status?.state || prevConv.status,
376
- lastUpdated: new Date().toISOString(),
410
+ const parts = data.parts || [];
411
+ const firstPart = parts?.[0];
412
+
413
+ const kind = getKind(firstPart);
414
+
415
+ if (kind === "text-content") {
416
+ let text = (firstPart as TextPart).text || "";
417
+ text = text.trim();
418
+ if (text.startsWith("<ui>")) {
419
+ const json = text.slice(4).trim();
420
+ try {
421
+ const partData = JSON.parse(json);
422
+ if (partData.root && partData.elements) {
423
+ fillData({
424
+ kind: 'ui-render',
425
+ taskId,
426
+ part: { kind: 'data', data: { type: "ui", spec: partData } },
427
+ handler: handlers['ui-render'],
428
+ isHistory: true,
429
+ historyId,
430
+ messageId,
431
+ status: { message: data },
432
+ });
433
+ }
434
+ } catch (error) {
435
+ // 解析失败,有可能是半截
436
+ }
437
+ return;
438
+ }
439
+ }
440
+
441
+ const handler = handlers[kind];
442
+ if (!handler) {
443
+ return;
444
+ }
445
+ fillData({
446
+ kind,
447
+ taskId,
448
+ part: firstPart,
449
+ handler,
450
+ isHistory: true,
451
+ historyId,
452
+ messageId,
453
+ status: { message: data },
377
454
  });
378
- return nextMap;
379
- });
380
- }, []);
455
+ },
456
+ [handleEnd],
457
+ );
381
458
 
382
459
  const conversations = useMemo(() => {
383
- return Array.from(conversationsMap.values());
384
- }, [conversationsMap]);
460
+ return state.order
461
+ .map((id) => state.byId[id])
462
+ .filter(Boolean) as Conversation[];
463
+ }, [state]);
385
464
 
386
465
  const getConversation = useCallback(
387
466
  (taskId: string) => {
388
- return conversationsMap.get(taskId);
467
+ return state.byId[taskId];
389
468
  },
390
- [conversationsMap],
469
+ [state],
391
470
  );
392
471
 
393
472
  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
- });
473
+ setState(
474
+ produce((draft) => {
475
+ if (!draft.byId[taskId]) return;
476
+ delete draft.byId[taskId];
477
+ draft.order = draft.order.filter((id) => id !== taskId);
478
+ }),
479
+ );
400
480
  }, []);
401
481
 
402
482
  const removeMessage = useCallback((taskId: string, messageId: string) => {
403
- setConversationsMap((prev) => {
404
- const conv = prev.get(taskId);
405
- if (!conv) return prev;
483
+ setState(
484
+ produce((draft) => {
485
+ const conv = draft.byId[taskId];
486
+ if (!conv) return;
406
487
 
407
- const nextConv = {
408
- ...conv,
409
- messages: conv.messages.filter((m) => m.messageId !== messageId),
410
- };
488
+ const nextMessages = conv.messages.filter(
489
+ (m) => m.messageId !== messageId,
490
+ );
411
491
 
412
- const nextMap = new Map(prev);
413
- nextMap.set(taskId, nextConv);
414
- return nextMap;
415
- });
492
+ draft.byId[taskId] = {
493
+ ...conv,
494
+ messages: nextMessages,
495
+ lastUpdated: new Date().toISOString(),
496
+ };
497
+
498
+ const index = draft.order.indexOf(taskId);
499
+ if (index !== -1) {
500
+ draft.order.splice(index, 1);
501
+ }
502
+ draft.order.push(taskId);
503
+ }),
504
+ );
416
505
  }, []);
417
506
 
418
- // 新增:更新已有消息
419
507
  const updateMessage = useCallback((input: UpdateMessageInput) => {
420
508
  const {
421
509
  taskId,
@@ -427,72 +515,75 @@ export function useConversationProcessor() {
427
515
  metadata = {},
428
516
  } = input;
429
517
 
430
- setConversationsMap((prev) => {
431
- const conv = prev.get(taskId);
432
- if (!conv) return prev;
518
+ setState(
519
+ produce((draft) => {
520
+ const conv = draft.byId[taskId];
521
+ if (!conv) return;
433
522
 
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}`,
523
+ const msgIndex = conv.messages.findIndex(
524
+ (m) => m.messageId === messageId,
440
525
  );
441
- return prev;
442
- }
526
+ if (msgIndex === -1) {
527
+ return;
528
+ }
443
529
 
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;
530
+ const oldMsg = conv.messages[msgIndex];
531
+ const updatedMsg: MessagesItem = { ...oldMsg, ...metadata };
532
+
533
+ if (content !== undefined) {
534
+ if ("content" in updatedMsg) {
535
+ updatedMsg.content = partial
536
+ ? (updatedMsg as TextMessage).content + content
537
+ : content;
538
+ } else if ("thought" in updatedMsg) {
539
+ updatedMsg.thought = partial
540
+ ? (updatedMsg as ThoughtMessage).thought + content
541
+ : content;
542
+ } else if (oldMsg.kind === "text-content") {
543
+ (updatedMsg as any).content = partial
544
+ ? ((oldMsg as any).content || "") + content
545
+ : content;
546
+ }
462
547
  }
463
- }
464
548
 
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
- }
549
+ if ("toolStatus" in updatedMsg || status) {
550
+ (updatedMsg as any).toolStatus =
551
+ status || (updatedMsg as any).toolStatus;
552
+ }
553
+ if (response !== undefined) {
554
+ (updatedMsg as any).response = response;
555
+ }
473
556
 
474
- const nextMessages = [...conv.messages];
475
- nextMessages[msgIndex] = updatedMsg;
557
+ const nextMessages = [...conv.messages];
558
+ nextMessages[msgIndex] = updatedMsg;
476
559
 
477
- const nextConv: Conversation = {
478
- ...conv,
479
- messages: nextMessages,
480
- lastUpdated: new Date().toISOString(),
481
- };
560
+ draft.byId[taskId] = {
561
+ ...conv,
562
+ messages: nextMessages,
563
+ lastUpdated: new Date().toISOString(),
564
+ };
482
565
 
483
- const nextMap = new Map(prev);
484
- nextMap.set(taskId, nextConv);
485
- return nextMap;
486
- });
566
+ const index = draft.order.indexOf(taskId);
567
+ if (index !== -1) {
568
+ draft.order.splice(index, 1);
569
+ }
570
+ draft.order.push(taskId);
571
+ }),
572
+ );
487
573
  }, []);
488
574
 
489
575
  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
- });
576
+ setState(
577
+ produce((draft) => {
578
+ const id = conversation.taskId;
579
+ draft.byId[id] = conversation;
580
+ const index = draft.order.indexOf(id);
581
+ if (index !== -1) {
582
+ draft.order.splice(index, 1);
583
+ }
584
+ draft.order.push(id);
585
+ }),
586
+ );
496
587
  }, []);
497
588
 
498
589
  const addLoadingPlaceholder = useCallback(() => {
@@ -510,260 +601,288 @@ export function useConversationProcessor() {
510
601
  ],
511
602
  lastUpdated: new Date().toISOString(),
512
603
  });
513
- }, [t.thinking]);
604
+ }, [addConversation]);
605
+
606
+ const removeLoadingPlaceholder = useCallback(() => {
607
+ clearConversation("loading-placeholder");
608
+ }, [clearConversation]);
514
609
 
515
610
  useEffect(() => {
516
611
  if (!isLoading) {
517
612
  removeLoadingPlaceholder();
518
613
  }
519
- }, [isLoading]);
614
+ }, [isLoading, removeLoadingPlaceholder]);
520
615
 
521
- const removeLoadingPlaceholder = useCallback(() => {
522
- clearConversation("loading-placeholder");
616
+ const onSendError = useCallback(() => {
617
+ addConversation({
618
+ taskId: generateId(),
619
+ status: "error",
620
+ messages: [
621
+ {
622
+ messageId: `error-${Date.now()}`,
623
+ role: "assistant",
624
+ kind: "error",
625
+ content: t.errorMessage,
626
+ timestamp: new Date().toISOString(),
627
+ } as ErrorMessage,
628
+ ],
629
+ lastUpdated: new Date().toISOString(),
630
+ });
631
+ setIsLoading(false);
632
+ }, [addConversation]);
633
+
634
+ const loadHistory = useCallback(async (limit = 20, next?: string) => {
635
+ setIsLoadingHistory(true);
636
+ let scrollToId = "";
637
+ try {
638
+ const copilot = client.copilot as any;
639
+ const result = await copilot.getHistory?.(limit, next);
640
+ const messages = result?.messages || [];
641
+ const length = messages.length;
642
+ scrollToId = length > 0 ? messages?.[length - 1]?.messageId : "";
643
+
644
+ let taskId = "";
645
+ let lastRole = "";
646
+ const taskIds: string[] = [];
647
+ messages.forEach((msg: any) => {
648
+ const role = msg.role === "agent" ? "assistant" : "user";
649
+ if (role !== lastRole) {
650
+ taskId = `history-conv-${generateId()}`;
651
+ taskIds.push(taskId);
652
+ lastRole = role;
653
+ }
654
+ processHistoryData(msg, taskId);
655
+ });
656
+ setState(
657
+ produce((draft) => {
658
+ draft.order = [...taskIds, ...draft.order];
659
+ }),
660
+ );
661
+ setHistoryState({
662
+ next: result?.next || null,
663
+ hasMore: result?.hasMore || false,
664
+ });
665
+ } catch (error) {
666
+ console.error("[Chat] Failed to load history:", error);
667
+ } finally {
668
+ setIsLoadingHistory(false);
669
+ scrollToId &&
670
+ next &&
671
+ setTimeout(() => {
672
+ document
673
+ .querySelector(`[data-conversation-message-id="${scrollToId}"]`)
674
+ ?.scrollIntoView({ behavior: "instant", block: "center" });
675
+ }, 20);
676
+ }
523
677
  }, []);
524
678
 
525
- // 发送用户消息,启动流式对话
526
- const sendMessage = useCallback(
527
- async (userContent: string, _placeholderText?: string) => {
528
- if (!userContent.trim()) return;
679
+ const loadMoreHistory = useCallback(async () => {
680
+ if (!historyState.hasMore || isLoadingHistory || !historyState.next) return;
681
+ await loadHistory(20, historyState.next);
682
+ }, [historyState.hasMore, historyState.next, isLoadingHistory]);
529
683
 
530
- const taskId = `conv-${Date.now()}`;
684
+ const checkActiveTask = useCallback(async () => {
685
+ try {
686
+ const result = await client.copilot.getChatStatus();
687
+ const taskId = result.taskId;
688
+ if (taskId && result.working) {
689
+ currentStreamingTaskIdRef.current = taskId;
531
690
 
532
- // 添加用户消息
533
- const userMsgId = generateId();
691
+ const stream = await client.copilot.chat([], { taskId });
692
+ for await (const chunk of stream) {
693
+ if (forceStopRef.current) break;
694
+ processLiveData(chunk);
695
+ }
696
+ }
697
+ } catch (error) {
698
+ console.error("[Chat] Failed to check active task:", error);
699
+ }
700
+ }, [processLiveData]);
534
701
 
702
+ const cancelChat = useCallback(
703
+ async (taskId?: string) => {
704
+ abort();
535
705
  addConversation({
536
- taskId,
537
- status: "submitted",
706
+ taskId: generateId(),
707
+ status: "error",
538
708
  messages: [
539
709
  {
540
- messageId: userMsgId,
541
- role: "user",
710
+ messageId: `cancel-${Date.now()}`,
711
+ role: "assistant",
542
712
  kind: "text-content",
543
- content: userContent,
713
+ content: t.cancelled,
544
714
  timestamp: new Date().toISOString(),
545
- },
715
+ } as TextMessage,
546
716
  ],
547
717
  lastUpdated: new Date().toISOString(),
548
718
  });
719
+ const targetTaskId = taskId || currentStreamingTaskIdRef.current;
720
+ if (!targetTaskId) return;
549
721
 
550
- addLoadingPlaceholder();
722
+ try {
723
+ await client.copilot.cancelChat(targetTaskId);
724
+ currentStreamingTaskIdRef.current = null;
725
+ } catch (error) {
726
+ console.error("[Chat] Failed to cancel chat:", error);
727
+ }
728
+ },
729
+ [abort],
730
+ );
551
731
 
552
- setIsLoading(true);
732
+ const resetConversation = useCallback(
733
+ (greeting?: string) => {
734
+ abort();
735
+ setState({ byId: {}, order: [] });
736
+ currentStreamingTaskIdRef.current = null;
737
+
738
+ if (greeting) {
739
+ const taskId = `conv-${Date.now()}`;
740
+ setState({
741
+ byId: {
742
+ [taskId]: {
743
+ taskId,
744
+ status: "submitted",
745
+ messages: [
746
+ {
747
+ messageId: `greeting-${Date.now()}`,
748
+ role: "assistant",
749
+ kind: "text-content",
750
+ content: greeting,
751
+ timestamp: new Date().toISOString(),
752
+ } as TextMessage,
753
+ ],
754
+ lastUpdated: new Date().toISOString(),
755
+ },
756
+ },
757
+ order: [taskId],
758
+ });
759
+ }
760
+ },
761
+ [abort],
762
+ );
763
+
764
+ const startNewConversation = useCallback(async () => {
765
+ addConversation({
766
+ taskId: generateId(),
767
+ status: "submitted",
768
+ messages: [],
769
+ lastUpdated: new Date().toISOString(),
770
+ system: { level: "newConversation" },
771
+ });
772
+ try {
773
+ const copilot = client.copilot as any;
774
+ await copilot.newConversation?.();
775
+ } catch (error) {
776
+ console.error("[Chat] Failed to create new conversation:", error);
777
+ }
778
+ }, []);
779
+
780
+ const subscribeChatEvents = useCallback(
781
+ async ({
782
+ userContent,
783
+ taskId,
784
+ }: {
785
+ userContent?: string;
786
+ taskId?: string;
787
+ }) => {
788
+ let hasResponse = false;
553
789
 
554
790
  if (abortControllerRef.current) {
555
791
  abortControllerRef.current.abort();
556
792
  }
557
- abortControllerRef.current = createAbortController();
793
+ if (typeof AbortController !== "undefined") {
794
+ abortControllerRef.current = createAbortController();
795
+ }
558
796
  const controller = abortControllerRef.current;
559
-
560
797
  forceStopRef.current = false;
561
798
 
799
+ // real chat
562
800
  try {
563
- const stream = client.copilot.chat([
564
- { role: "user", content: userContent },
565
- ]);
801
+ const stream = client.copilot.chat(
802
+ userContent ? [{ role: "user", content: userContent }] : [],
803
+ taskId ? { taskId } : undefined,
804
+ );
566
805
 
567
806
  for await (const chunk of stream) {
568
807
  if (controller?.signal.aborted || forceStopRef.current) break;
808
+ hasResponse = true;
569
809
 
570
- // 根据你的实际 stream 返回格式调整
571
- processDataLine(chunk);
810
+ processLiveData(chunk);
572
811
 
573
- // 检查是否结束
574
812
  if ((chunk as any)?.isFinal || (chunk as any)?.final) {
575
813
  break;
576
814
  }
577
815
  }
578
816
  } catch (err: any) {
579
817
  if (err.name === "AbortError" || forceStopRef.current) {
580
- console.log("[Chat] Request aborted");
818
+ console.error("[Chat] Request aborted", err);
581
819
  } else {
582
820
  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
- });
821
+ onSendError();
598
822
  }
599
823
  } finally {
600
- setIsLoading(false);
601
- if (abortControllerRef.current === controller) {
602
- abortControllerRef.current = null;
824
+ abort();
825
+ if (!hasResponse) {
826
+ onSendError();
603
827
  }
604
828
  }
605
829
  },
606
- [processDataLine],
830
+ [],
607
831
  );
608
832
 
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
- );
833
+ const sendMessage = useCallback(
834
+ async (userContent: string) => {
835
+ if (loadingRef.current) {
836
+ return;
640
837
  }
641
- },
642
- [abort],
643
- );
838
+ if (!userContent.trim()) return;
839
+ const userMsgId = generateId();
644
840
 
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
- });
841
+ addConversation({
842
+ taskId: generateId(),
843
+ status: "submitted",
844
+ messages: [
845
+ {
846
+ messageId: userMsgId,
847
+ role: "user",
848
+ kind: "text-content",
849
+ content: userContent,
850
+ timestamp: new Date().toISOString(),
851
+ },
852
+ ],
853
+ lastUpdated: new Date().toISOString(),
854
+ });
666
855
 
667
- setHistoryState({
668
- next: result?.next || null,
669
- hasMore: result?.hasMore || false,
670
- });
856
+ addLoadingPlaceholder();
671
857
 
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
- }
858
+ setIsLoading(true);
859
+ subscribeChatEvents({ userContent });
679
860
  },
680
- [processHistoryData],
861
+ [
862
+ addConversation,
863
+ addLoadingPlaceholder,
864
+ processLiveData,
865
+ onSendError,
866
+ abort,
867
+ ],
681
868
  );
682
869
 
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
870
  useEffect(() => {
753
871
  if (starting) {
754
872
  const init = async () => {
755
- console.log("[Chat] Initializing conversation...");
873
+ console.debug("[Chat] Initializing conversation...");
756
874
  try {
757
875
  await loadHistory();
758
876
  await checkActiveTask();
877
+ setStarting(false);
759
878
  } catch (error) {
760
- console.log("[Chat] Initialization error:", error);
879
+ console.error("[Chat] Initialization error:", error);
880
+ setStarting(false);
761
881
  }
762
- setStarting(false);
763
882
  };
764
883
  init();
765
884
  }
766
- }, [starting]);
885
+ }, [starting, loadHistory, checkActiveTask]);
767
886
 
768
887
  return {
769
888
  starting,
@@ -778,7 +897,6 @@ export function useConversationProcessor() {
778
897
  startNewConversation,
779
898
  loadHistory,
780
899
  loadMoreHistory,
781
- processDataLine,
782
900
  getConversation,
783
901
  clearConversation,
784
902
  removeMessage,