@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,309 @@
1
+ <template>
2
+ <div class="chat-panel">
3
+ <!-- 顶部标题栏 -->
4
+ <ChatHeader
5
+ v-if="!hideHeader"
6
+ :sessions="sessions"
7
+ :current-session-id="currentSessionId"
8
+ :show-close="!!onClose"
9
+ @new-session="createNewSession"
10
+ @switch-session="switchSession"
11
+ @delete-session="deleteSession"
12
+ @close="handleClose"
13
+ @clear-all="handleClearAll"
14
+ @close-others="handleCloseOthers"
15
+ @export="handleExport"
16
+ @copy-id="handleCopyId"
17
+ @feedback="handleFeedback"
18
+ @settings="handleSettings"
19
+ />
20
+
21
+ <!-- 消息列表 -->
22
+ <div ref="messagesRef" class="messages-container">
23
+ <WelcomeMessage
24
+ v-if="messages.length === 0"
25
+ @quick-action="handleQuickAction"
26
+ />
27
+ <template v-else>
28
+ <MessageBubble
29
+ v-for="(msg, index) in messages"
30
+ :key="msg.id"
31
+ :role="msg.role"
32
+ :content="msg.content"
33
+ :images="msg.images"
34
+ :thinking="msg.thinking"
35
+ :thinking-complete="msg.thinkingComplete"
36
+ :search-results="msg.searchResults"
37
+ :searching="msg.searching"
38
+ :tool-calls="msg.toolCalls"
39
+ :copied="msg.copied"
40
+ :loading="msg.loading"
41
+ @copy="copyMessage(msg.id)"
42
+ @regenerate="regenerateMessage(index)"
43
+ @send="(text) => handleResend(index, text)"
44
+ />
45
+ </template>
46
+ </div>
47
+
48
+ <!-- 输入区域 -->
49
+ <ChatInput
50
+ :selected-images="selectedImages"
51
+ :is-loading="isLoading"
52
+ :mode="mode"
53
+ :model="model"
54
+ :models="models"
55
+ :web-search-enabled="webSearch"
56
+ :thinking-enabled="thinking"
57
+ @send="handleSend"
58
+ @cancel="cancelRequest"
59
+ @remove-image="handleRemoveImage"
60
+ @upload-image="handleUploadImage"
61
+ @at-context="handleAtContext"
62
+ @update:mode="setMode"
63
+ @update:model="setModel"
64
+ @update:webSearch="setWebSearch"
65
+ @update:thinking="setThinking"
66
+ />
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import { ref, watch, nextTick, onMounted, provide, toRef } from 'vue';
72
+ import { useChat } from '../composables/useChat';
73
+ import type { ChatAdapter } from '../adapter';
74
+ import type { ModelConfig, ChatMode } from '../types';
75
+ import { DEFAULT_MODELS } from '../types';
76
+ import ChatHeader from './chat/ui/ChatHeader.vue';
77
+ import WelcomeMessage from './chat/ui/WelcomeMessage.vue';
78
+ import MessageBubble from './chat/messages/MessageBubble.vue';
79
+ import ChatInput from './ChatInput.vue';
80
+
81
+ interface Props {
82
+ /** Adapter 实例 */
83
+ adapter?: ChatAdapter;
84
+ /** 工作目录 */
85
+ workingDir?: string;
86
+ /** 默认模型 */
87
+ defaultModel?: string;
88
+ /** 默认模式 */
89
+ defaultMode?: ChatMode;
90
+ /** 可用模型列表 */
91
+ models?: ModelConfig[];
92
+ /** 隐藏标题栏 */
93
+ hideHeader?: boolean;
94
+ /** 关闭回调(有此属性时显示关闭按钮) */
95
+ onClose?: () => void;
96
+ /** 自定义类名 */
97
+ className?: string;
98
+ }
99
+
100
+ const props = withDefaults(defineProps<Props>(), {
101
+ adapter: undefined,
102
+ workingDir: undefined,
103
+ defaultModel: 'anthropic/claude-opus-4.5',
104
+ defaultMode: 'agent',
105
+ models: () => DEFAULT_MODELS,
106
+ hideHeader: false,
107
+ onClose: undefined,
108
+ className: '',
109
+ });
110
+
111
+ const emit = defineEmits<{
112
+ close: [];
113
+ }>();
114
+
115
+ // 使用 useChat composable
116
+ const chat = useChat({
117
+ adapter: props.adapter,
118
+ defaultModel: props.defaultModel,
119
+ defaultMode: props.defaultMode,
120
+ });
121
+
122
+ // 解构状态
123
+ const {
124
+ sessions,
125
+ currentSessionId,
126
+ messages,
127
+ isLoading,
128
+ mode,
129
+ model,
130
+ webSearch,
131
+ thinking,
132
+ loadSessions,
133
+ switchSession,
134
+ createNewSession,
135
+ deleteSession,
136
+ sendMessage,
137
+ cancelRequest,
138
+ copyMessage,
139
+ regenerateMessage,
140
+ setMode,
141
+ setModel,
142
+ setWebSearch,
143
+ setThinking,
144
+ setWorkingDirectory,
145
+ } = chat;
146
+
147
+ // 选中的图片
148
+ const selectedImages = ref<string[]>([]);
149
+
150
+ // 消息容器引用
151
+ const messagesRef = ref<HTMLDivElement | null>(null);
152
+
153
+ // 可用模型
154
+ const models = toRef(props, 'models');
155
+
156
+ // Provide adapter 给子组件使用
157
+ provide('chatAdapter', props.adapter);
158
+
159
+ // 初始化
160
+ onMounted(() => {
161
+ loadSessions();
162
+ });
163
+
164
+ // 工作目录变化时更新
165
+ watch(
166
+ () => props.workingDir,
167
+ (dir) => {
168
+ if (dir) {
169
+ setWorkingDirectory(dir);
170
+ }
171
+ },
172
+ { immediate: true }
173
+ );
174
+
175
+ // 滚动到底部
176
+ async function scrollToBottom() {
177
+ await nextTick();
178
+ if (messagesRef.value) {
179
+ messagesRef.value.scrollTop = messagesRef.value.scrollHeight;
180
+ }
181
+ }
182
+
183
+ // 消息变化时滚动
184
+ watch(messages, () => {
185
+ scrollToBottom();
186
+ }, { flush: 'post' });
187
+
188
+ // 发送消息
189
+ function handleSend(text: string) {
190
+ sendMessage(text, selectedImages.value.length > 0 ? selectedImages.value : undefined);
191
+ selectedImages.value = [];
192
+ }
193
+
194
+ // 快捷操作
195
+ function handleQuickAction(text: string) {
196
+ sendMessage(text);
197
+ }
198
+
199
+ // 重新发送(编辑后)
200
+ function handleResend(index: number, text: string) {
201
+ // 删除当前消息及后续消息,然后重新发送
202
+ messages.value = messages.value.slice(0, index);
203
+ sendMessage(text);
204
+ }
205
+
206
+ // 移除图片
207
+ function handleRemoveImage(index: number) {
208
+ if (index >= 0 && index < selectedImages.value.length) {
209
+ selectedImages.value.splice(index, 1);
210
+ }
211
+ }
212
+
213
+ // 上传图片
214
+ function handleUploadImage() {
215
+ // TODO: 实现图片上传
216
+ console.log('上传图片');
217
+ }
218
+
219
+ // @ 上下文
220
+ function handleAtContext() {
221
+ // TODO: 实现 @ 上下文
222
+ console.log('@ 上下文');
223
+ }
224
+
225
+ // 关闭
226
+ function handleClose() {
227
+ if (props.onClose) {
228
+ props.onClose();
229
+ }
230
+ emit('close');
231
+ }
232
+
233
+ // 清空所有对话
234
+ function handleClearAll() {
235
+ // TODO: 实现清空所有对话
236
+ console.log('清空所有对话');
237
+ }
238
+
239
+ // 关闭其他对话
240
+ function handleCloseOthers() {
241
+ // TODO: 实现关闭其他对话
242
+ console.log('关闭其他对话');
243
+ }
244
+
245
+ // 导出对话
246
+ function handleExport() {
247
+ // TODO: 实现导出对话
248
+ console.log('导出对话');
249
+ }
250
+
251
+ // 复制请求 ID
252
+ function handleCopyId() {
253
+ if (currentSessionId.value) {
254
+ navigator.clipboard.writeText(currentSessionId.value);
255
+ console.log('已复制请求 ID:', currentSessionId.value);
256
+ }
257
+ }
258
+
259
+ // 反馈
260
+ function handleFeedback() {
261
+ // TODO: 实现反馈
262
+ console.log('反馈');
263
+ }
264
+
265
+ // 设置
266
+ function handleSettings() {
267
+ // TODO: 实现设置
268
+ console.log('Agent 设置');
269
+ }
270
+ </script>
271
+
272
+ <style scoped>
273
+ .chat-panel {
274
+ display: flex;
275
+ flex-direction: column;
276
+ width: 420px;
277
+ height: 100%;
278
+ background: var(--chat-bg, #1e1e1e);
279
+ border-left: 1px solid var(--chat-border, #333);
280
+ flex-shrink: 0;
281
+ overflow: hidden;
282
+ }
283
+
284
+ .messages-container {
285
+ flex: 1;
286
+ min-height: 0;
287
+ overflow-y: auto;
288
+ padding: 12px;
289
+ scroll-behavior: smooth;
290
+ }
291
+
292
+ /* 滚动条样式 */
293
+ .messages-container::-webkit-scrollbar {
294
+ width: 6px;
295
+ }
296
+
297
+ .messages-container::-webkit-scrollbar-track {
298
+ background: transparent;
299
+ }
300
+
301
+ .messages-container::-webkit-scrollbar-thumb {
302
+ background: var(--chat-scrollbar, rgba(255, 255, 255, 0.2));
303
+ border-radius: 3px;
304
+ }
305
+
306
+ .messages-container::-webkit-scrollbar-thumb:hover {
307
+ background: var(--chat-scrollbar-hover, rgba(255, 255, 255, 0.3));
308
+ }
309
+ </style>
@@ -0,0 +1,155 @@
1
+ <template>
2
+ <div class="search-block">
3
+ <div class="search-header" @click="expanded = !expanded">
4
+ <div class="search-icon">
5
+ <Loader2 v-if="searching" :size="14" class="spinning" />
6
+ <Globe v-else :size="14" />
7
+ </div>
8
+ <span class="search-title">
9
+ {{ searching ? '正在搜索...' : `找到 ${results.length} 条结果` }}
10
+ </span>
11
+ <ChevronDown
12
+ v-if="!searching && results.length > 0"
13
+ :size="14"
14
+ :class="['chevron', { rotated: expanded }]"
15
+ />
16
+ </div>
17
+
18
+ <!-- 搜索结果列表 -->
19
+ <div v-if="!searching && expanded && results.length > 0" class="search-results">
20
+ <a
21
+ v-for="(item, index) in results.slice(0, 5)"
22
+ :key="index"
23
+ :href="item.url"
24
+ target="_blank"
25
+ class="result-item"
26
+ >
27
+ <span class="result-title">{{ item.title }}</span>
28
+ <span class="result-url">{{ formatUrl(item.url) }}</span>
29
+ </a>
30
+ </div>
31
+ </div>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { ref } from 'vue';
36
+ import { Globe, Loader2, ChevronDown } from 'lucide-vue-next';
37
+ import type { SearchResult } from '../../types';
38
+
39
+ defineProps<{
40
+ results: SearchResult[];
41
+ searching: boolean;
42
+ }>();
43
+
44
+ const expanded = ref(true);
45
+
46
+ function formatUrl(url: string): string {
47
+ try {
48
+ return new URL(url).hostname;
49
+ } catch {
50
+ return url;
51
+ }
52
+ }
53
+ </script>
54
+
55
+ <style scoped>
56
+ .search-block {
57
+ margin: 8px 0;
58
+ background: var(--chat-muted, #2d2d2d);
59
+ border-radius: 8px;
60
+ overflow: hidden;
61
+ }
62
+
63
+ .search-header {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 8px;
67
+ padding: 10px 12px;
68
+ cursor: pointer;
69
+ transition: background 0.15s;
70
+ }
71
+
72
+ .search-header:hover {
73
+ background: var(--chat-muted-hover, #3a3a3a);
74
+ }
75
+
76
+ .search-icon {
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ width: 24px;
81
+ height: 24px;
82
+ background: rgba(255, 255, 255, 0.1);
83
+ border-radius: 50%;
84
+ color: var(--chat-text, #ccc);
85
+ }
86
+
87
+ .spinning {
88
+ animation: spin 1s linear infinite;
89
+ }
90
+
91
+ @keyframes spin {
92
+ from {
93
+ transform: rotate(0deg);
94
+ }
95
+ to {
96
+ transform: rotate(360deg);
97
+ }
98
+ }
99
+
100
+ .search-title {
101
+ flex: 1;
102
+ font-size: 13px;
103
+ font-weight: 500;
104
+ color: var(--chat-text, #ccc);
105
+ }
106
+
107
+ .chevron {
108
+ color: var(--chat-text-muted, #666);
109
+ transition: transform 0.2s;
110
+ }
111
+
112
+ .chevron.rotated {
113
+ transform: rotate(180deg);
114
+ }
115
+
116
+ .search-results {
117
+ border-top: 1px solid var(--chat-border, #333);
118
+ padding: 8px;
119
+ display: flex;
120
+ flex-direction: column;
121
+ gap: 6px;
122
+ }
123
+
124
+ .result-item {
125
+ display: flex;
126
+ flex-direction: column;
127
+ gap: 2px;
128
+ padding: 8px;
129
+ background: var(--chat-bg, #1e1e1e);
130
+ border-radius: 6px;
131
+ text-decoration: none;
132
+ transition: background 0.15s;
133
+ }
134
+
135
+ .result-item:hover {
136
+ background: var(--chat-muted-hover, #333);
137
+ }
138
+
139
+ .result-title {
140
+ font-size: 13px;
141
+ color: var(--chat-text, #ccc);
142
+ overflow: hidden;
143
+ text-overflow: ellipsis;
144
+ white-space: nowrap;
145
+ }
146
+
147
+ .result-item:hover .result-title {
148
+ color: var(--chat-text, #fff);
149
+ }
150
+
151
+ .result-url {
152
+ font-size: 11px;
153
+ color: var(--chat-text-muted, #666);
154
+ }
155
+ </style>
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <div :class="['thinking-block', { expanded }]">
3
+ <button class="thinking-header" @click="expanded = !expanded">
4
+ <div class="thinking-icon">
5
+ <Loader2 v-if="!isComplete" :size="14" class="spinning" />
6
+ <Lightbulb v-else :size="14" />
7
+ </div>
8
+ <span class="thinking-title">{{ isComplete ? '思考完成' : '正在思考...' }}</span>
9
+ <ChevronDown :size="14" :class="['chevron', { rotated: expanded }]" />
10
+ </button>
11
+
12
+ <div v-if="expanded && content" class="thinking-content">
13
+ <div class="thinking-text">{{ content }}</div>
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { ref } from 'vue';
20
+ import { Loader2, Lightbulb, ChevronDown } from 'lucide-vue-next';
21
+
22
+ defineProps<{
23
+ content: string;
24
+ isComplete: boolean;
25
+ }>();
26
+
27
+ const expanded = ref(false);
28
+ </script>
29
+
30
+ <style scoped>
31
+ .thinking-block {
32
+ margin: 8px 0;
33
+ background: var(--chat-muted, #2d2d2d);
34
+ border-radius: 8px;
35
+ overflow: hidden;
36
+ }
37
+
38
+ .thinking-header {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 8px;
42
+ width: 100%;
43
+ padding: 10px 12px;
44
+ border: none;
45
+ background: transparent;
46
+ cursor: pointer;
47
+ transition: background 0.15s;
48
+ }
49
+
50
+ .thinking-header:hover {
51
+ background: var(--chat-muted-hover, #3a3a3a);
52
+ }
53
+
54
+ .thinking-icon {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ width: 24px;
59
+ height: 24px;
60
+ background: rgba(255, 255, 255, 0.1);
61
+ border-radius: 50%;
62
+ color: var(--chat-text, #ccc);
63
+ }
64
+
65
+ .spinning {
66
+ animation: spin 1s linear infinite;
67
+ }
68
+
69
+ @keyframes spin {
70
+ from {
71
+ transform: rotate(0deg);
72
+ }
73
+ to {
74
+ transform: rotate(360deg);
75
+ }
76
+ }
77
+
78
+ .thinking-title {
79
+ flex: 1;
80
+ text-align: left;
81
+ font-size: 13px;
82
+ color: var(--chat-text, #ccc);
83
+ font-weight: 500;
84
+ }
85
+
86
+ .chevron {
87
+ color: var(--chat-text-muted, #666);
88
+ transition: transform 0.2s;
89
+ }
90
+
91
+ .chevron.rotated {
92
+ transform: rotate(180deg);
93
+ }
94
+
95
+ .thinking-content {
96
+ padding: 0 12px 12px;
97
+ border-top: 1px solid var(--chat-border, #333);
98
+ margin-top: 4px;
99
+ padding-top: 8px;
100
+ }
101
+
102
+ .thinking-text {
103
+ font-size: 13px;
104
+ color: var(--chat-text-muted, #999);
105
+ line-height: 1.6;
106
+ white-space: pre-wrap;
107
+ word-break: break-word;
108
+ }
109
+ </style>