byt-lingxiao-ai 0.3.0 → 0.3.3

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 (40) hide show
  1. package/components/AiMessage.vue +39 -21
  2. package/components/ChatAvatar.vue +8 -1
  3. package/components/ChatMessageList.vue +29 -10
  4. package/components/ChatRobot.vue +5 -0
  5. package/components/ChatWindow.vue +115 -5
  6. package/components/ChatWindowHeader.vue +3 -0
  7. package/components/assets/arrow.png +0 -0
  8. package/components/assets/empty.png +0 -0
  9. package/components/assets/entering.png +0 -0
  10. package/components/assets/logo.png +0 -0
  11. package/components/assets/normal.png +0 -0
  12. package/components/assets/output.png +0 -0
  13. package/components/assets/speaking.png +0 -0
  14. package/components/assets/think.png +0 -0
  15. package/components/assets/thinking.png +0 -0
  16. package/components/assets/waiting.png +0 -0
  17. package/components/config/index.js +4 -0
  18. package/components/mixins/messageMixin.js +11 -5
  19. package/dist/img/empty.f36cb82e.png +0 -0
  20. package/dist/img/entering.4ef198fb.png +0 -0
  21. package/dist/img/normal.30197a82.png +0 -0
  22. package/dist/img/output.1dfa94eb.png +0 -0
  23. package/dist/img/speaking.fa87fedb.png +0 -0
  24. package/dist/img/thinking.21ad5ca5.png +0 -0
  25. package/dist/img/waiting.460478ef.png +0 -0
  26. package/dist/index.common.js +222 -89
  27. package/dist/index.common.js.map +1 -1
  28. package/dist/index.css +2 -2
  29. package/dist/index.umd.js +222 -89
  30. package/dist/index.umd.js.map +1 -1
  31. package/dist/index.umd.min.js +1 -1
  32. package/dist/index.umd.min.js.map +1 -1
  33. package/package.json +1 -1
  34. package/components/assets/byt.mp3 +0 -0
  35. package/dist/img/entering.42f05909.png +0 -0
  36. package/dist/img/normal.13f08ecb.png +0 -0
  37. package/dist/img/output.85c6bd8b.png +0 -0
  38. package/dist/img/speaking.3ce8b666.png +0 -0
  39. package/dist/img/thinking.05f29a84.png +0 -0
  40. package/dist/img/waiting.ac21d76e.png +0 -0
@@ -1,14 +1,14 @@
1
1
  <template>
2
2
  <div class="chat-window-message-ai">
3
3
  <div class="ai-render">
4
- <div class="ai-loading" v-if="loading">
4
+ <div class="ai-loading" v-if="isLoading">
5
5
  <div class="dot"></div>
6
6
  <div class="dot"></div>
7
7
  <div class="dot"></div>
8
8
  </div>
9
- <div class="ai-thinking" @click="$emit('thinking-click')" v-if="message.thinking">
10
- <div class="ai-thinking-time">思考用时{{ message.time }}秒</div>
11
- <div class="ai-thinking-content" v-if="thinkStatus">{{ message.thinking }}</div>
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
12
  </div>
13
13
  <div class="ai-content markdown-body" v-html="renderedContent"></div>
14
14
  </div>
@@ -30,7 +30,6 @@ marked.setOptions({
30
30
  breaks: true,
31
31
  gfm: true
32
32
  });
33
-
34
33
  function parseMarkdown(text) {
35
34
  if (!text) return '';
36
35
  let html = text;
@@ -58,18 +57,22 @@ function parseMarkdown(text) {
58
57
  html = html.replace(/^\s*\d+\.\s+(.+)$/gm, '<li>$1</li>');
59
58
 
60
59
  html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
61
-
60
+ // 解析Markdown链接
62
61
  html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
63
62
 
64
63
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />');
65
64
 
66
65
  html = html.replace(/^-{3,}$/gm, '<hr>');
66
+ // 解析简单URL
67
+ const simpleUrlRegex = /(https?:\/\/[^\s<>"']+)/g;
68
+ html = html.replace(simpleUrlRegex, (match) => {
69
+ return `<a href="${match}" aria-current="page">${match}</a>`;
70
+ });
67
71
 
68
- html = html.replace(/\n/g, '<br>');
72
+ // html = html.replace(/\n/g, '<br>');
69
73
 
70
74
  return html;
71
75
  }
72
-
73
76
  // 解析表格
74
77
  function parseTable(text) {
75
78
  const lines = text.split('\n');
@@ -108,7 +111,6 @@ function parseTable(text) {
108
111
 
109
112
  return result.join('\n');
110
113
  }
111
-
112
114
  // 渲染表格
113
115
  function renderTable(rows) {
114
116
  if (rows.length < 2) return rows.join('\n');
@@ -130,7 +132,7 @@ function renderTable(rows) {
130
132
  if (cells.length > 0) {
131
133
  html += '<tr>';
132
134
  cells.forEach(cell => {
133
- html += `<td>${cell.trim()}</td>`;
135
+ html += `<td><div class="table-cell">${cell.trim()}</div></td>`;
134
136
  });
135
137
  html += '</tr>';
136
138
  }
@@ -140,7 +142,6 @@ function renderTable(rows) {
140
142
  html += '</table></div>';
141
143
  return html;
142
144
  }
143
-
144
145
  // HTML 转义
145
146
  function escapeHtml(text) {
146
147
  const map = {
@@ -159,19 +160,17 @@ export default {
159
160
  message: {
160
161
  type: Object,
161
162
  required: true
162
- },
163
- thinkStatus: {
164
- type: Boolean,
165
- default: true
166
- },
167
- loading: {
168
- type: Boolean,
169
- default: false
170
163
  }
171
164
  },
172
165
  computed: {
166
+ thinkingExpanded() {
167
+ return this.message.thinkingExpanded !== false;
168
+ },
173
169
  renderedContent() {
174
170
  return parseMarkdown(this.message.content);
171
+ },
172
+ isLoading() {
173
+ return this.message.loading === true;
175
174
  }
176
175
  },
177
176
  watch: {
@@ -371,16 +370,35 @@ export default {
371
370
  .markdown-body ::v-deep .table-wrapper{
372
371
  overflow-x: auto;
373
372
  border: 1px solid #dfe2e5;
373
+ margin: 12px 0;
374
374
  }
375
375
  .markdown-body ::v-deep .markdown-table {
376
376
  border-collapse: collapse;
377
377
  width: 100%;
378
- margin: 12px 0;
379
378
  font-size: 14px;
380
379
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
381
380
  overflow: hidden;
381
+ margin: -1px 0 0 -1px;
382
+ }
383
+ /* 定义table的滚动条 */
384
+ .markdown-body ::v-deep .table-wrapper::-webkit-scrollbar {
385
+ height: 6px;
386
+ }
387
+ .markdown-body ::v-deep .table-wrapper::-webkit-scrollbar-track {
388
+ background-color: #f3f4f6;
389
+ }
390
+ .markdown-body ::v-deep .table-wrapper::-webkit-scrollbar-thumb {
391
+ background-color: #d1d5db;
392
+ border-radius: 3px;
393
+ }
394
+ .markdown-body ::v-deep .table-wrapper::-webkit-scrollbar-thumb:hover {
395
+ background-color: #9ca3af;
382
396
  }
383
397
 
398
+ .markdown-body ::v-deep .table-cell {
399
+ min-width: 100px;
400
+ text-align: left;
401
+ }
384
402
  .markdown-body ::v-deep .markdown-table th,
385
403
  .markdown-body ::v-deep .markdown-table td {
386
404
  border: 1px solid #dfe2e5;
@@ -459,6 +477,6 @@ export default {
459
477
  }
460
478
 
461
479
  .markdown-body ::v-deep br {
462
- line-height: 1.6;
480
+ line-height: 0.8em;
463
481
  }
464
482
  </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="chat-ai" @click="$emit('click')">
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>
@@ -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,8 +14,7 @@
13
14
  v-else
14
15
  :message="message"
15
16
  :think-status="thinkStatus"
16
- :loading="isLoading"
17
- @thinking-click="$emit('thinking-click')"
17
+ @thinking-toggle="handleThinkingToggle(message)"
18
18
  />
19
19
  </div>
20
20
  </div>
@@ -38,13 +38,22 @@ export default {
38
38
  thinkStatus: {
39
39
  type: Boolean,
40
40
  default: true
41
- },
42
- isLoading: {
43
- type: Boolean,
44
- default: false
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];
45
50
  }
46
51
  },
47
52
  methods: {
53
+ handleThinkingToggle(message) {
54
+ console.log('handleThinkingToggle', message)
55
+ this.$set(message, 'thinkingExpanded', !message.thinkingExpanded);
56
+ },
48
57
  scrollToBottom() {
49
58
  this.$nextTick(() => {
50
59
  const chatArea = this.$refs.chatArea
@@ -55,17 +64,27 @@ export default {
55
64
  }
56
65
  },
57
66
  watch: {
58
- messages: {
59
- handler() {
60
- this.scrollToBottom()
67
+ lastMessageObject: {
68
+ handler(newMsg) {
69
+ if (newMsg) {
70
+ this.scrollToBottom();
71
+ }
61
72
  },
62
- deep: true
73
+ deep: true,
74
+ immediate: true
63
75
  }
64
76
  }
65
77
  }
66
78
  </script>
67
79
 
68
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
+ }
69
88
  .chat-window-content {
70
89
  flex: 1;
71
90
  padding: 16px;
@@ -11,6 +11,11 @@ export default {
11
11
  required: true,
12
12
  validator: (value) => ['entering', 'waiting', 'speaking'].includes(value)
13
13
  }
14
+ },
15
+ watch: {
16
+ status(newStatus, oldStatus) {
17
+ console.log('机器人状态变化:', newStatus, oldStatus)
18
+ }
14
19
  }
15
20
  }
16
21
  </script>
@@ -1,5 +1,8 @@
1
1
  <template>
2
- <div class="chat">
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
- @click="toggleWindow"
27
+ @mousedown="startDrag"
25
28
  />
26
29
 
27
30
  <!-- 聊天窗口 -->
@@ -30,7 +33,6 @@
30
33
  :messages="messages"
31
34
  :input-message="inputMessage"
32
35
  :think-status="thinkStatus"
33
- :loading="isLoading"
34
36
  @update:inputMessage="inputMessage = $event"
35
37
  @send="handleSend"
36
38
  @thinking-click="handleThinkingClick"
@@ -77,7 +79,31 @@ export default {
77
79
  thinkStatus: true,
78
80
  jumpedTimePoints: new Set(),
79
81
  SAMPLE_RATE,
80
- 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
+ };
81
107
  }
82
108
  },
83
109
  mounted() {
@@ -85,6 +111,13 @@ export default {
85
111
  if (this.appendToBody) {
86
112
  this.appendToBodyHandler()
87
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
+ })
88
121
  },
89
122
  beforeDestroy() {
90
123
  if (this.appendToBody && this.$el.parentElement === document.body) {
@@ -92,10 +125,80 @@ export default {
92
125
  }
93
126
  this.closeWebSocket()
94
127
  this.stopRecording()
128
+
129
+ // 移除全局事件监听器
130
+ document.removeEventListener('mousemove', this.onDrag)
131
+ document.removeEventListener('mouseup', this.stopDrag)
95
132
  },
96
133
  methods: {
97
134
  toggleWindow() {
98
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
+ }
99
202
  },
100
203
  handleThinkingClick() {
101
204
  this.thinkStatus = !this.thinkStatus
@@ -123,8 +226,14 @@ export default {
123
226
  .then(response => response.json())
124
227
  .then(data => {
125
228
  console.log('时间跳转点:', data)
229
+ console.log('当前时间:', currentTime)
126
230
  data.forEach(point => {
127
- if (currentTime >= point.time && currentTime < point.time + 0.5 && !this.jumpedTimePoints.has(point.time)) {
231
+ console.log('跳转点:', point)
232
+ console.log('跳转时间:', point.time)
233
+ console.log('当前时间:', currentTime)
234
+ console.log('是否已跳转:', this.jumpedTimePoints.has(point.time))
235
+ console.log('跳转时间范围:', currentTime >= point.time && currentTime < point.time + 1)
236
+ if (currentTime >= point.time && currentTime < point.time + 1 && !this.jumpedTimePoints.has(point.time)) {
128
237
  this.jumpedTimePoints.add(point.time)
129
238
  this.$appOptions.store.dispatch('tags/addTagview', {
130
239
  path: point.url,
@@ -143,6 +252,7 @@ export default {
143
252
  },
144
253
  onAudioEnded() {
145
254
  this.robotStatus = 'leaving'
255
+ this.avaterStatus = 'normal'
146
256
  this.jumpedTimePoints.clear()
147
257
  }
148
258
  }
@@ -41,6 +41,8 @@ export default {
41
41
  name: 'ChatWindowHeader',
42
42
  methods: {
43
43
  handleOpen() {
44
+ // const chatId = ''
45
+ // const baseUrl = window.location.protocol + '//' + window.location.hostname + ':3100/c/' + chatId;
44
46
  const baseUrl = window.location.protocol + '//' + window.location.hostname + ':3100/';
45
47
  window.open(baseUrl, '_blank')
46
48
  }
@@ -56,6 +58,7 @@ export default {
56
58
  background: #fff;
57
59
  padding: 16px;
58
60
  flex-shrink: 0;
61
+ user-select: none;
59
62
  }
60
63
 
61
64
  .chat-window-header-title {
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,3 +1,7 @@
1
+ const baseUrl = window.location.protocol + '//' + window.location.hostname;
2
+ const chatPort = '3100';
3
+
4
+ console.log(baseUrl, chatPort);
1
5
  export const API_URL = 'http://192.168.8.87:3100/lingxiao-byt/api/v1/mcp/ask';
2
6
  export const WS_URL = 'ws://192.168.8.9:9999/ai_model/ws/voice-stream';
3
7
  export const AUDIO_URL = '/minio/lingxiaoai/byt.mp3';
@@ -4,8 +4,7 @@ import { API_URL } from '../config/index.js'
4
4
  export default {
5
5
  data() {
6
6
  return {
7
- streamParser: null,
8
- isLoading: false
7
+ streamParser: null
9
8
  }
10
9
  },
11
10
 
@@ -15,7 +14,6 @@ export default {
15
14
  updateInterval: 16, // 约60fps
16
15
  debug: process.env.NODE_ENV === 'development'
17
16
  });
18
- this.isLoading = true;
19
17
  },
20
18
 
21
19
  methods: {
@@ -28,6 +26,8 @@ export default {
28
26
  thinking: '',
29
27
  charts: [],
30
28
  content: '',
29
+ loading: true,
30
+ thinkingExpanded: true
31
31
  };
32
32
  this.messages.push(message);
33
33
  this.currentMessage = message;
@@ -95,7 +95,6 @@ export default {
95
95
 
96
96
  console.log(`流处理完成,总耗时: ${duration}ms`);
97
97
  this.avaterStatus = 'normal';
98
- this.isLoading = false;
99
98
 
100
99
  } catch (error) {
101
100
  console.error('发送消息失败:', error);
@@ -103,7 +102,11 @@ export default {
103
102
  this.currentMessage.content = '抱歉,发生了错误,请重试。';
104
103
  this.$forceUpdate();
105
104
  }
106
- this.isLoading = false;
105
+ } finally {
106
+ // 确保加载状态关闭
107
+ if (this.currentMessage) {
108
+ this.currentMessage.loading = false;
109
+ }
107
110
  }
108
111
  },
109
112
 
@@ -141,6 +144,9 @@ export default {
141
144
 
142
145
  console.log('收到更新:', result);
143
146
 
147
+ if (this.currentMessage.loading) {
148
+ this.currentMessage.loading = false;
149
+ }
144
150
  // 更新思考内容
145
151
  if (result.thinking) {
146
152
  this.currentMessage.thinking += result.thinking;
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file