byt-lingxiao-ai 0.2.4 → 0.2.6
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 +109 -0
- package/components/ChatAvatar.vue +86 -0
- package/components/ChatInputBox.vue +127 -0
- package/components/ChatMessageList.vue +81 -0
- package/components/ChatRobot.vue +38 -0
- package/components/ChatWindow.vue +78 -819
- package/components/ChatWindowDialog.vue +82 -0
- package/components/ChatWindowHeader.vue +98 -0
- package/components/UserMessage.vue +35 -0
- package/components/mixins/audioMixin.js +122 -0
- package/components/mixins/messageMixin.js +158 -0
- package/components/mixins/webSocketMixin.js +88 -0
- package/components/utils/StreamParser.js +277 -0
- package/dist/img/output.85c6bd8b.png +0 -0
- package/dist/img/thinking.05f29a84.png +0 -0
- package/dist/index.common.js +1363 -450
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.umd.js +1363 -450
- 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 +1 -1
|
@@ -10,118 +10,53 @@
|
|
|
10
10
|
<source :src="audioSrc" type="audio/mpeg">
|
|
11
11
|
您的浏览器不支持音频元素。
|
|
12
12
|
</audio>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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,43 @@ export default {
|
|
|
130
65
|
},
|
|
131
66
|
data() {
|
|
132
67
|
return {
|
|
133
|
-
audioSrc: '/minio/lingxiaoai/byt.mp3',
|
|
134
|
-
inputMessage: '',
|
|
135
|
-
visible: false,
|
|
68
|
+
audioSrc: '/minio/lingxiaoai/byt.mp3',
|
|
69
|
+
inputMessage: '',
|
|
70
|
+
visible: false,
|
|
136
71
|
messages: [
|
|
137
72
|
{
|
|
138
73
|
id: 1,
|
|
139
74
|
type: 'user',
|
|
140
75
|
sender: '',
|
|
141
76
|
time: '',
|
|
142
|
-
content: '
|
|
77
|
+
content: '你好,欢迎来到凌霄大模型AI对话。',
|
|
143
78
|
},
|
|
144
79
|
{
|
|
145
80
|
id: 2,
|
|
146
81
|
type: 'ai',
|
|
147
82
|
sender: 'AI',
|
|
148
83
|
time: '',
|
|
149
|
-
thinking: '
|
|
150
|
-
charts: [
|
|
151
|
-
|
|
152
|
-
title: '',
|
|
153
|
-
options: {}
|
|
154
|
-
}
|
|
155
|
-
],
|
|
156
|
-
content: '回转窑(Rotary Kiln)是一种长筒形旋转煅烧设备(类似倾斜安装的大管子),因其独特的旋转运动和高温耐火衬里设计,在多个工业领域都有广泛应用',
|
|
84
|
+
thinking: '嗯,用户问的是回转窑的工业应用。首先,我需要回忆一下之前对话的内容。用户之前让我解释了水泥的制作流程,特别是提到了回转窑在高温煅烧熟料中的作用。',
|
|
85
|
+
charts: [{ title: '', options: {} }],
|
|
86
|
+
content: '回转窑(Rotary Kiln)是一种长筒形旋转煅烧设备(类似倾斜安装的大管子),因其独特的旋转运动和高温耐火衬里设计,在多个工业领域都有广泛应用',
|
|
157
87
|
}
|
|
158
|
-
],
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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]
|
|
88
|
+
],
|
|
89
|
+
robotStatus: 'leaving',
|
|
90
|
+
avaterStatus: 'normal',
|
|
91
|
+
currentMessage: null,
|
|
92
|
+
thinkStatus: true,
|
|
93
|
+
jumpedTimePoints: new Set(),
|
|
94
|
+
SAMPLE_RATE,
|
|
95
|
+
FRAME_SIZE
|
|
186
96
|
}
|
|
187
97
|
},
|
|
188
98
|
mounted() {
|
|
189
99
|
this.initWebSocket()
|
|
190
|
-
|
|
191
|
-
// 处理append-to-body逻辑
|
|
192
100
|
if (this.appendToBody) {
|
|
193
101
|
this.appendToBodyHandler()
|
|
194
102
|
}
|
|
195
103
|
},
|
|
196
|
-
unmounted() {
|
|
197
|
-
this.closeWebSocket()
|
|
198
|
-
this.stopRecording()
|
|
199
|
-
},
|
|
200
104
|
beforeDestroy() {
|
|
201
|
-
// 组件销毁前,如果元素被移动到body中,需要移除
|
|
202
105
|
if (this.appendToBody && this.$el.parentElement === document.body) {
|
|
203
106
|
document.body.removeChild(this.$el)
|
|
204
107
|
}
|
|
@@ -206,12 +109,23 @@ export default {
|
|
|
206
109
|
this.stopRecording()
|
|
207
110
|
},
|
|
208
111
|
methods: {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
112
|
+
toggleWindow() {
|
|
113
|
+
this.visible = !this.visible
|
|
114
|
+
},
|
|
115
|
+
handleThinkingClick() {
|
|
116
|
+
this.thinkStatus = !this.thinkStatus
|
|
213
117
|
},
|
|
214
|
-
|
|
118
|
+
handleOverlayClick() {
|
|
119
|
+
this.visible = false
|
|
120
|
+
},
|
|
121
|
+
appendToBodyHandler() {
|
|
122
|
+
this.$nextTick(() => {
|
|
123
|
+
if (this.$el.parentElement !== document.body) {
|
|
124
|
+
document.body.appendChild(this.$el)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
},
|
|
128
|
+
// 音频时间更新处理
|
|
215
129
|
onTimeUpdate() {
|
|
216
130
|
const audio = this.$refs.audioPlayer
|
|
217
131
|
const currentTime = audio.currentTime
|
|
@@ -225,431 +139,31 @@ export default {
|
|
|
225
139
|
{ time: 65, url: '/permission/menu', name: 'permission_menu', title: '菜单管理' },
|
|
226
140
|
{ time: 75, url: '/permission/role', name: 'permission_role', title: '角色管理' }
|
|
227
141
|
]
|
|
228
|
-
|
|
142
|
+
|
|
229
143
|
timeJumpPoints.forEach(point => {
|
|
230
|
-
// 使用一定的误差范围,确保不会因为播放进度的微小差异而错过跳转点
|
|
231
144
|
if (currentTime >= point.time && currentTime < point.time + 0.5 && !this.jumpedTimePoints.has(point.time)) {
|
|
232
145
|
this.jumpedTimePoints.add(point.time)
|
|
233
|
-
console.log(`到达时间点 ${point.time} 秒,跳转到 ${point.title}`)
|
|
234
146
|
this.$appOptions.store.dispatch('tags/addTagview', {
|
|
235
147
|
path: point.url,
|
|
236
148
|
fullPath: point.url,
|
|
237
149
|
label: point.title,
|
|
238
150
|
name: point.title,
|
|
239
|
-
meta: {
|
|
240
|
-
title: point.title
|
|
241
|
-
},
|
|
151
|
+
meta: { title: point.title },
|
|
242
152
|
query: {},
|
|
243
153
|
params: {}
|
|
244
154
|
})
|
|
245
|
-
this.$appOptions.router.push({
|
|
246
|
-
path: point.url
|
|
247
|
-
})
|
|
155
|
+
this.$appOptions.router.push({ path: point.url })
|
|
248
156
|
}
|
|
249
157
|
})
|
|
250
|
-
|
|
251
|
-
console.log('当前播放时间:', currentTime)
|
|
252
|
-
},
|
|
253
|
-
play() {
|
|
254
|
-
this.robotStatus = 'speaking'
|
|
255
|
-
this.$refs.audioPlayer.play()
|
|
256
158
|
},
|
|
257
|
-
|
|
258
|
-
this.robotStatus = 'waiting'
|
|
259
|
-
this.$refs.audioPlayer.pause()
|
|
260
|
-
},
|
|
261
|
-
stop() {
|
|
159
|
+
onAudioEnded() {
|
|
262
160
|
this.robotStatus = 'leaving'
|
|
263
|
-
this.$refs.audioPlayer.pause()
|
|
264
|
-
this.$refs.audioPlayer.currentTime = 0
|
|
265
161
|
this.jumpedTimePoints.clear()
|
|
266
|
-
}
|
|
267
|
-
|
|
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
|
-
toggleWindow() {
|
|
593
|
-
this.visible = !this.visible
|
|
594
|
-
},
|
|
595
|
-
showWindow() {
|
|
596
|
-
this.visible = true
|
|
597
|
-
},
|
|
598
|
-
hideWindow() {
|
|
599
|
-
this.visible = false
|
|
600
|
-
},
|
|
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
|
-
appendToBodyHandler() {
|
|
629
|
-
// 确保DOM已经渲染完成
|
|
630
|
-
this.$nextTick(() => {
|
|
631
|
-
// 检查元素是否已经在body中
|
|
632
|
-
if (this.$el.parentElement !== document.body) {
|
|
633
|
-
// 将组件的根元素移动到body中
|
|
634
|
-
document.body.appendChild(this.$el)
|
|
635
|
-
}
|
|
636
|
-
})
|
|
637
|
-
},
|
|
638
|
-
// 处理点击遮罩层事件
|
|
639
|
-
handleOverlayClick() {
|
|
640
|
-
this.visible = false
|
|
641
|
-
},
|
|
642
|
-
},
|
|
643
|
-
watch: {
|
|
644
|
-
messages: {
|
|
645
|
-
handler() {
|
|
646
|
-
this.scrollToBottom()
|
|
647
|
-
},
|
|
648
|
-
deep: true,
|
|
649
|
-
},
|
|
650
|
-
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
651
164
|
}
|
|
652
165
|
</script>
|
|
166
|
+
|
|
653
167
|
<style scoped>
|
|
654
168
|
.hidden-audio {
|
|
655
169
|
position: absolute;
|
|
@@ -657,266 +171,11 @@ export default {
|
|
|
657
171
|
opacity: 0;
|
|
658
172
|
pointer-events: none;
|
|
659
173
|
}
|
|
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
174
|
|
|
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
175
|
.chat {
|
|
694
176
|
position: fixed;
|
|
695
177
|
bottom: 20px;
|
|
696
178
|
right: 10px;
|
|
697
179
|
z-index: 10001;
|
|
698
180
|
}
|
|
699
|
-
|
|
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>
|
|
181
|
+
</style>
|