@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,167 @@
1
+ import React, { useCallback, useRef, useState } from 'react';
2
+ import { Toast } from 'antd-mobile';
3
+ import { useVoiceRecording } from '../../hooks/useVoiceRecording';
4
+ import styles from './HoldToTalk.less';
5
+
6
+ type HoldToTalkProps = {
7
+ disabled?: boolean;
8
+ onResult: (text: string) => void;
9
+ };
10
+
11
+ const CANCEL_THRESHOLD_PX = 56;
12
+
13
+ const HoldToTalk: React.FC<HoldToTalkProps> = ({ disabled, onResult }) => {
14
+ const { phase, start, cancel, stop, ensureSupported } = useVoiceRecording();
15
+ const [willCancel, setWillCancel] = useState(false);
16
+ const startYRef = useRef(0);
17
+ const activePointerRef = useRef<number | null>(null);
18
+ const willCancelRef = useRef(false);
19
+ const recordingReadyRef = useRef(false);
20
+
21
+ /** 手指已抬起,但 start() 异步尚未返回 */
22
+ const releasedBeforeStartRef = useRef(false);
23
+ const releaseActionRef = useRef<'stop' | 'cancel' | null>(null);
24
+
25
+ const resetGesture = useCallback(() => {
26
+ activePointerRef.current = null;
27
+ willCancelRef.current = false;
28
+ setWillCancel(false);
29
+ }, []);
30
+
31
+ const finishByAction = useCallback(
32
+ async (action: 'stop' | 'cancel') => {
33
+ if (action === 'cancel') {
34
+ await cancel();
35
+ Toast.show({ content: '已取消' });
36
+ return;
37
+ }
38
+ const text = await stop();
39
+ if (text) {
40
+ onResult(text);
41
+ } else {
42
+ Toast.show({ content: '未识别到内容,请重试' });
43
+ }
44
+ },
45
+ [cancel, onResult, stop],
46
+ );
47
+
48
+ const handlePointerDown = useCallback(
49
+ async (event: React.PointerEvent<HTMLButtonElement>) => {
50
+ if (disabled || activePointerRef.current != null) return;
51
+ if (!ensureSupported()) return;
52
+
53
+ event.preventDefault();
54
+ event.currentTarget.setPointerCapture(event.pointerId);
55
+ activePointerRef.current = event.pointerId;
56
+ startYRef.current = event.clientY;
57
+ willCancelRef.current = false;
58
+ recordingReadyRef.current = false;
59
+ releasedBeforeStartRef.current = false;
60
+ releaseActionRef.current = null;
61
+ setWillCancel(false);
62
+
63
+ const ok = await start();
64
+ if (!ok) {
65
+ resetGesture();
66
+ releasedBeforeStartRef.current = false;
67
+ releaseActionRef.current = null;
68
+ recordingReadyRef.current = false;
69
+ return;
70
+ }
71
+
72
+ recordingReadyRef.current = true;
73
+
74
+ // start 完成时手指已抬起:按抬起意图收尾
75
+ if (releasedBeforeStartRef.current) {
76
+ const action = releaseActionRef.current || 'stop';
77
+ releasedBeforeStartRef.current = false;
78
+ releaseActionRef.current = null;
79
+ recordingReadyRef.current = false;
80
+ resetGesture();
81
+ await finishByAction(action);
82
+ }
83
+ },
84
+ [disabled, ensureSupported, finishByAction, resetGesture, start],
85
+ );
86
+
87
+ const handlePointerMove = useCallback((event: React.PointerEvent<HTMLButtonElement>) => {
88
+ if (activePointerRef.current !== event.pointerId) return;
89
+ const deltaY = startYRef.current - event.clientY;
90
+ const next = deltaY > CANCEL_THRESHOLD_PX;
91
+ if (next !== willCancelRef.current) {
92
+ willCancelRef.current = next;
93
+ setWillCancel(next);
94
+ }
95
+ }, []);
96
+
97
+ const handlePointerUp = useCallback(
98
+ async (event: React.PointerEvent<HTMLButtonElement>) => {
99
+ if (activePointerRef.current !== event.pointerId) return;
100
+
101
+ const shouldCancel = willCancelRef.current;
102
+ const action: 'stop' | 'cancel' = shouldCancel ? 'cancel' : 'stop';
103
+
104
+ // start() 尚未完成
105
+ if (!recordingReadyRef.current) {
106
+ releasedBeforeStartRef.current = true;
107
+ releaseActionRef.current = action;
108
+ resetGesture();
109
+ return;
110
+ }
111
+
112
+ recordingReadyRef.current = false;
113
+ resetGesture();
114
+ await finishByAction(action);
115
+ },
116
+ [finishByAction, resetGesture],
117
+ );
118
+
119
+ const handlePointerCancel = useCallback(
120
+ async (event: React.PointerEvent<HTMLButtonElement>) => {
121
+ if (activePointerRef.current !== event.pointerId) return;
122
+
123
+ if (!recordingReadyRef.current) {
124
+ releasedBeforeStartRef.current = true;
125
+ releaseActionRef.current = 'cancel';
126
+ resetGesture();
127
+ return;
128
+ }
129
+
130
+ recordingReadyRef.current = false;
131
+ resetGesture();
132
+ await cancel();
133
+ },
134
+ [cancel, resetGesture],
135
+ );
136
+
137
+ const isRecording = phase === 'recording';
138
+ let buttonLabel = '按住说话';
139
+ if (isRecording) {
140
+ buttonLabel = willCancel ? '松开取消' : '松开发送';
141
+ }
142
+
143
+ return (
144
+ <div className={styles.wrap}>
145
+ {isRecording ? (
146
+ <div className={`${styles.tip} ${willCancel ? styles.tipCancel : ''}`}>
147
+ {willCancel ? '松手取消' : '松手发送,上滑取消'}
148
+ </div>
149
+ ) : null}
150
+ <button
151
+ type="button"
152
+ className={`${styles.button} ${isRecording ? styles.buttonActive : ''} ${
153
+ willCancel ? styles.buttonCancel : ''
154
+ }`}
155
+ disabled={disabled}
156
+ onPointerDown={handlePointerDown}
157
+ onPointerMove={handlePointerMove}
158
+ onPointerUp={handlePointerUp}
159
+ onPointerCancel={handlePointerCancel}
160
+ >
161
+ {buttonLabel}
162
+ </button>
163
+ </div>
164
+ );
165
+ };
166
+
167
+ export default HoldToTalk;
@@ -0,0 +1,189 @@
1
+ .root {
2
+ width: 100%;
3
+ padding: 0 16Px 8Px;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ .shortcutSlot {
8
+ display: flex;
9
+ gap: 8Px;
10
+ overflow-x: auto;
11
+ margin-bottom: 8Px;
12
+ -webkit-overflow-scrolling: touch;
13
+ }
14
+
15
+ .shortcutSlot::-webkit-scrollbar {
16
+ display: none;
17
+ }
18
+
19
+ .mainRow {
20
+ display: flex;
21
+ align-items: flex-end;
22
+ gap: 8Px;
23
+ margin-bottom: 12Px;
24
+ }
25
+
26
+ .inputContainer {
27
+ flex: 1;
28
+ min-width: 0;
29
+ background: #ffffff;
30
+ border-radius: 24Px;
31
+ overflow: hidden;
32
+ box-shadow: 0 4Px 24Px 0 rgba(135, 52, 0, 0.08);
33
+ }
34
+
35
+ .disabled {
36
+ opacity: 0.5;
37
+ }
38
+
39
+ .inputArea {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 8Px;
43
+ min-height: 48Px;
44
+ padding: 0 12Px;
45
+ }
46
+
47
+ .iconBtn {
48
+ flex-shrink: 0;
49
+ width: 24Px;
50
+ height: 24Px;
51
+ border: none;
52
+ padding: 0;
53
+ background: transparent;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ }
58
+
59
+ .iconBtn:disabled {
60
+ opacity: 0.5;
61
+ }
62
+
63
+ .iconImg {
64
+ width: 24Px;
65
+ height: 24Px;
66
+ object-fit: contain;
67
+ }
68
+
69
+ .iconSpacer {
70
+ flex-shrink: 0;
71
+ width: 24Px;
72
+ height: 24Px;
73
+ }
74
+
75
+ .textarea {
76
+ flex: 1;
77
+ min-width: 0;
78
+ min-height: 48Px;
79
+ max-height: 76Px;
80
+ padding: 13Px 0 11Px;
81
+ border: none;
82
+ outline: none;
83
+ resize: none;
84
+ background: transparent;
85
+ font-size: 15Px;
86
+ line-height: 22Px;
87
+ color: #2a2b33;
88
+ font-family: 'PingFang SC', 'Noto Sans CJK SC', system-ui, -apple-system, sans-serif;
89
+ }
90
+
91
+ .textarea::placeholder {
92
+ color: #b0b2bf;
93
+ }
94
+
95
+ .textarea:disabled {
96
+ opacity: 0.6;
97
+ }
98
+
99
+ .micBtn {
100
+ flex-shrink: 0;
101
+ width: 32Px;
102
+ height: 32Px;
103
+ border: none;
104
+ padding: 0;
105
+ border-radius: 50%;
106
+ background: transparent;
107
+ color: #8e90a0;
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ }
112
+
113
+ .micBtn:disabled {
114
+ opacity: 0.45;
115
+ }
116
+
117
+ .micBtnActive {
118
+ color: #fe780f;
119
+ background: rgba(254, 120, 15, 0.12);
120
+ animation: micPulse 1.2s ease-in-out infinite;
121
+ }
122
+
123
+ .micSvg {
124
+ width: 22Px;
125
+ height: 22Px;
126
+ display: block;
127
+ }
128
+
129
+ @keyframes micPulse {
130
+ 0%,
131
+ 100% {
132
+ transform: scale(1);
133
+ }
134
+ 50% {
135
+ transform: scale(1.08);
136
+ }
137
+ }
138
+
139
+ .actionWrap {
140
+ flex-shrink: 0;
141
+ width: 50Px;
142
+ height: 50Px;
143
+ border-radius: 25Px;
144
+ background: #ffffff;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ overflow: hidden;
149
+ box-shadow: 0 4Px 24Px 0 rgba(135, 52, 0, 0.08);
150
+ }
151
+
152
+ .actionWrapEnabled {
153
+ background: #ffffff;
154
+ }
155
+
156
+ .actionBtn {
157
+ width: 24Px;
158
+ height: 24Px;
159
+ border: none;
160
+ padding: 0;
161
+ background: transparent;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ }
166
+
167
+ .actionBtn:disabled {
168
+ opacity: 1;
169
+ }
170
+
171
+ .actionIcon {
172
+ width: 24Px;
173
+ height: 24Px;
174
+ object-fit: contain;
175
+ }
176
+
177
+ .actionIconDisabled {
178
+ opacity: 0.35;
179
+ }
180
+
181
+ .disclaimer {
182
+ text-align: center;
183
+ color: #b0b2bf;
184
+ font-size: 10Px;
185
+ font-weight: 400;
186
+ line-height: 14Px;
187
+ margin-bottom: 8Px;
188
+ font-family: 'PingFang SC', 'Noto Sans CJK SC', system-ui, -apple-system, sans-serif;
189
+ }
@@ -0,0 +1,203 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import iconKeyboard from '../../assets/images/keyboard.png';
3
+ import iconVoice from '../../assets/images/icon-voice.png';
4
+ import iconSend from '../../assets/images/send-icon.png';
5
+ import iconStop from '../../assets/images/stop-icon.png';
6
+ import HoldToTalk from './HoldToTalk';
7
+ import { hideVibeNoteKeyboard } from '../../utils/bridge';
8
+ import { useRealtimeTranscription } from '../../hooks/useRealtimeTranscription';
9
+ import styles from './index.less';
10
+
11
+ type InputMode = 'keyboard' | 'voice';
12
+
13
+ type ChatInputProps = {
14
+ value: string;
15
+ onChange: (value: string) => void;
16
+ onSend: (text: string) => void;
17
+ onStop: () => void;
18
+ isStreaming?: boolean;
19
+ disabled?: boolean;
20
+ placeholder?: string;
21
+ shortcutSlot?: React.ReactNode;
22
+ onVoiceResult?: (question: string) => void;
23
+ };
24
+
25
+ const ChatInput: React.FC<ChatInputProps> = ({
26
+ value,
27
+ onChange,
28
+ onSend,
29
+ onStop,
30
+ isStreaming = false,
31
+ disabled = false,
32
+ placeholder = '请输入您想问的问题...',
33
+ shortcutSlot,
34
+ onVoiceResult,
35
+ }) => {
36
+ const [mode, setMode] = useState<InputMode>('keyboard');
37
+ const { transcribing, toggle, stop: stopTranscription } = useRealtimeTranscription({
38
+ value,
39
+ onChange,
40
+ });
41
+ const canSend = value.trim().length > 0 && !isStreaming && !disabled;
42
+ const inputLocked = disabled || isStreaming;
43
+ const showSend = mode === 'keyboard' && !isStreaming;
44
+ const showStop = isStreaming;
45
+
46
+ useEffect(() => {
47
+ if (isStreaming) {
48
+ setMode('keyboard');
49
+ stopTranscription({ silent: true });
50
+ }
51
+ }, [isStreaming, stopTranscription]);
52
+
53
+ const handleSend = useCallback(async () => {
54
+ if (isStreaming || disabled) return;
55
+ const latest = await stopTranscription({ silent: true });
56
+ const text = (latest || value).trim();
57
+ if (!text) return;
58
+ onSend(text);
59
+ }, [disabled, isStreaming, onSend, stopTranscription, value]);
60
+
61
+ const handleKeyDown = useCallback(
62
+ (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
63
+ if (event.key === 'Enter' && !event.shiftKey) {
64
+ event.preventDefault();
65
+ handleSend();
66
+ }
67
+ },
68
+ [handleSend],
69
+ );
70
+
71
+ const handleVoiceResult = useCallback(
72
+ (text: string) => {
73
+ if (onVoiceResult) {
74
+ onVoiceResult(text);
75
+ return;
76
+ }
77
+ onChange(text);
78
+ onSend(text);
79
+ },
80
+ [onChange, onSend, onVoiceResult],
81
+ );
82
+
83
+ const toggleMode = useCallback(() => {
84
+ if (inputLocked) return;
85
+ setMode((prev) => {
86
+ const next = prev === 'keyboard' ? 'voice' : 'keyboard';
87
+ if (next === 'voice') {
88
+ hideVibeNoteKeyboard();
89
+ stopTranscription({ silent: true });
90
+ }
91
+ return next;
92
+ });
93
+ }, [inputLocked, stopTranscription]);
94
+
95
+ const handleMicClick = useCallback(() => {
96
+ if (inputLocked) return;
97
+ hideVibeNoteKeyboard();
98
+ toggle();
99
+ }, [inputLocked, toggle]);
100
+
101
+ return (
102
+ <div className={styles.root}>
103
+ {shortcutSlot ? <div className={styles.shortcutSlot}>{shortcutSlot}</div> : null}
104
+
105
+ <div className={styles.mainRow}>
106
+ <div className={`${styles.inputContainer} ${inputLocked ? styles.disabled : ''}`}>
107
+ <div className={styles.inputArea}>
108
+ <button
109
+ type="button"
110
+ className={styles.iconBtn}
111
+ disabled={inputLocked}
112
+ onClick={toggleMode}
113
+ aria-label={mode === 'keyboard' ? '切换到按住说话' : '切换到键盘输入'}
114
+ >
115
+ <img
116
+ src={mode === 'voice' ? iconKeyboard : iconVoice}
117
+ alt=""
118
+ className={styles.iconImg}
119
+ />
120
+ </button>
121
+
122
+ {mode === 'keyboard' ? (
123
+ <>
124
+ <textarea
125
+ className={styles.textarea}
126
+ value={value}
127
+ onChange={(e) => onChange(e.target.value)}
128
+ onKeyDown={handleKeyDown}
129
+ placeholder={transcribing ? '正在聆听...' : placeholder}
130
+ disabled={inputLocked}
131
+ readOnly={transcribing}
132
+ rows={1}
133
+ maxLength={500}
134
+ />
135
+ <button
136
+ type="button"
137
+ className={`${styles.micBtn} ${transcribing ? styles.micBtnActive : ''}`}
138
+ disabled={inputLocked}
139
+ onClick={handleMicClick}
140
+ aria-label={transcribing ? '停止转写' : '开始语音转写'}
141
+ aria-pressed={transcribing}
142
+ >
143
+ <svg viewBox="0 0 24 24" className={styles.micSvg} aria-hidden>
144
+ <path
145
+ d="M12 3.4a3.4 3.4 0 0 0-3.4 3.4v4.4a3.4 3.4 0 1 0 6.8 0V6.8A3.4 3.4 0 0 0 12 3.4Z"
146
+ fill={transcribing ? 'currentColor' : 'none'}
147
+ stroke="currentColor"
148
+ strokeWidth="1.6"
149
+ />
150
+ <path
151
+ d="M6.5 11.2a5.5 5.5 0 0 0 11 0M12 16.6v3.4"
152
+ fill="none"
153
+ stroke="currentColor"
154
+ strokeWidth="1.6"
155
+ strokeLinecap="round"
156
+ />
157
+ </svg>
158
+ </button>
159
+ </>
160
+ ) : (
161
+ <>
162
+ <HoldToTalk disabled={inputLocked} onResult={handleVoiceResult} />
163
+ <span className={styles.iconSpacer} aria-hidden />
164
+ </>
165
+ )}
166
+ </div>
167
+ </div>
168
+
169
+ {(showSend || showStop) && (
170
+ <div
171
+ className={`${styles.actionWrap} ${
172
+ (showSend && canSend) || showStop ? styles.actionWrapEnabled : ''
173
+ }`}
174
+ >
175
+ {showStop ? (
176
+ <button type="button" className={styles.actionBtn} onClick={onStop} aria-label="停止生成">
177
+ <img src={iconStop} alt="" className={styles.actionIcon} />
178
+ </button>
179
+ ) : (
180
+ <button
181
+ type="button"
182
+ className={styles.actionBtn}
183
+ disabled={!canSend}
184
+ onClick={handleSend}
185
+ aria-label="发送"
186
+ >
187
+ <img
188
+ src={iconSend}
189
+ alt=""
190
+ className={`${styles.actionIcon} ${canSend ? '' : styles.actionIconDisabled}`}
191
+ />
192
+ </button>
193
+ )}
194
+ </div>
195
+ )}
196
+ </div>
197
+
198
+ <div className={styles.disclaimer}>本回答由AI生成,仅供参考</div>
199
+ </div>
200
+ );
201
+ };
202
+
203
+ export default ChatInput;
@@ -0,0 +1,80 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: flex-start;
5
+ gap: 8Px;
6
+ padding: 0 0 16Px;
7
+ width: 100%;
8
+ }
9
+
10
+ .titleRow {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 8Px;
14
+ padding: 4Px 0;
15
+ }
16
+
17
+ .spinIcon {
18
+ width: 20Px;
19
+ height: 20Px;
20
+ flex-shrink: 0;
21
+ animation: spin 1.5s linear infinite;
22
+ }
23
+
24
+ .titleText {
25
+ font-size: 14Px;
26
+ line-height: 20Px;
27
+ font-weight: 500;
28
+ background: linear-gradient(90deg, #2a2b33 0%, #fe780f 100%);
29
+ -webkit-background-clip: text;
30
+ background-clip: text;
31
+ color: transparent;
32
+ }
33
+
34
+ .content {
35
+ width: 100%;
36
+ max-height: 200Px;
37
+ overflow-y: auto;
38
+ -webkit-overflow-scrolling: touch;
39
+ }
40
+
41
+ .dots {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 4Px;
45
+ }
46
+
47
+ .dot {
48
+ width: 6Px;
49
+ height: 6Px;
50
+ border-radius: 50%;
51
+ background: #fe780f;
52
+ animation: bounce 1s ease-in-out infinite;
53
+ }
54
+
55
+ .dot:nth-child(2) {
56
+ animation-delay: 0.15s;
57
+ }
58
+
59
+ .dot:nth-child(3) {
60
+ animation-delay: 0.3s;
61
+ }
62
+
63
+ @keyframes spin {
64
+ to {
65
+ transform: rotate(360deg);
66
+ }
67
+ }
68
+
69
+ @keyframes bounce {
70
+ 0%,
71
+ 80%,
72
+ 100% {
73
+ opacity: 0.35;
74
+ transform: translateY(0);
75
+ }
76
+ 40% {
77
+ opacity: 1;
78
+ transform: translateY(-3Px);
79
+ }
80
+ }
@@ -0,0 +1,54 @@
1
+ /* eslint-disable react/no-multi-comp */
2
+ import React, { useEffect, useRef } from 'react';
3
+ import styles from './index.less';
4
+
5
+ type DeepThinkingWrapperProps = {
6
+ children: React.ReactNode;
7
+ };
8
+
9
+ const ThinkingSpinIcon: React.FC = () => {
10
+ const gradId = `xiaozhi-think-${React.useId().replace(/:/g, '')}`;
11
+ return (
12
+ <svg className={styles.spinIcon} viewBox="0 0 1024 1024" fill="none" aria-hidden>
13
+ <path
14
+ d="M433.472 548.672c19.648 41.408 68.736 60.16 110.528 41.856a85.12 85.12 0 0 0 44.8-115.2 85.056 85.056 0 0 0-117.312-38.848 85.12 85.12 0 0 0-38.016 112.192z m398.08-37.504c75.968-136.576 83.648-274.816 18.816-339.648-64.896-64.448-203.968-56.768-340.48 19.2C373.76 116.032 235.968 108.8 171.52 173.248c-64.448 64.384-57.152 201.792 17.92 337.92-75.072 135.68-82.368 273.472-17.92 337.472 20.032 20.032 47.36 34.112 78.912 40.96 70.848 14.912 162.56-5.568 259.008-58.432 97.28 53.76 190.272 75.072 261.568 60.16 31.104-6.848 58.88-20.928 78.912-40.96 20.032-20.096 34.56-47.36 40.96-78.528 15.36-71.232-5.952-163.84-59.712-260.672h0.384z m-599.04-277.376c26.048-25.984 73.856-28.544 142.528-8.064 17.088 5.12 34.112 11.52 51.2 18.752a858.24 858.24 0 0 0-97.28 84.032C297.408 359.68 268.8 392.96 243.2 427.52a468.16 468.16 0 0 1-18.752-51.648c-20.928-68.672-18.368-116.48 8.064-142.464v0.384zM375.04 796.16c-69.12 20.928-116.48 17.92-142.528-8.128-25.984-25.984-28.544-73.344-8.064-142.464 5.12-17.088 11.52-34.176 18.752-51.2a886.592 886.592 0 0 0 182.592 183.04 495.232 495.232 0 0 1-51.2 18.752h0.448z m14.528-163.392A758.4 758.4 0 0 1 288.832 510.72c26.88-41.408 60.608-81.92 100.736-122.048a780.8 780.8 0 0 1 120.32-99.392c41.344 26.88 82.304 60.608 122.432 100.672 39.68 39.68 72.96 79.808 99.84 120.32-26.88 40.576-60.16 80.64-99.84 120.32a779.712 779.712 0 0 1-122.432 100.736 780.8 780.8 0 0 1-120.32-99.456v0.896z m399.744 156.992c-25.984 26.048-73.792 28.608-142.464 8.128a545.28 545.28 0 0 1-53.76-19.648c34.944-25.6 68.224-54.208 99.84-85.76 31.104-30.72 59.264-63.552 84.48-97.728 7.68 17.536 14.08 35.456 19.584 53.376 20.928 68.672 18.368 116.48-8.064 142.464l0.384-0.832z m8.128-415.168c-5.568 17.92-11.968 35.84-19.648 53.376a868.48 868.48 0 0 0-84.864-98.176 856.128 856.128 0 0 0-99.84-85.76c17.92-7.68 35.84-14.464 53.76-20.032 69.12-20.928 116.48-17.92 142.464 8.128 26.048 25.984 28.608 73.344 8.128 142.464z"
15
+ fill={`url(#${gradId})`}
16
+ />
17
+ <defs>
18
+ <linearGradient id={gradId} x1="176" y1="128" x2="848" y2="896">
19
+ <stop stopColor="#2A2B33" />
20
+ <stop offset="1" stopColor="#FE780F" />
21
+ </linearGradient>
22
+ </defs>
23
+ </svg>
24
+ );
25
+ };
26
+
27
+ const DeepThinkingWrapper: React.FC<DeepThinkingWrapperProps> = ({ children }) => {
28
+ const scrollRef = useRef<HTMLDivElement>(null);
29
+
30
+ useEffect(() => {
31
+ const el = scrollRef.current;
32
+ if (!el) return;
33
+ el.scrollTop = el.scrollHeight;
34
+ }, [children]);
35
+
36
+ return (
37
+ <div className={styles.root}>
38
+ <div className={styles.titleRow}>
39
+ <ThinkingSpinIcon />
40
+ <span className={styles.titleText}>正在深度思考</span>
41
+ </div>
42
+ <div ref={scrollRef} className={styles.content}>
43
+ {children}
44
+ </div>
45
+ <div className={styles.dots} aria-hidden>
46
+ <span className={styles.dot} />
47
+ <span className={styles.dot} />
48
+ <span className={styles.dot} />
49
+ </div>
50
+ </div>
51
+ );
52
+ };
53
+
54
+ export default DeepThinkingWrapper;