@huyooo/ai-chat-frontend-vue 0.1.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.
Files changed (45) hide show
  1. package/dist/adapter.d.ts +87 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/components/ChatInput.vue.d.ts +54 -0
  4. package/dist/components/ChatInput.vue.d.ts.map +1 -0
  5. package/dist/components/ChatPanel.vue.d.ts +38 -0
  6. package/dist/components/ChatPanel.vue.d.ts.map +1 -0
  7. package/dist/components/chat/SearchResultBlock.vue.d.ts +8 -0
  8. package/dist/components/chat/SearchResultBlock.vue.d.ts.map +1 -0
  9. package/dist/components/chat/ThinkingBlock.vue.d.ts +7 -0
  10. package/dist/components/chat/ThinkingBlock.vue.d.ts.map +1 -0
  11. package/dist/components/chat/ToolCallBlock.vue.d.ts +9 -0
  12. package/dist/components/chat/ToolCallBlock.vue.d.ts.map +1 -0
  13. package/dist/components/chat/messages/ExecutionSteps.vue.d.ts +13 -0
  14. package/dist/components/chat/messages/ExecutionSteps.vue.d.ts.map +1 -0
  15. package/dist/components/chat/messages/MessageBubble.vue.d.ts +28 -0
  16. package/dist/components/chat/messages/MessageBubble.vue.d.ts.map +1 -0
  17. package/dist/components/chat/ui/ChatHeader.vue.d.ts +34 -0
  18. package/dist/components/chat/ui/ChatHeader.vue.d.ts.map +1 -0
  19. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts +7 -0
  20. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts.map +1 -0
  21. package/dist/composables/useChat.d.ts +96 -0
  22. package/dist/composables/useChat.d.ts.map +1 -0
  23. package/dist/index.d.ts +37 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +1497 -0
  26. package/dist/preload/preload.d.ts +6 -0
  27. package/dist/preload/preload.d.ts.map +1 -0
  28. package/dist/style.css +1 -0
  29. package/dist/types/index.d.ts +107 -0
  30. package/dist/types/index.d.ts.map +1 -0
  31. package/package.json +59 -0
  32. package/src/adapter.ts +160 -0
  33. package/src/components/ChatInput.vue +649 -0
  34. package/src/components/ChatPanel.vue +309 -0
  35. package/src/components/chat/SearchResultBlock.vue +155 -0
  36. package/src/components/chat/ThinkingBlock.vue +109 -0
  37. package/src/components/chat/ToolCallBlock.vue +213 -0
  38. package/src/components/chat/messages/ExecutionSteps.vue +281 -0
  39. package/src/components/chat/messages/MessageBubble.vue +272 -0
  40. package/src/components/chat/ui/ChatHeader.vue +535 -0
  41. package/src/components/chat/ui/WelcomeMessage.vue +135 -0
  42. package/src/composables/useChat.ts +423 -0
  43. package/src/index.ts +82 -0
  44. package/src/preload/preload.ts +79 -0
  45. package/src/types/index.ts +164 -0
@@ -0,0 +1,649 @@
1
+ <template>
2
+ <div :class="['chat-input', { 'message-variant': isMessageVariant }]">
3
+ <div
4
+ ref="containerRef"
5
+ :class="['input-container', { focused: isFocused }]"
6
+ >
7
+ <!-- 附件预览 -->
8
+ <div v-if="selectedImages.length > 0" class="attachment-preview">
9
+ <div class="preview-images">
10
+ <template v-for="(img, index) in selectedPreview" :key="`${img}-${index}`">
11
+ <div class="preview-item">
12
+ <img
13
+ :src="getImageUrl(img)"
14
+ class="preview-thumb"
15
+ @error="handleImageError"
16
+ />
17
+ <button
18
+ v-if="!isMessageVariant"
19
+ class="remove-btn"
20
+ :title="`移除图片 ${index + 1}`"
21
+ @click="removeAttachment(index)"
22
+ >
23
+ <X :size="10" />
24
+ </button>
25
+ </div>
26
+ </template>
27
+ <div v-if="selectedImages.length > 3" class="preview-more">
28
+ +{{ selectedImages.length - 3 }}
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <!-- 输入框区域 -->
34
+ <div class="input-field-wrapper">
35
+ <textarea
36
+ ref="inputRef"
37
+ v-model="inputText"
38
+ :placeholder="placeholder"
39
+ rows="1"
40
+ class="input-field"
41
+ @keydown="handleKeydown"
42
+ @input="adjustTextareaHeight"
43
+ @focus="isFocused = true"
44
+ ></textarea>
45
+ </div>
46
+
47
+ <!-- 底部控制栏 -->
48
+ <div v-if="showToolbar" class="input-controls">
49
+ <!-- 左侧:模式和模型选择 -->
50
+ <div class="input-left">
51
+ <!-- 模式选择 -->
52
+ <div class="selector mode-selector" @click.stop="toggleModeMenu">
53
+ <component :is="currentModeIcon" :size="12" />
54
+ <span>{{ currentModeName }}</span>
55
+ <ChevronDown :size="10" class="chevron" />
56
+
57
+ <div v-if="modeMenuOpen" class="dropdown-menu" @click.stop>
58
+ <button
59
+ v-for="m in modes"
60
+ :key="m.value"
61
+ :class="['dropdown-item', { active: mode === m.value }]"
62
+ @click="selectMode(m.value)"
63
+ >
64
+ <component :is="m.icon" :size="14" />
65
+ <span>{{ m.label }}</span>
66
+ <Check v-if="mode === m.value" :size="14" class="check-icon" />
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- 模型选择 -->
72
+ <div class="selector model-selector" @click.stop="toggleModelMenu">
73
+ <span>{{ currentModelName }}</span>
74
+ <ChevronDown :size="10" class="chevron" />
75
+
76
+ <div v-if="modelMenuOpen" class="dropdown-menu" @click.stop>
77
+ <button
78
+ v-for="m in models"
79
+ :key="m.model"
80
+ :class="['dropdown-item', { active: model === m.model }]"
81
+ @click="selectModel(m)"
82
+ >
83
+ <span>{{ m.displayName }}</span>
84
+ <Check v-if="model === m.model" :size="14" class="check-icon" />
85
+ </button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- 右侧:功能按钮 -->
91
+ <div class="input-right">
92
+ <!-- @ 上下文 -->
93
+ <button
94
+ class="icon-btn"
95
+ title="提及上下文 (@)"
96
+ @click="$emit('at-context')"
97
+ >
98
+ <AtSign :size="14" />
99
+ </button>
100
+
101
+ <!-- 深度思考 -->
102
+ <button
103
+ :class="['toggle-btn', { active: thinkingEnabled }]"
104
+ title="深度思考"
105
+ @click="$emit('update:thinking', !thinkingEnabled)"
106
+ >
107
+ <Sparkles :size="14" />
108
+ </button>
109
+
110
+ <!-- 联网搜索 -->
111
+ <button
112
+ :class="['toggle-btn', { active: webSearchEnabled }]"
113
+ title="联网搜索"
114
+ @click="$emit('update:webSearch', !webSearchEnabled)"
115
+ >
116
+ <Globe :size="14" />
117
+ </button>
118
+
119
+ <!-- 上传图片 -->
120
+ <button
121
+ class="icon-btn"
122
+ title="上传图片"
123
+ @click="$emit('upload-image')"
124
+ >
125
+ <ImageIcon :size="14" />
126
+ </button>
127
+
128
+ <!-- 发送/停止按钮 -->
129
+ <button
130
+ v-if="inputText.trim() || isLoading"
131
+ :class="['send-btn', { loading: isLoading }]"
132
+ :title="isLoading ? '停止' : isMessageVariant ? '重新发送' : '发送'"
133
+ @click="handleSendOrCancel"
134
+ >
135
+ <Square v-if="isLoading" :size="14" />
136
+ <ArrowUp v-else :size="14" />
137
+ </button>
138
+ <button v-else class="icon-btn" title="语音输入">
139
+ <Mic :size="14" />
140
+ </button>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </template>
146
+
147
+ <script setup lang="ts">
148
+ import { computed, ref, onMounted, onUnmounted, watch, markRaw } from 'vue';
149
+ import {
150
+ X,
151
+ ChevronDown,
152
+ Check,
153
+ Globe,
154
+ Sparkles,
155
+ ImageIcon,
156
+ Square,
157
+ ArrowUp,
158
+ Zap,
159
+ MessageCircle,
160
+ AtSign,
161
+ Mic,
162
+ } from 'lucide-vue-next';
163
+ import type { ChatMode, ModelConfig } from '../types';
164
+ import { DEFAULT_MODELS } from '../types';
165
+
166
+ const props = withDefaults(
167
+ defineProps<{
168
+ /** 变体模式:input-底部输入框,message-历史消息 */
169
+ variant?: 'input' | 'message';
170
+ /** 受控值(用于历史消息编辑) */
171
+ value?: string;
172
+ selectedImages?: string[];
173
+ isLoading?: boolean;
174
+ mode?: ChatMode;
175
+ model?: string;
176
+ models?: ModelConfig[];
177
+ webSearchEnabled?: boolean;
178
+ thinkingEnabled?: boolean;
179
+ }>(),
180
+ {
181
+ variant: 'input',
182
+ value: '',
183
+ selectedImages: () => [],
184
+ isLoading: false,
185
+ mode: 'agent',
186
+ model: '',
187
+ models: () => DEFAULT_MODELS,
188
+ webSearchEnabled: false,
189
+ thinkingEnabled: false,
190
+ }
191
+ );
192
+
193
+ const emit = defineEmits<{
194
+ send: [text: string];
195
+ 'remove-image': [index: number];
196
+ cancel: [];
197
+ 'upload-image': [];
198
+ 'at-context': [];
199
+ 'update:mode': [mode: ChatMode];
200
+ 'update:model': [model: string];
201
+ 'update:webSearch': [enabled: boolean];
202
+ 'update:thinking': [enabled: boolean];
203
+ }>();
204
+
205
+ // 是否为历史消息模式
206
+ const isMessageVariant = computed(() => props.variant === 'message');
207
+
208
+ const inputText = ref(props.value);
209
+ const inputRef = ref<HTMLTextAreaElement | null>(null);
210
+ const containerRef = ref<HTMLDivElement | null>(null);
211
+ const isFocused = ref(false);
212
+
213
+ // 同步外部 value
214
+ watch(
215
+ () => props.value,
216
+ (newVal) => {
217
+ inputText.value = newVal;
218
+ }
219
+ );
220
+
221
+ // 模式配置
222
+ const modes = [
223
+ { value: 'agent' as const, label: 'Agent', icon: markRaw(Zap) },
224
+ { value: 'ask' as const, label: 'Ask', icon: markRaw(MessageCircle) },
225
+ ];
226
+
227
+ // 菜单状态
228
+ const modeMenuOpen = ref(false);
229
+ const modelMenuOpen = ref(false);
230
+
231
+ // 是否显示工具栏
232
+ const showToolbar = computed(() => !isMessageVariant.value || isFocused.value);
233
+
234
+ // 预览
235
+ const selectedPreview = computed(() => props.selectedImages.slice(0, 3));
236
+
237
+ // 占位符
238
+ const placeholder = computed(() => {
239
+ if (props.selectedImages.length > 0) return '描述你想要的效果...';
240
+ if (props.mode === 'ask') return '有什么问题想问我?';
241
+ return '描述任务,@ 添加上下文';
242
+ });
243
+
244
+ // 当前模式
245
+ const currentModeName = computed(() => {
246
+ const m = modes.find((item) => item.value === props.mode);
247
+ return m?.label || 'Agent';
248
+ });
249
+
250
+ const currentModeIcon = computed(() => {
251
+ const m = modes.find((item) => item.value === props.mode);
252
+ return m?.icon || Zap;
253
+ });
254
+
255
+ // 当前模型
256
+ const currentModelName = computed(() => {
257
+ const m = props.models.find((item) => item.model === props.model);
258
+ return m?.displayName || 'Auto';
259
+ });
260
+
261
+ // 切换菜单
262
+ function toggleModeMenu() {
263
+ modeMenuOpen.value = !modeMenuOpen.value;
264
+ modelMenuOpen.value = false;
265
+ }
266
+
267
+ function toggleModelMenu() {
268
+ modelMenuOpen.value = !modelMenuOpen.value;
269
+ modeMenuOpen.value = false;
270
+ }
271
+
272
+ // 选择
273
+ function selectMode(value: ChatMode) {
274
+ emit('update:mode', value);
275
+ modeMenuOpen.value = false;
276
+ }
277
+
278
+ function selectModel(m: ModelConfig) {
279
+ emit('update:model', m.model);
280
+ modelMenuOpen.value = false;
281
+ }
282
+
283
+ // 图片相关
284
+ function getImageUrl(path: string): string {
285
+ if (
286
+ path.startsWith('app://') ||
287
+ path.startsWith('file://') ||
288
+ path.startsWith('data:') ||
289
+ path.startsWith('http')
290
+ ) {
291
+ return path;
292
+ }
293
+ if (path.match(/^[A-Z]:\\/i)) {
294
+ return `app://file${encodeURIComponent(path.replace(/\\/g, '/'))}`;
295
+ }
296
+ return `app://file${encodeURIComponent(path)}`;
297
+ }
298
+
299
+ function handleImageError(event: Event) {
300
+ const img = event.target as HTMLImageElement;
301
+ img.style.display = 'none';
302
+ }
303
+
304
+ function removeAttachment(index: number) {
305
+ emit('remove-image', index);
306
+ }
307
+
308
+ // 自动调整高度
309
+ function adjustTextareaHeight() {
310
+ if (inputRef.value) {
311
+ inputRef.value.style.height = 'auto';
312
+ const scrollHeight = inputRef.value.scrollHeight;
313
+ inputRef.value.style.height = `${Math.min(scrollHeight, 150)}px`;
314
+ }
315
+ }
316
+
317
+ // 发送或取消
318
+ function handleSendOrCancel() {
319
+ if (props.isLoading) {
320
+ emit('cancel');
321
+ return;
322
+ }
323
+
324
+ const text = inputText.value.trim();
325
+ if (!text) return;
326
+
327
+ emit('send', text);
328
+
329
+ if (!isMessageVariant.value) {
330
+ inputText.value = '';
331
+ if (inputRef.value) {
332
+ inputRef.value.style.height = 'auto';
333
+ }
334
+ inputRef.value?.focus();
335
+ } else {
336
+ isFocused.value = false;
337
+ }
338
+ }
339
+
340
+ function handleKeydown(event: KeyboardEvent) {
341
+ if (event.key === 'Enter' && !event.shiftKey) {
342
+ event.preventDefault();
343
+ handleSendOrCancel();
344
+ } else {
345
+ setTimeout(adjustTextareaHeight, 0);
346
+ }
347
+ }
348
+
349
+ // 点击外部
350
+ function handleClickOutside(event: MouseEvent) {
351
+ const target = event.target as HTMLElement;
352
+ if (!target.closest('.selector')) {
353
+ modeMenuOpen.value = false;
354
+ modelMenuOpen.value = false;
355
+ }
356
+ if (
357
+ isMessageVariant.value &&
358
+ containerRef.value &&
359
+ !containerRef.value.contains(target)
360
+ ) {
361
+ isFocused.value = false;
362
+ }
363
+ }
364
+
365
+ onMounted(() => {
366
+ document.addEventListener('click', handleClickOutside);
367
+ });
368
+
369
+ onUnmounted(() => {
370
+ document.removeEventListener('click', handleClickOutside);
371
+ });
372
+
373
+ defineExpose({
374
+ focus: () => inputRef.value?.focus(),
375
+ setText: (text: string) => {
376
+ inputText.value = text;
377
+ },
378
+ clear: () => {
379
+ inputText.value = '';
380
+ },
381
+ });
382
+ </script>
383
+
384
+ <style scoped>
385
+ .chat-input {
386
+ padding: 12px;
387
+ }
388
+
389
+ .chat-input.message-variant {
390
+ padding: 0;
391
+ margin-bottom: 16px;
392
+ }
393
+
394
+ .input-container {
395
+ display: flex;
396
+ flex-direction: column;
397
+ background: var(--chat-input-bg, #2d2d2d);
398
+ border: 1px solid var(--chat-border, #444);
399
+ border-radius: 12px;
400
+ padding: 12px;
401
+ transition: border-color 0.15s;
402
+ }
403
+
404
+ .input-container.focused {
405
+ border-color: rgba(255, 255, 255, 0.2);
406
+ }
407
+
408
+ /* 附件预览 */
409
+ .attachment-preview {
410
+ margin-bottom: 8px;
411
+ }
412
+
413
+ .preview-images {
414
+ display: flex;
415
+ gap: 6px;
416
+ flex-wrap: wrap;
417
+ }
418
+
419
+ .preview-item {
420
+ position: relative;
421
+ }
422
+
423
+ .preview-thumb {
424
+ width: 48px;
425
+ height: 48px;
426
+ object-fit: cover;
427
+ border-radius: 8px;
428
+ border: 1px solid var(--chat-border, #444);
429
+ }
430
+
431
+ .preview-more {
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ width: 48px;
436
+ height: 48px;
437
+ background: var(--chat-muted, #3c3c3c);
438
+ border-radius: 8px;
439
+ color: var(--chat-text-muted, #888);
440
+ font-size: 12px;
441
+ }
442
+
443
+ .remove-btn {
444
+ position: absolute;
445
+ top: -4px;
446
+ right: -4px;
447
+ width: 16px;
448
+ height: 16px;
449
+ display: flex;
450
+ align-items: center;
451
+ justify-content: center;
452
+ background: var(--chat-bg, #1e1e1e);
453
+ border: 1px solid var(--chat-border, #444);
454
+ border-radius: 50%;
455
+ color: var(--chat-text-muted, #888);
456
+ cursor: pointer;
457
+ padding: 0;
458
+ }
459
+
460
+ .remove-btn:hover {
461
+ background: var(--chat-muted, #3c3c3c);
462
+ color: var(--chat-text, #ccc);
463
+ }
464
+
465
+ /* 输入框 */
466
+ .input-field-wrapper {
467
+ margin-bottom: 8px;
468
+ }
469
+
470
+ .input-field {
471
+ width: 100%;
472
+ background: transparent;
473
+ border: none;
474
+ padding: 0;
475
+ color: var(--chat-text, #ccc);
476
+ font-size: 14px;
477
+ resize: none;
478
+ min-height: 24px;
479
+ max-height: 150px;
480
+ line-height: 1.5;
481
+ font-family: inherit;
482
+ }
483
+
484
+ .input-field:focus {
485
+ outline: none;
486
+ }
487
+
488
+ .input-field::placeholder {
489
+ color: var(--chat-text-muted, #666);
490
+ }
491
+
492
+ /* 底部控制栏 */
493
+ .input-controls {
494
+ display: flex;
495
+ align-items: center;
496
+ justify-content: space-between;
497
+ gap: 8px;
498
+ }
499
+
500
+ .input-left {
501
+ display: flex;
502
+ align-items: center;
503
+ gap: 4px;
504
+ }
505
+
506
+ /* 选择器 */
507
+ .selector {
508
+ position: relative;
509
+ display: flex;
510
+ align-items: center;
511
+ gap: 4px;
512
+ padding: 4px 8px;
513
+ background: var(--chat-muted, #3c3c3c);
514
+ border: none;
515
+ border-radius: 6px;
516
+ font-size: 12px;
517
+ color: var(--chat-text-muted, #888);
518
+ cursor: pointer;
519
+ transition: all 0.15s;
520
+ }
521
+
522
+ .selector:hover {
523
+ background: var(--chat-muted-hover, #444);
524
+ color: var(--chat-text, #ccc);
525
+ }
526
+
527
+ .chevron {
528
+ color: var(--chat-text-muted, #666);
529
+ }
530
+
531
+ /* 下拉菜单 */
532
+ .dropdown-menu {
533
+ position: absolute;
534
+ bottom: 100%;
535
+ left: 0;
536
+ margin-bottom: 4px;
537
+ min-width: 160px;
538
+ background: var(--chat-dropdown-bg, #252526);
539
+ border: 1px solid rgba(255, 255, 255, 0.1);
540
+ border-radius: 8px;
541
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
542
+ z-index: 100;
543
+ padding: 4px;
544
+ }
545
+
546
+ .dropdown-item {
547
+ display: flex;
548
+ align-items: center;
549
+ gap: 8px;
550
+ width: 100%;
551
+ padding: 8px 10px;
552
+ border: none;
553
+ background: transparent;
554
+ border-radius: 4px;
555
+ font-size: 13px;
556
+ color: var(--chat-text-muted, #999);
557
+ cursor: pointer;
558
+ transition: all 0.15s;
559
+ }
560
+
561
+ .dropdown-item:hover {
562
+ background: rgba(255, 255, 255, 0.08);
563
+ color: var(--chat-text, #ccc);
564
+ }
565
+
566
+ .dropdown-item.active {
567
+ background: rgba(255, 255, 255, 0.1);
568
+ color: var(--chat-text, #fff);
569
+ }
570
+
571
+ .check-icon {
572
+ margin-left: auto;
573
+ color: var(--chat-text, #ccc);
574
+ }
575
+
576
+ /* 右侧按钮 */
577
+ .input-right {
578
+ display: flex;
579
+ align-items: center;
580
+ gap: 2px;
581
+ }
582
+
583
+ .icon-btn {
584
+ display: flex;
585
+ align-items: center;
586
+ justify-content: center;
587
+ width: 28px;
588
+ height: 28px;
589
+ background: transparent;
590
+ border: none;
591
+ border-radius: 6px;
592
+ color: var(--chat-text-muted, #666);
593
+ cursor: pointer;
594
+ transition: all 0.15s;
595
+ }
596
+
597
+ .icon-btn:hover {
598
+ color: var(--chat-text, #ccc);
599
+ }
600
+
601
+ .toggle-btn {
602
+ display: flex;
603
+ align-items: center;
604
+ justify-content: center;
605
+ width: 28px;
606
+ height: 28px;
607
+ background: transparent;
608
+ border: none;
609
+ border-radius: 6px;
610
+ color: var(--chat-text-muted, #666);
611
+ cursor: pointer;
612
+ transition: all 0.15s;
613
+ }
614
+
615
+ .toggle-btn:hover {
616
+ color: var(--chat-text, #ccc);
617
+ }
618
+
619
+ .toggle-btn.active {
620
+ color: var(--chat-text, #fff);
621
+ background: rgba(255, 255, 255, 0.1);
622
+ }
623
+
624
+ .send-btn {
625
+ display: flex;
626
+ align-items: center;
627
+ justify-content: center;
628
+ width: 28px;
629
+ height: 28px;
630
+ background: rgba(255, 255, 255, 0.9);
631
+ border: none;
632
+ border-radius: 6px;
633
+ color: #1e1e1e;
634
+ cursor: pointer;
635
+ transition: all 0.15s;
636
+ }
637
+
638
+ .send-btn:hover {
639
+ background: #fff;
640
+ }
641
+
642
+ .send-btn.loading {
643
+ background: var(--chat-destructive, #ef4444);
644
+ }
645
+
646
+ .send-btn.loading:hover {
647
+ background: var(--chat-destructive-hover, #dc2626);
648
+ }
649
+ </style>