@huyooo/ai-chat-frontend-react 0.2.11 → 0.2.13
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.
- package/dist/index.css +94 -25
- package/dist/index.css.map +1 -1
- package/dist/index.js +558 -114
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/input/AtFilePicker.css +19 -19
- package/src/components/input/ChatInput.css +81 -1
- package/src/components/input/ChatInput.tsx +65 -18
- package/src/components/input/DropdownSelector.css +11 -5
- package/src/hooks/useVoiceInput.ts +454 -0
- package/src/hooks/useVoiceToTextInput.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huyooo/ai-chat-frontend-react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "AI Chat Frontend - React components with adapter pattern",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"react-dom": ">=18.0.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@huyooo/ai-chat-bridge-electron": "^0.2.
|
|
36
|
-
"@huyooo/ai-chat-shared": "^0.2.
|
|
35
|
+
"@huyooo/ai-chat-bridge-electron": "^0.2.13",
|
|
36
|
+
"@huyooo/ai-chat-shared": "^0.2.13",
|
|
37
37
|
"@iconify/react": "^5.0.2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
position: fixed;
|
|
3
3
|
width: 332px;
|
|
4
4
|
background: var(--chat-dropdown-bg, #252526);
|
|
5
|
-
border: 1px solid rgba(
|
|
5
|
+
border: 1px solid var(--chat-border, rgba(0, 0, 0, 0.12));
|
|
6
6
|
border-radius: 10px;
|
|
7
|
-
box-shadow: 0
|
|
7
|
+
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
|
|
8
8
|
z-index: 99999;
|
|
9
9
|
display: flex;
|
|
10
10
|
flex-direction: column;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
align-items: center;
|
|
17
17
|
gap: 8px;
|
|
18
18
|
padding: 10px 12px;
|
|
19
|
-
border-bottom: 1px solid rgba(
|
|
19
|
+
border-bottom: 1px solid var(--chat-border, rgba(0, 0, 0, 0.12));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
.at-picker-back {
|
|
@@ -28,18 +28,18 @@
|
|
|
28
28
|
background: transparent;
|
|
29
29
|
border: none;
|
|
30
30
|
border-radius: 4px;
|
|
31
|
-
color: #888;
|
|
31
|
+
color: var(--chat-text-muted, #888);
|
|
32
32
|
cursor: pointer;
|
|
33
33
|
flex-shrink: 0;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
.at-picker-back:hover {
|
|
37
|
-
background: rgba(
|
|
38
|
-
color: #
|
|
37
|
+
background: var(--chat-muted-hover, rgba(0, 0, 0, 0.06));
|
|
38
|
+
color: var(--chat-text, #26251eeb);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
.at-picker-search-icon {
|
|
42
|
-
color: #
|
|
42
|
+
color: var(--chat-text-muted, #888);
|
|
43
43
|
flex-shrink: 0;
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
background: transparent;
|
|
49
49
|
border: none;
|
|
50
50
|
outline: none;
|
|
51
|
-
color: #
|
|
51
|
+
color: var(--chat-text, #26251eeb);
|
|
52
52
|
font-size: 13px;
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
.at-picker-recent {
|
|
73
73
|
padding-bottom: 6px;
|
|
74
74
|
margin-bottom: 6px;
|
|
75
|
-
border-bottom: 1px solid rgba(
|
|
75
|
+
border-bottom: 1px solid var(--chat-border, rgba(0, 0, 0, 0.12));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
.at-picker-list {
|
|
@@ -91,27 +91,27 @@
|
|
|
91
91
|
border: 1px solid transparent;
|
|
92
92
|
background: transparent;
|
|
93
93
|
cursor: pointer;
|
|
94
|
-
color: #
|
|
94
|
+
color: var(--chat-text, #26251eeb);
|
|
95
95
|
width: 100%;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
.at-picker-item:hover {
|
|
99
|
-
background: rgba(
|
|
99
|
+
background: var(--chat-muted-hover, rgba(0, 0, 0, 0.06));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
.at-picker-item.active {
|
|
103
|
-
background: rgba(
|
|
104
|
-
border-color:
|
|
103
|
+
background: var(--chat-muted-hover, rgba(0, 0, 0, 0.06));
|
|
104
|
+
border-color: var(--chat-primary, #54a9ff);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
.at-picker-item-icon {
|
|
108
|
-
color: #
|
|
108
|
+
color: var(--chat-text-muted, #888);
|
|
109
109
|
flex-shrink: 0;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
.at-picker-item-name {
|
|
113
113
|
font-size: 13px;
|
|
114
|
-
color: #
|
|
114
|
+
color: var(--chat-text, #26251eeb);
|
|
115
115
|
flex-shrink: 0;
|
|
116
116
|
max-width: 160px;
|
|
117
117
|
overflow: hidden;
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
|
|
122
122
|
.at-picker-item-path {
|
|
123
123
|
font-size: 11px;
|
|
124
|
-
color: #
|
|
124
|
+
color: var(--chat-text-muted, #888);
|
|
125
125
|
min-width: 0;
|
|
126
126
|
overflow: hidden;
|
|
127
127
|
text-overflow: ellipsis;
|
|
@@ -135,13 +135,13 @@
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
.at-picker-chevron {
|
|
138
|
-
color: #
|
|
138
|
+
color: var(--chat-text-muted, #888);
|
|
139
139
|
flex-shrink: 0;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
.at-picker-footer {
|
|
143
143
|
padding: 8px 12px;
|
|
144
|
-
border-top: 1px solid rgba(
|
|
145
|
-
color: #
|
|
144
|
+
border-top: 1px solid var(--chat-border, rgba(0, 0, 0, 0.12));
|
|
145
|
+
color: var(--chat-text-muted, #888);
|
|
146
146
|
font-size: 11px;
|
|
147
147
|
}
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
background: rgba(37, 99, 235, 0.1);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/* 录音态不改变 textarea 样式(仅改变图标颜色) */
|
|
34
|
+
|
|
33
35
|
/* 图片预览区 */
|
|
34
36
|
.images-preview {
|
|
35
37
|
display: flex;
|
|
@@ -161,8 +163,64 @@
|
|
|
161
163
|
|
|
162
164
|
.input-right .icon-btn:hover {
|
|
163
165
|
color: var(--chat-text, #ccc);
|
|
166
|
+
background: rgba(0, 0, 0, 0.06);
|
|
167
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 10%, transparent);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.input-right .voice-btn {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
justify-content: center;
|
|
174
|
+
width: 28px;
|
|
175
|
+
height: 28px;
|
|
176
|
+
background: transparent;
|
|
177
|
+
border: none;
|
|
178
|
+
border-radius: 6px;
|
|
179
|
+
color: var(--chat-text-muted, #666);
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
transition: all 0.15s;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.input-right .voice-btn:hover {
|
|
185
|
+
color: var(--chat-text, #ccc);
|
|
186
|
+
background: rgba(0, 0, 0, 0.06);
|
|
187
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 10%, transparent);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.input-right .voice-btn.connecting {
|
|
191
|
+
color: #f59e0b;
|
|
192
|
+
background: transparent;
|
|
164
193
|
}
|
|
165
194
|
|
|
195
|
+
.input-right .voice-btn.connecting:hover {
|
|
196
|
+
color: #f59e0b;
|
|
197
|
+
background: rgba(0, 0, 0, 0.06);
|
|
198
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 10%, transparent);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.input-right .voice-btn .spin {
|
|
202
|
+
animation: spin 1s linear infinite;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.input-right .voice-btn:disabled {
|
|
206
|
+
opacity: 0.5;
|
|
207
|
+
cursor: not-allowed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.input-right .voice-btn.recording {
|
|
211
|
+
color: #ef4444;
|
|
212
|
+
background: transparent;
|
|
213
|
+
animation: none;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.input-right .voice-btn.recording:hover {
|
|
217
|
+
color: #ef4444;
|
|
218
|
+
background: rgba(0, 0, 0, 0.06);
|
|
219
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 10%, transparent);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* 录音态不加动效(使用实心暂停图标即可) */
|
|
223
|
+
|
|
166
224
|
.toggle-btn {
|
|
167
225
|
display: flex;
|
|
168
226
|
align-items: center;
|
|
@@ -179,10 +237,21 @@
|
|
|
179
237
|
|
|
180
238
|
.toggle-btn:hover {
|
|
181
239
|
color: var(--chat-text, #ccc);
|
|
240
|
+
background: rgba(0, 0, 0, 0.06);
|
|
241
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 10%, transparent);
|
|
182
242
|
}
|
|
183
243
|
|
|
184
244
|
.toggle-btn.active {
|
|
185
|
-
|
|
245
|
+
/* 激活态使用主色调(更清晰的对比度) */
|
|
246
|
+
color: var(--chat-primary, #54a9ff);
|
|
247
|
+
background: rgba(84, 169, 255, 0.14);
|
|
248
|
+
background: color-mix(in srgb, var(--chat-primary, #54a9ff) 14%, transparent);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.toggle-btn.active:hover {
|
|
252
|
+
color: var(--chat-primary-hover, #2f90ff);
|
|
253
|
+
background: rgba(84, 169, 255, 0.2);
|
|
254
|
+
background: color-mix(in srgb, var(--chat-primary, #54a9ff) 20%, transparent);
|
|
186
255
|
}
|
|
187
256
|
|
|
188
257
|
.send-btn {
|
|
@@ -201,4 +270,15 @@
|
|
|
201
270
|
|
|
202
271
|
.send-btn:hover {
|
|
203
272
|
color: var(--chat-text, #ccc);
|
|
273
|
+
background: rgba(0, 0, 0, 0.06);
|
|
274
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 10%, transparent);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.send-btn:disabled {
|
|
278
|
+
opacity: 0.5;
|
|
279
|
+
cursor: not-allowed;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.send-btn:disabled:hover {
|
|
283
|
+
color: var(--chat-text-muted, #666);
|
|
204
284
|
}
|
|
@@ -13,6 +13,7 @@ import { DropdownSelector } from './DropdownSelector';
|
|
|
13
13
|
import type { DropdownOption, GroupedOptions } from './DropdownSelector';
|
|
14
14
|
import { useChatInputContext } from '../../context/ChatInputContext';
|
|
15
15
|
import { useImageUpload } from '../../hooks/useImageUpload';
|
|
16
|
+
import { useVoiceToTextInput } from '../../hooks/useVoiceToTextInput';
|
|
16
17
|
import type { ImageData } from '../../adapter';
|
|
17
18
|
|
|
18
19
|
/** ChatInput 暴露给外部的方法 */
|
|
@@ -193,6 +194,23 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
193
194
|
return inputText.trim() || imageUpload.hasImages;
|
|
194
195
|
}, [inputText, imageUpload.hasImages]);
|
|
195
196
|
|
|
197
|
+
const voiceCtl = useVoiceToTextInput({
|
|
198
|
+
adapter,
|
|
199
|
+
inputText,
|
|
200
|
+
setInputText,
|
|
201
|
+
hasImages: imageUpload.hasImages,
|
|
202
|
+
isLoading,
|
|
203
|
+
});
|
|
204
|
+
const voiceInput = voiceCtl.voiceInput;
|
|
205
|
+
const toggleVoiceInput = useCallback(async () => {
|
|
206
|
+
await voiceCtl.toggleVoice();
|
|
207
|
+
// 停止/取消后聚焦输入框
|
|
208
|
+
if (voiceInput.status === 'recording' || voiceInput.status === 'connecting') {
|
|
209
|
+
pendingFocusRef.current = true;
|
|
210
|
+
pendingCaretRef.current = null;
|
|
211
|
+
}
|
|
212
|
+
}, [voiceCtl, voiceInput.status]);
|
|
213
|
+
|
|
196
214
|
// 自动调整高度
|
|
197
215
|
const adjustTextareaHeight = useCallback(() => {
|
|
198
216
|
if (inputRef.current) {
|
|
@@ -293,12 +311,13 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
293
311
|
// 处理键盘事件
|
|
294
312
|
const handleKeyDown = useCallback(
|
|
295
313
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
314
|
+
if (voiceCtl.handleKeyDownForVoice(e)) return;
|
|
296
315
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
297
316
|
e.preventDefault();
|
|
298
317
|
handleSendOrCancel();
|
|
299
318
|
}
|
|
300
319
|
},
|
|
301
|
-
[handleSendOrCancel]
|
|
320
|
+
[handleSendOrCancel, voiceCtl]
|
|
302
321
|
);
|
|
303
322
|
|
|
304
323
|
// 点击外部关闭菜单
|
|
@@ -327,7 +346,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
327
346
|
>
|
|
328
347
|
<div
|
|
329
348
|
ref={containerRef}
|
|
330
|
-
className={`input-container${isFocused ? ' focused' : ''}${imageUpload.isDragOver ? ' drag-over' : ''}`.trim()}
|
|
349
|
+
className={`input-container${isFocused ? ' focused' : ''}${imageUpload.isDragOver ? ' drag-over' : ''}${voiceInput.status === 'connecting' ? ' connecting' : ''}${voiceInput.isRecording ? ' recording' : ''}`.trim()}
|
|
331
350
|
>
|
|
332
351
|
{/* 图片预览区 */}
|
|
333
352
|
{imageUpload.hasImages && (
|
|
@@ -372,6 +391,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
372
391
|
placeholder={placeholder}
|
|
373
392
|
rows={1}
|
|
374
393
|
className="input-field chat-scrollbar"
|
|
394
|
+
readOnly={voiceInput.isRecording}
|
|
375
395
|
spellCheck={false}
|
|
376
396
|
autoCorrect="off"
|
|
377
397
|
autoComplete="off"
|
|
@@ -476,23 +496,50 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
476
496
|
)}
|
|
477
497
|
</div>
|
|
478
498
|
|
|
479
|
-
{
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
499
|
+
{/* 方案 A:录音按钮 + 发送按钮独立(停止录音入口永不消失) */}
|
|
500
|
+
<button
|
|
501
|
+
className={`voice-btn${voiceInput.status === 'connecting' ? ' connecting' : ''}${voiceInput.isRecording ? ' recording' : ''}`}
|
|
502
|
+
title={
|
|
503
|
+
voiceInput.status === 'connecting'
|
|
504
|
+
? '正在连接,点击取消'
|
|
505
|
+
: voiceInput.isRecording
|
|
506
|
+
? '点击停止'
|
|
507
|
+
: '点击录音'
|
|
508
|
+
}
|
|
509
|
+
onClick={() => toggleVoiceInput().catch(() => {})}
|
|
510
|
+
disabled={isLoading || !adapter}
|
|
511
|
+
>
|
|
512
|
+
{voiceInput.status === 'connecting' ? (
|
|
513
|
+
<Icon icon="lucide:loader-2" width={18} className="spin" />
|
|
514
|
+
) : voiceInput.isRecording ? (
|
|
515
|
+
<Icon icon="streamline-flex:button-pause-circle-remix" width={18} />
|
|
516
|
+
) : (
|
|
493
517
|
<Icon icon="lucide:mic" width={18} />
|
|
494
|
-
|
|
495
|
-
|
|
518
|
+
)}
|
|
519
|
+
</button>
|
|
520
|
+
|
|
521
|
+
<button
|
|
522
|
+
className={`send-btn${isLoading ? ' loading' : ''}`}
|
|
523
|
+
title={
|
|
524
|
+
isLoading
|
|
525
|
+
? '停止'
|
|
526
|
+
: voiceInput.status === 'connecting'
|
|
527
|
+
? '语音连接中,先取消/停止'
|
|
528
|
+
: voiceInput.isRecording
|
|
529
|
+
? '录音中,先停止'
|
|
530
|
+
: isMessageVariant
|
|
531
|
+
? '重新发送'
|
|
532
|
+
: '发送'
|
|
533
|
+
}
|
|
534
|
+
onClick={handleSendOrCancel}
|
|
535
|
+
disabled={(!hasContent && !isLoading) || voiceInput.isRecording || voiceInput.status === 'connecting'}
|
|
536
|
+
>
|
|
537
|
+
{isLoading ? (
|
|
538
|
+
<Icon icon="streamline-flex:button-pause-circle-remix" width={18} />
|
|
539
|
+
) : (
|
|
540
|
+
<Icon icon="streamline-flex:mail-send-email-message-circle-remix" width={18} />
|
|
541
|
+
)}
|
|
542
|
+
</button>
|
|
496
543
|
</div>
|
|
497
544
|
</div>
|
|
498
545
|
)}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
display: flex;
|
|
8
8
|
align-items: center;
|
|
9
9
|
gap: 4px;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
height: 28px; /* 与 ChatInput 右侧按钮一致 */
|
|
11
|
+
padding: 0 8px;
|
|
12
|
+
/* 避免用 --chat-muted 做背景(暗色下容易与 input 背景同色) */
|
|
13
|
+
background: rgba(0, 0, 0, 0.04);
|
|
14
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 6%, transparent);
|
|
12
15
|
border: none;
|
|
13
16
|
border-radius: 6px;
|
|
14
17
|
font-size: 14px;
|
|
@@ -18,7 +21,8 @@
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
.dropdown-selector:hover:not(.disabled) {
|
|
21
|
-
background:
|
|
24
|
+
background: rgba(0, 0, 0, 0.06);
|
|
25
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 10%, transparent);
|
|
22
26
|
color: var(--chat-text, #ccc);
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -87,12 +91,14 @@
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
.dropdown-selector .dropdown-item:hover {
|
|
90
|
-
background: rgba(
|
|
94
|
+
background: rgba(0, 0, 0, 0.06);
|
|
95
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 10%, transparent);
|
|
91
96
|
color: var(--chat-text, #ccc);
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
.dropdown-selector .dropdown-item.active {
|
|
95
|
-
background: rgba(
|
|
100
|
+
background: rgba(0, 0, 0, 0.08);
|
|
101
|
+
background: color-mix(in srgb, var(--chat-text, #ccc) 14%, transparent);
|
|
96
102
|
color: var(--chat-text, #fff);
|
|
97
103
|
}
|
|
98
104
|
|