@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huyooo/ai-chat-frontend-react",
3
- "version": "0.2.11",
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.11",
36
- "@huyooo/ai-chat-shared": "^0.2.11",
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(255, 255, 255, 0.1);
5
+ border: 1px solid var(--chat-border, rgba(0, 0, 0, 0.12));
6
6
  border-radius: 10px;
7
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
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(255, 255, 255, 0.06);
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(255, 255, 255, 0.08);
38
- color: #ccc;
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: #666;
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: #ddd;
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(255, 255, 255, 0.08);
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: #ccc;
94
+ color: var(--chat-text, #26251eeb);
95
95
  width: 100%;
96
96
  }
97
97
 
98
98
  .at-picker-item:hover {
99
- background: rgba(255, 255, 255, 0.06);
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(59, 130, 246, 0.15);
104
- border-color: rgba(59, 130, 246, 0.3);
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: #999;
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: #ddd;
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: #555;
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: #555;
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(255, 255, 255, 0.06);
145
- color: #555;
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
- color: var(--chat-text, #fff);
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
- {hasContent || isLoading ? (
480
- <button
481
- className={`send-btn${isLoading ? ' loading' : ''}`}
482
- title={isLoading ? '停止' : isMessageVariant ? '重新发送' : '发送'}
483
- onClick={handleSendOrCancel}
484
- >
485
- {isLoading ? (
486
- <Icon icon="streamline-flex:button-pause-circle-remix" width={18} />
487
- ) : (
488
- <Icon icon="streamline-flex:mail-send-email-message-circle-remix" width={18} />
489
- )}
490
- </button>
491
- ) : (
492
- <button className="icon-btn" title="语音输入">
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
- </button>
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
- padding: 4px 8px;
11
- background: var(--chat-muted, #3c3c3c);
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: var(--chat-muted-hover, #444);
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(255, 255, 255, 0.08);
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(255, 255, 255, 0.1);
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