@huyooo/ai-chat-frontend-vue 0.1.6 → 0.1.7

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 (159) hide show
  1. package/README.md +367 -0
  2. package/dist/adapter.d.ts +7 -7
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/components/ChatPanel.vue.d.ts +120 -9
  5. package/dist/components/ChatPanel.vue.d.ts.map +1 -1
  6. package/dist/components/common/ConfirmDialog.vue.d.ts +30 -0
  7. package/dist/components/common/ConfirmDialog.vue.d.ts.map +1 -0
  8. package/dist/components/common/CopyButton.vue.d.ts +18 -0
  9. package/dist/components/common/CopyButton.vue.d.ts.map +1 -0
  10. package/dist/components/common/IndexingSettings.vue.d.ts +3 -0
  11. package/dist/components/common/IndexingSettings.vue.d.ts.map +1 -0
  12. package/dist/components/common/SettingsPanel.vue.d.ts +16 -0
  13. package/dist/components/common/SettingsPanel.vue.d.ts.map +1 -0
  14. package/dist/components/common/Toast.vue.d.ts +18 -0
  15. package/dist/components/common/Toast.vue.d.ts.map +1 -0
  16. package/dist/components/common/ToggleSwitch.vue.d.ts +10 -0
  17. package/dist/components/common/ToggleSwitch.vue.d.ts.map +1 -0
  18. package/dist/components/{chat/ui → header}/ChatHeader.vue.d.ts +5 -3
  19. package/dist/components/header/ChatHeader.vue.d.ts.map +1 -0
  20. package/dist/components/input/AtFilePicker.vue.d.ts +21 -0
  21. package/dist/components/input/AtFilePicker.vue.d.ts.map +1 -0
  22. package/dist/components/{ChatInput.vue.d.ts → input/ChatInput.vue.d.ts} +16 -14
  23. package/dist/components/input/ChatInput.vue.d.ts.map +1 -0
  24. package/dist/components/input/DropdownSelector.vue.d.ts +42 -0
  25. package/dist/components/input/DropdownSelector.vue.d.ts.map +1 -0
  26. package/dist/components/input/ImagePreviewModal.vue.d.ts +17 -0
  27. package/dist/components/input/ImagePreviewModal.vue.d.ts.map +1 -0
  28. package/dist/components/input/at-views/AtBranchView.vue.d.ts +18 -0
  29. package/dist/components/input/at-views/AtBranchView.vue.d.ts.map +1 -0
  30. package/dist/components/input/at-views/AtBrowserView.vue.d.ts +18 -0
  31. package/dist/components/input/at-views/AtBrowserView.vue.d.ts.map +1 -0
  32. package/dist/components/input/at-views/AtChatsView.vue.d.ts +18 -0
  33. package/dist/components/input/at-views/AtChatsView.vue.d.ts.map +1 -0
  34. package/dist/components/input/at-views/AtDocsView.vue.d.ts +18 -0
  35. package/dist/components/input/at-views/AtDocsView.vue.d.ts.map +1 -0
  36. package/dist/components/input/at-views/AtFilesView.vue.d.ts +23 -0
  37. package/dist/components/input/at-views/AtFilesView.vue.d.ts.map +1 -0
  38. package/dist/components/input/at-views/AtTerminalsView.vue.d.ts +18 -0
  39. package/dist/components/input/at-views/AtTerminalsView.vue.d.ts.map +1 -0
  40. package/dist/components/message/MessageBubble.vue.d.ts +45 -0
  41. package/dist/components/message/MessageBubble.vue.d.ts.map +1 -0
  42. package/dist/components/message/PartsRenderer.vue.d.ts +15 -0
  43. package/dist/components/message/PartsRenderer.vue.d.ts.map +1 -0
  44. package/dist/components/message/WelcomeMessage.vue.d.ts +14 -0
  45. package/dist/components/message/WelcomeMessage.vue.d.ts.map +1 -0
  46. package/dist/components/message/blocks/CodeBlock.vue.d.ts +11 -0
  47. package/dist/components/message/blocks/CodeBlock.vue.d.ts.map +1 -0
  48. package/dist/components/{chat/SearchResultBlock.vue.d.ts → message/blocks/TextBlock.vue.d.ts} +3 -4
  49. package/dist/components/message/blocks/TextBlock.vue.d.ts.map +1 -0
  50. package/dist/components/message/blocks/index.d.ts +6 -0
  51. package/dist/components/message/blocks/index.d.ts.map +1 -0
  52. package/dist/components/message/parts/CollapsibleCard.vue.d.ts +45 -0
  53. package/dist/components/message/parts/CollapsibleCard.vue.d.ts.map +1 -0
  54. package/dist/components/{chat/ToolCallBlock.vue.d.ts → message/parts/ErrorPart.vue.d.ts} +4 -5
  55. package/dist/components/message/parts/ErrorPart.vue.d.ts.map +1 -0
  56. package/dist/components/{chat/ThinkingBlock.vue.d.ts → message/parts/ImagePart.vue.d.ts} +3 -3
  57. package/dist/components/message/parts/ImagePart.vue.d.ts.map +1 -0
  58. package/dist/components/message/parts/SearchPart.vue.d.ts +12 -0
  59. package/dist/components/message/parts/SearchPart.vue.d.ts.map +1 -0
  60. package/dist/components/{chat/messages/ExecutionSteps.vue.d.ts → message/parts/TextPart.vue.d.ts} +2 -9
  61. package/dist/components/message/parts/TextPart.vue.d.ts.map +1 -0
  62. package/dist/components/message/parts/ThinkingPart.vue.d.ts +12 -0
  63. package/dist/components/message/parts/ThinkingPart.vue.d.ts.map +1 -0
  64. package/dist/components/message/parts/ToolCallPart.vue.d.ts +19 -0
  65. package/dist/components/message/parts/ToolCallPart.vue.d.ts.map +1 -0
  66. package/dist/components/message/parts/ToolResultPart.vue.d.ts +14 -0
  67. package/dist/components/message/parts/ToolResultPart.vue.d.ts.map +1 -0
  68. package/dist/components/message/parts/index.d.ts +12 -0
  69. package/dist/components/message/parts/index.d.ts.map +1 -0
  70. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts +4 -0
  71. package/dist/components/message/tool-results/DefaultToolResult.vue.d.ts.map +1 -0
  72. package/dist/components/message/tool-results/SearchResults.vue.d.ts +4 -0
  73. package/dist/components/message/tool-results/SearchResults.vue.d.ts.map +1 -0
  74. package/dist/components/message/tool-results/WeatherCard.vue.d.ts +4 -0
  75. package/dist/components/message/tool-results/WeatherCard.vue.d.ts.map +1 -0
  76. package/dist/components/message/tool-results/index.d.ts +7 -0
  77. package/dist/components/message/tool-results/index.d.ts.map +1 -0
  78. package/dist/components/message/welcome-types.d.ts +28 -0
  79. package/dist/components/message/welcome-types.d.ts.map +1 -0
  80. package/dist/composables/useChat.d.ts +99 -44
  81. package/dist/composables/useChat.d.ts.map +1 -1
  82. package/dist/composables/useImageUpload.d.ts +55 -0
  83. package/dist/composables/useImageUpload.d.ts.map +1 -0
  84. package/dist/index.d.ts +25 -26
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +55871 -1252
  87. package/dist/style.css +1 -1
  88. package/dist/types/index.d.ts +113 -53
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/utils/fileIcon.d.ts +13 -0
  91. package/dist/utils/fileIcon.d.ts.map +1 -0
  92. package/package.json +12 -6
  93. package/src/adapter.ts +12 -70
  94. package/src/components/ChatPanel.vue +329 -110
  95. package/src/components/common/ConfirmDialog.vue +208 -0
  96. package/src/components/common/CopyButton.vue +71 -0
  97. package/src/components/common/IndexingSettings.vue +580 -0
  98. package/src/components/common/SettingsPanel.vue +293 -0
  99. package/src/components/common/Toast.vue +90 -0
  100. package/src/components/common/ToggleSwitch.vue +75 -0
  101. package/src/components/{chat/ui → header}/ChatHeader.vue +170 -93
  102. package/src/components/input/AtFilePicker.vue +657 -0
  103. package/src/components/input/ChatInput.vue +653 -0
  104. package/src/components/input/DropdownSelector.vue +322 -0
  105. package/src/components/input/ImagePreviewModal.vue +238 -0
  106. package/src/components/input/at-views/AtBranchView.vue +63 -0
  107. package/src/components/input/at-views/AtBrowserView.vue +63 -0
  108. package/src/components/input/at-views/AtChatsView.vue +63 -0
  109. package/src/components/input/at-views/AtDocsView.vue +63 -0
  110. package/src/components/input/at-views/AtFilesView.vue +255 -0
  111. package/src/components/input/at-views/AtTerminalsView.vue +63 -0
  112. package/src/components/message/ContentRenderer.vue +61 -0
  113. package/src/components/message/MessageBubble.vue +411 -0
  114. package/src/components/message/PartsRenderer.vue +101 -0
  115. package/src/components/message/ToolResultRenderer.vue +27 -0
  116. package/src/components/message/WelcomeMessage.vue +308 -0
  117. package/src/components/message/blocks/CodeBlock.vue +113 -0
  118. package/src/components/message/blocks/TextBlock.vue +21 -0
  119. package/src/components/message/blocks/index.ts +6 -0
  120. package/src/components/message/parts/CollapsibleCard.vue +135 -0
  121. package/src/components/message/parts/ErrorPart.vue +51 -0
  122. package/src/components/message/parts/ImagePart.vue +98 -0
  123. package/src/components/message/parts/SearchPart.vue +101 -0
  124. package/src/components/message/parts/TextPart.vue +28 -0
  125. package/src/components/message/parts/ThinkingPart.vue +54 -0
  126. package/src/components/message/parts/ToolCallPart.vue +460 -0
  127. package/src/components/message/parts/ToolResultPart.vue +78 -0
  128. package/src/components/message/parts/index.ts +13 -0
  129. package/src/components/message/tool-results/DefaultToolResult.vue +43 -0
  130. package/src/components/message/tool-results/SearchResults.vue +133 -0
  131. package/src/components/message/tool-results/WeatherCard.vue +139 -0
  132. package/src/components/message/tool-results/index.ts +7 -0
  133. package/src/components/message/welcome-types.ts +47 -0
  134. package/src/composables/useChat.ts +807 -155
  135. package/src/composables/useImageUpload.ts +228 -0
  136. package/src/index.ts +93 -46
  137. package/src/styles.css +47 -0
  138. package/src/types/index.ts +146 -98
  139. package/src/utils/fileIcon.ts +49 -0
  140. package/dist/components/ChatInput.vue.d.ts.map +0 -1
  141. package/dist/components/chat/SearchResultBlock.vue.d.ts.map +0 -1
  142. package/dist/components/chat/ThinkingBlock.vue.d.ts.map +0 -1
  143. package/dist/components/chat/ToolCallBlock.vue.d.ts.map +0 -1
  144. package/dist/components/chat/messages/ExecutionSteps.vue.d.ts.map +0 -1
  145. package/dist/components/chat/messages/MessageBubble.vue.d.ts +0 -28
  146. package/dist/components/chat/messages/MessageBubble.vue.d.ts.map +0 -1
  147. package/dist/components/chat/ui/ChatHeader.vue.d.ts.map +0 -1
  148. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts +0 -7
  149. package/dist/components/chat/ui/WelcomeMessage.vue.d.ts.map +0 -1
  150. package/dist/preload/preload.d.ts +0 -6
  151. package/dist/preload/preload.d.ts.map +0 -1
  152. package/src/components/ChatInput.vue +0 -649
  153. package/src/components/chat/SearchResultBlock.vue +0 -155
  154. package/src/components/chat/ThinkingBlock.vue +0 -109
  155. package/src/components/chat/ToolCallBlock.vue +0 -213
  156. package/src/components/chat/messages/ExecutionSteps.vue +0 -281
  157. package/src/components/chat/messages/MessageBubble.vue +0 -272
  158. package/src/components/chat/ui/WelcomeMessage.vue +0 -135
  159. package/src/preload/preload.ts +0 -79
@@ -0,0 +1,653 @@
1
+ <template>
2
+ <div
3
+ :class="['chat-input', { 'message-variant': isMessageVariant }]"
4
+ @dragover.prevent="imageUpload.handleDragOver"
5
+ @dragleave="imageUpload.handleDragLeave"
6
+ @drop.prevent="imageUpload.handleDrop"
7
+ >
8
+ <div ref="containerRef" :class="['input-container', { focused: isFocused, 'drag-over': imageUpload.isDragOver.value }]">
9
+ <!-- 图片预览区 -->
10
+ <div v-if="imageUpload.hasImages.value" class="images-preview">
11
+ <div v-for="(img, i) in imageUpload.images.value" :key="i" class="image-preview-item">
12
+ <img :src="img.dataUrl" alt="预览" class="image-thumbnail" @click="imageUpload.openPreview(i)" />
13
+ <button class="image-remove-btn" title="移除图片" @click.stop="imageUpload.removeImage(i)">
14
+ <Icon icon="lucide:x" width="10" />
15
+ </button>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- 图片预览弹窗 -->
20
+ <ImagePreviewModal
21
+ v-model:index="previewIndex"
22
+ :visible="imageUpload.previewVisible.value"
23
+ :images="imageUpload.imageUrls.value"
24
+ @close="imageUpload.closePreview"
25
+ />
26
+
27
+ <!-- 输入框区域 -->
28
+ <div class="input-field-wrapper">
29
+ <textarea
30
+ ref="inputRef"
31
+ v-model="inputText"
32
+ :placeholder="placeholder"
33
+ rows="1"
34
+ class="input-field chat-scrollbar"
35
+ spellcheck="false"
36
+ autocorrect="off"
37
+ autocomplete="off"
38
+ autocapitalize="off"
39
+ @keydown="handleKeydown"
40
+ @input="adjustTextareaHeight"
41
+ @focus="isFocused = true"
42
+ @paste="imageUpload.handlePaste"
43
+ ></textarea>
44
+ </div>
45
+
46
+ <!-- 底部控制栏 -->
47
+ <div v-if="showToolbar" class="input-controls">
48
+ <!-- 左侧:模式和模型选择 -->
49
+ <div class="input-left">
50
+ <!-- 模式选择 -->
51
+ <DropdownSelector
52
+ :value="mode"
53
+ :options="modeOptions"
54
+ :container-ref="containerRef"
55
+ @select="selectMode"
56
+ />
57
+
58
+ <!-- 模型选择(使用后端返回的分组数据) -->
59
+ <DropdownSelector
60
+ :value="model"
61
+ :grouped-options="groupedModelOptions"
62
+ :container-ref="containerRef"
63
+ @select="selectModel"
64
+ />
65
+ </div>
66
+
67
+ <!-- 右侧:功能按钮 -->
68
+ <div class="input-right">
69
+ <!-- 深度思考 -->
70
+ <button
71
+ :class="['toggle-btn', { active: thinkingEnabled }]"
72
+ title="深度思考"
73
+ @click="$emit('update:thinking', !thinkingEnabled)"
74
+ >
75
+ <Icon icon="lucide:sparkles" width="18" />
76
+ </button>
77
+
78
+ <!-- 联网搜索 -->
79
+ <button
80
+ :class="['toggle-btn', { active: webSearchEnabled }]"
81
+ title="联网搜索"
82
+ @click="$emit('update:webSearch', !webSearchEnabled)"
83
+ >
84
+ <Icon icon="lucide:globe" width="18" />
85
+ </button>
86
+
87
+ <!-- 图片上传按钮 -->
88
+ <button class="icon-btn" title="添加图片" @click="triggerImageUpload">
89
+ <Icon icon="lucide:image" width="18" />
90
+ </button>
91
+ <input
92
+ ref="imageInputRef"
93
+ type="file"
94
+ accept="image/*"
95
+ multiple
96
+ class="hidden-input"
97
+ @change="imageUpload.handleImageSelect"
98
+ />
99
+
100
+ <!-- @ 上下文选择器 -->
101
+ <div ref="atSelectorRef" class="at-picker-wrapper" @click.stop="toggleAtPicker">
102
+ <button class="icon-btn" title="提及上下文 (@)">
103
+ <Icon icon="lucide:at-sign" width="18" />
104
+ </button>
105
+
106
+ <!-- @ 下拉菜单 -->
107
+ <AtFilePicker
108
+ v-if="chatAdapter"
109
+ :visible="atPickerVisible"
110
+ :adapter="chatAdapter"
111
+ :anchor-el="atSelectorRef"
112
+ @close="closeAtPicker"
113
+ @select="applyAtPath"
114
+ />
115
+ </div>
116
+
117
+ <!-- 发送/停止按钮 -->
118
+ <button
119
+ v-if="inputText.trim() || imageUpload.hasImages.value || isLoading"
120
+ :class="['send-btn', { loading: isLoading }]"
121
+ :title="isLoading ? '停止' : isMessageVariant ? '重新发送' : '发送'"
122
+ @click="handleSendOrCancel"
123
+ >
124
+ <Icon v-if="isLoading" icon="streamline-flex:button-pause-circle-remix" width="18" />
125
+ <Icon v-else icon="streamline-flex:mail-send-email-message-circle-remix" width="18" />
126
+ </button>
127
+ <button v-else class="icon-btn" title="语音输入">
128
+ <Icon icon="lucide:mic" width="18" />
129
+ </button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </template>
135
+
136
+ <script setup lang="ts">
137
+ import { computed, inject, ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
138
+ import { Icon } from '@iconify/vue';
139
+ import type { ChatMode, ModelOption } from '../../types';
140
+ import AtFilePicker from './AtFilePicker.vue';
141
+ import ImagePreviewModal from './ImagePreviewModal.vue';
142
+ import DropdownSelector, { type GroupedOptions } from './DropdownSelector.vue';
143
+ import type { ChatAdapter, ImageData } from '../../adapter';
144
+ import { useImageUpload } from '../../composables/useImageUpload';
145
+
146
+ const props = withDefaults(
147
+ defineProps<{
148
+ /** 变体模式:input-底部输入框,message-历史消息 */
149
+ variant?: 'input' | 'message';
150
+ /** 受控值(用于历史消息编辑) */
151
+ value?: string;
152
+ isLoading?: boolean;
153
+ mode?: ChatMode;
154
+ model?: string;
155
+ /** 模型列表 */
156
+ models?: ModelOption[];
157
+ webSearchEnabled?: boolean;
158
+ thinkingEnabled?: boolean;
159
+ }>(),
160
+ {
161
+ variant: 'input',
162
+ value: '',
163
+ isLoading: false,
164
+ mode: 'agent',
165
+ model: '',
166
+ models: () => [],
167
+ webSearchEnabled: false,
168
+ thinkingEnabled: false,
169
+ }
170
+ );
171
+
172
+ const emit = defineEmits<{
173
+ send: [text: string, images?: ImageData[]];
174
+ cancel: [];
175
+ 'at-context': [];
176
+ 'update:mode': [mode: ChatMode];
177
+ 'update:model': [model: string];
178
+ 'update:webSearch': [enabled: boolean];
179
+ 'update:thinking': [enabled: boolean];
180
+ }>();
181
+
182
+ // 是否为历史消息模式
183
+ const isMessageVariant = computed(() => props.variant === 'message');
184
+
185
+ const inputText = ref(props.value);
186
+ const inputRef = ref<HTMLTextAreaElement | null>(null);
187
+ const containerRef = ref<HTMLDivElement | null>(null);
188
+ const isFocused = ref(false);
189
+ const lastInputText = ref(props.value);
190
+
191
+ // 注入 adapter(由 ChatPanel 提供)
192
+ const chatAdapter = inject<ChatAdapter | null>('chatAdapter', null);
193
+
194
+ // ============ 图片上传功能(使用 composable) ============
195
+ const imageUpload = useImageUpload();
196
+ const imageInputRef = ref<HTMLInputElement | null>(null);
197
+
198
+ // 预览索引(使用 computed 双向绑定到 composable)
199
+ const previewIndex = computed({
200
+ get: () => imageUpload.previewIndex.value,
201
+ set: (v) => (imageUpload.previewIndex.value = v),
202
+ });
203
+
204
+ // 触发文件选择
205
+ function triggerImageUpload() {
206
+ imageInputRef.value?.click();
207
+ }
208
+
209
+ // @ 文件选择面板
210
+ const atPickerVisible = ref(false);
211
+ const replaceRange = ref<{ start: number; end: number } | null>(null);
212
+ const atSelectorRef = ref<HTMLDivElement | null>(null);
213
+
214
+ function toggleAtPicker() {
215
+ if (!chatAdapter) return;
216
+ if (!atPickerVisible.value) {
217
+ replaceRange.value = null;
218
+ }
219
+ atPickerVisible.value = !atPickerVisible.value;
220
+ }
221
+
222
+ function closeAtPicker() {
223
+ atPickerVisible.value = false;
224
+ replaceRange.value = null;
225
+ nextTick(() => inputRef.value?.focus());
226
+ }
227
+
228
+ function applyAtPath(path: string) {
229
+ const insert = `@${path}\n`;
230
+ const el = inputRef.value;
231
+ const current = inputText.value || '';
232
+ if (!el) {
233
+ inputText.value = current + insert;
234
+ closeAtPicker();
235
+ return;
236
+ }
237
+
238
+ // 如果是 typed 模式且光标前确实是 '@',替换这个字符
239
+ const range = replaceRange.value;
240
+ if (range && current.slice(range.start, range.end) === '@') {
241
+ const next = current.slice(0, range.start) + insert + current.slice(range.end);
242
+ inputText.value = next;
243
+ nextTick(() => {
244
+ adjustTextareaHeight();
245
+ const pos = range.start + insert.length;
246
+ el.focus();
247
+ el.setSelectionRange(pos, pos);
248
+ });
249
+ closeAtPicker();
250
+ return;
251
+ }
252
+
253
+ // 否则插入到当前光标处
254
+ const start = el.selectionStart ?? current.length;
255
+ const end = el.selectionEnd ?? start;
256
+ inputText.value = current.slice(0, start) + insert + current.slice(end);
257
+ nextTick(() => {
258
+ adjustTextareaHeight();
259
+ const pos = start + insert.length;
260
+ el.focus();
261
+ el.setSelectionRange(pos, pos);
262
+ });
263
+ closeAtPicker();
264
+ }
265
+
266
+ // 同步外部 value
267
+ watch(
268
+ () => props.value,
269
+ (newVal) => {
270
+ inputText.value = newVal;
271
+ }
272
+ );
273
+
274
+ // 模式选项(用于 DropdownSelector)
275
+ const modeOptions = computed(() => [
276
+ { value: 'agent', label: 'Agent', icon: 'lucide:zap' },
277
+ { value: 'ask', label: 'Ask', icon: 'lucide:message-circle' },
278
+ ]);
279
+
280
+ // 模型选项(用于 DropdownSelector,支持分组显示和搜索)
281
+ // 将模型列表转换为分组格式(完全使用后端返回的 group 字段)
282
+ const groupedModelOptions = computed(() => {
283
+ const groups: Record<string, Array<{ value: string; label: string; icon?: string }>> = {};
284
+
285
+ props.models.forEach((m) => {
286
+ // 完全使用后端返回的 group 字段
287
+ const groupName = m.group;
288
+
289
+ if (!groups[groupName]) {
290
+ groups[groupName] = [];
291
+ }
292
+
293
+ groups[groupName].push({
294
+ value: m.modelId,
295
+ label: m.displayName,
296
+ });
297
+ });
298
+
299
+ // 对每个分组内的选项进行排序
300
+ Object.keys(groups).forEach(groupName => {
301
+ groups[groupName].sort((a, b) => a.label.localeCompare(b.label));
302
+ });
303
+
304
+ return groups;
305
+ });
306
+
307
+ // 选择模式
308
+ function selectMode(value: string) {
309
+ emit('update:mode', value as ChatMode);
310
+ }
311
+
312
+ // 选择模型
313
+ function selectModel(value: string) {
314
+ emit('update:model', value);
315
+ }
316
+
317
+ // 是否显示工具栏
318
+ const showToolbar = computed(() => !isMessageVariant.value || isFocused.value);
319
+
320
+ // 占位符
321
+ const placeholder = computed(() => {
322
+ if (props.mode === 'ask') return '有什么问题想问我?';
323
+ return '描述任务,@ 添加上下文';
324
+ });
325
+
326
+ // 自动调整高度
327
+ function adjustTextareaHeight() {
328
+ if (inputRef.value) {
329
+ inputRef.value.style.height = 'auto';
330
+ const scrollHeight = inputRef.value.scrollHeight;
331
+ inputRef.value.style.height = `${Math.min(scrollHeight, 150)}px`;
332
+ }
333
+
334
+ // 检测是否新增输入了 '@',用事件驱动打开面板
335
+ const cur = inputText.value || '';
336
+ const prev = lastInputText.value || '';
337
+ if (chatAdapter && cur.length === prev.length + 1 && inputRef.value) {
338
+ const cursor = inputRef.value.selectionStart ?? cur.length;
339
+ const inserted = cur.slice(cursor - 1, cursor);
340
+ if (inserted === '@') {
341
+ replaceRange.value = { start: cursor - 1, end: cursor };
342
+ atPickerVisible.value = true;
343
+ }
344
+ }
345
+ lastInputText.value = cur;
346
+ }
347
+
348
+ // 发送或取消
349
+ function handleSendOrCancel() {
350
+ if (props.isLoading) {
351
+ emit('cancel');
352
+ return;
353
+ }
354
+
355
+ const text = inputText.value.trim();
356
+ const hasContent = text || imageUpload.hasImages.value;
357
+ if (!hasContent) return;
358
+
359
+ // 获取图片数据
360
+ const images = imageUpload.imageData.value.length > 0 ? imageUpload.imageData.value : undefined;
361
+
362
+ emit('send', text, images);
363
+
364
+ if (!isMessageVariant.value) {
365
+ inputText.value = '';
366
+ imageUpload.clearImages();
367
+ if (inputRef.value) {
368
+ inputRef.value.style.height = 'auto';
369
+ }
370
+ nextTick(() => {
371
+ adjustTextareaHeight();
372
+ inputRef.value?.focus();
373
+ });
374
+ } else {
375
+ isFocused.value = false;
376
+ }
377
+ }
378
+
379
+ function handleKeydown(event: KeyboardEvent) {
380
+ if (event.key === 'Enter' && !event.shiftKey) {
381
+ event.preventDefault();
382
+ handleSendOrCancel();
383
+ } else {
384
+ nextTick(() => adjustTextareaHeight());
385
+ }
386
+ }
387
+
388
+ // 点击外部
389
+ function handleClickOutside(event: MouseEvent) {
390
+ const target = event.target as HTMLElement;
391
+ // 关闭 @ 选择器
392
+ if (!target.closest('.at-picker-wrapper')) {
393
+ atPickerVisible.value = false;
394
+ }
395
+ if (isMessageVariant.value && containerRef.value && !containerRef.value.contains(target)) {
396
+ isFocused.value = false;
397
+ }
398
+ }
399
+
400
+ onMounted(() => {
401
+ document.addEventListener('click', handleClickOutside);
402
+ });
403
+
404
+ onUnmounted(() => {
405
+ document.removeEventListener('click', handleClickOutside);
406
+ });
407
+
408
+ defineExpose({
409
+ focus: () => inputRef.value?.focus(),
410
+ setText: (text: string) => {
411
+ inputText.value = text;
412
+ nextTick(() => {
413
+ adjustTextareaHeight();
414
+ });
415
+ },
416
+ /** 在光标位置插入文本(用于 @ 上下文) */
417
+ insertText: (text: string) => {
418
+ if (!text) return;
419
+ const el = inputRef.value;
420
+ if (!el) {
421
+ inputText.value = (inputText.value || '') + text;
422
+ nextTick(() => adjustTextareaHeight());
423
+ return;
424
+ }
425
+
426
+ const start = el.selectionStart ?? (inputText.value || '').length;
427
+ const end = el.selectionEnd ?? start;
428
+ const current = inputText.value || '';
429
+ inputText.value = current.slice(0, start) + text + current.slice(end);
430
+
431
+ nextTick(() => {
432
+ adjustTextareaHeight();
433
+ const nextPos = start + text.length;
434
+ el.focus();
435
+ el.setSelectionRange(nextPos, nextPos);
436
+ });
437
+ },
438
+ clear: () => {
439
+ inputText.value = '';
440
+ imageUpload.clearImages();
441
+ if (inputRef.value) {
442
+ inputRef.value.style.height = 'auto';
443
+ }
444
+ },
445
+ /** 添加图片 */
446
+ addImages: (files: File[]) => {
447
+ imageUpload.addImages(files);
448
+ },
449
+ });
450
+ </script>
451
+
452
+ <style scoped>
453
+ .chat-input {
454
+ padding: 12px;
455
+ }
456
+
457
+ .chat-input.message-variant {
458
+ padding: 0;
459
+ margin-bottom: 16px;
460
+ }
461
+
462
+ .input-container {
463
+ display: flex;
464
+ flex-direction: column;
465
+ background: var(--chat-input-bg, #2d2d2d);
466
+ border: 1px solid var(--chat-border, #444);
467
+ border-radius: 12px;
468
+ padding: 12px;
469
+ transition: border-color 0.15s;
470
+ }
471
+
472
+ .input-container.focused {
473
+ border-color: rgba(255, 255, 255, 0.2);
474
+ }
475
+
476
+ .input-container.drag-over {
477
+ border-color: var(--chat-primary, #2563eb);
478
+ background: rgba(37, 99, 235, 0.1);
479
+ }
480
+
481
+ /* 图片预览区 */
482
+ .images-preview {
483
+ display: flex;
484
+ flex-wrap: wrap;
485
+ gap: 6px;
486
+ margin-bottom: 8px;
487
+ }
488
+
489
+ .image-preview-item {
490
+ position: relative;
491
+ width: 48px;
492
+ height: 48px;
493
+ border-radius: 6px;
494
+ overflow: hidden;
495
+ background: rgba(0, 0, 0, 0.2);
496
+ }
497
+
498
+ .image-thumbnail {
499
+ width: 100%;
500
+ height: 100%;
501
+ object-fit: cover;
502
+ cursor: pointer;
503
+ transition: opacity 0.15s;
504
+ }
505
+
506
+ .image-thumbnail:hover {
507
+ opacity: 0.8;
508
+ }
509
+
510
+ .image-remove-btn {
511
+ position: absolute;
512
+ top: 2px;
513
+ right: 2px;
514
+ width: 16px;
515
+ height: 16px;
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ background: rgba(0, 0, 0, 0.5);
520
+ border: none;
521
+ border-radius: 50%;
522
+ color: #fff;
523
+ cursor: pointer;
524
+ opacity: 0;
525
+ transition: all 0.15s;
526
+ }
527
+
528
+ .image-preview-item:hover .image-remove-btn {
529
+ opacity: 1;
530
+ }
531
+
532
+ .image-remove-btn:hover {
533
+ background: rgba(239, 68, 68, 0.9);
534
+ }
535
+
536
+ /* 隐藏的文件输入 */
537
+ .hidden-input {
538
+ display: none;
539
+ }
540
+
541
+ /* 输入框 */
542
+ .input-field-wrapper {
543
+ margin-bottom: 8px;
544
+ }
545
+
546
+ .input-field {
547
+ width: 100%;
548
+ background: transparent;
549
+ border: none;
550
+ padding: 0;
551
+ padding-right: 8px;
552
+ color: var(--chat-text, #ccc);
553
+ font-size: 14px;
554
+ resize: none;
555
+ min-height: 24px;
556
+ max-height: 150px;
557
+ line-height: 1.5;
558
+ font-family: inherit;
559
+ overflow-y: auto;
560
+ }
561
+
562
+ .input-field:focus {
563
+ outline: none;
564
+ }
565
+
566
+ .input-field::placeholder {
567
+ color: var(--chat-text-muted, #666);
568
+ }
569
+
570
+ /* 底部控制栏 */
571
+ .input-controls {
572
+ display: flex;
573
+ align-items: center;
574
+ justify-content: space-between;
575
+ gap: 8px;
576
+ }
577
+
578
+ .input-left {
579
+ display: flex;
580
+ align-items: center;
581
+ gap: 4px;
582
+ }
583
+
584
+ /* 右侧按钮 */
585
+ .input-right {
586
+ display: flex;
587
+ align-items: center;
588
+ gap: 2px;
589
+ }
590
+
591
+ /* @ 选择器容器 */
592
+ .at-picker-wrapper {
593
+ position: relative;
594
+ }
595
+
596
+ .input-right .icon-btn {
597
+ display: flex;
598
+ align-items: center;
599
+ justify-content: center;
600
+ width: 28px;
601
+ height: 28px;
602
+ background: transparent;
603
+ border: none;
604
+ border-radius: 6px;
605
+ color: var(--chat-text-muted, #666);
606
+ cursor: pointer;
607
+ transition: all 0.15s;
608
+ }
609
+
610
+ .input-right .icon-btn:hover {
611
+ color: var(--chat-text, #ccc);
612
+ }
613
+
614
+ .toggle-btn {
615
+ display: flex;
616
+ align-items: center;
617
+ justify-content: center;
618
+ width: 28px;
619
+ height: 28px;
620
+ background: transparent;
621
+ border: none;
622
+ border-radius: 6px;
623
+ color: var(--chat-text-muted, #666);
624
+ cursor: pointer;
625
+ transition: all 0.15s;
626
+ }
627
+
628
+ .toggle-btn:hover {
629
+ color: var(--chat-text, #ccc);
630
+ }
631
+
632
+ .toggle-btn.active {
633
+ color: var(--chat-text, #fff);
634
+ }
635
+
636
+ .send-btn {
637
+ display: flex;
638
+ align-items: center;
639
+ justify-content: center;
640
+ width: 28px;
641
+ height: 28px;
642
+ background: transparent;
643
+ border: none;
644
+ border-radius: 6px;
645
+ color: var(--chat-text-muted, #666);
646
+ cursor: pointer;
647
+ transition: all 0.15s;
648
+ }
649
+
650
+ .send-btn:hover {
651
+ color: var(--chat-text, #ccc);
652
+ }
653
+ </style>