@huyooo/ai-chat-frontend-vue 0.2.15 → 0.2.16
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 +4 -5
- package/src/adapter.ts +0 -72
- package/src/components/ChatPanel.vue +0 -591
- package/src/components/common/ConfirmDialog.vue +0 -208
- package/src/components/common/CopyButton.vue +0 -71
- package/src/components/common/IndexingSettings.vue +0 -580
- package/src/components/common/SettingsPanel.vue +0 -462
- package/src/components/common/Toast.vue +0 -90
- package/src/components/common/ToggleSwitch.vue +0 -75
- package/src/components/header/ChatHeader.vue +0 -612
- package/src/components/input/AtFilePicker.vue +0 -657
- package/src/components/input/ChatInput.vue +0 -823
- package/src/components/input/DropdownSelector.vue +0 -485
- package/src/components/input/ImagePreviewModal.vue +0 -238
- package/src/components/input/at-views/AtBranchView.vue +0 -63
- package/src/components/input/at-views/AtBrowserView.vue +0 -63
- package/src/components/input/at-views/AtChatsView.vue +0 -63
- package/src/components/input/at-views/AtDocsView.vue +0 -63
- package/src/components/input/at-views/AtFilesView.vue +0 -255
- package/src/components/input/at-views/AtTerminalsView.vue +0 -63
- package/src/components/message/MessageBubble.vue +0 -433
- package/src/components/message/PartsRenderer.vue +0 -180
- package/src/components/message/WelcomeMessage.vue +0 -308
- package/src/components/message/parts/CollapsibleCard.vue +0 -140
- package/src/components/message/parts/ErrorPart.vue +0 -51
- package/src/components/message/parts/ImagePart.vue +0 -97
- package/src/components/message/parts/SearchPart.vue +0 -103
- package/src/components/message/parts/TextPart.vue +0 -770
- package/src/components/message/parts/ThinkingPart.vue +0 -54
- package/src/components/message/parts/ToolCallPart.vue +0 -531
- package/src/components/message/parts/index.ts +0 -17
- package/src/components/message/parts/visual-predicate.ts +0 -43
- package/src/components/message/parts/visual-render.ts +0 -19
- package/src/components/message/parts/visual.ts +0 -12
- package/src/components/message/welcome-types.ts +0 -47
- package/src/composables/useChat.ts +0 -1542
- package/src/composables/useImageUpload.ts +0 -332
- package/src/composables/useVoiceInput.ts +0 -531
- package/src/composables/useVoiceToTextInput.ts +0 -94
- package/src/index.ts +0 -137
- package/src/styles.css +0 -274
- package/src/types/index.ts +0 -198
- package/src/utils/fileIcon.ts +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huyooo/ai-chat-frontend-vue",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "AI Chat Frontend - Vue components with adapter pattern",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
"./style.css": "./dist/style.css"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"dist"
|
|
20
|
-
"src"
|
|
19
|
+
"dist"
|
|
21
20
|
],
|
|
22
21
|
"scripts": {
|
|
23
22
|
"prebuild": "npm run clean",
|
|
@@ -31,8 +30,8 @@
|
|
|
31
30
|
"vue": "^3.4.0"
|
|
32
31
|
},
|
|
33
32
|
"dependencies": {
|
|
34
|
-
"@huyooo/ai-chat-bridge-electron": "^0.2.
|
|
35
|
-
"@huyooo/ai-chat-shared": "^0.2.
|
|
33
|
+
"@huyooo/ai-chat-bridge-electron": "^0.2.16",
|
|
34
|
+
"@huyooo/ai-chat-shared": "^0.2.16",
|
|
36
35
|
"@iconify/vue": "^4.1.2",
|
|
37
36
|
"vscode-uri": "^3.1.0",
|
|
38
37
|
"marked": "^12.0.0",
|
package/src/adapter.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chat Adapter 辅助类型和工具
|
|
3
|
-
* 核心 ChatAdapter 接口从 bridge-electron 导入
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
ChatAdapter as ChatAdapterType,
|
|
8
|
-
ChatMode,
|
|
9
|
-
ThinkingMode,
|
|
10
|
-
} from '@huyooo/ai-chat-bridge-electron/renderer'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 重新导出 ChatAdapter 类型供内部组件使用
|
|
14
|
-
*/
|
|
15
|
-
export type ChatAdapter = ChatAdapterType
|
|
16
|
-
|
|
17
|
-
/** 思考数据 */
|
|
18
|
-
export interface ThinkingData {
|
|
19
|
-
content: string
|
|
20
|
-
isComplete: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** 工具调用数据 */
|
|
24
|
-
export interface ToolCallData {
|
|
25
|
-
name: string
|
|
26
|
-
args: Record<string, unknown>
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 工具结果数据 */
|
|
30
|
-
export interface ToolResultData {
|
|
31
|
-
name: string
|
|
32
|
-
result: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 图片数据 */
|
|
36
|
-
export interface ImageData {
|
|
37
|
-
base64: string
|
|
38
|
-
mimeType: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 发送消息选项 */
|
|
42
|
-
export interface SendMessageOptions {
|
|
43
|
-
mode: ChatMode
|
|
44
|
-
model: string
|
|
45
|
-
enableWebSearch: boolean
|
|
46
|
-
/** 深度思考开关(每个 provider 内部使用最优参数) */
|
|
47
|
-
thinkingMode: ThinkingMode
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** 创建会话选项 */
|
|
51
|
-
export interface CreateSessionOptions {
|
|
52
|
-
title: string
|
|
53
|
-
model: string
|
|
54
|
-
mode: ChatMode
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** 更新会话选项 */
|
|
58
|
-
export interface UpdateSessionOptions {
|
|
59
|
-
title?: string
|
|
60
|
-
model?: string
|
|
61
|
-
mode?: ChatMode
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** 保存消息选项 */
|
|
65
|
-
export interface SaveMessageOptions {
|
|
66
|
-
sessionId: string
|
|
67
|
-
role: 'user' | 'assistant'
|
|
68
|
-
content: string
|
|
69
|
-
/** 执行步骤列表 JSON */
|
|
70
|
-
steps?: string
|
|
71
|
-
operationIds?: string
|
|
72
|
-
}
|
|
@@ -1,591 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="chat-panel">
|
|
3
|
-
<!-- 确认弹窗 -->
|
|
4
|
-
<ConfirmDialog
|
|
5
|
-
v-model:visible="confirmDialog.visible"
|
|
6
|
-
:title="confirmDialog.title"
|
|
7
|
-
:message="confirmDialog.message"
|
|
8
|
-
:type="confirmDialog.type"
|
|
9
|
-
:confirm-text="confirmDialog.confirmText"
|
|
10
|
-
@confirm="confirmDialog.onConfirm"
|
|
11
|
-
/>
|
|
12
|
-
|
|
13
|
-
<!-- Toast 消息 -->
|
|
14
|
-
<Toast v-model:visible="toast.visible" :message="toast.message" :type="toast.type" />
|
|
15
|
-
|
|
16
|
-
<!-- 工具批准现在内嵌在 ToolCallPart 中,不再需要全局对话框 -->
|
|
17
|
-
|
|
18
|
-
<!-- 设置面板 -->
|
|
19
|
-
<SettingsPanel
|
|
20
|
-
v-model:visible="settingsPanelVisible"
|
|
21
|
-
v-model:config="autoRunConfig"
|
|
22
|
-
:all-tools="allTools"
|
|
23
|
-
:enabled-tools="enabledTools"
|
|
24
|
-
@save="handleSaveSettings"
|
|
25
|
-
@update:enabled-tools="handleUpdateEnabledTools"
|
|
26
|
-
/>
|
|
27
|
-
|
|
28
|
-
<!-- 顶部标题栏 -->
|
|
29
|
-
<ChatHeader
|
|
30
|
-
v-if="!hideHeader"
|
|
31
|
-
:sessions="sessions"
|
|
32
|
-
:current-session-id="currentSessionId"
|
|
33
|
-
:show-close="!!onClose"
|
|
34
|
-
@new-session="handleNewSession"
|
|
35
|
-
@switch-session="switchSession"
|
|
36
|
-
@delete-session="deleteSession"
|
|
37
|
-
@hide-session="hideSession"
|
|
38
|
-
@close="handleClose"
|
|
39
|
-
@clear-all="handleClearAll"
|
|
40
|
-
@close-others="handleCloseOthers"
|
|
41
|
-
@export="handleExport"
|
|
42
|
-
@copy-id="handleCopyId"
|
|
43
|
-
@feedback="handleFeedback"
|
|
44
|
-
@settings="handleSettings"
|
|
45
|
-
/>
|
|
46
|
-
|
|
47
|
-
<!-- 消息列表容器 -->
|
|
48
|
-
<div class="messages-wrapper">
|
|
49
|
-
<div ref="messagesRef" class="messages-container chat-scrollbar" @scroll="handleScroll">
|
|
50
|
-
<WelcomeMessage v-if="messages.length === 0" :config="props.welcomeConfig" @quick-action="handleQuickAction" />
|
|
51
|
-
<template v-else>
|
|
52
|
-
<MessageBubble
|
|
53
|
-
v-for="(msg, index) in messages"
|
|
54
|
-
:key="msg.id"
|
|
55
|
-
:role="msg.role"
|
|
56
|
-
:parts="msg.parts"
|
|
57
|
-
:model="msg.model"
|
|
58
|
-
:mode="msg.mode"
|
|
59
|
-
:images="msg.images"
|
|
60
|
-
:copied="msg.copied"
|
|
61
|
-
:loading="msg.loading"
|
|
62
|
-
:timestamp="msg.timestamp"
|
|
63
|
-
:steps-expanded-type="stepsExpandedType"
|
|
64
|
-
:adapter="props.adapter"
|
|
65
|
-
:auto-run-config="autoRunConfig"
|
|
66
|
-
:on-save-config="saveAutoRunConfig"
|
|
67
|
-
:on-cancel-tool-call="handleCancelToolCall"
|
|
68
|
-
@copy="copyMessage(msg.id)"
|
|
69
|
-
@regenerate="regenerateMessage(index)"
|
|
70
|
-
@send="(text) => handleResend(index, text)"
|
|
71
|
-
/>
|
|
72
|
-
</template>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<!-- 滚动到底部按钮 -->
|
|
76
|
-
<Transition name="fade">
|
|
77
|
-
<button
|
|
78
|
-
v-if="!shouldAutoScroll && messages.length > 0"
|
|
79
|
-
class="scroll-to-bottom-btn"
|
|
80
|
-
@click="scrollToBottom(true)"
|
|
81
|
-
title="滚动到底部"
|
|
82
|
-
>
|
|
83
|
-
<Icon icon="lucide:arrow-down" width="16" />
|
|
84
|
-
</button>
|
|
85
|
-
</Transition>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
<!-- 输入区域 -->
|
|
89
|
-
<ChatInput
|
|
90
|
-
ref="chatInputRef"
|
|
91
|
-
:adapter="props.adapter"
|
|
92
|
-
:is-loading="isLoading"
|
|
93
|
-
:mode="mode"
|
|
94
|
-
:model="model"
|
|
95
|
-
:models="models"
|
|
96
|
-
:web-search-enabled="webSearch"
|
|
97
|
-
:thinking-enabled="thinking"
|
|
98
|
-
@send="handleSend"
|
|
99
|
-
@cancel="cancelRequest"
|
|
100
|
-
@at-context="handleAtContext"
|
|
101
|
-
@update:mode="setMode"
|
|
102
|
-
@update:model="setModel"
|
|
103
|
-
@update:webSearch="setWebSearch"
|
|
104
|
-
@update:thinking="setThinking"
|
|
105
|
-
/>
|
|
106
|
-
</div>
|
|
107
|
-
</template>
|
|
108
|
-
|
|
109
|
-
<script setup lang="ts">
|
|
110
|
-
import { ref, computed, watch, nextTick, onMounted, provide, toRef, type Component } from 'vue';
|
|
111
|
-
import { useChat } from '../composables/useChat';
|
|
112
|
-
import type { ChatAdapter, ImageData } from '../adapter';
|
|
113
|
-
import type { ModelOption, ChatMode } from '../types';
|
|
114
|
-
import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer';
|
|
115
|
-
import { Icon } from '@iconify/vue';
|
|
116
|
-
import ChatHeader from './header/ChatHeader.vue';
|
|
117
|
-
import WelcomeMessage from './message/WelcomeMessage.vue';
|
|
118
|
-
import MessageBubble from './message/MessageBubble.vue';
|
|
119
|
-
import ChatInput from './input/ChatInput.vue';
|
|
120
|
-
import ConfirmDialog from './common/ConfirmDialog.vue';
|
|
121
|
-
import Toast from './common/Toast.vue';
|
|
122
|
-
// ToolApprovalDialog 已移除,工具批准现在内嵌在 ToolCallPart 中
|
|
123
|
-
import SettingsPanel from './common/SettingsPanel.vue';
|
|
124
|
-
import type { ToolCompleteEvent } from '../composables/useChat';
|
|
125
|
-
import type { WelcomeConfig } from './message/welcome-types';
|
|
126
|
-
|
|
127
|
-
interface Props {
|
|
128
|
-
/** Adapter 实例 */
|
|
129
|
-
adapter: ChatAdapter;
|
|
130
|
-
/** 默认模型 */
|
|
131
|
-
defaultModel?: string;
|
|
132
|
-
/** 默认模式 */
|
|
133
|
-
defaultMode?: ChatMode;
|
|
134
|
-
/** 可用模型列表 */
|
|
135
|
-
models?: ModelOption[];
|
|
136
|
-
/** 隐藏标题栏 */
|
|
137
|
-
hideHeader?: boolean;
|
|
138
|
-
/** 关闭回调(有此属性时显示关闭按钮) */
|
|
139
|
-
onClose?: () => void;
|
|
140
|
-
/** 自定义类名 */
|
|
141
|
-
className?: string;
|
|
142
|
-
/** 欢迎页配置 */
|
|
143
|
-
welcomeConfig?: Partial<WelcomeConfig>;
|
|
144
|
-
/** 自定义 Part 渲染器 - 根据 part.type 选择渲染组件(如 weather, stock 等) */
|
|
145
|
-
partRenderers?: Record<string, Component>;
|
|
146
|
-
/**
|
|
147
|
-
* 执行步骤折叠模式
|
|
148
|
-
* - 'open': 始终展开
|
|
149
|
-
* - 'close': 始终折叠
|
|
150
|
-
* - 'auto': 执行时展开,完成后折叠
|
|
151
|
-
*/
|
|
152
|
-
stepsExpandedType?: 'open' | 'close' | 'auto';
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
156
|
-
defaultModel: 'anthropic/claude-opus-4.5',
|
|
157
|
-
defaultMode: 'agent',
|
|
158
|
-
models: () => [],
|
|
159
|
-
hideHeader: false,
|
|
160
|
-
onClose: undefined,
|
|
161
|
-
className: '',
|
|
162
|
-
welcomeConfig: undefined,
|
|
163
|
-
partRenderers: () => ({}),
|
|
164
|
-
stepsExpandedType: 'auto',
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const emit = defineEmits<{
|
|
168
|
-
close: [];
|
|
169
|
-
/** 工具执行完成事件 */
|
|
170
|
-
'tool-complete': [event: ToolCompleteEvent];
|
|
171
|
-
}>();
|
|
172
|
-
|
|
173
|
-
// 使用 useChat composable
|
|
174
|
-
const chat = useChat({
|
|
175
|
-
adapter: props.adapter,
|
|
176
|
-
defaultModel: props.defaultModel,
|
|
177
|
-
defaultMode: props.defaultMode,
|
|
178
|
-
onToolComplete: (event) => emit('tool-complete', event),
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// 解构状态
|
|
182
|
-
const {
|
|
183
|
-
sessions,
|
|
184
|
-
currentSessionId,
|
|
185
|
-
messages,
|
|
186
|
-
isLoading,
|
|
187
|
-
mode,
|
|
188
|
-
model,
|
|
189
|
-
webSearch,
|
|
190
|
-
thinking,
|
|
191
|
-
// 工具管理
|
|
192
|
-
allTools,
|
|
193
|
-
enabledTools,
|
|
194
|
-
loadSessions,
|
|
195
|
-
switchSession,
|
|
196
|
-
createNewSession,
|
|
197
|
-
deleteSession,
|
|
198
|
-
hideSession,
|
|
199
|
-
clearAllSessions,
|
|
200
|
-
hideOtherSessions,
|
|
201
|
-
exportCurrentSession,
|
|
202
|
-
sendMessage,
|
|
203
|
-
cancelRequest,
|
|
204
|
-
copyMessage,
|
|
205
|
-
regenerateMessage,
|
|
206
|
-
resendFromIndex,
|
|
207
|
-
setMode,
|
|
208
|
-
setModel,
|
|
209
|
-
setWebSearch,
|
|
210
|
-
setThinking,
|
|
211
|
-
setWorkingDirectory,
|
|
212
|
-
autoRunConfig,
|
|
213
|
-
saveAutoRunConfig,
|
|
214
|
-
saveEnabledTools,
|
|
215
|
-
} = chat;
|
|
216
|
-
|
|
217
|
-
function handleCancelToolCall() {
|
|
218
|
-
cancelRequest()
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 消息容器引用
|
|
222
|
-
const messagesRef = ref<HTMLDivElement | null>(null);
|
|
223
|
-
|
|
224
|
-
// 是否应该自动滚动(用户在底部附近时才自动滚动)
|
|
225
|
-
const shouldAutoScroll = ref(true);
|
|
226
|
-
// 距离底部多少像素内算"在底部"
|
|
227
|
-
const SCROLL_THRESHOLD = 25;
|
|
228
|
-
// 上次滚动位置(用于检测滚动方向)
|
|
229
|
-
let lastScrollTop = 0;
|
|
230
|
-
// 是否正在程序化滚动(用于区分用户手动滚动)
|
|
231
|
-
let isProgrammaticScroll = false;
|
|
232
|
-
|
|
233
|
-
// ChatInput 引用
|
|
234
|
-
const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null);
|
|
235
|
-
|
|
236
|
-
// 设置面板状态
|
|
237
|
-
const settingsPanelVisible = ref(false);
|
|
238
|
-
|
|
239
|
-
// 确认弹窗状态
|
|
240
|
-
const confirmDialog = ref({
|
|
241
|
-
visible: false,
|
|
242
|
-
title: '确认',
|
|
243
|
-
message: '',
|
|
244
|
-
type: 'warning' as 'info' | 'warning' | 'danger',
|
|
245
|
-
confirmText: '确定',
|
|
246
|
-
onConfirm: () => {},
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// Toast 消息状态
|
|
250
|
-
const toast = ref({
|
|
251
|
-
visible: false,
|
|
252
|
-
message: '',
|
|
253
|
-
type: 'info' as 'info' | 'success' | 'warning' | 'error',
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
/** 显示 Toast 消息 */
|
|
257
|
-
function showToast(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') {
|
|
258
|
-
toast.value = { visible: true, message, type };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** 显示确认弹窗 */
|
|
262
|
-
function showConfirm(options: {
|
|
263
|
-
title?: string;
|
|
264
|
-
message: string;
|
|
265
|
-
type?: 'info' | 'warning' | 'danger';
|
|
266
|
-
confirmText?: string;
|
|
267
|
-
onConfirm: () => void;
|
|
268
|
-
}) {
|
|
269
|
-
confirmDialog.value = {
|
|
270
|
-
visible: true,
|
|
271
|
-
title: options.title || '确认',
|
|
272
|
-
message: options.message,
|
|
273
|
-
type: options.type || 'warning',
|
|
274
|
-
confirmText: options.confirmText || '确定',
|
|
275
|
-
onConfirm: options.onConfirm,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// 可用模型(如果没有传入,从后端获取)
|
|
280
|
-
const models = ref<ModelOption[]>(props.models || []);
|
|
281
|
-
|
|
282
|
-
// 从后端获取模型列表
|
|
283
|
-
onMounted(async () => {
|
|
284
|
-
try {
|
|
285
|
-
models.value = await props.adapter.getModels();
|
|
286
|
-
} catch (err) {
|
|
287
|
-
console.warn('获取模型列表失败:', err);
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// 注意:cwd 已解耦,不再通过 prop 传递
|
|
292
|
-
// FileBrowser 会直接通过 adapter.setCwd() 同步到 Agent
|
|
293
|
-
// getCwdTool 会自动从 Agent 的 context.cwd 读取最新值
|
|
294
|
-
// 如果需要 cwd 给子组件使用,可以通过 getCwdTool 获取
|
|
295
|
-
|
|
296
|
-
// Provide 全局 input 状态给子组件(让 message 中的 input 也能修改)
|
|
297
|
-
provide('chatInputState', {
|
|
298
|
-
mode,
|
|
299
|
-
model,
|
|
300
|
-
webSearch,
|
|
301
|
-
thinking,
|
|
302
|
-
models,
|
|
303
|
-
isLoading,
|
|
304
|
-
setMode,
|
|
305
|
-
setModel,
|
|
306
|
-
setWebSearch,
|
|
307
|
-
setThinking,
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// Provide 自定义 Part 渲染器(用于 PartsRenderer)
|
|
311
|
-
provide('partRenderers', props.partRenderers);
|
|
312
|
-
|
|
313
|
-
// 初始化
|
|
314
|
-
onMounted(() => {
|
|
315
|
-
loadSessions();
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// 注意:cwd 已解耦,不再需要监听 prop 变化
|
|
319
|
-
|
|
320
|
-
// 检查是否在底部附近
|
|
321
|
-
function isNearBottom(): boolean {
|
|
322
|
-
if (!messagesRef.value) return true;
|
|
323
|
-
const { scrollTop, scrollHeight, clientHeight } = messagesRef.value;
|
|
324
|
-
return scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// 处理滚动事件(优化:检测滚动方向,区分程序化滚动)
|
|
328
|
-
function handleScroll() {
|
|
329
|
-
if (!messagesRef.value) return;
|
|
330
|
-
|
|
331
|
-
// 忽略程序化滚动触发的事件
|
|
332
|
-
if (isProgrammaticScroll) return;
|
|
333
|
-
|
|
334
|
-
const { scrollTop } = messagesRef.value;
|
|
335
|
-
const isScrollingUp = scrollTop < lastScrollTop;
|
|
336
|
-
lastScrollTop = scrollTop;
|
|
337
|
-
|
|
338
|
-
// 用户向上滚动时,立即停止自动滚动
|
|
339
|
-
if (isScrollingUp) {
|
|
340
|
-
shouldAutoScroll.value = false;
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// 用户向下滚动到底部附近时,恢复自动滚动
|
|
345
|
-
if (isNearBottom()) {
|
|
346
|
-
shouldAutoScroll.value = true;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// 滚动到底部
|
|
351
|
-
async function scrollToBottom(force = false) {
|
|
352
|
-
await nextTick();
|
|
353
|
-
if (messagesRef.value && (force || shouldAutoScroll.value)) {
|
|
354
|
-
// 标记为程序化滚动,避免触发 handleScroll 逻辑
|
|
355
|
-
isProgrammaticScroll = true;
|
|
356
|
-
messagesRef.value.scrollTop = messagesRef.value.scrollHeight;
|
|
357
|
-
lastScrollTop = messagesRef.value.scrollTop;
|
|
358
|
-
// 强制滚动时才恢复自动滚动
|
|
359
|
-
if (force) {
|
|
360
|
-
shouldAutoScroll.value = true;
|
|
361
|
-
}
|
|
362
|
-
// 延迟重置标志,确保 scroll 事件已处理
|
|
363
|
-
requestAnimationFrame(() => {
|
|
364
|
-
isProgrammaticScroll = false;
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// 消息变化时滚动(只在用户在底部时)
|
|
370
|
-
watch(
|
|
371
|
-
messages,
|
|
372
|
-
() => {
|
|
373
|
-
scrollToBottom();
|
|
374
|
-
},
|
|
375
|
-
{ flush: 'post', deep: true }
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
// 发送新消息时强制滚动到底部
|
|
379
|
-
watch(isLoading, (loading, prevLoading) => {
|
|
380
|
-
// 开始加载时(发送消息时)强制滚动到底部
|
|
381
|
-
if (loading && !prevLoading) {
|
|
382
|
-
scrollToBottom(true);
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// 发送消息
|
|
387
|
-
function handleSend(text: string, images?: ImageData[]) {
|
|
388
|
-
// 将 ImageData[] 转换为 string[] (data URL)
|
|
389
|
-
const imageUrls = images?.map(img => `data:${img.mimeType};base64,${img.base64}`)
|
|
390
|
-
sendMessage(text, imageUrls);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// 快捷操作
|
|
394
|
-
function handleQuickAction(text: string) {
|
|
395
|
-
sendMessage(text);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// 重新发送(编辑后)
|
|
399
|
-
function handleResend(index: number, text: string) {
|
|
400
|
-
resendFromIndex(index, text);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// @ 上下文
|
|
404
|
-
function handleAtContext() {
|
|
405
|
-
// TODO: 实现 @ 上下文
|
|
406
|
-
console.log('@ 上下文');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// 创建新会话(重置输入框状态)
|
|
410
|
-
async function handleNewSession() {
|
|
411
|
-
await createNewSession();
|
|
412
|
-
// 重置输入框状态
|
|
413
|
-
nextTick(() => {
|
|
414
|
-
chatInputRef.value?.clear();
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// 关闭
|
|
419
|
-
function handleClose() {
|
|
420
|
-
if (props.onClose) {
|
|
421
|
-
props.onClose();
|
|
422
|
-
}
|
|
423
|
-
emit('close');
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// 清空所有对话
|
|
427
|
-
function handleClearAll() {
|
|
428
|
-
showConfirm({
|
|
429
|
-
title: '清空所有对话',
|
|
430
|
-
message: '确定要清空所有对话吗?此操作不可恢复。',
|
|
431
|
-
type: 'danger',
|
|
432
|
-
confirmText: '清空',
|
|
433
|
-
onConfirm: () => clearAllSessions(),
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// 关闭其他对话
|
|
438
|
-
async function handleCloseOthers() {
|
|
439
|
-
await hideOtherSessions()
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// 导出对话
|
|
443
|
-
function handleExport() {
|
|
444
|
-
const data = exportCurrentSession()
|
|
445
|
-
if (!data) {
|
|
446
|
-
showToast('当前会话没有内容可导出', 'warning')
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// 创建下载链接
|
|
451
|
-
const blob = new Blob([data], { type: 'application/json' })
|
|
452
|
-
const url = URL.createObjectURL(blob)
|
|
453
|
-
const a = document.createElement('a')
|
|
454
|
-
const session = sessions.value.find((s) => s.id === currentSessionId.value)
|
|
455
|
-
const filename = `chat-${session?.title || 'export'}-${new Date().toISOString().slice(0, 10)}.json`
|
|
456
|
-
a.href = url
|
|
457
|
-
a.download = filename
|
|
458
|
-
document.body.appendChild(a)
|
|
459
|
-
a.click()
|
|
460
|
-
document.body.removeChild(a)
|
|
461
|
-
URL.revokeObjectURL(url)
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// 复制会话 ID
|
|
465
|
-
async function handleCopyId() {
|
|
466
|
-
if (!currentSessionId.value) return
|
|
467
|
-
try {
|
|
468
|
-
await navigator.clipboard.writeText(currentSessionId.value)
|
|
469
|
-
showToast('已复制会话 ID', 'success')
|
|
470
|
-
} catch (error) {
|
|
471
|
-
console.error('复制失败:', error)
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// 反馈
|
|
476
|
-
function handleFeedback() {
|
|
477
|
-
// TODO: 实现反馈
|
|
478
|
-
console.log('反馈');
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// 设置
|
|
482
|
-
function handleSettings() {
|
|
483
|
-
settingsPanelVisible.value = true;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// 保存设置(静默保存,不显示提示)
|
|
487
|
-
async function handleSaveSettings(config: AutoRunConfig) {
|
|
488
|
-
try {
|
|
489
|
-
await saveAutoRunConfig(config);
|
|
490
|
-
} catch (error) {
|
|
491
|
-
console.error('保存设置失败:', error);
|
|
492
|
-
showToast('保存设置失败', 'error');
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function handleUpdateEnabledTools(tools: string[] | undefined) {
|
|
497
|
-
saveEnabledTools(tools);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// 模式变更现在在 ToolCallPart 中处理,不再需要此函数
|
|
501
|
-
|
|
502
|
-
// 暴露给外部的方法
|
|
503
|
-
defineExpose({
|
|
504
|
-
/** 设置输入框内容 */
|
|
505
|
-
setInputText: (text: string) => {
|
|
506
|
-
chatInputRef.value?.setText(text);
|
|
507
|
-
},
|
|
508
|
-
/** 在光标位置插入文本(用于 @ 上下文) */
|
|
509
|
-
insertInputText: (text: string) => {
|
|
510
|
-
chatInputRef.value?.insertText?.(text);
|
|
511
|
-
},
|
|
512
|
-
/** 聚焦输入框 */
|
|
513
|
-
focusInput: () => {
|
|
514
|
-
chatInputRef.value?.focus();
|
|
515
|
-
},
|
|
516
|
-
/** 发送消息 */
|
|
517
|
-
sendMessage: (text: string) => {
|
|
518
|
-
sendMessage(text);
|
|
519
|
-
},
|
|
520
|
-
/** 当前工作目录 */
|
|
521
|
-
setCwd: setWorkingDirectory,
|
|
522
|
-
});
|
|
523
|
-
</script>
|
|
524
|
-
|
|
525
|
-
<style scoped>
|
|
526
|
-
.chat-panel {
|
|
527
|
-
display: flex;
|
|
528
|
-
flex-direction: column;
|
|
529
|
-
width: 100%;
|
|
530
|
-
height: 100%;
|
|
531
|
-
background: var(--chat-bg, #1e1e1e);
|
|
532
|
-
overflow: hidden;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
.messages-wrapper {
|
|
536
|
-
flex: 1;
|
|
537
|
-
min-height: 0;
|
|
538
|
-
position: relative;
|
|
539
|
-
display: flex;
|
|
540
|
-
flex-direction: column;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
.messages-container {
|
|
544
|
-
flex: 1;
|
|
545
|
-
min-height: 0;
|
|
546
|
-
overflow-y: auto;
|
|
547
|
-
padding: 12px;
|
|
548
|
-
scroll-behavior: smooth;
|
|
549
|
-
display: flex;
|
|
550
|
-
flex-direction: column;
|
|
551
|
-
gap: 8px;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/* 滚动到底部按钮 */
|
|
555
|
-
.scroll-to-bottom-btn {
|
|
556
|
-
position: absolute;
|
|
557
|
-
bottom: 16px;
|
|
558
|
-
right: 16px;
|
|
559
|
-
display: flex;
|
|
560
|
-
align-items: center;
|
|
561
|
-
justify-content: center;
|
|
562
|
-
width: 32px;
|
|
563
|
-
height: 32px;
|
|
564
|
-
border-radius: 50%;
|
|
565
|
-
background: var(--chat-fab-bg, var(--chat-muted));
|
|
566
|
-
border: 1px solid var(--chat-border);
|
|
567
|
-
color: var(--chat-text-muted);
|
|
568
|
-
cursor: pointer;
|
|
569
|
-
transition: all 0.2s ease;
|
|
570
|
-
box-shadow: var(--chat-fab-shadow, 0 2px 8px rgba(0, 0, 0, 0.3));
|
|
571
|
-
z-index: 10;
|
|
572
|
-
pointer-events: auto;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
.scroll-to-bottom-btn:hover {
|
|
576
|
-
background: var(--chat-fab-bg-hover, var(--chat-muted-hover));
|
|
577
|
-
color: var(--chat-text);
|
|
578
|
-
transform: scale(1.1);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/* 淡入淡出动画 */
|
|
582
|
-
.fade-enter-active,
|
|
583
|
-
.fade-leave-active {
|
|
584
|
-
transition: opacity 0.2s ease;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
.fade-enter-from,
|
|
588
|
-
.fade-leave-to {
|
|
589
|
-
opacity: 0;
|
|
590
|
-
}
|
|
591
|
-
</style>
|