@amaster.ai/components-templates 1.4.1 → 1.4.2

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.
@@ -0,0 +1,856 @@
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
+ Message1,
15
+ SendStreamingMessageResponse,
16
+ SendStreamingMessageSuccessResponse,
17
+ } from "@a2a-js/sdk";
18
+ import { client } from "@/lib/client";
19
+ import { getText } from "../i18n";
20
+
21
+ interface HistoryState {
22
+ next: string | null;
23
+ hasMore: boolean;
24
+ }
25
+
26
+ export interface UpdateMessageInput {
27
+ taskId: string;
28
+ messageId: string;
29
+ content?: string;
30
+ partial?: boolean;
31
+ status?: string;
32
+ response?: any;
33
+ metadata?: Record<string, any>;
34
+ }
35
+
36
+ type MessageHandler = (
37
+ conv: Conversation,
38
+ part: any,
39
+ id: string,
40
+ role: Role,
41
+ status?: any,
42
+ ) => Conversation;
43
+
44
+ const handlers: Record<string, MessageHandler> = {
45
+ "text-content": (conv, part, id, role, status) => {
46
+ if (!id) return conv;
47
+ const existing = conv.messages.find((m) => m.messageId === id);
48
+ const newText = part.text || "";
49
+ if (existing && "content" in existing) {
50
+ return {
51
+ ...conv,
52
+ messages: conv.messages.map((m) =>
53
+ m.messageId === id
54
+ ? { ...m, content: (m as TextMessage).content + newText }
55
+ : m,
56
+ ),
57
+ lastUpdated: new Date().toISOString(),
58
+ };
59
+ }
60
+
61
+ return {
62
+ ...conv,
63
+ messages: [
64
+ ...conv.messages,
65
+ {
66
+ messageId: id,
67
+ role,
68
+ kind: "text-content",
69
+ content: newText,
70
+ timestamp: status.timestamp || new Date().toISOString(),
71
+ } as TextMessage,
72
+ ],
73
+ lastUpdated: new Date().toISOString(),
74
+ };
75
+ },
76
+
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
+ tool: (conv, part, _id, role, status) => {
110
+ const partData = part?.data || {};
111
+ const tool = partData?.tool;
112
+ const name = tool?.displayName || tool?.name || "";
113
+ const request = partData?.request;
114
+ const id = request?.callId;
115
+ if (!id || !name) return conv;
116
+
117
+ const toolStatus = partData?.status;
118
+ const existing = conv.messages.find((m) => m.messageId === id);
119
+
120
+ if (existing && "toolStatus" in existing) {
121
+ return {
122
+ ...conv,
123
+ messages: conv.messages.map((m) =>
124
+ m.messageId === id
125
+ ? {
126
+ ...m,
127
+ toolStatus: toolStatus || (m as ToolMessage).toolStatus,
128
+ }
129
+ : m,
130
+ ),
131
+ lastUpdated: new Date().toISOString(),
132
+ };
133
+ }
134
+
135
+ return {
136
+ ...conv,
137
+ messages: [
138
+ ...conv.messages,
139
+ {
140
+ messageId: id,
141
+ role,
142
+ kind: "tool",
143
+ toolName: name,
144
+ toolDescription: request?.args?.query || "",
145
+ toolStatus: toolStatus || "pending",
146
+ timestamp: status?.timestamp || new Date().toISOString(),
147
+ } as ToolMessage,
148
+ ],
149
+ lastUpdated: new Date().toISOString(),
150
+ };
151
+ },
152
+
153
+ error: (conv, part, id, role, status) => {
154
+ const errorMsg = part?.text || part?.data?.error || "发生错误";
155
+ return {
156
+ ...conv,
157
+ messages: [
158
+ ...conv.messages,
159
+ {
160
+ messageId: id || `error-${Date.now()}`,
161
+ role,
162
+ kind: "error",
163
+ content: errorMsg,
164
+ timestamp: new Date().toISOString(),
165
+ } as ErrorMessage,
166
+ ],
167
+ status: "error",
168
+ lastUpdated: new Date().toISOString(),
169
+ };
170
+ },
171
+
172
+ "ui-render": (conv, part, id, role, status) => {
173
+ if (!id) return conv;
174
+ const existing = conv.messages.find((m) => m.messageId === id);
175
+ const partData = part?.data || {};
176
+ const spec = partData.spec || { root: "", elements: {} };
177
+
178
+ if (existing && existing.kind === "ui-render") {
179
+ return {
180
+ ...conv,
181
+ messages: conv.messages.map((m) =>
182
+ m.messageId === id ? { ...(m as UIRenderMessage), spec } : m,
183
+ ),
184
+ lastUpdated: new Date().toISOString(),
185
+ };
186
+ }
187
+
188
+ return {
189
+ ...conv,
190
+ messages: [
191
+ ...conv.messages,
192
+ {
193
+ messageId: id,
194
+ role,
195
+ kind: "ui-render",
196
+ spec,
197
+ timestamp: status?.timestamp || new Date().toISOString(),
198
+ } as UIRenderMessage,
199
+ ],
200
+ lastUpdated: new Date().toISOString(),
201
+ };
202
+ },
203
+ };
204
+
205
+ const getKind = (part: any, metadata?: any) => {
206
+ let kind = "unknown";
207
+ if (part?.data?.tool) {
208
+ kind = "tool";
209
+ } else if (part?.data?.type === "ui") {
210
+ kind = "ui-render";
211
+ } else if (part?.kind === "text") {
212
+ kind = "text-content";
213
+ } else if (
214
+ part?.data?.type === "though" ||
215
+ metadata?.coderAgent?.kind === "thought"
216
+ ) {
217
+ kind = "thought";
218
+ } else if (metadata?.error) {
219
+ kind = "error";
220
+ }
221
+ return kind;
222
+ };
223
+
224
+ const generateId = () =>
225
+ `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
226
+
227
+ type ConversationsState = {
228
+ byId: Record<string, Conversation>;
229
+ order: string[];
230
+ };
231
+
232
+ export function useConversation() {
233
+ const [state, setState] = useState<ConversationsState>({
234
+ byId: {},
235
+ order: [],
236
+ });
237
+ const [isLoading, setIsLoading] = useState(false);
238
+ const [isLoadingHistory, setIsLoadingHistory] = useState(false);
239
+ const [historyState, setHistoryState] = useState<HistoryState>({
240
+ next: null,
241
+ hasMore: false,
242
+ });
243
+ const loadingRef = useRef(false);
244
+ const abortControllerRef = useRef<AbortController | null>(null);
245
+ const forceStopRef = useRef(false);
246
+ const lastMessageTypeRef = useRef<string>("unknown");
247
+ const lastMessageIdRef = useRef<string>("");
248
+ const lastMessageRoleRef = useRef<Role | string>("");
249
+ const currentStreamingTaskIdRef = useRef<string | null>(null);
250
+ const [starting, setStarting] = useState(true);
251
+
252
+ useEffect(() => {
253
+ loadingRef.current = isLoading;
254
+ }, [isLoading]);
255
+
256
+ const abort = useCallback(() => {
257
+ if (abortControllerRef.current) {
258
+ abortControllerRef.current.abort();
259
+ abortControllerRef.current = null;
260
+ }
261
+ forceStopRef.current = true;
262
+ setIsLoading(false);
263
+ }, []);
264
+
265
+ const handleEnd = useCallback(() => {
266
+ abort();
267
+ lastMessageTypeRef.current = "";
268
+ lastMessageIdRef.current = "";
269
+ lastMessageRoleRef.current = "";
270
+ currentStreamingTaskIdRef.current = null;
271
+ }, [abort]);
272
+
273
+ // 填充数据到 conversation 中
274
+ const fillData = ({
275
+ kind,
276
+ taskId,
277
+ part,
278
+ status,
279
+ handler,
280
+ isHistory,
281
+ historyId,
282
+ messageId,
283
+ }: {
284
+ kind: string;
285
+ taskId: string;
286
+ part: any;
287
+ status: any;
288
+ handler: MessageHandler;
289
+ isHistory?: boolean;
290
+ historyId?: string;
291
+ messageId?: string;
292
+ }) => {
293
+ setState(
294
+ produce((draft) => {
295
+ let id = messageId || lastMessageIdRef.current;
296
+ const role = status?.message?.role === "agent" ? "assistant" : "user";
297
+
298
+ if (
299
+ !messageId &&
300
+ (kind === "ui-render" ||
301
+ (kind !== "unknown" && kind !== lastMessageTypeRef.current))
302
+ ) {
303
+ lastMessageTypeRef.current = kind;
304
+ id = generateId();
305
+ lastMessageIdRef.current = id;
306
+ }
307
+
308
+ const prevConv = draft.byId[taskId] ?? {
309
+ taskId,
310
+ status: isHistory ? "completed" : "submitted",
311
+ messages: [],
312
+ role,
313
+ historyId,
314
+ lastUpdated: new Date().toISOString(),
315
+ };
316
+
317
+ const nextConv = handler(prevConv, part, id, role, status);
318
+
319
+ draft.byId[taskId] = {
320
+ ...nextConv,
321
+ status: status?.state || prevConv.status,
322
+ lastUpdated: new Date().toISOString(),
323
+ };
324
+
325
+ const index = draft.order.indexOf(taskId);
326
+ if (index !== -1) {
327
+ draft.order.splice(index, 1);
328
+ }
329
+
330
+ if (!isHistory) {
331
+ draft.order.push(taskId);
332
+ }
333
+ }),
334
+ );
335
+ };
336
+
337
+ // 处理实时数据
338
+ const processLiveData = useCallback(
339
+ (data: SendStreamingMessageResponse) => {
340
+ if (!(data as any)?.result) return;
341
+ const result = (data as SendStreamingMessageSuccessResponse).result;
342
+
343
+ const { taskId, status, metadata, final } = result as any;
344
+
345
+ if (final) {
346
+ handleEnd();
347
+ return;
348
+ }
349
+
350
+ if (!taskId) return;
351
+
352
+ currentStreamingTaskIdRef.current = taskId;
353
+
354
+ const parts = status?.message?.parts || [];
355
+ const firstPart = parts?.[0];
356
+ const kind = getKind(firstPart, metadata);
357
+
358
+ const handler = handlers[kind];
359
+ if (!handler) {
360
+ return;
361
+ }
362
+ removeLoadingPlaceholder();
363
+
364
+ fillData({
365
+ kind,
366
+ taskId,
367
+ part: firstPart,
368
+ status,
369
+ handler,
370
+ });
371
+ },
372
+ [handleEnd],
373
+ );
374
+
375
+ const processHistoryData = useCallback(
376
+ (data: Message1, taskId: string) => {
377
+ const { messageId, historyId } = (data || {}) as any;
378
+ if (!messageId) return;
379
+
380
+ const parts = data.parts || [];
381
+ const firstPart = parts?.[0];
382
+
383
+ const kind = getKind(firstPart);
384
+
385
+ const handler = handlers[kind];
386
+ if (!handler) {
387
+ return;
388
+ }
389
+ fillData({
390
+ kind,
391
+ taskId,
392
+ part: firstPart,
393
+ handler,
394
+ isHistory: true,
395
+ historyId,
396
+ messageId,
397
+ status: { message: data },
398
+ });
399
+ },
400
+ [handleEnd],
401
+ );
402
+
403
+ const conversations = useMemo(() => {
404
+ return state.order
405
+ .map((id) => state.byId[id])
406
+ .filter(Boolean) as Conversation[];
407
+ }, [state]);
408
+
409
+ const getConversation = useCallback(
410
+ (taskId: string) => {
411
+ return state.byId[taskId];
412
+ },
413
+ [state],
414
+ );
415
+
416
+ const clearConversation = useCallback((taskId: string) => {
417
+ setState(
418
+ produce((draft) => {
419
+ if (!draft.byId[taskId]) return;
420
+ delete draft.byId[taskId];
421
+ draft.order = draft.order.filter((id) => id !== taskId);
422
+ }),
423
+ );
424
+ }, []);
425
+
426
+ const removeMessage = useCallback((taskId: string, messageId: string) => {
427
+ setState(
428
+ produce((draft) => {
429
+ const conv = draft.byId[taskId];
430
+ if (!conv) return;
431
+
432
+ const nextMessages = conv.messages.filter(
433
+ (m) => m.messageId !== messageId,
434
+ );
435
+
436
+ draft.byId[taskId] = {
437
+ ...conv,
438
+ messages: nextMessages,
439
+ lastUpdated: new Date().toISOString(),
440
+ };
441
+
442
+ const index = draft.order.indexOf(taskId);
443
+ if (index !== -1) {
444
+ draft.order.splice(index, 1);
445
+ }
446
+ draft.order.push(taskId);
447
+ }),
448
+ );
449
+ }, []);
450
+
451
+ const updateMessage = useCallback((input: UpdateMessageInput) => {
452
+ const {
453
+ taskId,
454
+ messageId,
455
+ content,
456
+ partial = false,
457
+ status,
458
+ response,
459
+ metadata = {},
460
+ } = input;
461
+
462
+ setState(
463
+ produce((draft) => {
464
+ const conv = draft.byId[taskId];
465
+ if (!conv) return;
466
+
467
+ const msgIndex = conv.messages.findIndex(
468
+ (m) => m.messageId === messageId,
469
+ );
470
+ if (msgIndex === -1) {
471
+ return;
472
+ }
473
+
474
+ const oldMsg = conv.messages[msgIndex];
475
+ let updatedMsg: MessagesItem = { ...oldMsg, ...metadata };
476
+
477
+ if (content !== undefined) {
478
+ if ("content" in updatedMsg) {
479
+ updatedMsg.content = partial
480
+ ? (updatedMsg as TextMessage).content + content
481
+ : content;
482
+ } else if ("thought" in updatedMsg) {
483
+ updatedMsg.thought = partial
484
+ ? (updatedMsg as ThoughtMessage).thought + content
485
+ : content;
486
+ } else if (oldMsg.kind === "text-content") {
487
+ (updatedMsg as any).content = partial
488
+ ? ((oldMsg as any).content || "") + content
489
+ : content;
490
+ }
491
+ }
492
+
493
+ if ("toolStatus" in updatedMsg || status) {
494
+ (updatedMsg as any).toolStatus =
495
+ status || (updatedMsg as any).toolStatus;
496
+ }
497
+ if (response !== undefined) {
498
+ (updatedMsg as any).response = response;
499
+ }
500
+
501
+ const nextMessages = [...conv.messages];
502
+ nextMessages[msgIndex] = updatedMsg;
503
+
504
+ draft.byId[taskId] = {
505
+ ...conv,
506
+ messages: nextMessages,
507
+ lastUpdated: new Date().toISOString(),
508
+ };
509
+
510
+ const index = draft.order.indexOf(taskId);
511
+ if (index !== -1) {
512
+ draft.order.splice(index, 1);
513
+ }
514
+ draft.order.push(taskId);
515
+ }),
516
+ );
517
+ }, []);
518
+
519
+ const addConversation = useCallback((conversation: Conversation) => {
520
+ setState(
521
+ produce((draft) => {
522
+ const id = conversation.taskId;
523
+ draft.byId[id] = conversation;
524
+ const index = draft.order.indexOf(id);
525
+ if (index !== -1) {
526
+ draft.order.splice(index, 1);
527
+ }
528
+ draft.order.push(id);
529
+ }),
530
+ );
531
+ }, []);
532
+
533
+ const addLoadingPlaceholder = useCallback(() => {
534
+ addConversation({
535
+ taskId: "loading-placeholder",
536
+ status: "working",
537
+ messages: [
538
+ {
539
+ messageId: "loading-placeholder",
540
+ role: "assistant",
541
+ kind: "text-content",
542
+ content: getText().thinking,
543
+ timestamp: new Date().toISOString(),
544
+ },
545
+ ],
546
+ lastUpdated: new Date().toISOString(),
547
+ });
548
+ }, [addConversation]);
549
+
550
+ const removeLoadingPlaceholder = useCallback(() => {
551
+ clearConversation("loading-placeholder");
552
+ }, [clearConversation]);
553
+
554
+ useEffect(() => {
555
+ if (!isLoading) {
556
+ removeLoadingPlaceholder();
557
+ }
558
+ }, [isLoading, removeLoadingPlaceholder]);
559
+
560
+ const onSendError = useCallback(() => {
561
+ addConversation({
562
+ taskId: generateId(),
563
+ status: "error",
564
+ messages: [
565
+ {
566
+ messageId: `error-${Date.now()}`,
567
+ role: "assistant",
568
+ kind: "error",
569
+ content: getText().errorMessage,
570
+ timestamp: new Date().toISOString(),
571
+ } as ErrorMessage,
572
+ ],
573
+ lastUpdated: new Date().toISOString(),
574
+ });
575
+ setIsLoading(false);
576
+ }, [addConversation]);
577
+
578
+ const loadHistory = useCallback(async (limit = 10, next?: string) => {
579
+ setIsLoadingHistory(true);
580
+ let scrollToId = "";
581
+ try {
582
+ const copilot = client.copilot as any;
583
+ const result = await copilot.getHistory?.(limit, next);
584
+ let messages = result?.messages || [];
585
+ const length = messages.length;
586
+ scrollToId = length > 0 ? messages?.[length - 1]?.messageId : "";
587
+
588
+ let taskId = "";
589
+ let lastRole = "";
590
+ let taskIds: string[] = [];
591
+ messages.forEach((msg: any) => {
592
+ let role = msg.role === "agent" ? "assistant" : "user";
593
+ if (role !== lastRole) {
594
+ taskId = `history-conv-${generateId()}`;
595
+ taskIds.push(taskId);
596
+ lastRole = role;
597
+ }
598
+ processHistoryData(msg, taskId);
599
+ });
600
+ setState(
601
+ produce((draft) => {
602
+ draft.order = [...taskIds, ...draft.order];
603
+ }),
604
+ );
605
+ setHistoryState({
606
+ next: result?.next || null,
607
+ hasMore: result?.hasMore || false,
608
+ });
609
+ } catch (error) {
610
+ console.error("[Chat] Failed to load history:", error);
611
+ } finally {
612
+ setIsLoadingHistory(false);
613
+ scrollToId &&
614
+ next &&
615
+ setTimeout(() => {
616
+ document
617
+ .querySelector(`[data-conversation-message-id="${scrollToId}"]`)
618
+ ?.scrollIntoView({ behavior: "instant", block: "center" });
619
+ }, 20);
620
+ }
621
+ }, []);
622
+
623
+ const loadMoreHistory = useCallback(async () => {
624
+ if (!historyState.hasMore || isLoadingHistory || !historyState.next) return;
625
+ await loadHistory(20, historyState.next);
626
+ }, [historyState.hasMore, historyState.next, isLoadingHistory]);
627
+
628
+ const checkActiveTask = useCallback(async () => {
629
+ try {
630
+ const result = await client.copilot.getChatStatus();
631
+ const taskId = result.taskId;
632
+ if (taskId && result.working) {
633
+ currentStreamingTaskIdRef.current = taskId;
634
+
635
+ const stream = await client.copilot.chat([], { taskId });
636
+ for await (const chunk of stream) {
637
+ if (forceStopRef.current) break;
638
+ processLiveData(chunk);
639
+ }
640
+ }
641
+ } catch (error) {
642
+ console.error("[Chat] Failed to check active task:", error);
643
+ }
644
+ }, [processLiveData]);
645
+
646
+ const cancelChat = useCallback(
647
+ async (taskId?: string) => {
648
+ abort();
649
+ const targetTaskId = taskId || currentStreamingTaskIdRef.current;
650
+ if (!targetTaskId) return;
651
+
652
+ try {
653
+ await client.copilot.cancelChat(targetTaskId);
654
+ currentStreamingTaskIdRef.current = null;
655
+ } catch (error) {
656
+ console.error("[Chat] Failed to cancel chat:", error);
657
+ }
658
+ },
659
+ [abort],
660
+ );
661
+
662
+ const resetConversation = useCallback(
663
+ (greeting?: string) => {
664
+ abort();
665
+ setState({ byId: {}, order: [] });
666
+ currentStreamingTaskIdRef.current = null;
667
+
668
+ if (greeting) {
669
+ const taskId = `conv-${Date.now()}`;
670
+ setState({
671
+ byId: {
672
+ [taskId]: {
673
+ taskId,
674
+ status: "submitted",
675
+ messages: [
676
+ {
677
+ messageId: `greeting-${Date.now()}`,
678
+ role: "assistant",
679
+ kind: "text-content",
680
+ content: greeting,
681
+ timestamp: new Date().toISOString(),
682
+ } as TextMessage,
683
+ ],
684
+ lastUpdated: new Date().toISOString(),
685
+ },
686
+ },
687
+ order: [taskId],
688
+ });
689
+ }
690
+ },
691
+ [abort],
692
+ );
693
+
694
+ const startNewConversation = useCallback(async () => {
695
+ addConversation({
696
+ taskId: generateId(),
697
+ status: "submitted",
698
+ messages: [],
699
+ lastUpdated: new Date().toISOString(),
700
+ system: { level: "newConversation" },
701
+ });
702
+ try {
703
+ const copilot = client.copilot as any;
704
+ await copilot.newConversation?.();
705
+ } catch (error) {
706
+ console.error("[Chat] Failed to create new conversation:", error);
707
+ }
708
+ }, []);
709
+
710
+ const subscribeChatEvents = useCallback(
711
+ async ({
712
+ userContent,
713
+ taskId,
714
+ }: {
715
+ userContent?: string;
716
+ taskId?: string;
717
+ }) => {
718
+ let hasResponse = false;
719
+
720
+ if (abortControllerRef.current) {
721
+ abortControllerRef.current.abort();
722
+ }
723
+ if (typeof AbortController !== "undefined") {
724
+ abortControllerRef.current = new AbortController();
725
+ }
726
+ const controller = abortControllerRef.current;
727
+ forceStopRef.current = false;
728
+
729
+ // mock
730
+ if (userContent === "MAGA") {
731
+ const { generateMockUIStream } = await import("../mock/mock-data");
732
+ const cancelMock = generateMockUIStream(
733
+ taskId || generateId(),
734
+ (chunk) => {
735
+ if (forceStopRef.current) return;
736
+ hasResponse = true;
737
+ processLiveData(chunk as any);
738
+ if (chunk.result.final) {
739
+ setIsLoading(false);
740
+ }
741
+ },
742
+ );
743
+ abortControllerRef.current?.signal.addEventListener(
744
+ "abort",
745
+ cancelMock,
746
+ );
747
+ return;
748
+ }
749
+
750
+ // real chat
751
+ try {
752
+ const stream = client.copilot.chat(
753
+ userContent ? [{ role: "user", content: userContent }] : [],
754
+ taskId ? { taskId } : undefined,
755
+ );
756
+
757
+ for await (const chunk of stream) {
758
+ if (controller?.signal.aborted || forceStopRef.current) break;
759
+ hasResponse = true;
760
+
761
+ processLiveData(chunk);
762
+
763
+ if ((chunk as any)?.isFinal || (chunk as any)?.final) {
764
+ break;
765
+ }
766
+ }
767
+ } catch (err: any) {
768
+ if (err.name === "AbortError" || forceStopRef.current) {
769
+ console.error("[Chat] Request aborted", err);
770
+ } else {
771
+ console.error("[Chat] Stream error:", err);
772
+ onSendError();
773
+ }
774
+ } finally {
775
+ abort();
776
+ if (!hasResponse) {
777
+ onSendError();
778
+ }
779
+ }
780
+ },
781
+ [],
782
+ );
783
+
784
+ const sendMessage = useCallback(
785
+ async (userContent: string) => {
786
+ if (loadingRef.current) {
787
+ return;
788
+ }
789
+ if (!userContent.trim()) return;
790
+ const userMsgId = generateId();
791
+
792
+ addConversation({
793
+ taskId: generateId(),
794
+ status: "submitted",
795
+ messages: [
796
+ {
797
+ messageId: userMsgId,
798
+ role: "user",
799
+ kind: "text-content",
800
+ content: userContent,
801
+ timestamp: new Date().toISOString(),
802
+ },
803
+ ],
804
+ lastUpdated: new Date().toISOString(),
805
+ });
806
+
807
+ addLoadingPlaceholder();
808
+
809
+ setIsLoading(true);
810
+ subscribeChatEvents({ userContent });
811
+ },
812
+ [
813
+ addConversation,
814
+ addLoadingPlaceholder,
815
+ processLiveData,
816
+ onSendError,
817
+ abort,
818
+ ],
819
+ );
820
+
821
+ useEffect(() => {
822
+ if (starting) {
823
+ const init = async () => {
824
+ console.debug("[Chat] Initializing conversation...");
825
+ try {
826
+ await loadHistory();
827
+ await checkActiveTask();
828
+ setStarting(false);
829
+ } catch (error) {
830
+ console.error("[Chat] Initialization error:", error);
831
+ setStarting(false);
832
+ }
833
+ };
834
+ init();
835
+ }
836
+ }, [starting, loadHistory, checkActiveTask]);
837
+
838
+ return {
839
+ starting,
840
+ isLoading,
841
+ isLoadingHistory,
842
+ historyState,
843
+ conversations,
844
+ sendMessage,
845
+ abort,
846
+ cancelChat,
847
+ resetConversation,
848
+ startNewConversation,
849
+ loadHistory,
850
+ loadMoreHistory,
851
+ getConversation,
852
+ clearConversation,
853
+ removeMessage,
854
+ updateMessage,
855
+ };
856
+ }