byt-lingxiao-ai 0.2.8 → 0.3.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.
@@ -1,16 +1,158 @@
1
1
  <template>
2
2
  <div class="chat-window-message-ai">
3
3
  <div class="ai-render">
4
- <div class="ai-thinking" @click="$emit('thinking-click')">
4
+ <div class="ai-loading" v-if="loading">
5
+ <div class="dot"></div>
6
+ <div class="dot"></div>
7
+ <div class="dot"></div>
8
+ </div>
9
+ <div class="ai-thinking" @click="$emit('thinking-click')" v-if="message.thinking">
5
10
  <div class="ai-thinking-time">思考用时{{ message.time }}秒</div>
6
11
  <div class="ai-thinking-content" v-if="thinkStatus">{{ message.thinking }}</div>
7
12
  </div>
8
- <div class="ai-content">{{ message.content }}</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
+
34
+ function parseMarkdown(text) {
35
+ if (!text) return '';
36
+ let html = text;
37
+
38
+ html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
39
+ return `<pre><code class="language-${lang || 'text'}">${escapeHtml(code.trim())}</code></pre>`;
40
+ });
41
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
42
+ html = parseTable(html);
43
+
44
+ html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>');
45
+ html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
46
+ html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
47
+ html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
48
+
49
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
50
+
51
+ html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
52
+
53
+ html = html.replace(/~~(.*?)~~/g, '<del>$1</del>');
54
+
55
+ html = html.replace(/^\s*[-*+]\s+(.+)$/gm, '<li>$1</li>');
56
+ html = html.replace(/(<li>.*?<\/li>)/gs, '<ul>$1</ul>');
57
+
58
+ html = html.replace(/^\s*\d+\.\s+(.+)$/gm, '<li>$1</li>');
59
+
60
+ html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
61
+
62
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
63
+
64
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />');
65
+
66
+ html = html.replace(/^-{3,}$/gm, '<hr>');
67
+
68
+ html = html.replace(/\n/g, '<br>');
69
+
70
+ return html;
71
+ }
72
+
73
+ // 解析表格
74
+ function parseTable(text) {
75
+ const lines = text.split('\n');
76
+ let result = [];
77
+ let inTable = false;
78
+ let tableRows = [];
79
+
80
+ for (let i = 0; i < lines.length; i++) {
81
+ const line = lines[i].trim();
82
+
83
+ // 检测表格行
84
+ if (line.includes('|') && line.split('|').length >= 3) {
85
+ if (!inTable) {
86
+ inTable = true;
87
+ tableRows = [];
88
+ }
89
+ tableRows.push(line);
90
+
91
+ // 检查下一行是否还是表格
92
+ if (i === lines.length - 1 || !lines[i + 1].includes('|')) {
93
+ // 表格结束
94
+ result.push(renderTable(tableRows));
95
+ inTable = false;
96
+ tableRows = [];
97
+ }
98
+ } else {
99
+ if (inTable) {
100
+ // 表格意外结束
101
+ result.push(renderTable(tableRows));
102
+ inTable = false;
103
+ tableRows = [];
104
+ }
105
+ result.push(line);
106
+ }
107
+ }
108
+
109
+ return result.join('\n');
110
+ }
111
+
112
+ // 渲染表格
113
+ function renderTable(rows) {
114
+ if (rows.length < 2) return rows.join('\n');
115
+
116
+ let html = '<div class="table-wrapper"><table class="markdown-table">';
117
+
118
+ // 表头
119
+ const headerCells = rows[0].split('|').filter(cell => cell.trim());
120
+ html += '<thead><tr>';
121
+ headerCells.forEach(cell => {
122
+ html += `<th>${cell.trim()}</th>`;
123
+ });
124
+ html += '</tr></thead>';
125
+
126
+ // 表体(跳过分隔行)
127
+ html += '<tbody>';
128
+ for (let i = 2; i < rows.length; i++) {
129
+ const cells = rows[i].split('|').filter(cell => cell.trim());
130
+ if (cells.length > 0) {
131
+ html += '<tr>';
132
+ cells.forEach(cell => {
133
+ html += `<td>${cell.trim()}</td>`;
134
+ });
135
+ html += '</tr>';
136
+ }
137
+ }
138
+ html += '</tbody>';
139
+
140
+ html += '</table></div>';
141
+ return html;
142
+ }
143
+
144
+ // HTML 转义
145
+ function escapeHtml(text) {
146
+ const map = {
147
+ '&': '&amp;',
148
+ '<': '&lt;',
149
+ '>': '&gt;',
150
+ '"': '&quot;',
151
+ "'": '&#039;'
152
+ };
153
+ return text.replace(/[&<>"']/g, m => map[m]);
154
+ }
155
+
14
156
  export default {
15
157
  name: 'AiMessage',
16
158
  props: {
@@ -21,12 +163,66 @@ export default {
21
163
  thinkStatus: {
22
164
  type: Boolean,
23
165
  default: true
166
+ },
167
+ loading: {
168
+ type: Boolean,
169
+ default: false
170
+ }
171
+ },
172
+ computed: {
173
+ renderedContent() {
174
+ return parseMarkdown(this.message.content);
175
+ }
176
+ },
177
+ watch: {
178
+ thinkStatus(newVal, oldVal) {
179
+ console.log('thinkStatus 变化:', newVal, oldVal);
24
180
  }
25
181
  }
26
182
  }
27
183
  </script>
28
184
 
29
185
  <style scoped>
186
+ /* Loading 容器 */
187
+ .ai-loading {
188
+ display: flex;
189
+ align-items: center;
190
+ padding: 12px 0;
191
+ height: 24px;
192
+ }
193
+
194
+ /* 单个圆点样式 */
195
+ .dot {
196
+ width: 8px;
197
+ height: 8px;
198
+ margin-right: 6px;
199
+ background-color: #86909C; /* 与你的思考字体颜色一致 */
200
+ border-radius: 50%;
201
+ animation: dot-bounce 1.4s infinite ease-in-out both;
202
+ }
203
+
204
+ .dot:nth-child(1) {
205
+ animation-delay: -0.32s;
206
+ }
207
+
208
+ .dot:nth-child(2) {
209
+ animation-delay: -0.16s;
210
+ }
211
+ .dot:nth-child(3) {
212
+ animation-delay: 0s;
213
+ }
214
+
215
+ /* 定义跳动动画 */
216
+ @keyframes dot-bounce {
217
+ 0%, 80%, 100% {
218
+ transform: scale(0);
219
+ opacity: 0.5;
220
+ }
221
+ 40% {
222
+ transform: scale(1);
223
+ opacity: 1;
224
+ }
225
+ }
30
226
  .chat-window-message-ai {
31
227
  display: flex;
32
228
  gap: 12px;
@@ -99,11 +295,170 @@ export default {
99
295
 
100
296
  .ai-content {
101
297
  color: #4E5969;
102
- text-align: justify;
103
298
  font-family: "Alibaba PuHuiTi 2.0";
104
299
  font-size: 16px;
105
300
  font-style: normal;
106
301
  font-weight: 400;
107
302
  line-height: 24px;
108
303
  }
304
+
305
+ /* Markdown 样式 */
306
+ .markdown-body {
307
+ word-wrap: break-word;
308
+ }
309
+
310
+ .markdown-body ::v-deep h1,
311
+ .markdown-body ::v-deep h2,
312
+ .markdown-body ::v-deep h3,
313
+ .markdown-body ::v-deep h4 {
314
+ margin: 16px 0 8px 0;
315
+ font-weight: 600;
316
+ line-height: 1.4;
317
+ color: #1f2937;
318
+ }
319
+
320
+ .markdown-body ::v-deep h1 {
321
+ font-size: 24px;
322
+ border-bottom: 2px solid #e5e7eb;
323
+ padding-bottom: 8px;
324
+ }
325
+
326
+ .markdown-body ::v-deep h2 {
327
+ font-size: 20px;
328
+ border-bottom: 1px solid #e5e7eb;
329
+ padding-bottom: 6px;
330
+ }
331
+
332
+ .markdown-body ::v-deep h3 {
333
+ font-size: 18px;
334
+ }
335
+
336
+ .markdown-body ::v-deep h4 {
337
+ font-size: 16px;
338
+ }
339
+
340
+ .markdown-body ::v-deep code {
341
+ background-color: rgba(175, 184, 193, 0.2);
342
+ border-radius: 3px;
343
+ font-size: 85%;
344
+ margin: 0;
345
+ padding: 0.2em 0.4em;
346
+ font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
347
+ color: #e83e8c;
348
+ }
349
+
350
+ .markdown-body ::v-deep pre {
351
+ background-color: #f6f8fa;
352
+ border-radius: 6px;
353
+ padding: 16px;
354
+ overflow: auto;
355
+ margin: 12px 0;
356
+ border: 1px solid #e1e4e8;
357
+ }
358
+
359
+ .markdown-body ::v-deep pre code {
360
+ background-color: transparent;
361
+ border: 0;
362
+ display: inline;
363
+ line-height: inherit;
364
+ margin: 0;
365
+ overflow: visible;
366
+ padding: 0;
367
+ word-wrap: normal;
368
+ color: #24292e;
369
+ font-size: 14px;
370
+ }
371
+ .markdown-body ::v-deep .table-wrapper{
372
+ overflow-x: auto;
373
+ border: 1px solid #dfe2e5;
374
+ }
375
+ .markdown-body ::v-deep .markdown-table {
376
+ border-collapse: collapse;
377
+ width: 100%;
378
+ margin: 12px 0;
379
+ font-size: 14px;
380
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
381
+ overflow: hidden;
382
+ }
383
+
384
+ .markdown-body ::v-deep .markdown-table th,
385
+ .markdown-body ::v-deep .markdown-table td {
386
+ border: 1px solid #dfe2e5;
387
+ padding: 10px 14px;
388
+ text-align: left;
389
+ }
390
+
391
+ .markdown-body ::v-deep .markdown-table th {
392
+ background-color: #f3f4f6;
393
+ font-weight: 600;
394
+ color: #374151;
395
+ }
396
+
397
+ .markdown-body ::v-deep .markdown-table tr:nth-child(even) {
398
+ background-color: #f9fafb;
399
+ }
400
+
401
+ .markdown-body ::v-deep .markdown-table tr:hover {
402
+ background-color: #f3f4f6;
403
+ transition: background-color 0.2s;
404
+ }
405
+
406
+ .markdown-body ::v-deep ul,
407
+ .markdown-body ::v-deep ol {
408
+ padding-left: 24px;
409
+ margin: 8px 0;
410
+ }
411
+
412
+ .markdown-body ::v-deep li {
413
+ margin: 4px 0;
414
+ line-height: 1.6;
415
+ }
416
+
417
+ .markdown-body ::v-deep strong {
418
+ font-weight: 600;
419
+ color: #1f2937;
420
+ }
421
+
422
+ .markdown-body ::v-deep em {
423
+ font-style: italic;
424
+ }
425
+
426
+ .markdown-body ::v-deep del {
427
+ text-decoration: line-through;
428
+ opacity: 0.7;
429
+ }
430
+
431
+ .markdown-body ::v-deep blockquote {
432
+ border-left: 4px solid #dfe2e5;
433
+ padding-left: 16px;
434
+ margin: 12px 0;
435
+ color: #6b7280;
436
+ font-style: italic;
437
+ }
438
+
439
+ .markdown-body ::v-deep a {
440
+ color: #3b82f6;
441
+ text-decoration: none;
442
+ }
443
+
444
+ .markdown-body ::v-deep a:hover {
445
+ text-decoration: underline;
446
+ color: #2563eb;
447
+ }
448
+
449
+ .markdown-body ::v-deep img {
450
+ max-width: 100%;
451
+ border-radius: 6px;
452
+ margin: 12px 0;
453
+ }
454
+
455
+ .markdown-body ::v-deep hr {
456
+ border: none;
457
+ border-top: 1px solid #e5e7eb;
458
+ margin: 16px 0;
459
+ }
460
+
461
+ .markdown-body ::v-deep br {
462
+ line-height: 1.6;
463
+ }
109
464
  </style>
@@ -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
  }
@@ -13,6 +13,7 @@
13
13
  v-else
14
14
  :message="message"
15
15
  :think-status="thinkStatus"
16
+ :loading="isLoading"
16
17
  @thinking-click="$emit('thinking-click')"
17
18
  />
18
19
  </div>
@@ -37,6 +38,10 @@ export default {
37
38
  thinkStatus: {
38
39
  type: Boolean,
39
40
  default: true
41
+ },
42
+ isLoading: {
43
+ type: Boolean,
44
+ default: false
40
45
  }
41
46
  },
42
47
  methods: {
@@ -30,6 +30,7 @@
30
30
  :messages="messages"
31
31
  :input-message="inputMessage"
32
32
  :think-status="thinkStatus"
33
+ :loading="isLoading"
33
34
  @update:inputMessage="inputMessage = $event"
34
35
  @send="handleSend"
35
36
  @thinking-click="handleThinkingClick"
@@ -45,6 +46,7 @@ import ChatWindowDialog from './ChatWindowDialog.vue'
45
46
  import audioMixin from './mixins/audioMixin'
46
47
  import webSocketMixin from './mixins/webSocketMixin'
47
48
  import messageMixin from './mixins/messageMixin'
49
+ import { AUDIO_URL, TIME_JUMP_POINTS_URL } from './config/index.js'
48
50
 
49
51
  const SAMPLE_RATE = 16000;
50
52
  const FRAME_SIZE = 512;
@@ -65,7 +67,7 @@ export default {
65
67
  },
66
68
  data() {
67
69
  return {
68
- audioSrc: '/minio/lingxiaoai/byt.mp3',
70
+ audioSrc: AUDIO_URL,
69
71
  inputMessage: '',
70
72
  visible: false,
71
73
  messages: [],
@@ -117,7 +119,7 @@ export default {
117
119
  this.jumpedTimePoints = new Set()
118
120
  }
119
121
 
120
- fetch('/minio/lingxiaoai/timeJumpPoints.json')
122
+ fetch(TIME_JUMP_POINTS_URL)
121
123
  .then(response => response.json())
122
124
  .then(data => {
123
125
  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
  }
@@ -0,0 +1,4 @@
1
+ export const API_URL = 'http://192.168.8.87:3100/lingxiao-byt/api/v1/mcp/ask';
2
+ export const WS_URL = 'ws://192.168.8.9:9999/ai_model/ws/voice-stream';
3
+ export const AUDIO_URL = '/minio/lingxiaoai/byt.mp3';
4
+ export const TIME_JUMP_POINTS_URL = '/minio/lingxiaoai/timeJumpPoints.json';
@@ -1,9 +1,11 @@
1
1
  import { StreamParser } from '../utils/StreamParser'
2
+ import { API_URL } from '../config/index.js'
2
3
 
3
4
  export default {
4
5
  data() {
5
6
  return {
6
- streamParser: null
7
+ streamParser: null,
8
+ isLoading: false
7
9
  }
8
10
  },
9
11
 
@@ -13,6 +15,7 @@ export default {
13
15
  updateInterval: 16, // 约60fps
14
16
  debug: process.env.NODE_ENV === 'development'
15
17
  });
18
+ this.isLoading = true;
16
19
  },
17
20
 
18
21
  methods: {
@@ -59,12 +62,9 @@ export default {
59
62
  try {
60
63
  const startTime = Date.now();
61
64
  const controller = new AbortController();
62
- const token = `Bearer ac627d0a-8346-4ae9-b93a-f37ff6210adc`;
63
-
64
- const baseUrl = window.location.protocol + '//' + window.location.hostname + ':3100';
65
- const apiUrl = baseUrl + '/lingxiao-byt/api/chat/completions';
65
+ const token = `Bearer e298f087-85bc-48c2-afb9-7c69ffc911aa`;
66
66
 
67
- const response = await fetch(apiUrl, {
67
+ const response = await fetch(API_URL, {
68
68
  method: 'POST',
69
69
  signal: controller.signal,
70
70
  headers: {
@@ -82,7 +82,10 @@ export default {
82
82
  await this.consumeStream(response.body);
83
83
 
84
84
  // 完成解析
85
- this.streamParser.finish(this.handleStreamUpdate);
85
+ const self = this;
86
+ this.streamParser.finish(function(result) {
87
+ self.handleStreamUpdate(result);
88
+ });
86
89
 
87
90
  // 记录耗时
88
91
  const duration = Date.now() - startTime;
@@ -91,6 +94,8 @@ export default {
91
94
  }
92
95
 
93
96
  console.log(`流处理完成,总耗时: ${duration}ms`);
97
+ this.avaterStatus = 'normal';
98
+ this.isLoading = false;
94
99
 
95
100
  } catch (error) {
96
101
  console.error('发送消息失败:', error);
@@ -98,6 +103,7 @@ export default {
98
103
  this.currentMessage.content = '抱歉,发生了错误,请重试。';
99
104
  this.$forceUpdate();
100
105
  }
106
+ this.isLoading = false;
101
107
  }
102
108
  },
103
109
 
@@ -107,6 +113,7 @@ export default {
107
113
  async consumeStream(readableStream) {
108
114
  const reader = readableStream.getReader();
109
115
  const decoder = new TextDecoder('utf-8');
116
+ const self = this;
110
117
 
111
118
  try {
112
119
  // eslint-disable-next-line no-constant-condition
@@ -116,8 +123,10 @@ export default {
116
123
 
117
124
  const chunk = decoder.decode(value, { stream: true });
118
125
 
119
- // 使用解析器处理数据块
120
- this.streamParser.processChunk(chunk, this.handleStreamUpdate);
126
+ // 使用解析器处理数据块,确保this指向正确
127
+ this.streamParser.processChunk(chunk, function(result) {
128
+ self.handleStreamUpdate(result);
129
+ });
121
130
  }
122
131
  } finally {
123
132
  reader.releaseLock();
@@ -130,6 +139,8 @@ export default {
130
139
  handleStreamUpdate(result) {
131
140
  if (!this.currentMessage) return;
132
141
 
142
+ console.log('收到更新:', result);
143
+
133
144
  // 更新思考内容
134
145
  if (result.thinking) {
135
146
  this.currentMessage.thinking += result.thinking;
@@ -1,8 +1,10 @@
1
+ import { WS_URL } from '../config/index.js'
2
+
1
3
  export default {
2
4
  data() {
3
5
  return {
4
6
  ws: null,
5
- wsUrl: 'ws://192.168.8.9:9999/ai_model/ws/voice-stream',
7
+ wsUrl: WS_URL,
6
8
  isConnected: false,
7
9
  reconnectCount: 0, // 重连尝试次数
8
10
  maxReconnectAttempts: 3 // 最大重连尝试次数