byt-lingxiao-ai 0.2.5 → 0.2.7

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.
@@ -10,118 +10,53 @@
10
10
  <source :src="audioSrc" type="audio/mpeg">
11
11
  您的浏览器不支持音频元素。
12
12
  </audio>
13
- <div v-if="robotStatus !== 'leaving'" :class="['chat-robot', robotStatus]" ></div>
14
- <div v-else class="chat-ai" @click="toggleWindow">
15
- <div :class="['chat-ai-avater', avaterStatus]"></div>
16
- <div class="chat-ai-text">{{ avaterText }}</div>
17
- </div>
18
- <!-- 添加一个遮罩层,用于捕获点击外部事件 -->
19
- <div class="chat-overlay" v-show="visible" @click="handleOverlayClick">
20
- <div class="chat-window" @click.stop>
21
- <div class="chat-window-header">
22
- <div class="chat-window-header-title">凌霄大模型AI对话</div>
23
- <div class="chat-window-header-close" @click="visible = false">
24
- <svg
25
- xmlns="http://www.w3.org/2000/svg"
26
- width="24"
27
- height="24"
28
- viewBox="0 0 24 24"
29
- fill="none"
30
- >
31
- <path
32
- d="M5.50002 5.5L18.5 18.5"
33
- stroke="#4E5969"
34
- stroke-width="1.89404"
35
- stroke-linecap="round"
36
- stroke-linejoin="round"
37
- />
38
- <path
39
- d="M5.50002 18.5L18.5 5.5"
40
- stroke="#4E5969"
41
- stroke-width="1.89404"
42
- stroke-linecap="round"
43
- stroke-linejoin="round"
44
- />
45
- </svg>
46
- </div>
47
- </div>
48
- <div ref="chatArea" class="chat-window-content scrollbar-hide">
49
- <div
50
- class="chat-window-message"
51
- v-for="message in messages"
52
- :key="message.id"
53
- >
54
- <div
55
- class="chat-window-message-user"
56
- v-if="message.type === 'user'"
57
- >
58
- <div class="user-message">{{ message.content }}</div>
59
- </div>
60
- <div class="chat-window-message-ai" v-else>
61
- <div class="ai-render">
62
- <div class="ai-thinking">
63
- <div class="ai-thinking-time">思考用时{{ message.time }}秒</div>
64
- <div class="ai-thinking-content">{{ message.thinking }}</div>
65
- </div>
66
- <div class="ai-content">{{ message.content }}</div>
67
- </div>
68
- </div>
69
- </div>
70
- </div>
71
- <div class="chat-window-footer">
72
- <div class="chat-window-textarea">
73
- <el-input
74
- type="textarea"
75
- class="chat-window-input"
76
- placeholder="有什么我能帮您的吗?"
77
- rows="2"
78
- resize="none"
79
- v-model="inputMessage"
80
- @keydown="handleKeyDown"
81
- ></el-input>
82
- <div class="chat-window-bar">
83
- <div class="chat-window-send" @click="handleSend">
84
- <svg
85
- xmlns="http://www.w3.org/2000/svg"
86
- width="20"
87
- height="20"
88
- viewBox="0 0 20 20"
89
- fill="none"
90
- >
91
- <g clip-path="url(#clip0_640_2107)">
92
- <path
93
- d="M18.6427 2.37822C19.3253 2.47432 19.8025 3.10738 19.7065 3.79002C19.6871 3.97072 19.5381 4.41327 19.5403 4.41161L14.9673 17.8079L14.9632 17.8093C14.7858 18.3838 14.212 18.7607 13.5971 18.6744C13.4173 18.6492 13.2504 18.5862 13.1055 18.4949L13.0973 18.4977L9.83449 16.3686C9.58371 16.2584 9.4276 15.9939 9.46729 15.7115C9.5154 15.3691 9.83278 15.1317 10.1751 15.1798C10.293 15.1964 10.3988 15.2448 10.4853 15.3161L13.4054 17.2314L13.4073 17.2317C13.4452 17.2566 13.4882 17.2746 13.5364 17.2814C13.6911 17.3029 13.8371 17.2052 13.8793 17.0593L18.0469 4.89796L8.27396 14.3367L7.77435 17.8916C7.72706 18.2281 7.41369 18.464 7.07727 18.4169C6.74073 18.3696 6.50469 18.0564 6.55198 17.7198L7.07633 13.9889C7.08231 13.9464 7.09382 13.9066 7.10727 13.867C7.13549 13.7645 7.19079 13.6657 7.27291 13.5866L17.0754 4.12041L2.68514 8.17767L2.68487 8.1796C2.58481 8.21873 2.50686 8.31058 2.49042 8.42643C2.47412 8.5424 2.52417 8.65007 2.6093 8.71729L2.60903 8.71922L3.28261 9.16101L3.28013 9.16461C3.47505 9.29254 3.58819 9.52563 3.55386 9.77111C3.50575 10.1134 3.18836 10.3509 2.84602 10.3028C2.75512 10.29 2.6708 10.2584 2.59833 10.2127L2.59806 10.2147L1.37843 9.42001L1.37951 9.41227C1.02215 9.14901 0.816644 8.70195 0.882687 8.23203C0.951867 7.74096 1.29791 7.35545 1.73968 7.21445L1.73995 7.21251L17.7833 2.57104C18.0287 2.41036 18.3294 2.3342 18.6427 2.37822Z"
94
- fill="#013378"
95
- />
96
- <path
97
- d="M3.1309 10.9936C3.2178 10.5684 3.82528 10.5684 3.91218 10.9936C4.10411 11.9326 4.83794 12.6664 5.77697 12.8584C6.20213 12.9453 6.20213 13.5527 5.77697 13.6397C4.83794 13.8316 4.10411 14.5654 3.91218 15.5044C3.82528 15.9296 3.2178 15.9296 3.1309 15.5044C2.93897 14.5654 2.20513 13.8316 1.26611 13.6397C0.840944 13.5527 0.840944 12.9453 1.26611 12.8584C2.20513 12.6664 2.93897 11.9326 3.1309 10.9936Z"
98
- fill="#2B80F6"
99
- />
100
- <path
101
- d="M6.20382 8.56242C6.25596 8.30732 6.62045 8.30732 6.67259 8.56242C6.78775 9.12583 7.22805 9.56613 7.79146 9.68129C8.04656 9.73343 8.04656 10.0979 7.79146 10.1501C7.22805 10.2652 6.78775 10.7055 6.67259 11.2689C6.62045 11.524 6.25596 11.524 6.20382 11.2689C6.08866 10.7055 5.64836 10.2652 5.08495 10.1501C4.82985 10.0979 4.82985 9.73343 5.08495 9.68129C5.64836 9.56613 6.08866 9.12583 6.20382 8.56242Z"
102
- fill="#2B80F6"
103
- />
104
- </g>
105
- <defs>
106
- <clipPath id="clip0_640_2107">
107
- <rect width="20" height="20" fill="white" />
108
- </clipPath>
109
- </defs>
110
- </svg>
111
- </div>
112
- </div>
113
- </div>
114
- </div>
115
- </div>
116
- </div>
13
+
14
+ <!-- 机器人动画 -->
15
+ <ChatRobot
16
+ v-if="robotStatus !== 'leaving'"
17
+ :status="robotStatus"
18
+ />
19
+
20
+ <!-- AI头像入口 -->
21
+ <ChatAvatar
22
+ v-else
23
+ :status="avaterStatus"
24
+ @click="toggleWindow"
25
+ />
26
+
27
+ <!-- 聊天窗口 -->
28
+ <ChatWindowDialog
29
+ v-model="visible"
30
+ :messages="messages"
31
+ :input-message="inputMessage"
32
+ :think-status="thinkStatus"
33
+ @update:inputMessage="inputMessage = $event"
34
+ @send="handleSend"
35
+ @thinking-click="handleThinkingClick"
36
+ @overlay-click="handleOverlayClick"
37
+ />
117
38
  </div>
118
39
  </template>
40
+
119
41
  <script>
42
+ import ChatRobot from './ChatRobot.vue'
43
+ import ChatAvatar from './ChatAvatar.vue'
44
+ import ChatWindowDialog from './ChatWindowDialog.vue'
45
+ import audioMixin from './mixins/audioMixin'
46
+ import webSocketMixin from './mixins/webSocketMixin'
47
+ import messageMixin from './mixins/messageMixin'
48
+
120
49
  const SAMPLE_RATE = 16000;
121
50
  const FRAME_SIZE = 512;
122
51
 
123
52
  export default {
124
53
  name: 'ChatWindow',
54
+ components: {
55
+ ChatRobot,
56
+ ChatAvatar,
57
+ ChatWindowDialog
58
+ },
59
+ mixins: [audioMixin, webSocketMixin, messageMixin],
125
60
  props: {
126
61
  appendToBody: {
127
62
  type: Boolean,
@@ -130,75 +65,26 @@ export default {
130
65
  },
131
66
  data() {
132
67
  return {
133
- audioSrc: '/minio/lingxiaoai/byt.mp3', // 音频URL
134
- inputMessage: '', // 输入消息
135
- visible: false, // 窗口是否可见
136
- messages: [
137
- {
138
- id: 1,
139
- type: 'user',
140
- sender: '',
141
- time: '',
142
- content: '你好,欢迎来到凌霄大模型AI对话。',
143
- },
144
- {
145
- id: 2,
146
- type: 'ai',
147
- sender: 'AI',
148
- time: '',
149
- thinking: '嗯,用户问的是回转窑的工业应用。首先,我需要回忆一下之前对话的内容。用户之前让我解释了水泥的制作流程,特别是提到了回转窑在高温煅烧熟料中的作用。',
150
- charts: [
151
- {
152
- title: '',
153
- options: {}
154
- }
155
- ],
156
- content: '回转窑(Rotary Kiln)是一种长筒形旋转煅烧设备(类似倾斜安装的大管子),因其独特的旋转运动和高温耐火衬里设计,在多个工业领域都有广泛应用',
157
- }
158
- ], // 消息列表
159
- isRecording: false, // 正在录制
160
- isMicAvailable: false, // 麦克风是否可用
161
- mediaRecorder: null, // 媒体录制器
162
- ws: null, // WebSocket连接
163
- // wsUrl: 'wss://mes.shnfonline.top:8316/ai_model/ws/voice-stream', // WebSocket URL
164
- wsUrl: 'ws://192.168.8.9:9999/ai_model/ws/voice-stream', // WebSocket URL
165
- isConnected: false, // WebSocket是否已连接
166
- audioContext: null, // 音频上下文
167
- microphone: null, // 麦克风输入节点
168
- processor: null, // 音频处理节点
169
- robotStatus: 'leaving', // 机器人状态 waiting, speaking, leaving, entering
170
- avaterStatus: 'normal', // 头像状态 normal output thinking
171
- buffer: '', // 音频缓冲区
172
- currentMessage: null, // 当前消息
173
- inTag: false, // 是否在标签页
174
- tagBuilder: '', // 标签构建器
175
- audioBuffer: new Float32Array(0) // 音频缓冲区
176
- }
177
- },
178
- computed: {
179
- avaterText() {
180
- const textMap = {
181
- 'normal': '凌霄AI',
182
- 'thinking': '思考中',
183
- 'output': '语音中'
184
- }
185
- return textMap[this.avaterStatus]
68
+ audioSrc: '/minio/lingxiaoai/byt.mp3',
69
+ inputMessage: '',
70
+ visible: false,
71
+ messages: [],
72
+ robotStatus: 'leaving',
73
+ avaterStatus: 'normal',
74
+ currentMessage: null,
75
+ thinkStatus: true,
76
+ jumpedTimePoints: new Set(),
77
+ SAMPLE_RATE,
78
+ FRAME_SIZE
186
79
  }
187
80
  },
188
81
  mounted() {
189
82
  this.initWebSocket()
190
-
191
- // 处理append-to-body逻辑
192
83
  if (this.appendToBody) {
193
84
  this.appendToBodyHandler()
194
85
  }
195
86
  },
196
- unmounted() {
197
- this.closeWebSocket()
198
- this.stopRecording()
199
- },
200
87
  beforeDestroy() {
201
- // 组件销毁前,如果元素被移动到body中,需要移除
202
88
  if (this.appendToBody && this.$el.parentElement === document.body) {
203
89
  document.body.removeChild(this.$el)
204
90
  }
@@ -206,450 +92,61 @@ export default {
206
92
  this.stopRecording()
207
93
  },
208
94
  methods: {
209
- // 处理音频播放结束事件
210
- onAudioEnded() {
211
- this.robotStatus = 'leaving'
212
- this.jumpedTimePoints.clear()
213
- },
214
- // 处理音频播放时间更新事件
215
- onTimeUpdate() {
216
- const audio = this.$refs.audioPlayer
217
- const currentTime = audio.currentTime
218
-
219
- if (!this.jumpedTimePoints) {
220
- this.jumpedTimePoints = new Set()
221
- }
222
-
223
- const timeJumpPoints = [
224
- { time: 40, url: '/permission/user', name: 'permission_user', title: '用户管理' },
225
- { time: 65, url: '/permission/menu', name: 'permission_menu', title: '菜单管理' },
226
- { time: 75, url: '/permission/role', name: 'permission_role', title: '角色管理' }
227
- ]
228
- // 检查当前时间是否达到跳转点
229
- timeJumpPoints.forEach(point => {
230
- // 使用一定的误差范围,确保不会因为播放进度的微小差异而错过跳转点
231
- if (currentTime >= point.time && currentTime < point.time + 0.5 && !this.jumpedTimePoints.has(point.time)) {
232
- this.jumpedTimePoints.add(point.time)
233
- console.log(`到达时间点 ${point.time} 秒,跳转到 ${point.title}`)
234
- this.$appOptions.store.dispatch('tags/addTagview', {
235
- path: point.url,
236
- fullPath: point.url,
237
- label: point.title,
238
- name: point.title,
239
- meta: {
240
- title: point.title
241
- },
242
- query: {},
243
- params: {}
244
- })
245
- this.$appOptions.router.push({
246
- path: point.url
247
- })
248
- }
249
- })
250
-
251
- console.log('当前播放时间:', currentTime)
252
- },
253
- play() {
254
- this.robotStatus = 'speaking'
255
- this.$refs.audioPlayer.play()
256
- },
257
- pause() {
258
- this.robotStatus = 'waiting'
259
- this.$refs.audioPlayer.pause()
260
- },
261
- stop() {
262
- this.robotStatus = 'leaving'
263
- this.$refs.audioPlayer.pause()
264
- this.$refs.audioPlayer.currentTime = 0
265
- this.jumpedTimePoints.clear()
266
- },
267
- initWebSocket() {
268
- try {
269
- this.ws = new WebSocket(this.wsUrl)
270
- this.ws.binaryType = 'arraybuffer'
271
-
272
- this.ws.onopen = async () => {
273
- console.log('连接成功')
274
- this.isConnected = true
275
- this.initAudio()
276
- }
277
- this.ws.onmessage = (event) => {
278
- try {
279
- console.log("收到原始消息:", event.data);
280
- // 二进制数据直接返回
281
- if (event.data instanceof ArrayBuffer) {
282
- console.log("收到二进制音频数据");
283
- return;
284
- }
285
- // 解析JSON数据
286
- const data = JSON.parse(event.data);
287
- console.log("解析后的数据:", data);
288
-
289
- if (data.type === 'config'){
290
- console.log('配置信息:', data);
291
- } else if (data.code === 0) {
292
- if (data.data.type === 'detection') {
293
- console.log('检测到唤醒词...');
294
- this.avaterStatus = 'normal'
295
- } else if (data.data.type === 'Collecting') {
296
- console.log('状态: 采集中...');
297
- this.avaterStatus = 'thinking'
298
- } else if (data.data.type === 'command') {
299
- // 根据指令改变机器人的状态
300
- console.log('状态: 处理中...')
301
- this.analyzeAudioCommand(data.data.category)
302
- } else {
303
- console.log('状态: 其他...')
304
- }
305
- } else {
306
- console.error("服务器返回错误:", data.msg);
307
- }
308
- } catch (error) {
309
- console.error("消息解析错误:", error);
310
- }
311
- }
312
- this.ws.onclose = () => {
313
- console.log('连接关闭')
314
- this.isConnected = false
315
- if (this.isRecording) {
316
- this.stopRecording()
317
- }
318
- setTimeout(() => {
319
- console.log('尝试重新建立连接')
320
- if (!this.isConnected) {
321
- // 尝试重连
322
- this.initWebSocket()
323
- }
324
- }, 3000)
325
- }
326
- } catch (error) {
327
- console.error('WebSocket连接失败:', error);
328
- }
329
- },
330
- closeWebSocket() {
331
- if (this.ws) {
332
- this.ws.close()
333
- }
334
- },
335
- createAiMessage() {
336
- const message = {
337
- id: this.messages.length + 1,
338
- type: 'ai',
339
- sender: 'AI',
340
- time: '',
341
- thinking: '',
342
- charts: [],
343
- content: '',
344
- }
345
- this.messages.push(message)
346
- this.currentMessage = message
347
- return message
348
- },
349
- createUserMessage(content) {
350
- const message = {
351
- id: this.messages.length + 1,
352
- type: 'user',
353
- sender: '用户',
354
- time: '',
355
- content,
356
- }
357
- this.messages.push(message)
358
- this.inputMessage = ''
359
- return message
360
- },
361
- async initAudio() {
362
- if (this.isRecording) return;
363
- try {
364
- this.isMicAvailable = true;
365
- // 2. 获取麦克风权限
366
- const stream = await navigator.mediaDevices.getUserMedia({
367
- audio: {
368
- sampleRate: SAMPLE_RATE, // 请求指定采样率
369
- channelCount: 1, // 单声道
370
- noiseSuppression: true,
371
- echoCancellation: true
372
- }
373
- });
374
-
375
- // 3. 创建音频处理环境
376
- this.audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
377
- const actualSampleRate = this.audioContext.sampleRate;
378
- this.microphone = this.audioContext.createMediaStreamSource(stream);
379
-
380
- // 4. 创建音频处理器
381
- this.processor = this.audioContext.createScriptProcessor(FRAME_SIZE, 1, 1);
382
- this.processor.onaudioprocess = this.processAudio;
383
- // 连接处理链
384
- this.microphone.connect(this.processor);
385
- this.processor.connect(this.audioContext.destination);
386
-
387
- this.isRecording = true;
388
-
389
- console.log(`状态: 录音中 (采样率: ${actualSampleRate}Hz)`)
390
- } catch (error) {
391
- console.error("音频初始化失败:", error);
392
- this.isRecording = false
393
- this.isMicAvailable = false
394
- }
395
- },
396
- async handleSend() {
397
- if (!this.inputMessage.trim()) {
398
- return
399
- }
400
- const message = this.inputMessage.trim();
401
- // 初始化信息
402
- this.initState()
403
- // 发送消息
404
- this.createUserMessage(message)
405
- // 创建AI消息
406
- this.createAiMessage()
407
-
408
- try {
409
- const controller = new AbortController();
410
- const token = `Bearer e298f087-85bc-48c2-afb9-7c69ffc911aa`
411
- const response = await fetch('/bytserver/api-model/chat/stream', {
412
- timeout: 30000,
413
- method: 'POST',
414
- signal: controller.signal,
415
- headers: {
416
- 'Content-Type': 'application/json' ,
417
- 'Authorization': token,
418
- },
419
- body: JSON.stringify({ content: message })
420
- });
421
- if (!response.ok) {
422
- throw new Error(`${response.status}`);
423
- }
424
-
425
- const render = response.body.getReader()
426
- const decoder = new TextDecoder()
427
-
428
- // eslint-disable-next-line no-constant-condition
429
- while (true) {
430
- const { done, value } = await render.read()
431
- if (done) break
432
- const chunk = decoder.decode(value, { stream: true })
433
- this.processStreamChunk(chunk)
434
- }
435
- } catch (error) {
436
- console.error('发送消息失败:', error)
437
- }
438
- },
439
- initState() {},
440
- // 分析音频命令
441
- analyzeAudioCommand(command) {
442
- console.log('分析音频命令:', command)
443
- // 解析到开始导览,执行机器人进入动画
444
- if (command === 'C5') {
445
- this.robotStatus = 'entering'
446
- setTimeout(() => {
447
- this.robotStatus = 'speaking'
448
- this.play()
449
- }, 3000)
450
- }
451
- // 继续导览
452
- else if (command === 'C8') {
453
- this.robotStatus = 'speaking'
454
- this.play()
455
- }
456
- // 解析到暂停导览,执行机器人暂停动画
457
- else if (command === 'C7') {
458
- this.robotStatus = 'waiting'
459
- this.pause()
460
- }
461
- // 解析到结束导览,执行机器人离开动画
462
- else if (command === 'C6') {
463
- this.robotStatus = 'leaving'
464
- this.stop()
465
- }
466
- },
467
- processStreamChunk(chunk) {
468
- console.log('原始数据:', chunk)
469
- try {
470
- this.buffer += chunk;
471
-
472
- // eslint-disable-next-line no-constant-condition
473
- while (true) {
474
- const eventEnd = this.buffer.indexOf('\n\n');
475
- if (eventEnd === -1) break;
476
-
477
- const eventData = this.buffer.slice(0, eventEnd);
478
- this.buffer = this.buffer.slice(eventEnd + 2);
479
- console.log('解析数据:', eventData)
480
- this.processEventData(eventData);
481
- }
482
- } catch (error) {
483
- console.error('流数据处理异常:', error);
484
- this.streaming = false;
485
- this.response = this.$t('3d.chat.dataFormatError');
486
- this.$forceUpdate();
487
- }
488
- },
489
- processEventData(data) {
490
- data.split('\n').forEach(line => {
491
- console.log('原始数据:', line)
492
- if (!line.startsWith('data:')) return;
493
-
494
- const jsonStr = line.replace(/^data:\s*/, '').trim();
495
- if (jsonStr === '[DONE]') return;
496
-
497
- try {
498
- const data = this.safeJsonParse(jsonStr);
499
- this.processContentDelta(data.choices[0].delta);
500
- } catch (e) {
501
- console.warn('JSON解析跳过:', e.message);
502
- }
503
- });
504
- },
505
- safeJsonParse(jsonStr) {
506
- try {
507
- return JSON.parse(jsonStr);
508
- } catch (error) {
509
- console.warn('JSON parse failed:', jsonStr);
510
- return null;
511
- }
512
- },
513
- processContentDelta(delta) {
514
- const content = delta.content || '';
515
- if (!content || !this.currentMessage) return;
516
-
517
- for (let i = 0; i < content.length; i++) {
518
- const char = content[i];
519
-
520
- // 处理正在拼接的标签
521
- if (this.inTag) {
522
- this.tagBuilder += char;
523
-
524
- if (char === '>') {
525
- const tag = this.tagBuilder;
526
-
527
- if (tag === '<think>') {
528
- this.avaterStatus = 'thinking'
529
- } else if (tag === '</think>') {
530
- this.avaterStatus = 'output'
531
- } else {
532
- console.log('无效标签:', tag)
533
- }
534
-
535
- // 重置
536
- this.inTag = false;
537
- this.tagBuilder = '';
538
- }
539
- continue;
540
- }
541
-
542
- // 检测是否是标签开始
543
- if (char === '<') {
544
- this.inTag = true;
545
- this.tagBuilder = '<';
546
- continue;
547
- }
548
-
549
- // 正常字符处理
550
- if (this.avaterStatus === 'thinking') {
551
- this.currentMessage.thinking += char;
552
- } else {
553
- this.currentMessage.content += char;
554
- }
555
- }
556
-
557
- this.$forceUpdate();
558
- },
559
- processAudio(event) {
560
- if (!this.isRecording) return;
561
- // 5. 获取音频数据并处理
562
- const inputData = event.inputBuffer.getChannelData(0);
563
-
564
- // 累积音频数据
565
- const tempBuffer = new Float32Array(this.audioBuffer.length + inputData.length);
566
- tempBuffer.set(this.audioBuffer, 0);
567
- tempBuffer.set(inputData, this.audioBuffer.length);
568
- this.audioBuffer = tempBuffer;
569
-
570
- // 当累积足够一帧时发送
571
- while (this.audioBuffer.length >= FRAME_SIZE) {
572
- const frame = this.audioBuffer.slice(0, FRAME_SIZE);
573
- this.audioBuffer = this.audioBuffer.slice(FRAME_SIZE);
574
-
575
- // 转换为16位PCM
576
- const pcmData = this.floatTo16BitPCM(frame);
577
-
578
- // 通过WebSocket发送
579
- if (this.ws && this.ws.readyState === this.ws.OPEN) {
580
- this.ws.send(pcmData);
581
- }
582
- }
583
- },
584
- floatTo16BitPCM(input) {
585
- const output = new Int16Array(input.length);
586
- for (let i = 0; i < input.length; i++) {
587
- const s = Math.max(-1, Math.min(1, input[i]));
588
- output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
589
- }
590
- return output.buffer;
591
- },
592
95
  toggleWindow() {
593
96
  this.visible = !this.visible
594
97
  },
595
- showWindow() {
596
- this.visible = true
98
+ handleThinkingClick() {
99
+ this.thinkStatus = !this.thinkStatus
597
100
  },
598
- hideWindow() {
101
+ handleOverlayClick() {
599
102
  this.visible = false
600
103
  },
601
- scrollToBottom() {
602
- this.$nextTick(() => {
603
- const chatArea = this.$refs.chatArea
604
- if (chatArea) {
605
- chatArea.scrollTop = chatArea.scrollHeight
606
- }
607
- })
608
- },
609
- handleKeyDown(e) {
610
- if (e.key === 'Enter' && !e.shiftKey) {
611
- e.preventDefault()
612
- this.handleSend()
613
- }
614
- },
615
- stopRecording() {
616
- if (!this.isRecording) return;
617
- if (this.microphone) this.microphone.disconnect();
618
- if (this.processor) this.processor.disconnect();
619
- if (this.analyser) this.analyser.disconnect();
620
- if (this.audioContext) {
621
- this.audioContext.close().catch(e => {
622
- console.error("关闭音频上下文失败:", e);
623
- });
624
- }
625
- this.isRecording = false;
626
- },
627
- // 添加到body的处理函数
628
104
  appendToBodyHandler() {
629
- // 确保DOM已经渲染完成
630
105
  this.$nextTick(() => {
631
- // 检查元素是否已经在body中
632
106
  if (this.$el.parentElement !== document.body) {
633
- // 将组件的根元素移动到body中
634
107
  document.body.appendChild(this.$el)
635
108
  }
636
109
  })
637
110
  },
638
- // 处理点击遮罩层事件
639
- handleOverlayClick() {
640
- this.visible = false
641
- },
642
- },
643
- watch: {
644
- messages: {
645
- handler() {
646
- this.scrollToBottom()
647
- },
648
- deep: true,
111
+ // 音频时间更新处理
112
+ onTimeUpdate() {
113
+ const audio = this.$refs.audioPlayer
114
+ const currentTime = audio.currentTime
115
+
116
+ if (!this.jumpedTimePoints) {
117
+ this.jumpedTimePoints = new Set()
118
+ }
119
+
120
+ fetch('/minio/lingxiaoai/timeJumpPoints.json')
121
+ .then(response => response.json())
122
+ .then(data => {
123
+ console.log('时间跳转点:', data)
124
+ data.forEach(point => {
125
+ if (currentTime >= point.time && currentTime < point.time + 0.5 && !this.jumpedTimePoints.has(point.time)) {
126
+ this.jumpedTimePoints.add(point.time)
127
+ this.$appOptions.store.dispatch('tags/addTagview', {
128
+ path: point.url,
129
+ fullPath: point.url,
130
+ label: point.title,
131
+ name: point.title,
132
+ meta: { title: point.title },
133
+ query: {},
134
+ params: {}
135
+ })
136
+ this.$appOptions.router.push({ path: point.url })
137
+ }
138
+ })
139
+ })
140
+ .catch(error => console.error('获取时间跳转点失败:', error))
649
141
  },
650
- },
142
+ onAudioEnded() {
143
+ this.robotStatus = 'leaving'
144
+ this.jumpedTimePoints.clear()
145
+ }
146
+ }
651
147
  }
652
148
  </script>
149
+
653
150
  <style scoped>
654
151
  .hidden-audio {
655
152
  position: absolute;
@@ -657,266 +154,11 @@ export default {
657
154
  opacity: 0;
658
155
  pointer-events: none;
659
156
  }
660
- .chat-overlay {
661
- position: fixed;
662
- top: 0;
663
- left: 0;
664
- width: 100%;
665
- height: 100%;
666
- background: transparent;
667
- z-index: 10000;
668
- }
669
157
 
670
- ::v-deep .el-textarea__inner {
671
- border: none !important;
672
- padding: 0 5px;
673
- font-family: 'PingFang SC' !important;
674
- }
675
- ::v-deep .el-textarea__inner::-webkit-scrollbar {
676
- width: 6px;
677
- height: 6px;
678
- }
679
- ::v-deep .el-textarea__inner::-webkit-scrollbar-thumb {
680
- background: rgba(0, 0, 0, 0.1);
681
- border-radius: 3px;
682
- }
683
- ::v-deep .el-textarea__inner::-webkit-scrollbar-track {
684
- background: transparent;
685
- }
686
- .scrollbar-hide::-webkit-scrollbar {
687
- display: none;
688
- }
689
- .scrollbar-hide {
690
- -ms-overflow-style: none;
691
- scrollbar-width: none;
692
- }
693
158
  .chat {
694
159
  position: fixed;
695
160
  bottom: 20px;
696
161
  right: 10px;
697
162
  z-index: 10001;
698
163
  }
699
- .chat-ai {
700
- display: flex;
701
- width: 38px;
702
- padding: 2px 2px 9px 2px;
703
- flex-direction: column;
704
- align-items: center;
705
- gap: 6px;
706
- border-radius: 40px;
707
- background: linear-gradient(180deg, #3e5beb 0%, #5ca5f9 100%);
708
- box-shadow: 0 2px 11.6px 0 rgba(0, 0, 0, 0.1);
709
- cursor: pointer;
710
- user-select: none;
711
- }
712
- .chat-robot {
713
- width: 150px;
714
- height: 200px;
715
- }
716
- .chat-robot.entering {
717
- background-image: url('./assets/entering.png');
718
- background-size: cover;
719
- }
720
- .chat-robot.waiting {
721
- background-image: url('./assets/waiting.png');
722
- background-size: cover;
723
- }
724
- .chat-robot.speaking {
725
- background-image: url('./assets/speaking.png');
726
- background-size: cover;
727
- }
728
- .chat-ai-avater {
729
- border-radius: 67px;
730
- border: 1px solid #0f66e4;
731
- background: #124087;
732
- display: flex;
733
- width: 34px;
734
- height: 33px;
735
- padding: 2px 2px 1px 2px;
736
- justify-content: center;
737
- align-items: center;
738
- background-size: cover;
739
- background-position: center;
740
- }
741
- .chat-ai-avater.normal {
742
- background-image: url('./assets/normal.png');
743
- }
744
- .chat-ai-avater.thinking {
745
- background-image: url('./assets/thinking.png');
746
- }
747
- .chat-ai-avater.output {
748
- background-image: url('./assets/output.png');
749
- }
750
- .chat-ai-text {
751
- color: #fff;
752
- font-family: 'PingFang SC';
753
- font-size: 16px;
754
- font-style: normal;
755
- font-weight: 500;
756
- line-height: 20px;
757
- align-self: stretch;
758
- display: flex;
759
- align-items: center;
760
- width: 20px;
761
- margin: 0 auto;
762
- text-align: center;
763
- }
764
- .chat-window-message-user {
765
- display: flex;
766
- justify-content: flex-end;
767
- }
768
- .user-message {
769
- max-width: 80%;
770
- display: flex;
771
- padding: 8px 12px;
772
- justify-content: center;
773
- align-items: center;
774
- gap: 10px;
775
- border-radius: 12px;
776
- background: #e3ecff;
777
- }
778
- .chat-window-bar {
779
- display: flex;
780
- justify-content: flex-end;
781
- padding: 8px;
782
- }
783
- .chat-window-message-ai {
784
- display: flex;
785
- gap: 12px;
786
- align-items: flex-start;
787
- max-width: 100%;
788
- border-radius: 12px;
789
- background: #f6f8fc;
790
- padding: 8px 12px;
791
- }
792
- .chat-window {
793
- width: 480px;
794
- height: 740px;
795
- border-radius: 14px;
796
- background: #fff;
797
- box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.16);
798
- position: absolute;
799
- bottom: 20px;
800
- right: 60px;
801
- overflow: hidden;
802
- display: flex;
803
- flex-direction: column;
804
- }
805
- .chat-window-header {
806
- display: flex;
807
- justify-content: space-between;
808
- align-items: center;
809
- border-bottom: 1px solid #eaeaea;
810
- background: #fff;
811
- padding: 16px;
812
- flex-shrink: 0;
813
- }
814
- .chat-window-content {
815
- flex: 1;
816
- padding: 16px;
817
- overflow-y: auto;
818
- display: flex;
819
- flex-direction: column;
820
- gap: 8px;
821
- }
822
- .chat-window-header-title {
823
- color: #29414e;
824
- font-size: 18px;
825
- font-weight: 900;
826
- position: relative;
827
- padding-left:36px;
828
- }
829
- .chat-window-header-title::before {
830
- content: '';
831
- position: absolute;
832
- left: 0;
833
- top: 50%;
834
- transform: translateY(-50%);
835
- width: 32px;
836
- height: 32px;
837
- background-image: url('./assets/logo.png');
838
- background-size: cover;
839
- }
840
- .chat-window-header-close {
841
- width: 24px;
842
- height: 24px;
843
- overflow: hidden;
844
- cursor: pointer;
845
- }
846
- .chat-window-footer {
847
- padding: 16px;
848
- }
849
- .chat-window-textarea {
850
- min-height: 99px;
851
- max-height: 180px;
852
- border-radius: 8px;
853
- border: 1px solid #f2f2f2;
854
- background: #fff;
855
- display: flex;
856
- flex-direction: column;
857
- }
858
- .chat-window-input {
859
- padding: 10px 12px;
860
- font-size: 16px;
861
- font-family: 'PingFang SC';
862
- }
863
- .chat-window-send {
864
- width: 70px;
865
- height: 36px;
866
- flex-shrink: 0;
867
- border-radius: 6px;
868
- background: rgba(43, 128, 246, 0.1);
869
- display: flex;
870
- align-items: center;
871
- justify-content: center;
872
- cursor: pointer;
873
- }
874
- .ai-thinking-time {
875
- border-radius: 9px;
876
- background: #ECEDF4;
877
- color: #86909C;
878
- font-family: "Alibaba PuHuiTi 2.0";
879
- font-size: 16px;
880
- font-style: normal;
881
- font-weight: 500;
882
- padding: 0 26px 0 26px;
883
- height: 28px;
884
- box-sizing: border-box;
885
- display: inline-flex;
886
- align-items: center;
887
- position: relative;
888
- user-select: none;
889
- cursor: pointer;
890
- }
891
- .ai-thinking-time::before {
892
- content: '';
893
- position: absolute;
894
- left: 6px;
895
- top: 50%;
896
- transform: translateY(-50%);
897
- width: 16px;
898
- height: 16px;
899
- background: url('./assets/think.png') no-repeat;
900
- background-size: cover;
901
- }
902
- .ai-thinking-time::after {
903
- content: '';
904
- position: absolute;
905
- right: 6px;
906
- top: 50%;
907
- transform: translateY(-50%);
908
- width: 16px;
909
- height: 16px;
910
- background: url('./assets/arrow.png') no-repeat;
911
- background-size: cover;
912
- }
913
- .ai-thinking-content {
914
- color: #86909C;
915
- font-family: "Alibaba PuHuiTi 2.0";
916
- font-size: 14px;
917
- font-style: normal;
918
- font-weight: 400;
919
- line-height: 24px;
920
- padding: 8px 0 8px 12px;
921
- }
922
- </style>
164
+ </style>