byt-lingxiao-ai 0.2.8 → 0.3.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.
- package/components/AiMessage.vue +357 -8
- package/components/ChatAvatar.vue +9 -2
- package/components/ChatMessageList.vue +29 -5
- package/components/ChatWindow.vue +110 -5
- package/components/ChatWindowDialog.vue +5 -0
- package/components/ChatWindowHeader.vue +1 -0
- package/components/assets/arrow.png +0 -0
- package/components/assets/empty.png +0 -0
- package/components/assets/entering.png +0 -0
- package/components/assets/logo.png +0 -0
- package/components/assets/normal.png +0 -0
- package/components/assets/output.png +0 -0
- package/components/assets/speaking.png +0 -0
- package/components/assets/think.png +0 -0
- package/components/assets/thinking.png +0 -0
- package/components/assets/waiting.png +0 -0
- package/components/config/index.js +4 -0
- package/components/mixins/messageMixin.js +25 -8
- package/components/mixins/webSocketMixin.js +3 -1
- package/components/utils/StreamParser.js +67 -39
- package/dist/img/empty.f36cb82e.png +0 -0
- package/dist/img/entering.4ef198fb.png +0 -0
- package/dist/img/normal.30197a82.png +0 -0
- package/dist/img/output.1dfa94eb.png +0 -0
- package/dist/img/speaking.fa87fedb.png +0 -0
- package/dist/img/thinking.21ad5ca5.png +0 -0
- package/dist/img/waiting.460478ef.png +0 -0
- package/dist/index.common.js +29750 -2967
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +11 -1
- package/dist/index.umd.js +29742 -2959
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/package.json +5 -3
- package/components/assets/byt.mp3 +0 -0
- package/dist/img/entering.42f05909.png +0 -0
- package/dist/img/normal.13f08ecb.png +0 -0
- package/dist/img/output.85c6bd8b.png +0 -0
- package/dist/img/speaking.3ce8b666.png +0 -0
- package/dist/img/thinking.05f29a84.png +0 -0
- package/dist/img/waiting.ac21d76e.png +0 -0
package/components/AiMessage.vue
CHANGED
|
@@ -1,32 +1,222 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="chat-window-message-ai">
|
|
3
3
|
<div class="ai-render">
|
|
4
|
-
<div class="ai-
|
|
5
|
-
<div class="
|
|
6
|
-
<div class="
|
|
4
|
+
<div class="ai-loading" v-if="isLoading">
|
|
5
|
+
<div class="dot"></div>
|
|
6
|
+
<div class="dot"></div>
|
|
7
|
+
<div class="dot"></div>
|
|
7
8
|
</div>
|
|
8
|
-
<div class="ai-
|
|
9
|
+
<div class="ai-thinking" @click="$emit('thinking-toggle')" v-if="message.thinking">
|
|
10
|
+
<div class="ai-thinking-time">{{ message.time ? `思考用时${message.time}秒` : '思考中...' }}</div>
|
|
11
|
+
<div class="ai-thinking-content" v-if="thinkingExpanded">{{ message.thinking }}</div>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="ai-content markdown-body" v-html="renderedContent"></div>
|
|
9
14
|
</div>
|
|
10
15
|
</div>
|
|
11
16
|
</template>
|
|
12
17
|
|
|
13
18
|
<script>
|
|
19
|
+
import { marked } from 'marked';
|
|
20
|
+
import hljs from 'highlight.js';
|
|
21
|
+
import 'highlight.js/styles/github.css';
|
|
22
|
+
|
|
23
|
+
marked.setOptions({
|
|
24
|
+
highlight: function(code, lang) {
|
|
25
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
26
|
+
return hljs.highlight(code, { language: lang }).value;
|
|
27
|
+
}
|
|
28
|
+
return hljs.highlightAuto(code).value;
|
|
29
|
+
},
|
|
30
|
+
breaks: true,
|
|
31
|
+
gfm: true
|
|
32
|
+
});
|
|
33
|
+
function parseMarkdown(text) {
|
|
34
|
+
if (!text) return '';
|
|
35
|
+
let html = text;
|
|
36
|
+
|
|
37
|
+
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
|
38
|
+
return `<pre><code class="language-${lang || 'text'}">${escapeHtml(code.trim())}</code></pre>`;
|
|
39
|
+
});
|
|
40
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
41
|
+
html = parseTable(html);
|
|
42
|
+
|
|
43
|
+
html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>');
|
|
44
|
+
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
|
|
45
|
+
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
|
|
46
|
+
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
|
|
47
|
+
|
|
48
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
49
|
+
|
|
50
|
+
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
51
|
+
|
|
52
|
+
html = html.replace(/~~(.*?)~~/g, '<del>$1</del>');
|
|
53
|
+
|
|
54
|
+
html = html.replace(/^\s*[-*+]\s+(.+)$/gm, '<li>$1</li>');
|
|
55
|
+
html = html.replace(/(<li>.*?<\/li>)/gs, '<ul>$1</ul>');
|
|
56
|
+
|
|
57
|
+
html = html.replace(/^\s*\d+\.\s+(.+)$/gm, '<li>$1</li>');
|
|
58
|
+
|
|
59
|
+
html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
|
60
|
+
|
|
61
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
62
|
+
|
|
63
|
+
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />');
|
|
64
|
+
|
|
65
|
+
html = html.replace(/^-{3,}$/gm, '<hr>');
|
|
66
|
+
|
|
67
|
+
// html = html.replace(/\n/g, '<br>');
|
|
68
|
+
|
|
69
|
+
return html;
|
|
70
|
+
}
|
|
71
|
+
// 解析表格
|
|
72
|
+
function parseTable(text) {
|
|
73
|
+
const lines = text.split('\n');
|
|
74
|
+
let result = [];
|
|
75
|
+
let inTable = false;
|
|
76
|
+
let tableRows = [];
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i].trim();
|
|
80
|
+
|
|
81
|
+
// 检测表格行
|
|
82
|
+
if (line.includes('|') && line.split('|').length >= 3) {
|
|
83
|
+
if (!inTable) {
|
|
84
|
+
inTable = true;
|
|
85
|
+
tableRows = [];
|
|
86
|
+
}
|
|
87
|
+
tableRows.push(line);
|
|
88
|
+
|
|
89
|
+
// 检查下一行是否还是表格
|
|
90
|
+
if (i === lines.length - 1 || !lines[i + 1].includes('|')) {
|
|
91
|
+
// 表格结束
|
|
92
|
+
result.push(renderTable(tableRows));
|
|
93
|
+
inTable = false;
|
|
94
|
+
tableRows = [];
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
if (inTable) {
|
|
98
|
+
// 表格意外结束
|
|
99
|
+
result.push(renderTable(tableRows));
|
|
100
|
+
inTable = false;
|
|
101
|
+
tableRows = [];
|
|
102
|
+
}
|
|
103
|
+
result.push(line);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result.join('\n');
|
|
108
|
+
}
|
|
109
|
+
// 渲染表格
|
|
110
|
+
function renderTable(rows) {
|
|
111
|
+
if (rows.length < 2) return rows.join('\n');
|
|
112
|
+
|
|
113
|
+
let html = '<div class="table-wrapper"><table class="markdown-table">';
|
|
114
|
+
|
|
115
|
+
// 表头
|
|
116
|
+
const headerCells = rows[0].split('|').filter(cell => cell.trim());
|
|
117
|
+
html += '<thead><tr>';
|
|
118
|
+
headerCells.forEach(cell => {
|
|
119
|
+
html += `<th>${cell.trim()}</th>`;
|
|
120
|
+
});
|
|
121
|
+
html += '</tr></thead>';
|
|
122
|
+
|
|
123
|
+
// 表体(跳过分隔行)
|
|
124
|
+
html += '<tbody>';
|
|
125
|
+
for (let i = 2; i < rows.length; i++) {
|
|
126
|
+
const cells = rows[i].split('|').filter(cell => cell.trim());
|
|
127
|
+
if (cells.length > 0) {
|
|
128
|
+
html += '<tr>';
|
|
129
|
+
cells.forEach(cell => {
|
|
130
|
+
html += `<td>${cell.trim()}</td>`;
|
|
131
|
+
});
|
|
132
|
+
html += '</tr>';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
html += '</tbody>';
|
|
136
|
+
|
|
137
|
+
html += '</table></div>';
|
|
138
|
+
return html;
|
|
139
|
+
}
|
|
140
|
+
// HTML 转义
|
|
141
|
+
function escapeHtml(text) {
|
|
142
|
+
const map = {
|
|
143
|
+
'&': '&',
|
|
144
|
+
'<': '<',
|
|
145
|
+
'>': '>',
|
|
146
|
+
'"': '"',
|
|
147
|
+
"'": '''
|
|
148
|
+
};
|
|
149
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
150
|
+
}
|
|
151
|
+
|
|
14
152
|
export default {
|
|
15
153
|
name: 'AiMessage',
|
|
16
154
|
props: {
|
|
17
155
|
message: {
|
|
18
156
|
type: Object,
|
|
19
157
|
required: true
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
computed: {
|
|
161
|
+
thinkingExpanded() {
|
|
162
|
+
return this.message.thinkingExpanded !== false;
|
|
163
|
+
},
|
|
164
|
+
renderedContent() {
|
|
165
|
+
return parseMarkdown(this.message.content);
|
|
20
166
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
167
|
+
isLoading() {
|
|
168
|
+
return this.message.loading === true;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
watch: {
|
|
172
|
+
thinkStatus(newVal, oldVal) {
|
|
173
|
+
console.log('thinkStatus 变化:', newVal, oldVal);
|
|
24
174
|
}
|
|
25
175
|
}
|
|
26
176
|
}
|
|
27
177
|
</script>
|
|
28
178
|
|
|
29
179
|
<style scoped>
|
|
180
|
+
/* Loading 容器 */
|
|
181
|
+
.ai-loading {
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
padding: 12px 0;
|
|
185
|
+
height: 24px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* 单个圆点样式 */
|
|
189
|
+
.dot {
|
|
190
|
+
width: 8px;
|
|
191
|
+
height: 8px;
|
|
192
|
+
margin-right: 6px;
|
|
193
|
+
background-color: #86909C; /* 与你的思考字体颜色一致 */
|
|
194
|
+
border-radius: 50%;
|
|
195
|
+
animation: dot-bounce 1.4s infinite ease-in-out both;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.dot:nth-child(1) {
|
|
199
|
+
animation-delay: -0.32s;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.dot:nth-child(2) {
|
|
203
|
+
animation-delay: -0.16s;
|
|
204
|
+
}
|
|
205
|
+
.dot:nth-child(3) {
|
|
206
|
+
animation-delay: 0s;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* 定义跳动动画 */
|
|
210
|
+
@keyframes dot-bounce {
|
|
211
|
+
0%, 80%, 100% {
|
|
212
|
+
transform: scale(0);
|
|
213
|
+
opacity: 0.5;
|
|
214
|
+
}
|
|
215
|
+
40% {
|
|
216
|
+
transform: scale(1);
|
|
217
|
+
opacity: 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
30
220
|
.chat-window-message-ai {
|
|
31
221
|
display: flex;
|
|
32
222
|
gap: 12px;
|
|
@@ -99,11 +289,170 @@ export default {
|
|
|
99
289
|
|
|
100
290
|
.ai-content {
|
|
101
291
|
color: #4E5969;
|
|
102
|
-
text-align: justify;
|
|
103
292
|
font-family: "Alibaba PuHuiTi 2.0";
|
|
104
293
|
font-size: 16px;
|
|
105
294
|
font-style: normal;
|
|
106
295
|
font-weight: 400;
|
|
107
296
|
line-height: 24px;
|
|
108
297
|
}
|
|
298
|
+
|
|
299
|
+
/* Markdown 样式 */
|
|
300
|
+
.markdown-body {
|
|
301
|
+
word-wrap: break-word;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.markdown-body ::v-deep h1,
|
|
305
|
+
.markdown-body ::v-deep h2,
|
|
306
|
+
.markdown-body ::v-deep h3,
|
|
307
|
+
.markdown-body ::v-deep h4 {
|
|
308
|
+
margin: 16px 0 8px 0;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
line-height: 1.4;
|
|
311
|
+
color: #1f2937;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.markdown-body ::v-deep h1 {
|
|
315
|
+
font-size: 24px;
|
|
316
|
+
border-bottom: 2px solid #e5e7eb;
|
|
317
|
+
padding-bottom: 8px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.markdown-body ::v-deep h2 {
|
|
321
|
+
font-size: 20px;
|
|
322
|
+
border-bottom: 1px solid #e5e7eb;
|
|
323
|
+
padding-bottom: 6px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.markdown-body ::v-deep h3 {
|
|
327
|
+
font-size: 18px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.markdown-body ::v-deep h4 {
|
|
331
|
+
font-size: 16px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.markdown-body ::v-deep code {
|
|
335
|
+
background-color: rgba(175, 184, 193, 0.2);
|
|
336
|
+
border-radius: 3px;
|
|
337
|
+
font-size: 85%;
|
|
338
|
+
margin: 0;
|
|
339
|
+
padding: 0.2em 0.4em;
|
|
340
|
+
font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
|
|
341
|
+
color: #e83e8c;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.markdown-body ::v-deep pre {
|
|
345
|
+
background-color: #f6f8fa;
|
|
346
|
+
border-radius: 6px;
|
|
347
|
+
padding: 16px;
|
|
348
|
+
overflow: auto;
|
|
349
|
+
margin: 12px 0;
|
|
350
|
+
border: 1px solid #e1e4e8;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.markdown-body ::v-deep pre code {
|
|
354
|
+
background-color: transparent;
|
|
355
|
+
border: 0;
|
|
356
|
+
display: inline;
|
|
357
|
+
line-height: inherit;
|
|
358
|
+
margin: 0;
|
|
359
|
+
overflow: visible;
|
|
360
|
+
padding: 0;
|
|
361
|
+
word-wrap: normal;
|
|
362
|
+
color: #24292e;
|
|
363
|
+
font-size: 14px;
|
|
364
|
+
}
|
|
365
|
+
.markdown-body ::v-deep .table-wrapper{
|
|
366
|
+
overflow-x: auto;
|
|
367
|
+
border: 1px solid #dfe2e5;
|
|
368
|
+
}
|
|
369
|
+
.markdown-body ::v-deep .markdown-table {
|
|
370
|
+
border-collapse: collapse;
|
|
371
|
+
width: 100%;
|
|
372
|
+
margin: 12px 0;
|
|
373
|
+
font-size: 14px;
|
|
374
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
375
|
+
overflow: hidden;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.markdown-body ::v-deep .markdown-table th,
|
|
379
|
+
.markdown-body ::v-deep .markdown-table td {
|
|
380
|
+
border: 1px solid #dfe2e5;
|
|
381
|
+
padding: 10px 14px;
|
|
382
|
+
text-align: left;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.markdown-body ::v-deep .markdown-table th {
|
|
386
|
+
background-color: #f3f4f6;
|
|
387
|
+
font-weight: 600;
|
|
388
|
+
color: #374151;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.markdown-body ::v-deep .markdown-table tr:nth-child(even) {
|
|
392
|
+
background-color: #f9fafb;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.markdown-body ::v-deep .markdown-table tr:hover {
|
|
396
|
+
background-color: #f3f4f6;
|
|
397
|
+
transition: background-color 0.2s;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.markdown-body ::v-deep ul,
|
|
401
|
+
.markdown-body ::v-deep ol {
|
|
402
|
+
padding-left: 24px;
|
|
403
|
+
margin: 8px 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.markdown-body ::v-deep li {
|
|
407
|
+
margin: 4px 0;
|
|
408
|
+
line-height: 1.6;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.markdown-body ::v-deep strong {
|
|
412
|
+
font-weight: 600;
|
|
413
|
+
color: #1f2937;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.markdown-body ::v-deep em {
|
|
417
|
+
font-style: italic;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.markdown-body ::v-deep del {
|
|
421
|
+
text-decoration: line-through;
|
|
422
|
+
opacity: 0.7;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.markdown-body ::v-deep blockquote {
|
|
426
|
+
border-left: 4px solid #dfe2e5;
|
|
427
|
+
padding-left: 16px;
|
|
428
|
+
margin: 12px 0;
|
|
429
|
+
color: #6b7280;
|
|
430
|
+
font-style: italic;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.markdown-body ::v-deep a {
|
|
434
|
+
color: #3b82f6;
|
|
435
|
+
text-decoration: none;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.markdown-body ::v-deep a:hover {
|
|
439
|
+
text-decoration: underline;
|
|
440
|
+
color: #2563eb;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.markdown-body ::v-deep img {
|
|
444
|
+
max-width: 100%;
|
|
445
|
+
border-radius: 6px;
|
|
446
|
+
margin: 12px 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.markdown-body ::v-deep hr {
|
|
450
|
+
border: none;
|
|
451
|
+
border-top: 1px solid #e5e7eb;
|
|
452
|
+
margin: 16px 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.markdown-body ::v-deep br {
|
|
456
|
+
line-height: 0.8em;
|
|
457
|
+
}
|
|
109
458
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="chat-ai" @
|
|
2
|
+
<div class="chat-ai" @mousedown="$emit('mousedown')">
|
|
3
3
|
<div :class="['chat-ai-avater', status]"></div>
|
|
4
4
|
<div class="chat-ai-text">{{ statusText }}</div>
|
|
5
5
|
</div>
|
|
@@ -20,7 +20,7 @@ export default {
|
|
|
20
20
|
const textMap = {
|
|
21
21
|
'normal': '凌霄AI',
|
|
22
22
|
'thinking': '思考中',
|
|
23
|
-
'output': '
|
|
23
|
+
'output': '输出中'
|
|
24
24
|
}
|
|
25
25
|
return textMap[this.status]
|
|
26
26
|
}
|
|
@@ -41,6 +41,13 @@ export default {
|
|
|
41
41
|
box-shadow: 0 2px 11.6px 0 rgba(0, 0, 0, 0.1);
|
|
42
42
|
cursor: pointer;
|
|
43
43
|
user-select: none;
|
|
44
|
+
transition: box-shadow 0.2s;
|
|
45
|
+
}
|
|
46
|
+
.chat-ai:hover {
|
|
47
|
+
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.2);
|
|
48
|
+
}
|
|
49
|
+
.chat-ai:active {
|
|
50
|
+
cursor: grabbing;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
.chat-ai-avater {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div ref="chatArea" class="chat-window-content scrollbar-hide">
|
|
3
|
+
<div v-if="messages.length === 0" class="chat-window-empty"></div>
|
|
3
4
|
<div
|
|
4
5
|
class="chat-window-message"
|
|
5
6
|
v-for="message in messages"
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
v-else
|
|
14
15
|
:message="message"
|
|
15
16
|
:think-status="thinkStatus"
|
|
16
|
-
@thinking-
|
|
17
|
+
@thinking-toggle="handleThinkingToggle(message)"
|
|
17
18
|
/>
|
|
18
19
|
</div>
|
|
19
20
|
</div>
|
|
@@ -39,7 +40,20 @@ export default {
|
|
|
39
40
|
default: true
|
|
40
41
|
}
|
|
41
42
|
},
|
|
43
|
+
computed: {
|
|
44
|
+
lastMessageObject() {
|
|
45
|
+
const len = this.messages.length;
|
|
46
|
+
if (len === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return this.messages[len - 1];
|
|
50
|
+
}
|
|
51
|
+
},
|
|
42
52
|
methods: {
|
|
53
|
+
handleThinkingToggle(message) {
|
|
54
|
+
console.log('handleThinkingToggle', message)
|
|
55
|
+
this.$set(message, 'thinkingExpanded', !message.thinkingExpanded);
|
|
56
|
+
},
|
|
43
57
|
scrollToBottom() {
|
|
44
58
|
this.$nextTick(() => {
|
|
45
59
|
const chatArea = this.$refs.chatArea
|
|
@@ -50,17 +64,27 @@ export default {
|
|
|
50
64
|
}
|
|
51
65
|
},
|
|
52
66
|
watch: {
|
|
53
|
-
|
|
54
|
-
handler() {
|
|
55
|
-
|
|
67
|
+
lastMessageObject: {
|
|
68
|
+
handler(newMsg) {
|
|
69
|
+
if (newMsg) {
|
|
70
|
+
this.scrollToBottom();
|
|
71
|
+
}
|
|
56
72
|
},
|
|
57
|
-
deep: true
|
|
73
|
+
deep: true,
|
|
74
|
+
immediate: true
|
|
58
75
|
}
|
|
59
76
|
}
|
|
60
77
|
}
|
|
61
78
|
</script>
|
|
62
79
|
|
|
63
80
|
<style scoped>
|
|
81
|
+
.chat-window-empty{
|
|
82
|
+
width: 300px;
|
|
83
|
+
height: 300px;
|
|
84
|
+
background: url('./assets/empty.png') no-repeat;
|
|
85
|
+
background-size: cover;
|
|
86
|
+
margin: auto;
|
|
87
|
+
}
|
|
64
88
|
.chat-window-content {
|
|
65
89
|
flex: 1;
|
|
66
90
|
padding: 16px;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="chat"
|
|
4
|
+
:style="chatStyle"
|
|
5
|
+
>
|
|
3
6
|
<!-- 隐藏的音频播放器 -->
|
|
4
7
|
<audio
|
|
5
8
|
ref="audioPlayer"
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
<ChatAvatar
|
|
22
25
|
v-else
|
|
23
26
|
:status="avaterStatus"
|
|
24
|
-
@
|
|
27
|
+
@mousedown="startDrag"
|
|
25
28
|
/>
|
|
26
29
|
|
|
27
30
|
<!-- 聊天窗口 -->
|
|
@@ -45,6 +48,7 @@ import ChatWindowDialog from './ChatWindowDialog.vue'
|
|
|
45
48
|
import audioMixin from './mixins/audioMixin'
|
|
46
49
|
import webSocketMixin from './mixins/webSocketMixin'
|
|
47
50
|
import messageMixin from './mixins/messageMixin'
|
|
51
|
+
import { AUDIO_URL, TIME_JUMP_POINTS_URL } from './config/index.js'
|
|
48
52
|
|
|
49
53
|
const SAMPLE_RATE = 16000;
|
|
50
54
|
const FRAME_SIZE = 512;
|
|
@@ -65,7 +69,7 @@ export default {
|
|
|
65
69
|
},
|
|
66
70
|
data() {
|
|
67
71
|
return {
|
|
68
|
-
audioSrc:
|
|
72
|
+
audioSrc: AUDIO_URL,
|
|
69
73
|
inputMessage: '',
|
|
70
74
|
visible: false,
|
|
71
75
|
messages: [],
|
|
@@ -75,7 +79,31 @@ export default {
|
|
|
75
79
|
thinkStatus: true,
|
|
76
80
|
jumpedTimePoints: new Set(),
|
|
77
81
|
SAMPLE_RATE,
|
|
78
|
-
FRAME_SIZE
|
|
82
|
+
FRAME_SIZE,
|
|
83
|
+
dragThreshold: 5, // 拖拽阈值
|
|
84
|
+
isDragging: false,
|
|
85
|
+
dragStartX: 0,
|
|
86
|
+
dragStartY: 0,
|
|
87
|
+
currentX: 10,
|
|
88
|
+
currentY: 20,
|
|
89
|
+
initialX: 10,
|
|
90
|
+
initialY: 20,
|
|
91
|
+
hasMoved: false,
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
computed: {
|
|
95
|
+
chatStyle() {
|
|
96
|
+
if (this.robotStatus === 'leaving' && !this.visible) {
|
|
97
|
+
return {
|
|
98
|
+
right: `${this.currentX}px`,
|
|
99
|
+
bottom: `${this.currentY}px`,
|
|
100
|
+
cursor: this.isDragging ? 'grabbing' : 'grab',
|
|
101
|
+
transition: this.isDragging ? 'none' : 'right 0.3s ease, bottom 0.3s ease',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
cursor: 'pointer',
|
|
106
|
+
};
|
|
79
107
|
}
|
|
80
108
|
},
|
|
81
109
|
mounted() {
|
|
@@ -83,6 +111,13 @@ export default {
|
|
|
83
111
|
if (this.appendToBody) {
|
|
84
112
|
this.appendToBodyHandler()
|
|
85
113
|
}
|
|
114
|
+
|
|
115
|
+
this.$nextTick(() => {
|
|
116
|
+
const chatEl = this.$el;
|
|
117
|
+
const style = window.getComputedStyle(chatEl);
|
|
118
|
+
this.initialX = this.currentX = parseInt(style.right, 10) || 10;
|
|
119
|
+
this.initialY = this.currentY = parseInt(style.bottom, 10) || 20;
|
|
120
|
+
})
|
|
86
121
|
},
|
|
87
122
|
beforeDestroy() {
|
|
88
123
|
if (this.appendToBody && this.$el.parentElement === document.body) {
|
|
@@ -90,10 +125,80 @@ export default {
|
|
|
90
125
|
}
|
|
91
126
|
this.closeWebSocket()
|
|
92
127
|
this.stopRecording()
|
|
128
|
+
|
|
129
|
+
// 移除全局事件监听器
|
|
130
|
+
document.removeEventListener('mousemove', this.onDrag)
|
|
131
|
+
document.removeEventListener('mouseup', this.stopDrag)
|
|
93
132
|
},
|
|
94
133
|
methods: {
|
|
95
134
|
toggleWindow() {
|
|
96
135
|
this.visible = !this.visible
|
|
136
|
+
|
|
137
|
+
if (this.visible) {
|
|
138
|
+
this.currentX = this.initialX
|
|
139
|
+
this.currentY = this.initialY
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
startDrag() {
|
|
143
|
+
console.log('startDrag')
|
|
144
|
+
if (this.robotStatus !== 'leaving' && this.visible) return;
|
|
145
|
+
|
|
146
|
+
this.isDragging = true;
|
|
147
|
+
this.hasMoved = false;
|
|
148
|
+
|
|
149
|
+
// 记录鼠标的初始位置
|
|
150
|
+
this.dragStartX = event.clientX;
|
|
151
|
+
this.dragStartY = event.clientY;
|
|
152
|
+
|
|
153
|
+
// 绑定全局事件监听器
|
|
154
|
+
document.addEventListener('mousemove', this.onDrag);
|
|
155
|
+
document.addEventListener('mouseup', this.stopDrag);
|
|
156
|
+
},
|
|
157
|
+
onDrag(event) {
|
|
158
|
+
if (!this.isDragging) return;
|
|
159
|
+
|
|
160
|
+
// 鼠标位移量
|
|
161
|
+
const deltaX = event.clientX - this.dragStartX;
|
|
162
|
+
const deltaY = event.clientY - this.dragStartY;
|
|
163
|
+
|
|
164
|
+
if (Math.abs(deltaX) > this.dragThreshold || Math.abs(deltaY) > this.dragThreshold) {
|
|
165
|
+
console.log('移动超过阈值')
|
|
166
|
+
this.hasMoved = true; // 只要移动超过阈值,就标记为拖拽
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 获取 .chat 容器的尺寸
|
|
170
|
+
const chatEl = this.$el;
|
|
171
|
+
const chatWidth = chatEl.offsetWidth;
|
|
172
|
+
const chatHeight = chatEl.offsetHeight;
|
|
173
|
+
|
|
174
|
+
let newX = this.currentX - deltaX;
|
|
175
|
+
let newY = this.currentY - deltaY;
|
|
176
|
+
|
|
177
|
+
// 视口宽度和高度
|
|
178
|
+
const viewportWidth = window.innerWidth;
|
|
179
|
+
const viewportHeight = window.innerHeight;
|
|
180
|
+
|
|
181
|
+
newX = Math.max(0, newX);
|
|
182
|
+
newX = Math.min(viewportWidth - chatWidth, newX);
|
|
183
|
+
|
|
184
|
+
newY = Math.max(0, newY);
|
|
185
|
+
newY = Math.min(viewportHeight - chatHeight, newY);
|
|
186
|
+
|
|
187
|
+
// 更新位置
|
|
188
|
+
this.currentX = newX;
|
|
189
|
+
this.currentY = newY;
|
|
190
|
+
|
|
191
|
+
// 重新设置新的拖拽起始点,实现平滑拖拽
|
|
192
|
+
this.dragStartX = event.clientX;
|
|
193
|
+
this.dragStartY = event.clientY;
|
|
194
|
+
},
|
|
195
|
+
stopDrag() {
|
|
196
|
+
this.isDragging = false;
|
|
197
|
+
document.removeEventListener('mousemove', this.onDrag);
|
|
198
|
+
document.removeEventListener('mouseup', this.stopDrag);
|
|
199
|
+
if (!this.hasMoved) {
|
|
200
|
+
this.toggleWindow()
|
|
201
|
+
}
|
|
97
202
|
},
|
|
98
203
|
handleThinkingClick() {
|
|
99
204
|
this.thinkStatus = !this.thinkStatus
|
|
@@ -117,7 +222,7 @@ export default {
|
|
|
117
222
|
this.jumpedTimePoints = new Set()
|
|
118
223
|
}
|
|
119
224
|
|
|
120
|
-
fetch(
|
|
225
|
+
fetch(TIME_JUMP_POINTS_URL)
|
|
121
226
|
.then(response => response.json())
|
|
122
227
|
.then(data => {
|
|
123
228
|
console.log('时间跳转点:', data)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
ref="messageList"
|
|
10
10
|
:messages="messages"
|
|
11
11
|
:think-status="thinkStatus"
|
|
12
|
+
:loading="loading"
|
|
12
13
|
@thinking-click="$emit('thinking-click')"
|
|
13
14
|
/>
|
|
14
15
|
|
|
@@ -50,6 +51,10 @@ export default {
|
|
|
50
51
|
thinkStatus: {
|
|
51
52
|
type: Boolean,
|
|
52
53
|
default: true
|
|
54
|
+
},
|
|
55
|
+
loading: {
|
|
56
|
+
type: Boolean,
|
|
57
|
+
default: false
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|