ai-chat-vue3 0.1.0

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.
@@ -0,0 +1,432 @@
1
+ <template>
2
+ <section class="chat-container">
3
+ <div class="messages" ref="messageList">
4
+ <div
5
+ v-for="(item, idx) in messages"
6
+ :key="idx"
7
+ class="message"
8
+ :class="item.role === 'user' ? 'user-message' : 'system-message'"
9
+ >
10
+ <template v-if="item.role === 'system'">
11
+ <div class="avatar" :class="'system'">AI</div>
12
+ <div class="bubble">
13
+ <div v-html="parseMarkdown(item.content)"></div>
14
+ </div>
15
+ </template>
16
+ <template v-else>
17
+ <div class="bubble">
18
+ {{ item.content }}
19
+ </div>
20
+ <div class="avatar" :class="'user'">我</div>
21
+ </template>
22
+ </div>
23
+ <div v-if="loading" class="loading">AI 正在思考...</div>
24
+ </div>
25
+
26
+ <div class="toolbar">
27
+ <div class="input">
28
+ <textarea
29
+ v-model="input"
30
+ class="prompt-input"
31
+ :placeholder="placeholder"
32
+ :disabled="loading"
33
+ @keydown.enter.exact.prevent="handleSubmit"
34
+ />
35
+ <button class="send-btn" :disabled="loading || !input.trim()" @click="handleSubmit">
36
+ {{ loading ? '等待中…' : '发送' }}
37
+ </button>
38
+ </div>
39
+ </div>
40
+ </section>
41
+ </template>
42
+
43
+ <script setup>
44
+ import { ref, nextTick, watch, onMounted } from 'vue';
45
+ import { marked } from 'marked';
46
+
47
+ const placeholder = '向大模型提问,例:帮我起一个智能助手的名字。';
48
+ const input = ref('');
49
+ const messages = ref([
50
+
51
+ ]);
52
+ const loading = ref(false);
53
+ const messageList = ref(null);
54
+ const nowModel = ref('glm-4.6v-flash');
55
+
56
+ // 模型列表
57
+ const LLM_MODELS = {
58
+ 'mimo-v2-flash': {
59
+ model: 'mimo-v2-flash',
60
+ name: 'Mimo V2 Flash',
61
+ apiKey: 'sk-cna17w0o4spxrtgp1ieaaq77u38vifffe3dpjbw1z9alrnif',
62
+ apiUrl: 'https://api.xiaomimimo.com/v1/chat/completions',
63
+ },
64
+ 'doubao-seed-1-8-251215': {
65
+ model: 'doubao-seed-1-8-251215',
66
+ name: 'Doubao Seed 1.8 251215',
67
+ apiKey: 'e54eb32a-75ba-4190-8a4f-fc05d5d857cb',
68
+ apiUrl: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
69
+ },
70
+ 'glm-4.6v-flash': {
71
+ model: 'glm-4.6v-flash',
72
+ name: 'GLM 4.6v Flash',
73
+ apiKey: '9f29ea7cbab7437bb3161d9c2adf490f.mPv5XskGyi0SqaMl',
74
+ apiUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
75
+ },
76
+ };
77
+
78
+ // 解析Markdown内容
79
+ const parseMarkdown = (content) => {
80
+ try {
81
+ return marked(content || '');
82
+ } catch (err) {
83
+ console.error('Markdown解析失败:', err);
84
+ return content || '';
85
+ }
86
+ };
87
+
88
+ const scrollToBottom = () => {
89
+ nextTick(() => {
90
+ // window.scrollTo({
91
+ // top: document.documentElement.scrollHeight, // 文档总高度
92
+ // behavior: 'smooth' // 平滑滚动
93
+ // });
94
+ const el = messageList.value;
95
+ if (el) {
96
+ // 使用scrollIntoView代替直接设置scrollTop,兼容性更好
97
+ // 创建一个临时元素放在底部
98
+ const tempEl = document.createElement('div');
99
+ el.appendChild(tempEl);
100
+ // 滚动到底部
101
+ tempEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
102
+ // 移除临时元素
103
+ el.removeChild(tempEl);
104
+ }
105
+ });
106
+ };
107
+
108
+ // 添加immediate选项,确保初始渲染时也执行滚动
109
+ watch(messages, scrollToBottom, { deep: true, immediate: true });
110
+
111
+ const callRealLLM = async (chatMessages) => {
112
+ loading.value = true;
113
+ try {
114
+ let headers = {
115
+ 'Content-Type': 'application/json',
116
+ }
117
+ const apiKey = LLM_MODELS[nowModel.value].apiKey;
118
+ if (nowModel.value === 'glm-4.6v-flash' || nowModel.value === 'doubao-seed-1-8-251215') {
119
+ headers['Authorization'] = `Bearer ${apiKey}`;
120
+ } else {
121
+ headers['api-key'] = apiKey;
122
+ }
123
+ const res = await fetch(LLM_MODELS[nowModel.value].apiUrl, {
124
+ method: 'POST',
125
+ headers: headers,
126
+ body: JSON.stringify({
127
+ "model": LLM_MODELS[nowModel.value].model,
128
+ "messages": chatMessages,
129
+ "stream": true // 启用流式响应
130
+ })
131
+ });
132
+
133
+ if (!res.ok) {
134
+ throw new Error(`LLM 接口响应异常: ${res.status}`);
135
+ }
136
+
137
+ // 检查是否支持流式响应
138
+ if (res.body && typeof res.body.getReader === 'function') {
139
+ // 添加一个临时的AI回复消息
140
+ const streamMessageIndex = messages.value.length;
141
+ messages.value.push({
142
+ role: 'system',
143
+ content: '',
144
+ });
145
+
146
+ // 创建解码器来处理文本流
147
+ const decoder = new TextDecoder();
148
+ const reader = res.body.getReader();
149
+ let accumulatedText = '';
150
+
151
+ // 循环读取流中的数据
152
+ while (true) {
153
+ const { done, value } = await reader.read();
154
+ if (done) break;
155
+
156
+ // 解码接收到的数据
157
+ const chunk = decoder.decode(value, { stream: true });
158
+
159
+ // 处理SSE格式 (data: {...}\n\n)
160
+ const lines = chunk.split('\n');
161
+ for (const line of lines) {
162
+ const trimmedLine = line.trim();
163
+ if (trimmedLine.startsWith('data:')) {
164
+ const data = trimmedLine.substring(5).trim();
165
+ if (data === '[DONE]') break;
166
+
167
+ try {
168
+ const parsedData = JSON.parse(data);
169
+ // 从流式响应中提取内容
170
+ const delta = parsedData?.choices?.[0]?.delta?.content || '';
171
+ if (delta) {
172
+ accumulatedText += delta;
173
+ // 更新消息内容以实现打字机效果
174
+ messages.value[streamMessageIndex].content = accumulatedText;
175
+ // 滚动到底部以显示新内容
176
+ scrollToBottom();
177
+ }
178
+ } catch (e) {
179
+ console.error('解析流式数据失败:', e);
180
+ }
181
+ } else if (trimmedLine) {
182
+ // 处理非SSE格式的流式响应
183
+ try {
184
+ const parsedData = JSON.parse(trimmedLine);
185
+ const delta = parsedData?.choices?.[0]?.delta?.content || '';
186
+ if (delta) {
187
+ accumulatedText += delta;
188
+ messages.value[streamMessageIndex].content = accumulatedText;
189
+ scrollToBottom();
190
+ }
191
+ } catch (e) {
192
+ console.error('解析非SSE流式数据失败:', e);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ // 如果最终没有内容,使用默认提示
199
+ if (!accumulatedText) {
200
+ messages.value[streamMessageIndex].content = '(流式响应未返回有效内容)';
201
+ }
202
+ } else {
203
+ // 非流式响应处理(兼容原有逻辑)
204
+ const data = await res.json();
205
+ const reply = data?.choices?.[0]?.message?.content || data?.reply || data?.message || '(接口未返回有效内容)';
206
+ messages.value.push({
207
+ role: 'system',
208
+ content: reply,
209
+ });
210
+ }
211
+ } catch (err) {
212
+ messages.value.push({
213
+ role: 'system',
214
+ content: `调用模型失败:${err.message || err}`,
215
+ });
216
+ } finally {
217
+ loading.value = false;
218
+ }
219
+ };
220
+
221
+ const handleSubmit = async () => {
222
+ if (!input.value.trim() || loading.value) return;
223
+ const current = input.value.trim();
224
+ messages.value.push({ role: 'user', content: current });
225
+ input.value = '';
226
+ await callRealLLM(messages.value);
227
+ };
228
+ </script>
229
+
230
+ <style lang="scss" scoped>
231
+ // 导入全局变量
232
+ @import '../assets/_globals';
233
+
234
+ /* 为Markdown内容添加基本样式 */
235
+ :deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
236
+ color: $text-secondary;
237
+ margin: $spacing-xl 0 $spacing-sm 0;
238
+ font-weight: 600;
239
+ }
240
+
241
+ :deep(p) {
242
+ margin: $spacing-sm 0;
243
+ }
244
+
245
+ :deep(code) {
246
+ background: rgba(255, 255, 255, 0.1);
247
+ padding: $spacing-xs $spacing-sm;
248
+ border-radius: 4px;
249
+ font-family: 'Consolas', 'Monaco', monospace;
250
+ }
251
+
252
+ :deep(pre) {
253
+ background: rgba(0, 0, 0, 0.3);
254
+ border: 1px solid $white-trans-08;
255
+ border-radius: $border-radius-small;
256
+ padding: $spacing-md;
257
+ overflow-x: auto;
258
+ margin: $spacing-sm 0;
259
+ }
260
+
261
+ :deep(pre code) {
262
+ background: transparent;
263
+ padding: 0;
264
+ }
265
+
266
+ :deep(blockquote) {
267
+ border-left: 3px solid rgba($primary-color, 0.5);
268
+ margin: $spacing-sm 0;
269
+ padding: 0 $spacing-xl;
270
+ color: #b0c8f8;
271
+ }
272
+
273
+ :deep(ul), :deep(ol) {
274
+ margin: $spacing-sm 0;
275
+ padding-left: 24px;
276
+ }
277
+
278
+ :deep(li) {
279
+ margin: $spacing-xs 0;
280
+ }
281
+
282
+ :deep(a) {
283
+ color: $text-link;
284
+ text-decoration: none;
285
+ }
286
+
287
+ :deep(a:hover) {
288
+ text-decoration: underline;
289
+ }
290
+
291
+ .chat-container {
292
+ display: grid;
293
+ grid-template-rows: 1fr auto;
294
+ gap: $spacing-md;
295
+ padding: $spacing-xl;
296
+
297
+ .messages {
298
+ overflow-y: auto;
299
+ padding-right: 6px;
300
+
301
+ .message {
302
+ display: grid;
303
+ gap: $spacing-md;
304
+ padding: $spacing-md 0;
305
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
306
+
307
+ &.system-message {
308
+ grid-template-columns: 60px 1fr;
309
+ }
310
+
311
+ &.user-message {
312
+ grid-template-columns: 1fr 60px;
313
+ justify-items: end;
314
+
315
+ .bubble {
316
+ text-align: right;
317
+ max-width: $message-width - 194px;
318
+ }
319
+ }
320
+
321
+ .avatar {
322
+ height: 44px;
323
+ width: 44px;
324
+ display: grid;
325
+ place-items: center;
326
+ border-radius: $border-radius-small;
327
+ font-weight: 700;
328
+
329
+ &.user {
330
+ background: linear-gradient(135deg, #3557ff, #4db8ff);
331
+ color: #fff;
332
+ }
333
+
334
+ &.system {
335
+ background: linear-gradient(135deg, $secondary-color, #2c9b7d);
336
+ color: #0c221a;
337
+ }
338
+ }
339
+
340
+ .bubble {
341
+ background: rgba(255, 255, 255, 0.06);
342
+ border: 1px solid $white-trans-08;
343
+ border-radius: $border-radius;
344
+ padding: $spacing-md $spacing-lg;
345
+ color: $text-message;
346
+ white-space: pre-wrap;
347
+ max-width: $message-width - 194px;
348
+ box-sizing: border-box;
349
+ }
350
+ }
351
+
352
+ .loading {
353
+ color: #8fb1ff;
354
+ font-size: 14px;
355
+ display: flex;
356
+ align-items: center;
357
+ gap: $spacing-sm;
358
+
359
+ &::after {
360
+ content: '';
361
+ width: 10px;
362
+ height: 10px;
363
+ border-radius: $border-radius-round;
364
+ border: 2px solid rgba(143, 177, 255, 0.35);
365
+ border-top-color: #8fb1ff;
366
+ animation: spin 1s linear infinite;
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ .toolbar {
373
+ padding: $spacing-sm;
374
+ position: fixed;
375
+ bottom: 0;
376
+ left: 50%;
377
+ transform: translateX(-50%);
378
+ width: 100%;
379
+ min-width: 600px;
380
+ height: 88px;
381
+ background: $background-color;
382
+
383
+ .input{
384
+ display: flex;
385
+ gap: $spacing-sm;
386
+ align-items: stretch;
387
+ width: $message-width;
388
+ margin: auto;
389
+ .prompt-input {
390
+ width: 100%;
391
+ flex: 1;
392
+ resize: none;
393
+ border: 1px solid $white-trans-08;
394
+ background: $white-trans-05;
395
+ color: $text-message;
396
+ padding: $spacing-md;
397
+ border-radius: $border-radius-small;
398
+ font-size: 15px;
399
+ outline: none;
400
+ min-height: 64px;
401
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
402
+
403
+ &:focus {
404
+ border-color: $primary-color;
405
+ box-shadow: 0 0 0 2px rgba($primary-color, 0.25);
406
+ }
407
+ }
408
+ }
409
+
410
+
411
+ .send-btn {
412
+ min-width: 110px;
413
+ border: none;
414
+ border-radius: $border-radius-small;
415
+ font-weight: 700;
416
+ color: #0b1220;
417
+ background: linear-gradient(135deg, $primary-color, $accent-color);
418
+ cursor: pointer;
419
+ transition: transform 0.15s ease, box-shadow 0.2s ease;
420
+
421
+ &:disabled {
422
+ opacity: 0.5;
423
+ cursor: not-allowed;
424
+ }
425
+
426
+ &:not(:disabled):hover {
427
+ transform: translateY(-1px);
428
+ box-shadow: $shadow-md;
429
+ }
430
+ }
431
+ }
432
+ </style>
package/src/install.js ADDED
@@ -0,0 +1,15 @@
1
+ import appMain from './App.vue'
2
+
3
+ // 单独导出
4
+ export { appMain }
5
+
6
+ // 全局注册
7
+ const components = [appMain]
8
+
9
+ export default {
10
+ install(app) {
11
+ components.forEach(component => {
12
+ app.component(component.name, component)
13
+ })
14
+ }
15
+ }
package/src/main.js ADDED
@@ -0,0 +1,7 @@
1
+ import { createApp } from 'vue';
2
+ import App from './App.vue';
3
+ import './style.scss';
4
+
5
+ createApp(App).mount('#app');
6
+
7
+
package/src/style.css ADDED
@@ -0,0 +1,78 @@
1
+ @charset "UTF-8";
2
+ :root {
3
+ font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
4
+ color: #f7f7f7;
5
+ background-color: #0b1220;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ }
15
+
16
+ #app {
17
+ min-height: 100vh;
18
+ }
19
+
20
+ .page {
21
+ width: 1200px;
22
+ margin: 0 auto;
23
+ padding: 32px 0 100px;
24
+ }
25
+
26
+ .page .card {
27
+ background: rgba(255, 255, 255, 0.04);
28
+ border: 1px solid rgba(255, 255, 255, 0.08);
29
+ border-radius: 14px;
30
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
31
+ }
32
+
33
+ .page .card .title {
34
+ color: #d0e0ff;
35
+ font-weight: 700;
36
+ font-size: 28px;
37
+ letter-spacing: 0.5px;
38
+ margin: 0 0 8px;
39
+ }
40
+
41
+ .page .card .subtitle {
42
+ color: #89a5d6;
43
+ margin: 0 0 20px;
44
+ line-height: 1.6;
45
+ }
46
+
47
+ @keyframes spin {
48
+ to {
49
+ transform: rotate(360deg);
50
+ }
51
+ }
52
+
53
+ /* 自定义滚动条样式 */
54
+ .messages {
55
+ /* Firefox 滚动条样式 */
56
+ scrollbar-width: thin;
57
+ scrollbar-color: rgba(255, 255, 255, 0.15) rgba(255, 255, 255, 0.03);
58
+ /* Chrome/Safari 滚动条样式 */
59
+ }
60
+
61
+ .messages::-webkit-scrollbar {
62
+ width: 6px;
63
+ }
64
+
65
+ .messages::-webkit-scrollbar-track {
66
+ background: rgba(255, 255, 255, 0.03);
67
+ border-radius: 3px;
68
+ }
69
+
70
+ .messages::-webkit-scrollbar-thumb {
71
+ background: rgba(255, 255, 255, 0.15);
72
+ border-radius: 3px;
73
+ transition: background 0.3s ease;
74
+ }
75
+
76
+ .messages::-webkit-scrollbar-thumb:hover {
77
+ background: rgba(255, 255, 255, 0.25);
78
+ }
@@ -0,0 +1 @@
1
+ :root{font-family:'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;color:#f7f7f7;background-color:#0b1220}*{box-sizing:border-box}body{margin:0}#app{min-height:100vh}.page{width:1200px;margin:0 auto;padding:32px 0 100px}.page .card{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,0.35)}.page .card .title{color:#d0e0ff;font-weight:700;font-size:28px;letter-spacing:0.5px;margin:0 0 8px}.page .card .subtitle{color:#89a5d6;margin:0 0 20px;line-height:1.6}@keyframes spin{to{transform:rotate(360deg)}}.messages{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,0.15) rgba(255,255,255,0.03)}.messages::-webkit-scrollbar{width:6px}.messages::-webkit-scrollbar-track{background:rgba(255,255,255,0.03);border-radius:3px}.messages::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.15);border-radius:3px;transition:background 0.3s ease}.messages::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.25)}
package/src/style.scss ADDED
@@ -0,0 +1,81 @@
1
+ // 导入全局变量
2
+ @import './assets/_globals';
3
+
4
+ :root {
5
+ font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
6
+ color: $text-primary;
7
+ background-color: $background-color;
8
+ }
9
+
10
+ * {
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ margin: 0;
16
+ }
17
+
18
+ #app {
19
+ min-height: 100vh;
20
+ // background: $background-gradient;
21
+ }
22
+
23
+ .page {
24
+ width: $message-width;
25
+ margin: 0 auto;
26
+ padding: $spacing-xxl 0 100px;
27
+
28
+ .card {
29
+ background: rgba(255, 255, 255, 0.04);
30
+ border: 1px solid $white-trans-08;
31
+ border-radius: $border-radius;
32
+ box-shadow: $shadow-lg;
33
+
34
+ .title {
35
+ color: $text-secondary;
36
+ font-weight: 700;
37
+ font-size: 28px;
38
+ letter-spacing: 0.5px;
39
+ margin: 0 0 $spacing-sm;
40
+ }
41
+
42
+ .subtitle {
43
+ color: $text-tertiary;
44
+ margin: 0 0 $spacing-xl;
45
+ line-height: 1.6;
46
+ }
47
+ }
48
+ }
49
+
50
+ @keyframes spin {
51
+ to {
52
+ transform: rotate(360deg);
53
+ }
54
+ }
55
+
56
+ /* 自定义滚动条样式 */
57
+ .messages {
58
+ /* Firefox 滚动条样式 */
59
+ scrollbar-width: thin;
60
+ scrollbar-color: $white-trans-15 rgba(255, 255, 255, 0.03);
61
+
62
+ /* Chrome/Safari 滚动条样式 */
63
+ &::-webkit-scrollbar {
64
+ width: 6px;
65
+ }
66
+
67
+ &::-webkit-scrollbar-track {
68
+ background: rgba(255, 255, 255, 0.03);
69
+ border-radius: 3px;
70
+ }
71
+
72
+ &::-webkit-scrollbar-thumb {
73
+ background: $white-trans-15;
74
+ border-radius: 3px;
75
+ transition: background 0.3s ease;
76
+
77
+ &:hover {
78
+ background: rgba(255, 255, 255, 0.25);
79
+ }
80
+ }
81
+ }
package/vite.config.js ADDED
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from 'vite';
2
+ import vue from '@vitejs/plugin-vue';
3
+
4
+ // 基础 Vite 配置,适配 Vue 3
5
+ export default defineConfig({
6
+ plugins: [vue()],
7
+ server: {
8
+ port: 5173
9
+ },
10
+ build: {
11
+ lib: {
12
+ entry: 'src/install.js', // 入口文件
13
+ name: 'AiVue3Lib', // 全局变量名
14
+ fileName: (format) => `ai-vue3-lib.${format}.js`
15
+ },
16
+ rollupOptions: {
17
+ external: ['vue'], // 将 Vue 作为外部依赖
18
+ output: {
19
+ globals: {
20
+ vue: 'Vue'
21
+ }
22
+ }
23
+ }
24
+ }
25
+ });
26
+