@talknote/create-talknote-h5 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/bin/create-talknote-h5.js +14 -0
  2. package/package.json +1 -1
  3. package/template/.agents/skills/add-h5-page/SKILL.md +116 -0
  4. package/template/.eslintrc.js +2 -0
  5. package/template/README.md +143 -117
  6. package/template/_gitignore +0 -3
  7. package/template/config/config.ts +49 -4
  8. package/template/config/proxy.ts +28 -31
  9. package/template/docker/nginx/error_page.conf +51 -0
  10. package/template/docker/nginx/nginx.conf +54 -0
  11. package/template/docker/nginx/webApp.conf +13 -0
  12. package/template/docker/webApp.Dockerfile +12 -0
  13. package/template/package.json +6 -2
  14. package/template/packages/xiaozhi-chat/README.md +53 -0
  15. package/template/packages/xiaozhi-chat/XiaozhiChatPage.less +50 -0
  16. package/template/packages/xiaozhi-chat/XiaozhiChatPage.tsx +172 -0
  17. package/template/packages/xiaozhi-chat/assets/images/bg.jpg +0 -0
  18. package/template/packages/xiaozhi-chat/assets/images/icon-arrow-back.png +0 -0
  19. package/template/packages/xiaozhi-chat/assets/images/icon-close.png +0 -0
  20. package/template/packages/xiaozhi-chat/assets/images/icon-voice.png +0 -0
  21. package/template/packages/xiaozhi-chat/assets/images/keyboard.png +0 -0
  22. package/template/packages/xiaozhi-chat/assets/images/send-icon.png +0 -0
  23. package/template/packages/xiaozhi-chat/assets/images/stop-icon.png +0 -0
  24. package/template/packages/xiaozhi-chat/components/AssistantRichContent/index.less +3 -0
  25. package/template/packages/xiaozhi-chat/components/AssistantRichContent/index.tsx +44 -0
  26. package/template/packages/xiaozhi-chat/components/ChatHeader/index.less +84 -0
  27. package/template/packages/xiaozhi-chat/components/ChatHeader/index.tsx +44 -0
  28. package/template/packages/xiaozhi-chat/components/ChatInput/HoldToTalk.less +59 -0
  29. package/template/packages/xiaozhi-chat/components/ChatInput/HoldToTalk.tsx +167 -0
  30. package/template/packages/xiaozhi-chat/components/ChatInput/index.less +189 -0
  31. package/template/packages/xiaozhi-chat/components/ChatInput/index.tsx +203 -0
  32. package/template/packages/xiaozhi-chat/components/DeepThinkingWrapper/index.less +80 -0
  33. package/template/packages/xiaozhi-chat/components/DeepThinkingWrapper/index.tsx +54 -0
  34. package/template/packages/xiaozhi-chat/components/MarkdownContent/index.less +209 -0
  35. package/template/packages/xiaozhi-chat/components/MarkdownContent/index.tsx +41 -0
  36. package/template/packages/xiaozhi-chat/components/MessageList/index.less +74 -0
  37. package/template/packages/xiaozhi-chat/components/MessageList/index.tsx +195 -0
  38. package/template/packages/xiaozhi-chat/components/ThinkingCompleteEntry/index.less +56 -0
  39. package/template/packages/xiaozhi-chat/components/ThinkingCompleteEntry/index.tsx +65 -0
  40. package/template/packages/xiaozhi-chat/components/ToolCallList/index.less +41 -0
  41. package/template/packages/xiaozhi-chat/components/ToolCallList/index.tsx +51 -0
  42. package/template/packages/xiaozhi-chat/components/TypingIndicator/index.less +35 -0
  43. package/template/packages/xiaozhi-chat/components/TypingIndicator/index.tsx +12 -0
  44. package/template/packages/xiaozhi-chat/components/Welcome/index.less +105 -0
  45. package/template/packages/xiaozhi-chat/components/Welcome/index.tsx +84 -0
  46. package/template/packages/xiaozhi-chat/hooks/useChatSession.ts +235 -0
  47. package/template/packages/xiaozhi-chat/hooks/useRealtimeTranscription.ts +226 -0
  48. package/template/packages/xiaozhi-chat/hooks/useTypewriter.ts +215 -0
  49. package/template/packages/xiaozhi-chat/hooks/useViewportHeight.ts +43 -0
  50. package/template/packages/xiaozhi-chat/hooks/useVoiceRecording.ts +128 -0
  51. package/template/packages/xiaozhi-chat/index.ts +11 -0
  52. package/template/packages/xiaozhi-chat/package.json +34 -0
  53. package/template/packages/xiaozhi-chat/services/chatApi.ts +585 -0
  54. package/template/packages/xiaozhi-chat/types.ts +80 -0
  55. package/template/packages/xiaozhi-chat/utils/bridge.ts +131 -0
  56. package/template/packages/xiaozhi-chat/utils/deepThinking.ts +200 -0
  57. package/template/packages/xiaozhi-chat/utils/parseAssistantContent.ts +233 -0
  58. package/template/packages/xiaozhi-chat/utils/typewriter.ts +265 -0
  59. package/template/scripts/dev.js +6 -7
  60. package/template/src/app.tsx +1 -73
  61. package/template/src/assets/error-network.png +0 -0
  62. package/template/src/assets/error.png +0 -0
  63. package/template/src/assets/success.png +0 -0
  64. package/template/src/assets/welcome.png +0 -0
  65. package/template/src/components/Header/index.less +70 -0
  66. package/template/src/components/Header/index.tsx +46 -0
  67. package/template/src/components/TabBar/index.less +13 -0
  68. package/template/src/components/TabBar/index.tsx +58 -0
  69. package/template/src/global.css +27 -0
  70. package/template/src/layouts/BlankLayout/index.less +5 -0
  71. package/template/src/layouts/BlankLayout/index.tsx +14 -0
  72. package/template/src/layouts/HeaderLayout/index.less +25 -0
  73. package/template/src/layouts/HeaderLayout/index.tsx +42 -0
  74. package/template/src/layouts/LayoutOutlet.tsx +7 -0
  75. package/template/src/layouts/SSOLoginLayout/index.less +28 -0
  76. package/template/src/layouts/SSOLoginLayout/index.tsx +92 -0
  77. package/template/src/layouts/TabBarLayout/index.less +21 -0
  78. package/template/src/layouts/TabBarLayout/index.tsx +29 -0
  79. package/template/src/layouts/index.tsx +30 -0
  80. package/template/src/layouts/usePageHeaderProps.ts +9 -0
  81. package/template/src/pages/index/index.css +5 -5
  82. package/template/src/pages/index/index.tsx +15 -2
  83. package/template/src/pages/list/index.tsx +5 -5
  84. package/template/src/pages/test/index.tsx +12 -0
  85. package/template/src/pages/xiaozhi-chat/index.less +4 -0
  86. package/template/src/pages/xiaozhi-chat/index.tsx +26 -0
  87. package/template/src/routes.ts +49 -0
  88. package/template/src/services/api.ts +7 -2
  89. package/template/src/theme/variables.css +5 -0
  90. package/template/src/utils/ensureSsoLogin.ts +28 -0
  91. package/template/src/utils/goBack.ts +10 -0
  92. package/template/src/utils/request.ts +106 -0
  93. package/template/tsconfig.json +4 -1
  94. package/template/typings.d.ts +15 -0
  95. package/template/yarn.lock +2168 -61
  96. package/template/src/assets/demoIcon/home.png +0 -0
  97. package/template/src/assets/demoIcon/home1.png +0 -0
  98. package/template/src/assets/demoIcon/list.png +0 -0
  99. package/template/src/assets/demoIcon/list1.png +0 -0
  100. package/template/src/assets/demoIcon/setting.png +0 -0
  101. package/template/src/assets/demoIcon/setting1.png +0 -0
  102. package/template/src/assets/yay.jpg +0 -0
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import type { ToolCallGroup } from '../../utils/parseAssistantContent';
3
+ import styles from './index.less';
4
+
5
+ type ToolCallListProps = {
6
+ groups: ToolCallGroup[];
7
+ };
8
+
9
+ const ToolCallList: React.FC<ToolCallListProps> = ({ groups }) => {
10
+ if (!groups.length) return null;
11
+
12
+ return (
13
+ <div className={styles.list}>
14
+ {groups.map((group) => {
15
+ const { toolNameAlias, total, executed, hasError } = group;
16
+ const showProgress = total > 0 && executed > 1;
17
+ const isFinish = executed === total && total > 0;
18
+
19
+ return (
20
+ <span
21
+ key={toolNameAlias}
22
+ className={`${styles.chip} ${isFinish ? '' : styles.chipPending}`}
23
+ >
24
+ <svg className={styles.icon} viewBox="0 0 16 16" fill="none" aria-hidden>
25
+ <path
26
+ d="M3.2 8.4 6.1 11.2 12.8 4.4"
27
+ stroke="currentColor"
28
+ strokeWidth="1.6"
29
+ strokeLinecap="round"
30
+ strokeLinejoin="round"
31
+ />
32
+ </svg>
33
+ <span className={styles.label}>
34
+ {showProgress ? (
35
+ <>
36
+ {`${toolNameAlias}(`}
37
+ <span className={hasError ? styles.errorCount : undefined}>{executed}</span>
38
+ )
39
+ </>
40
+ ) : (
41
+ toolNameAlias
42
+ )}
43
+ </span>
44
+ </span>
45
+ );
46
+ })}
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export default ToolCallList;
@@ -0,0 +1,35 @@
1
+ .root {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 2Px;
5
+ padding: 8Px 0;
6
+ }
7
+
8
+ .dot {
9
+ width: 4Px;
10
+ height: 4Px;
11
+ border-radius: 50%;
12
+ background: #fe780f;
13
+ animation: bounce 1s ease-in-out infinite;
14
+ }
15
+
16
+ .dot:nth-child(2) {
17
+ animation-delay: 0.15s;
18
+ }
19
+
20
+ .dot:nth-child(3) {
21
+ animation-delay: 0.3s;
22
+ }
23
+
24
+ @keyframes bounce {
25
+ 0%,
26
+ 80%,
27
+ 100% {
28
+ opacity: 0.35;
29
+ transform: translateY(0);
30
+ }
31
+ 40% {
32
+ opacity: 1;
33
+ transform: translateY(-3Px);
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import styles from './index.less';
3
+
4
+ const TypingIndicator: React.FC = () => (
5
+ <div className={styles.root} aria-hidden>
6
+ <span className={styles.dot} />
7
+ <span className={styles.dot} />
8
+ <span className={styles.dot} />
9
+ </div>
10
+ );
11
+
12
+ export default TypingIndicator;
@@ -0,0 +1,105 @@
1
+ .welcome {
2
+ padding: 24Px 16Px 8Px;
3
+ }
4
+
5
+ .greeting {
6
+ display: flex;
7
+ flex-direction: column;
8
+ align-items: flex-start;
9
+ margin-bottom: 20Px;
10
+ }
11
+
12
+ .title {
13
+ margin: 0 0 8Px;
14
+ font-size: 20Px;
15
+ font-weight: 500;
16
+ line-height: 24Px;
17
+ color: #2a2b33;
18
+ font-family: 'PingFang SC', 'Noto Sans CJK SC', system-ui, -apple-system, sans-serif;
19
+ }
20
+
21
+ .subtitle {
22
+ margin: 0;
23
+ font-size: 15Px;
24
+ line-height: 22Px;
25
+ color: #2a2b33;
26
+ font-family: 'PingFang SC', 'Noto Sans CJK SC', system-ui, -apple-system, sans-serif;
27
+ }
28
+
29
+ .questions {
30
+ display: flex;
31
+ flex-direction: column;
32
+ align-items: flex-start;
33
+ }
34
+
35
+ .questionsHeader {
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ width: 100%;
40
+ margin-bottom: 12Px;
41
+ display: none;
42
+ }
43
+
44
+ .questionsTitle {
45
+ margin: 0;
46
+ font-size: 13Px;
47
+ font-weight: 500;
48
+ line-height: 20Px;
49
+ color: #9092a3;
50
+ }
51
+
52
+ .refresh {
53
+ padding: 0;
54
+ border: 0;
55
+ background: transparent;
56
+ color: #9092a3;
57
+ font-size: 13Px;
58
+ line-height: 20Px;
59
+ cursor: pointer;
60
+ }
61
+
62
+ .questionList {
63
+ display: flex;
64
+ flex-direction: column;
65
+ align-items: flex-start;
66
+ gap: 8Px;
67
+ max-width: 100%;
68
+ }
69
+
70
+ .capsule {
71
+ display: inline-flex;
72
+ max-width: 100%;
73
+ padding: 8Px 14Px;
74
+ border: 0.5Px solid rgba(254, 120, 15, 0.18);
75
+ border-radius: 999Px;
76
+ background: rgba(255, 255, 255, 0.82);
77
+ box-shadow: 0 1Px 4Px rgba(100, 103, 122, 0.06);
78
+ color: #414352;
79
+ font-size: 14Px;
80
+ line-height: 22Px;
81
+ text-align: left;
82
+ cursor: pointer;
83
+ opacity: 0;
84
+ transform: translateX(-12Px);
85
+ animation: capsuleIn 0.38s cubic-bezier(0.22, 1, 0.36, 1) forwards;
86
+ }
87
+
88
+ @keyframes capsuleIn {
89
+ from {
90
+ opacity: 0;
91
+ transform: translateX(-12Px);
92
+ }
93
+ to {
94
+ opacity: 1;
95
+ transform: translateX(0);
96
+ }
97
+ }
98
+
99
+ @media (prefers-reduced-motion: reduce) {
100
+ .capsule {
101
+ opacity: 1;
102
+ transform: none;
103
+ animation: none;
104
+ }
105
+ }
@@ -0,0 +1,84 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import styles from './index.less';
3
+
4
+ const OPPORTUNITY_QUESTIONS = [
5
+ '本周有哪些高意向商机?',
6
+ '哪些商机本周需要重点跟进?',
7
+ '最近丢单的主要原因是什么?',
8
+ '帮我总结本月商机进展',
9
+ '哪些客户出现了新的采购意向?',
10
+ '当前管道里金额最大的商机有哪些?',
11
+ '有哪些商机卡在报价或决策阶段?',
12
+ '本周新发现的商机有哪些?',
13
+ '哪些商机预计本月能够成交?',
14
+ '帮我看看重点客户的商机风险',
15
+ ];
16
+
17
+ const PICK_COUNT = 5;
18
+
19
+ const pickRandomQuestions = (source: string[], count: number, exclude: string[] = []) => {
20
+ const pool = source.filter((item) => !exclude.includes(item));
21
+ const from = pool.length >= count ? pool : source;
22
+ const shuffled = [...from].sort(() => Math.random() - 0.5);
23
+ return shuffled.slice(0, Math.min(count, shuffled.length));
24
+ };
25
+
26
+ type WelcomeProps = {
27
+ title?: string;
28
+ subtitle?: string;
29
+ onQuestionPress?: (question: string) => void;
30
+ };
31
+
32
+ const Welcome: React.FC<WelcomeProps> = ({
33
+ title = 'Hi😄,快来聊聊~',
34
+ subtitle = '发现更多内容',
35
+ onQuestionPress,
36
+ }) => {
37
+ const [round, setRound] = useState(0);
38
+ const [questions, setQuestions] = useState(() =>
39
+ pickRandomQuestions(OPPORTUNITY_QUESTIONS, PICK_COUNT),
40
+ );
41
+
42
+ const handleRefresh = useCallback(() => {
43
+ setQuestions((prev) => pickRandomQuestions(OPPORTUNITY_QUESTIONS, PICK_COUNT, prev));
44
+ setRound((prev) => prev + 1);
45
+ }, []);
46
+
47
+ const items = useMemo(
48
+ () => questions.map((question, index) => ({ question, index })),
49
+ [questions],
50
+ );
51
+
52
+ return (
53
+ <div className={styles.welcome}>
54
+ <div className={styles.greeting}>
55
+ <h1 className={styles.title}>{title}</h1>
56
+ <p className={styles.subtitle}>{subtitle}</p>
57
+ </div>
58
+
59
+ <section className={styles.questions} aria-label="大家都在问">
60
+ <div className={styles.questionsHeader}>
61
+ <h2 className={styles.questionsTitle}>大家都在问</h2>
62
+ <button type="button" className={styles.refresh} onClick={handleRefresh}>
63
+ 换一换
64
+ </button>
65
+ </div>
66
+ <div key={round} className={styles.questionList}>
67
+ {items.map(({ question, index }) => (
68
+ <button
69
+ key={`${round}-${question}`}
70
+ type="button"
71
+ className={styles.capsule}
72
+ style={{ animationDelay: `${60 + index * 60}ms` }}
73
+ onClick={() => onQuestionPress?.(question)}
74
+ >
75
+ {question}
76
+ </button>
77
+ ))}
78
+ </div>
79
+ </section>
80
+ </div>
81
+ );
82
+ };
83
+
84
+ export default Welcome;
@@ -0,0 +1,235 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { createSession, streamChat } from '../services/chatApi';
3
+ import type { ChatMessage, ChatSession } from '../types';
4
+ import { DEFAULT_CHAT_VERSION } from '../types';
5
+ import {
6
+ KNOWLEDGE_CHAT_TYPEWRITER_ATOMIC_CLOSED_TAGS,
7
+ KNOWLEDGE_CHAT_TYPEWRITER_ATOMIC_TOKENS,
8
+ } from '../utils/deepThinking';
9
+ import { useTypewriter } from './useTypewriter';
10
+
11
+ const createId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
12
+ const EMPTY_REPLY_FALLBACK = '抱歉,我没有找到相关信息。';
13
+
14
+ export const useChatSession = () => {
15
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
16
+ const [isStreaming, setIsStreaming] = useState(false);
17
+ const [isSSEComplete, setIsSSEComplete] = useState(false);
18
+ const sessionRef = useRef<ChatSession | null>(null);
19
+ const abortRef = useRef<AbortController | null>(null);
20
+ const streamingContentRef = useRef('');
21
+ const streamingAssistantIdRef = useRef('');
22
+ const finalizeStartedRef = useRef(false);
23
+
24
+ const {
25
+ displayedContent,
26
+ isOutputComplete,
27
+ append,
28
+ markStreamComplete,
29
+ reset: resetTypewriter,
30
+ skip: skipTypewriter,
31
+ } = useTypewriter({
32
+ atomicTokens: [...KNOWLEDGE_CHAT_TYPEWRITER_ATOMIC_TOKENS],
33
+ atomicClosedTags: [...KNOWLEDGE_CHAT_TYPEWRITER_ATOMIC_CLOSED_TAGS],
34
+ });
35
+
36
+ const clearChat = useCallback(() => {
37
+ abortRef.current?.abort();
38
+ abortRef.current = null;
39
+ sessionRef.current = null;
40
+ streamingContentRef.current = '';
41
+ streamingAssistantIdRef.current = '';
42
+ finalizeStartedRef.current = true;
43
+ setIsStreaming(false);
44
+ setIsSSEComplete(false);
45
+ resetTypewriter();
46
+ setMessages([]);
47
+ }, [resetTypewriter]);
48
+
49
+ const stop = useCallback(() => {
50
+ if (!abortRef.current) return;
51
+
52
+ abortRef.current.abort();
53
+ abortRef.current = null;
54
+ finalizeStartedRef.current = true;
55
+ setIsStreaming(false);
56
+ setIsSSEComplete(false);
57
+
58
+ const assistantId = streamingAssistantIdRef.current;
59
+ const content = streamingContentRef.current || displayedContent;
60
+ if (assistantId) {
61
+ setMessages((prev) =>
62
+ prev.map((msg) =>
63
+ msg.id === assistantId
64
+ ? { ...msg, content, isStopped: true }
65
+ : msg,
66
+ ),
67
+ );
68
+ }
69
+
70
+ streamingContentRef.current = '';
71
+ streamingAssistantIdRef.current = '';
72
+ skipTypewriter();
73
+ resetTypewriter();
74
+ }, [displayedContent, resetTypewriter, skipTypewriter]);
75
+
76
+ useEffect(() => {
77
+ const assistantId = streamingAssistantIdRef.current;
78
+ if (!assistantId || !isStreaming) return;
79
+ setMessages((prev) =>
80
+ prev.map((msg) =>
81
+ msg.id === assistantId ? { ...msg, content: displayedContent } : msg,
82
+ ),
83
+ );
84
+ }, [displayedContent, isStreaming]);
85
+
86
+ useEffect(() => {
87
+ if (!isStreaming || !isSSEComplete || !isOutputComplete || finalizeStartedRef.current) {
88
+ return;
89
+ }
90
+
91
+ finalizeStartedRef.current = true;
92
+ const assistantId = streamingAssistantIdRef.current;
93
+ const content = displayedContent || streamingContentRef.current || EMPTY_REPLY_FALLBACK;
94
+
95
+ if (assistantId) {
96
+ setMessages((prev) =>
97
+ prev.map((msg) =>
98
+ msg.id === assistantId ? { ...msg, content } : msg,
99
+ ),
100
+ );
101
+ }
102
+
103
+ streamingContentRef.current = '';
104
+ streamingAssistantIdRef.current = '';
105
+ abortRef.current = null;
106
+ setIsStreaming(false);
107
+ setIsSSEComplete(false);
108
+ requestAnimationFrame(() => {
109
+ resetTypewriter();
110
+ });
111
+ }, [displayedContent, isOutputComplete, isSSEComplete, isStreaming, resetTypewriter]);
112
+
113
+ const send = useCallback(async (rawQuery: string) => {
114
+ const query = rawQuery.trim();
115
+ if (!query || isStreaming) return;
116
+
117
+ const userMessage: ChatMessage = {
118
+ id: createId(),
119
+ role: 'user',
120
+ content: query,
121
+ timestamp: Date.now(),
122
+ };
123
+ const assistantId = createId();
124
+ const assistantMessage: ChatMessage = {
125
+ id: assistantId,
126
+ role: 'assistant',
127
+ content: '',
128
+ timestamp: Date.now(),
129
+ chatVersion: DEFAULT_CHAT_VERSION,
130
+ };
131
+
132
+ streamingAssistantIdRef.current = assistantId;
133
+ streamingContentRef.current = '';
134
+ finalizeStartedRef.current = false;
135
+ resetTypewriter();
136
+ setIsSSEComplete(false);
137
+ setMessages((prev) => [...prev, userMessage, assistantMessage]);
138
+ setIsStreaming(true);
139
+
140
+ const controller = new AbortController();
141
+ abortRef.current = controller;
142
+
143
+ try {
144
+ if (!sessionRef.current) {
145
+ sessionRef.current = await createSession({
146
+ query,
147
+ chatVersion: DEFAULT_CHAT_VERSION,
148
+ signal: controller.signal,
149
+ });
150
+ }
151
+
152
+ if (controller.signal.aborted) return;
153
+
154
+ const { chatId, contextId } = sessionRef.current;
155
+
156
+ await streamChat(
157
+ {
158
+ query,
159
+ chatId,
160
+ contextId,
161
+ chatVersion: DEFAULT_CHAT_VERSION,
162
+ signal: controller.signal,
163
+ },
164
+ {
165
+ onChunk: (chunk) => {
166
+ if (controller.signal.aborted) return;
167
+ streamingContentRef.current += chunk;
168
+ append(chunk);
169
+ },
170
+ onDone: () => {
171
+ if (controller.signal.aborted) return;
172
+ setIsSSEComplete(true);
173
+ markStreamComplete();
174
+ },
175
+ onSession: (session) => {
176
+ sessionRef.current = {
177
+ chatId: session.chatId || sessionRef.current?.chatId || '',
178
+ contextId: session.contextId || sessionRef.current?.contextId || '',
179
+ };
180
+ },
181
+ onError: (message) => {
182
+ if (controller.signal.aborted) return;
183
+ finalizeStartedRef.current = true;
184
+ setMessages((prev) =>
185
+ prev.map((msg) =>
186
+ msg.id === assistantId
187
+ ? {
188
+ ...msg,
189
+ content: message || '抱歉,回答时出现了错误,请稍后重试。',
190
+ isReplyError: true,
191
+ }
192
+ : msg,
193
+ ),
194
+ );
195
+ setIsStreaming(false);
196
+ setIsSSEComplete(false);
197
+ streamingContentRef.current = '';
198
+ streamingAssistantIdRef.current = '';
199
+ abortRef.current = null;
200
+ resetTypewriter();
201
+ },
202
+ },
203
+ );
204
+ } catch (error) {
205
+ if (controller.signal.aborted) {
206
+ return;
207
+ }
208
+ const message =
209
+ error instanceof Error ? error.message : '抱歉,发送消息失败,请检查网络连接后重试。';
210
+ finalizeStartedRef.current = true;
211
+ setMessages((prev) =>
212
+ prev.map((msg) =>
213
+ msg.id === assistantId
214
+ ? { ...msg, content: message, isReplyError: true }
215
+ : msg,
216
+ ),
217
+ );
218
+ setIsStreaming(false);
219
+ setIsSSEComplete(false);
220
+ streamingContentRef.current = '';
221
+ streamingAssistantIdRef.current = '';
222
+ abortRef.current = null;
223
+ resetTypewriter();
224
+ }
225
+ }, [append, isStreaming, markStreamComplete, resetTypewriter]);
226
+
227
+ return {
228
+ messages,
229
+ isStreaming,
230
+ showWelcome: messages.length === 0,
231
+ send,
232
+ stop,
233
+ clearChat,
234
+ };
235
+ };