yt-chat-components 1.5.1 → 1.5.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.
- package/package.json +1 -1
- package/src/YtChatView/chatWidget/chatWindow/callInterface/StreamAudioPlayer.tsx +194 -0
- package/src/YtChatView/chatWidget/chatWindow/callInterface/index.tsx +105 -29
- package/src/YtChatView/chatWidget/chatWindow/controllers/index.ts +9 -0
- package/src/YtChatView/chatWidget/chatWindow/index.tsx +39 -28
- package/src/YtChatView/components/TabSelector/index.jsx +15 -18
- package/src/YtChatView/previewDialogV2/index.jsx +62 -11
package/package.json
CHANGED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
export class StreamAudioPlayer {
|
|
3
|
+
private audioContext: AudioContext;
|
|
4
|
+
private audioQueue: AudioBuffer[] = [];
|
|
5
|
+
private pendingBuffer: Float32Array[] = [];
|
|
6
|
+
private pendingBufferSizeSec = 0;
|
|
7
|
+
|
|
8
|
+
private isPlaying = false;
|
|
9
|
+
private lastPlaybackTime = 0;
|
|
10
|
+
private FADE_MS = 50; // 渐入/渐出时间(毫秒)
|
|
11
|
+
private FADE_SAMPLES = 50;
|
|
12
|
+
|
|
13
|
+
private onPlaybackCompleteCallback: (() => void) | null = null;
|
|
14
|
+
private source: AudioBufferSourceNode | null = null;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext;
|
|
18
|
+
// 强制创建 16000 Hz 的音频上下文
|
|
19
|
+
const contextOptions = {
|
|
20
|
+
sampleRate: 16000
|
|
21
|
+
};
|
|
22
|
+
this.audioContext = new AudioContextClass(contextOptions);
|
|
23
|
+
// 确保 FADE_SAMPLES 正确计算
|
|
24
|
+
this.FADE_SAMPLES = Math.floor((this.audioContext.sampleRate * this.FADE_MS) / 1000);
|
|
25
|
+
// console.log(`[音频] 使用采样率: ${this.audioContext.sampleRate} Hz`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public get context(): AudioContext {
|
|
29
|
+
return this.audioContext;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 添加 PCM 音频数据到播放队列中
|
|
34
|
+
* @param audioData base64 编码的 PCM 数据
|
|
35
|
+
*/
|
|
36
|
+
public enqueueAudioChunk(audioData: string): void {
|
|
37
|
+
try {
|
|
38
|
+
const byteCharacters = atob(audioData);
|
|
39
|
+
const pcmBytes = new Uint8Array(byteCharacters.length);
|
|
40
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
41
|
+
pcmBytes[i] = byteCharacters.charCodeAt(i);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sampleCount = pcmBytes.length / 2;
|
|
45
|
+
const durationSec = sampleCount / this.audioContext.sampleRate;
|
|
46
|
+
|
|
47
|
+
const dataView = new DataView(pcmBytes.buffer);
|
|
48
|
+
const channelData = new Float32Array(sampleCount);
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
51
|
+
const int16 = dataView.getInt16(i * 2, true); // 小端读取有符号 16bit
|
|
52
|
+
let value = int16 / 32768.0;
|
|
53
|
+
|
|
54
|
+
// 渐入渐出处理
|
|
55
|
+
if (i < this.FADE_SAMPLES) {
|
|
56
|
+
value *= i / this.FADE_SAMPLES;
|
|
57
|
+
}
|
|
58
|
+
if (i >= sampleCount - this.FADE_SAMPLES) {
|
|
59
|
+
value *= (sampleCount - i) / this.FADE_SAMPLES;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
channelData[i] = value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 合并小块
|
|
66
|
+
this.pendingBuffer.push(channelData);
|
|
67
|
+
this.pendingBufferSizeSec += durationSec;
|
|
68
|
+
|
|
69
|
+
// 如果累计 ≥ 0.5s 才创建 buffer 并推入队列
|
|
70
|
+
if (this.pendingBufferSizeSec >= 0.5) {
|
|
71
|
+
const totalLength = this.pendingBuffer.reduce((sum, b) => sum + b.length, 0);
|
|
72
|
+
const mergedChannel = new Float32Array(totalLength);
|
|
73
|
+
|
|
74
|
+
let offset = 0;
|
|
75
|
+
for (const ch of this.pendingBuffer) {
|
|
76
|
+
mergedChannel.set(ch, offset);
|
|
77
|
+
offset += ch.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const audioBuffer = this.audioContext.createBuffer(
|
|
81
|
+
1,
|
|
82
|
+
mergedChannel.length,
|
|
83
|
+
this.audioContext.sampleRate
|
|
84
|
+
);
|
|
85
|
+
audioBuffer.copyToChannel(mergedChannel, 0);
|
|
86
|
+
this.audioQueue.push(audioBuffer);
|
|
87
|
+
|
|
88
|
+
// 清空缓存
|
|
89
|
+
this.pendingBuffer = [];
|
|
90
|
+
this.pendingBufferSizeSec = 0;
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error('[音频] 解码音频失败:', e);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 开始播放音频队列
|
|
99
|
+
* @param onComplete 所有音频播放完成后的回调
|
|
100
|
+
*/
|
|
101
|
+
public play(onComplete?: () => void): void {
|
|
102
|
+
if (this.isPlaying) return;
|
|
103
|
+
|
|
104
|
+
this.onPlaybackCompleteCallback = onComplete || null;
|
|
105
|
+
|
|
106
|
+
if (this.audioQueue.length === 0) {
|
|
107
|
+
this.handlePlaybackComplete();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.playNextChunk();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private playNextChunk(): void {
|
|
115
|
+
if (!this.audioContext || this.isPlaying || this.audioQueue.length === 0) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const nextBuffer = this.audioQueue.shift();
|
|
120
|
+
if (!nextBuffer) {
|
|
121
|
+
this.handlePlaybackComplete();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.source = this.audioContext.createBufferSource();
|
|
126
|
+
this.source.buffer = nextBuffer;
|
|
127
|
+
this.source.connect(this.audioContext.destination);
|
|
128
|
+
|
|
129
|
+
const now = this.audioContext.currentTime;
|
|
130
|
+
const startTime = Math.max(now, this.lastPlaybackTime);
|
|
131
|
+
const bufferDuration = nextBuffer.duration;
|
|
132
|
+
|
|
133
|
+
this.source.start(startTime);
|
|
134
|
+
|
|
135
|
+
this.lastPlaybackTime = startTime + bufferDuration;
|
|
136
|
+
this.isPlaying = true;
|
|
137
|
+
|
|
138
|
+
this.source.onended = () => {
|
|
139
|
+
this.isPlaying = false;
|
|
140
|
+
this.source = null;
|
|
141
|
+
|
|
142
|
+
// 继续播放下一个 chunk
|
|
143
|
+
this.playNextChunk();
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private handlePlaybackComplete(): void {
|
|
148
|
+
if (this.onPlaybackCompleteCallback) {
|
|
149
|
+
this.onPlaybackCompleteCallback();
|
|
150
|
+
this.onPlaybackCompleteCallback = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 停止当前播放并清空队列
|
|
156
|
+
*/
|
|
157
|
+
public stop(): void {
|
|
158
|
+
if (this.source) {
|
|
159
|
+
this.source.stop();
|
|
160
|
+
this.source.disconnect();
|
|
161
|
+
this.source = null;
|
|
162
|
+
}
|
|
163
|
+
this.isPlaying = false;
|
|
164
|
+
this.audioQueue = [];
|
|
165
|
+
this.pendingBuffer = [];
|
|
166
|
+
this.pendingBufferSizeSec = 0;
|
|
167
|
+
this.lastPlaybackTime = 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 暂停当前播放,不清空队列
|
|
172
|
+
*/
|
|
173
|
+
public pause(): void {
|
|
174
|
+
this.stop();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 恢复播放
|
|
179
|
+
*/
|
|
180
|
+
public resume(): void {
|
|
181
|
+
// @ts-ignore
|
|
182
|
+
this.play(this.onPlaybackCompleteCallback);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 销毁音频上下文(页面卸载时调用)
|
|
187
|
+
*/
|
|
188
|
+
public destroy(): void {
|
|
189
|
+
this.stop();
|
|
190
|
+
if (this.audioContext && this.audioContext.close) {
|
|
191
|
+
this.audioContext.close();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import { Button,
|
|
1
|
+
import React, {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {Avatar, Button, message} from 'antd';
|
|
3
3
|
import './index.module.css';
|
|
4
|
-
import {
|
|
5
|
-
import { ChatMessageType } from '../../chatWindow/types/chatWidget';
|
|
4
|
+
import {callStyle} from './style';
|
|
6
5
|
import Recorder from 'recorder-core'
|
|
7
6
|
import 'recorder-core/src/engine/mp3'
|
|
8
7
|
import 'recorder-core/src/engine/mp3-engine'
|
|
9
8
|
// import 'recorder-core/src/extensions/waveview'
|
|
10
9
|
import ai_call_thinking from '../../../../assets/aicenter/ai_call_thinking.png'
|
|
11
10
|
import ai_call_working from '../../../../assets/aicenter/ai_call_working.gif'
|
|
11
|
+
import {StreamAudioPlayer} from "./StreamAudioPlayer";
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
interface CallInterfaceProps {
|
|
@@ -19,7 +19,7 @@ interface CallInterfaceProps {
|
|
|
19
19
|
flowId: string;
|
|
20
20
|
sessionId: React.MutableRefObject<string>;
|
|
21
21
|
hostUrl: string;
|
|
22
|
-
addMessage:
|
|
22
|
+
addMessage: Function;
|
|
23
23
|
userInfo: {
|
|
24
24
|
code: string;
|
|
25
25
|
};
|
|
@@ -63,7 +63,7 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
63
63
|
const vadProcessorRef = useRef<ScriptProcessorNode | null>(null);
|
|
64
64
|
const audioContextRef = useRef<AudioContext | null>(null);
|
|
65
65
|
const [isSpeakingNow,setIsSpeakingNow] = useState(false);
|
|
66
|
-
|
|
66
|
+
const audioPlayerInstance = new StreamAudioPlayer();
|
|
67
67
|
|
|
68
68
|
// 添加统一的WebSocket消息发送方法
|
|
69
69
|
const sendWebSocketMessage = (type: string, data: any) => {
|
|
@@ -120,6 +120,27 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
120
120
|
});
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
+
const handleAudioChunkData = async (audioData: string) => {
|
|
124
|
+
try {
|
|
125
|
+
// 添加音频片段到播放器
|
|
126
|
+
audioPlayerInstance.enqueueAudioChunk(audioData);
|
|
127
|
+
|
|
128
|
+
// 设置状态为“正在回复”
|
|
129
|
+
setWorkStatus('正在回复');
|
|
130
|
+
|
|
131
|
+
// 开始播放,并在所有音频完成后恢复状态
|
|
132
|
+
audioPlayerInstance.play(() => {
|
|
133
|
+
console.log("全部音频播放完成!");
|
|
134
|
+
setWorkStatus('正在聆听');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error('[音频] 解码音频失败:', e);
|
|
139
|
+
setWorkStatus('正在聆听');
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
|
|
123
144
|
const initWebRTC = async () => {
|
|
124
145
|
try {
|
|
125
146
|
const peerConnection = new RTCPeerConnection({
|
|
@@ -139,7 +160,7 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
139
160
|
console.log('开始连接WebSocket');
|
|
140
161
|
const ws = new WebSocket(`${hostUrl.replace('http', 'ws')}/api/v1/ws/webrtc?flow_id=${flowId}&session_id=${sessionId}&api_key=${api_key}&operator_id=${opeartorId}`);
|
|
141
162
|
wsRef.current = ws;
|
|
142
|
-
|
|
163
|
+
|
|
143
164
|
// 添加连接超时处理
|
|
144
165
|
const connectionTimeout = setTimeout(() => {
|
|
145
166
|
if (ws.readyState === WebSocket.CONNECTING) {
|
|
@@ -148,29 +169,29 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
148
169
|
ws.close();
|
|
149
170
|
}
|
|
150
171
|
}, 60000); // 60秒超时
|
|
151
|
-
|
|
172
|
+
|
|
152
173
|
// 创建和发送offer的函数
|
|
153
174
|
const createAndSendOffer = async (_peerConnection: RTCPeerConnection) => {
|
|
154
175
|
try {
|
|
155
176
|
const offer = await _peerConnection.createOffer();
|
|
156
177
|
await _peerConnection.setLocalDescription(offer);
|
|
157
|
-
|
|
178
|
+
|
|
158
179
|
sendWebSocketMessage('offer', { offer });
|
|
159
180
|
} catch (error) {
|
|
160
181
|
console.error('创建offer失败:', error);
|
|
161
182
|
}
|
|
162
183
|
};
|
|
163
|
-
|
|
184
|
+
|
|
164
185
|
ws.onopen = () => {
|
|
165
186
|
console.log('WebSocket连接已建立');
|
|
166
187
|
clearTimeout(connectionTimeout); // 清除超时定时器
|
|
167
|
-
|
|
188
|
+
|
|
168
189
|
// 只有在WebSocket连接成功后才创建和发送offer
|
|
169
190
|
if (peerConnectionRef.current) {
|
|
170
191
|
createAndSendOffer(peerConnectionRef.current);
|
|
171
192
|
}
|
|
172
193
|
};
|
|
173
|
-
|
|
194
|
+
|
|
174
195
|
const localStream = await navigator.mediaDevices.getUserMedia({
|
|
175
196
|
audio: {
|
|
176
197
|
echoCancellation: true, // 启用回声消除
|
|
@@ -180,11 +201,11 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
180
201
|
video: false
|
|
181
202
|
});
|
|
182
203
|
localStreamRef.current = localStream;
|
|
183
|
-
|
|
204
|
+
|
|
184
205
|
// if (localAudioRef.current) {
|
|
185
206
|
// localAudioRef.current.srcObject = localStream;
|
|
186
207
|
// }
|
|
187
|
-
|
|
208
|
+
|
|
188
209
|
// 设置VAD(语音活动检测)
|
|
189
210
|
const audioContext = new AudioContext();
|
|
190
211
|
audioContextRef.current = audioContext;
|
|
@@ -192,14 +213,20 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
192
213
|
analyser.fftSize = 256;
|
|
193
214
|
const bufferLength = analyser.frequencyBinCount;
|
|
194
215
|
const dataArray = new Uint8Array(bufferLength);
|
|
195
|
-
|
|
216
|
+
|
|
196
217
|
const source = audioContext.createMediaStreamSource(localStream);
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
// 创建高通滤波器
|
|
219
|
+
const highpassFilter = audioContext.createBiquadFilter();
|
|
220
|
+
highpassFilter.type = "highpass";
|
|
221
|
+
highpassFilter.frequency.setValueAtTime(100, audioContext.currentTime); // 过滤低于100Hz的声音
|
|
222
|
+
// 连接顺序:source -> highpassFilter -> analyser 和 vadProcessor
|
|
223
|
+
source.connect(highpassFilter);
|
|
224
|
+
highpassFilter.connect(analyser);
|
|
199
225
|
// 创建音频处理器用于VAD
|
|
200
226
|
const vadProcessor = audioContext.createScriptProcessor(2048, 1, 1);
|
|
227
|
+
highpassFilter.connect(vadProcessor);
|
|
201
228
|
vadProcessorRef.current = vadProcessor;
|
|
202
|
-
|
|
229
|
+
|
|
203
230
|
vadProcessor.onaudioprocess = (e) => {
|
|
204
231
|
// 正在听对方说话 or 思考,不处理
|
|
205
232
|
if (workStatus === '正在回复'){
|
|
@@ -209,16 +236,16 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
209
236
|
console.log("--------------------- 正在思考")
|
|
210
237
|
return;
|
|
211
238
|
}
|
|
212
|
-
|
|
239
|
+
|
|
213
240
|
analyser.getByteFrequencyData(dataArray);
|
|
214
|
-
|
|
241
|
+
|
|
215
242
|
// 计算音量平均值
|
|
216
243
|
let sum = 0;
|
|
217
244
|
for (let i = 0; i < bufferLength; i++) {
|
|
218
245
|
sum += dataArray[i];
|
|
219
246
|
}
|
|
220
247
|
const average = sum / bufferLength;
|
|
221
|
-
|
|
248
|
+
|
|
222
249
|
// 检测是否有语音
|
|
223
250
|
const isSpeakingNow = average > SILENCE_THRESHOLD;
|
|
224
251
|
|
|
@@ -288,6 +315,8 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
288
315
|
sampleRate:16000,
|
|
289
316
|
msgId: msgIdRef.current,
|
|
290
317
|
});
|
|
318
|
+
// 暂停当前正在播放的内容
|
|
319
|
+
audioPlayerInstance.stop()
|
|
291
320
|
console.log('发送完毕');
|
|
292
321
|
|
|
293
322
|
// 添加用户消息到聊天记录
|
|
@@ -326,16 +355,16 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
326
355
|
}
|
|
327
356
|
}
|
|
328
357
|
};
|
|
329
|
-
|
|
358
|
+
|
|
330
359
|
// 连接VAD处理器
|
|
331
360
|
source.connect(vadProcessor);
|
|
332
361
|
vadProcessor.connect(audioContext.destination);
|
|
333
|
-
|
|
362
|
+
|
|
334
363
|
// 修改WebSocket消息处理
|
|
335
364
|
ws.onmessage = async (event) => {
|
|
336
365
|
const message = JSON.parse(event.data);
|
|
337
|
-
console.log('收到消息', message);
|
|
338
|
-
|
|
366
|
+
// console.log('收到消息', message);
|
|
367
|
+
|
|
339
368
|
if (message.type === 'offer') {
|
|
340
369
|
await handleOffer(message);
|
|
341
370
|
} else if (message.type === 'answer') {
|
|
@@ -353,16 +382,62 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
353
382
|
if (message.audioData) {
|
|
354
383
|
handleAudioData(message.audioData);
|
|
355
384
|
}else {
|
|
385
|
+
console.log('收到后端语音发送结束指令:恢复聆听状态')
|
|
356
386
|
setWorkStatus('正在聆听');
|
|
357
387
|
}
|
|
388
|
+
}if (message.type === 'audio-chunk') {
|
|
389
|
+
// 处理从后端返回的音频流响应
|
|
390
|
+
msgIdRef.current = null
|
|
391
|
+
if (message.audioData) {
|
|
392
|
+
handleAudioChunkData(message.audioData);
|
|
393
|
+
}
|
|
394
|
+
}if (message.type === 'user-audio-text') {
|
|
395
|
+
// 处理从后端返回的音频转文字展示用
|
|
396
|
+
// msgIdRef.current = null
|
|
397
|
+
if (message.textResponse) {
|
|
398
|
+
console.log("user-audio-text")
|
|
399
|
+
console.log(message.textResponse)
|
|
400
|
+
// 清理当前正在回复的内容
|
|
401
|
+
audioPlayerInstance.stop()
|
|
402
|
+
// 插入语音到聊天记录
|
|
403
|
+
addMessage({
|
|
404
|
+
messageItemList: [
|
|
405
|
+
{
|
|
406
|
+
id: new Date().getTime() + "",
|
|
407
|
+
name: '[语音转文字]',
|
|
408
|
+
message: message.textResponse,
|
|
409
|
+
thinkMessage: '',
|
|
410
|
+
}
|
|
411
|
+
],
|
|
412
|
+
isSend: true,
|
|
413
|
+
receivingMessage: false,
|
|
414
|
+
host_url: hostUrl,
|
|
415
|
+
// timestamp: new Date().toISOString()
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}if (message.type === 'audio-chunk-text') {
|
|
419
|
+
// 处理从后端返回的音频转文字展示用
|
|
420
|
+
// msgIdRef.current = null
|
|
421
|
+
if (message.textResponse && addMessage) {
|
|
422
|
+
// 流式插入消息到聊天记录
|
|
423
|
+
// console.log(message.textResponse)
|
|
424
|
+
// handleMessageContent('token',{
|
|
425
|
+
// chunk: message.textResponse,
|
|
426
|
+
// name:'agent',
|
|
427
|
+
// id: '9999999999',
|
|
428
|
+
// timestamp: new Date()
|
|
429
|
+
// })
|
|
430
|
+
}
|
|
431
|
+
}if (message.type === 'complete-chunk-audio'){
|
|
432
|
+
console.log('本次语音流传输完毕')
|
|
358
433
|
}
|
|
359
434
|
};
|
|
360
|
-
|
|
435
|
+
|
|
361
436
|
// 添加本地音频轨道到对等连接
|
|
362
437
|
localStream.getTracks().forEach(track => {
|
|
363
438
|
peerConnection.addTrack(track, localStream);
|
|
364
439
|
});
|
|
365
|
-
|
|
440
|
+
|
|
366
441
|
// 处理远程流
|
|
367
442
|
peerConnection.ontrack = (event) => {
|
|
368
443
|
remoteStreamRef.current = event.streams[0];
|
|
@@ -370,7 +445,7 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
370
445
|
remoteAudioRef.current.srcObject = event.streams[0];
|
|
371
446
|
}
|
|
372
447
|
};
|
|
373
|
-
|
|
448
|
+
|
|
374
449
|
// 处理ICE候选
|
|
375
450
|
peerConnection.onicecandidate = (event) => {
|
|
376
451
|
if (event.candidate && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
@@ -490,7 +565,8 @@ const CallInterface: React.FC<CallInterfaceProps> = ({
|
|
|
490
565
|
if (localStreamRef.current) {
|
|
491
566
|
localStreamRef.current.getTracks().forEach(track => track.stop());
|
|
492
567
|
}
|
|
493
|
-
|
|
568
|
+
// 关闭音频播放
|
|
569
|
+
audioPlayerInstance.destroy();
|
|
494
570
|
onHangup();
|
|
495
571
|
};
|
|
496
572
|
|
|
@@ -247,4 +247,13 @@ export async function getFlowInfo(baseUrl: string, flowId: string, api_key: stri
|
|
|
247
247
|
timeout:600000
|
|
248
248
|
};
|
|
249
249
|
return axios.get(`${baseUrl}/api/t1/flow/detail/${flowId}`,{headers});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function getKnowledgeInfo(baseUrl: string, flowId: string, api_key: string) {
|
|
253
|
+
const headers = {
|
|
254
|
+
'Content-Type': 'multipart/form-data',
|
|
255
|
+
'x-api-key': api_key,
|
|
256
|
+
timeout:600000
|
|
257
|
+
};
|
|
258
|
+
return axios.get(`${baseUrl}/api/t1/knowledge/detail/${flowId}`,{headers});
|
|
250
259
|
}
|
|
@@ -189,7 +189,7 @@ export default function ChatWindow({
|
|
|
189
189
|
if (delayMessageList.length > 0) {
|
|
190
190
|
const data = delayMessageList.shift();
|
|
191
191
|
const {r_id, form_config} = data;
|
|
192
|
-
updateMessageItem({chunk:"\n```form\n" + JSON.stringify(form_config) + "\n```\n"})
|
|
192
|
+
updateMessageItem({updateSrc: "7", chunk:"\n```form\n" + JSON.stringify(form_config) + "\n```\n"})
|
|
193
193
|
|
|
194
194
|
// ?????? done
|
|
195
195
|
// addMessage({
|
|
@@ -206,8 +206,8 @@ export default function ChatWindow({
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
const addMessageItem = ({chunk, id, name, icon = null, status = null, isThinkChunk = false, loadingMessage = null}) => {
|
|
210
|
-
// console.log("--- addMessageItem", chunk, status, loadingMessage)
|
|
209
|
+
const addMessageItem = ({addSrc = null, chunk, id, name, icon = null, status = null, isThinkChunk = false, loadingMessage = null}) => {
|
|
210
|
+
// console.log("--- addMessageItem", addSrc, chunk, status, loadingMessage)
|
|
211
211
|
setNowAIContentList((prevState) => {
|
|
212
212
|
const content = {}
|
|
213
213
|
if(isThinkChunk){
|
|
@@ -232,8 +232,8 @@ export default function ChatWindow({
|
|
|
232
232
|
})
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
const updateMessageItem = ({chunk, status = null, isThinkChunk = false, loadingMessage = null}) => {
|
|
236
|
-
// console.log("--- updateMessageItem", chunk, status, loadingMessage)
|
|
235
|
+
const updateMessageItem = ({updateSrc = null, chunk, status = null, isThinkChunk = false, loadingMessage = null}) => {
|
|
236
|
+
// console.log("--- updateMessageItem", updateSrc, chunk, status, loadingMessage)
|
|
237
237
|
// chunk 和 status 都为空,则不更新
|
|
238
238
|
if(isEmpty(chunk) && isEmpty(status)){
|
|
239
239
|
return
|
|
@@ -261,7 +261,6 @@ export default function ChatWindow({
|
|
|
261
261
|
|
|
262
262
|
const newState = [...prevState]
|
|
263
263
|
newState[newState.length - 1] = newMessageItem
|
|
264
|
-
|
|
265
264
|
nowAIContentListRef.current = newState
|
|
266
265
|
return newState
|
|
267
266
|
})
|
|
@@ -287,12 +286,12 @@ export default function ChatWindow({
|
|
|
287
286
|
// 如果刚才在调用工具,则追加信息
|
|
288
287
|
if (content_id === 'WAIT'){
|
|
289
288
|
content_id = id
|
|
290
|
-
updateMessageItem({chunk, loadingMessage: loading_message || ''})
|
|
289
|
+
updateMessageItem({updateSrc: "1", chunk, loadingMessage: loading_message || ''})
|
|
291
290
|
}
|
|
292
291
|
// 新建信息
|
|
293
292
|
else{
|
|
294
293
|
content_id = id
|
|
295
|
-
addMessageItem({chunk, id, name, icon, loadingMessage: loading_message || ''})
|
|
294
|
+
addMessageItem({addSrc: "1", chunk, id, name, icon, loadingMessage: loading_message || ''})
|
|
296
295
|
}
|
|
297
296
|
}
|
|
298
297
|
// 输出主体没有变化
|
|
@@ -304,18 +303,18 @@ export default function ChatWindow({
|
|
|
304
303
|
}
|
|
305
304
|
|
|
306
305
|
// id切换表示一句话说完了, 新增信息
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
updateMessageItem({chunk, loadingMessage: loading_message || ''})
|
|
318
|
-
|
|
306
|
+
if (content_id !== id){
|
|
307
|
+
content_id = id
|
|
308
|
+
|
|
309
|
+
// 如果 event_latest 是 token,此刻变成了 t_full_token,表示之前都在输出占位符,所以更新信息
|
|
310
|
+
if (event_latest === 'token' && event === 't_full_token'){
|
|
311
|
+
updateMessageItem({updateSrc: "2", chunk, loadingMessage: loading_message || ''})
|
|
312
|
+
} else {
|
|
313
|
+
addMessageItem({addSrc: "2", chunk, id, name, icon, loadingMessage:loading_message})
|
|
314
|
+
}
|
|
315
|
+
}else{
|
|
316
|
+
updateMessageItem({updateSrc: "3", chunk, loadingMessage: loading_message || ''})
|
|
317
|
+
}
|
|
319
318
|
}
|
|
320
319
|
|
|
321
320
|
if (lastMessage.current) {
|
|
@@ -323,16 +322,16 @@ export default function ChatWindow({
|
|
|
323
322
|
}
|
|
324
323
|
}
|
|
325
324
|
else if (event == 't_token') {
|
|
326
|
-
let { chunk, id, r_id, ns, name, loading_message } = data
|
|
325
|
+
let { chunk, id, r_id, ns, name, icon, loading_message } = data
|
|
327
326
|
|
|
328
327
|
// ns切换表示切换了智能体
|
|
329
328
|
if (content_ns !== ns){
|
|
330
329
|
content_ns = ns
|
|
331
330
|
// 新建信息
|
|
332
331
|
content_id = id
|
|
333
|
-
addMessageItem({chunk, id, name, icon, status:null, isThinkChunk:true, loadingMessage: loading_message || ''})
|
|
332
|
+
addMessageItem({addSrc: "3", chunk, id, name, icon, status:null, isThinkChunk:true, loadingMessage: loading_message || ''})
|
|
334
333
|
}else {
|
|
335
|
-
updateMessageItem({chunk, status:null, isThinkChunk:true, loadingMessage: loading_message || ''})
|
|
334
|
+
updateMessageItem({updateSrc: "4", chunk, status:null, isThinkChunk:true, loadingMessage: loading_message || ''})
|
|
336
335
|
}
|
|
337
336
|
|
|
338
337
|
if (lastMessage.current) {
|
|
@@ -341,23 +340,23 @@ export default function ChatWindow({
|
|
|
341
340
|
}
|
|
342
341
|
else if (event == 'status') {
|
|
343
342
|
// 更新状态
|
|
344
|
-
const {r_id, id, ns, name, status, loading_message} = data
|
|
343
|
+
const {r_id, id, ns, name, icon, status, loading_message} = data
|
|
345
344
|
|
|
346
345
|
// ns变化表示切换了智能体,表示智能体一上来就调用工具
|
|
347
346
|
if (content_ns !== ns){
|
|
348
347
|
content_ns = ns
|
|
349
348
|
// 表示智能体连续调用工具
|
|
350
349
|
if (content_id === 'WAIT') {
|
|
351
|
-
updateMessageItem({chunk:"", status, loadingMessage: loading_message || ''})
|
|
350
|
+
updateMessageItem({updateSrc: "5", chunk:"", status, loadingMessage: loading_message || ''})
|
|
352
351
|
}else{
|
|
353
352
|
// 这个时候还没有信息的id,所以 content_id 给特殊值
|
|
354
353
|
content_id = "WAIT"
|
|
355
|
-
addMessageItem({chunk:"", id: "WAIT", name, icon, status})
|
|
354
|
+
addMessageItem({addSrc: "4", chunk:"", id: "WAIT", name, icon, status})
|
|
356
355
|
}
|
|
357
356
|
}
|
|
358
357
|
// 当前智能体在调用工具
|
|
359
358
|
else{
|
|
360
|
-
updateMessageItem({chunk:"", status, loadingMessage: loading_message || ''})
|
|
359
|
+
updateMessageItem({updateSrc: "6", chunk:"", status, loadingMessage: loading_message || ''})
|
|
361
360
|
}
|
|
362
361
|
|
|
363
362
|
setAiStatus(data.status)
|
|
@@ -635,6 +634,15 @@ export default function ChatWindow({
|
|
|
635
634
|
// 处理挂断电话
|
|
636
635
|
const handleHangup = () => {
|
|
637
636
|
setShowCallInterface(false);
|
|
637
|
+
|
|
638
|
+
fetchChatHistory().then();
|
|
639
|
+
|
|
640
|
+
setTimeout(() => {
|
|
641
|
+
if (lastMessage.current) {
|
|
642
|
+
console.error('开始滚动最后一条消息到可视区域')
|
|
643
|
+
lastMessage.current.scrollIntoView({ behavior: 'smooth' });
|
|
644
|
+
}
|
|
645
|
+
}, 600);
|
|
638
646
|
};
|
|
639
647
|
|
|
640
648
|
/**
|
|
@@ -1361,8 +1369,11 @@ export default function ChatWindow({
|
|
|
1361
1369
|
flowId={flowId}
|
|
1362
1370
|
sessionId={sessionId}
|
|
1363
1371
|
hostUrl={hostUrl}
|
|
1364
|
-
onMessage={addMessage}
|
|
1365
1372
|
userInfo={userInfo}
|
|
1373
|
+
addMessage={e => {
|
|
1374
|
+
// addMessage(e)
|
|
1375
|
+
setDropDownList([])
|
|
1376
|
+
}}
|
|
1366
1377
|
/>
|
|
1367
1378
|
:
|
|
1368
1379
|
<div className="cl-middle-container">
|
|
@@ -16,19 +16,17 @@ const defaultMap = {
|
|
|
16
16
|
'就业实习': ['是否提供实习机会?', '就业指导服务如何?', '有哪些合作企业?', '实习是否计入学分?', '就业率有多高?', '毕业生去向有哪些?', '能否推荐实习单位?', '是否有职业规划课?', '能考研继续深造吗?', '就业双选会多不多?'],
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
const TabSelector = ({
|
|
20
|
-
const [activeKey, setActiveKey] = useState(
|
|
19
|
+
const TabSelector = ({ customUrl, handleRowClick, welcomeWords, isSimple, dataList = [] }) => {
|
|
20
|
+
const [activeKey, setActiveKey] = useState(dataList[0]?.category);
|
|
21
21
|
const [valuePage, setValuePage] = useState(0);
|
|
22
22
|
const [tabLocation, setTabLocation] = useState(0);
|
|
23
23
|
const valuesPerPage = 5; // Number of values to show per page
|
|
24
24
|
const [pageLoading, setPageLoading] = useState(false);
|
|
25
25
|
const keyShowSize = isSimple ? 2 : 4;
|
|
26
26
|
|
|
27
|
-
const keys =
|
|
28
|
-
const values = map[
|
|
29
|
-
|
|
27
|
+
const keys = dataList.map(e=>e.category)||[];
|
|
28
|
+
const values = dataList.filter(e => activeKey === e.category).map(e => e.data)[0] || [];
|
|
30
29
|
const visibleValues = isSimple ? values : values.slice(valuePage * valuesPerPage, (valuePage + 1) * valuesPerPage);
|
|
31
|
-
|
|
32
30
|
const handleTabChange = (key) => {
|
|
33
31
|
setActiveKey(key);
|
|
34
32
|
setValuePage(0); // Reset value page when changing tabs
|
|
@@ -136,22 +134,21 @@ const TabSelector = ({ map = defaultMap, customUrl, handleRowClick, welcomeWords
|
|
|
136
134
|
/>
|
|
137
135
|
</div>
|
|
138
136
|
<div className={styles.valueList}>
|
|
139
|
-
{visibleValues.map((item, index) =>
|
|
140
|
-
<div className={styles.row} key={index} onClick={() => handleRowClick(item)}>
|
|
137
|
+
{visibleValues.map((item, index) => {
|
|
138
|
+
return (<div className={styles.row} key={index} onClick={() => handleRowClick(item.q)}>
|
|
141
139
|
<Typography.Paragraph
|
|
142
140
|
className={styles.text}
|
|
143
141
|
style={{ maxWidth: isSimple ? '210px' : '300px' }}
|
|
144
|
-
ellipsis={{ rows: 1, tooltip: `${item}`, }}
|
|
145
|
-
style={{ marginRight: '12px' }}>·</span>
|
|
142
|
+
ellipsis={{ rows: 1, tooltip: `${item.q}`, }}>
|
|
143
|
+
{isSimple && <span style={{ marginRight: '12px' }}>·</span>}
|
|
144
|
+
{item.q}
|
|
146
145
|
</Typography.Paragraph>
|
|
147
|
-
{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
</div>
|
|
154
|
-
))}
|
|
146
|
+
{ !isSimple && <RightOutlined
|
|
147
|
+
className={styles.rightArrow}
|
|
148
|
+
style={{ float: 'right' }}
|
|
149
|
+
/>}
|
|
150
|
+
</div>)
|
|
151
|
+
})}
|
|
155
152
|
</div>
|
|
156
153
|
</div>
|
|
157
154
|
</div>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import ChatWidget from '../chatWidget/index';
|
|
3
3
|
import aiAvatarPng from '../../assets/aicenter/aiavatar.png';
|
|
4
|
-
import { getFlowInfo, getHistoryList } from '../chatWidget/chatWindow/controllers/index';
|
|
4
|
+
import { getFlowInfo, getHistoryList, getKnowledgeInfo } from '../chatWidget/chatWindow/controllers/index';
|
|
5
5
|
import './index.module.css'
|
|
6
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
7
|
import historyListEmptyPng from '../../assets/aicenter/history-list-empty.png';
|
|
8
8
|
import { isEmpty, isFunction } from "lodash";
|
|
9
|
-
import { Carousel, message, message as messageTip, Progress, Skeleton, Tooltip } from 'antd'
|
|
10
|
-
import Icon, { HistoryOutlined, PlusOutlined } from "@ant-design/icons";
|
|
9
|
+
import { Carousel, message, message as messageTip, Progress, Skeleton, Tooltip, Image } from 'antd'
|
|
10
|
+
import Icon, { EyeOutlined, HistoryOutlined, PlusOutlined } from "@ant-design/icons";
|
|
11
11
|
import TabSelector from "../components/TabSelector";
|
|
12
12
|
|
|
13
13
|
|
|
@@ -23,6 +23,28 @@ const CommentIcon = (props) => (
|
|
|
23
23
|
<Icon component={commentSvg} {...props} />
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
+
const knowledgeInfo = {
|
|
27
|
+
media: [
|
|
28
|
+
{
|
|
29
|
+
a: "https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/smartSchool/appCreator/school/bcyz/user/ai/banner.png",
|
|
30
|
+
a_type: "image"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
a: "https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/temp/test.mp4",
|
|
34
|
+
a_type: "video"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
question: [
|
|
38
|
+
{category:'学校文化',data: [{q:'学校最有特色的传统是什么?'}, {q:'有哪些值得传承的校训?'}, {q:'学校重视学生个性发展吗?'}, {q:'有什么知名校友吗?'}, {q:'新生入学是否有迎新活动?'}, {q:'学校支持学生创业吗?'}, {q:'学校节日有哪些特色?'}, {q:'学校的学习氛围怎么样?'}, {q:'学生组织有哪些?'}, {q:'是否有艺术或文化类社团?'}]},
|
|
39
|
+
{category:'校园风景',data: [{q:'校园内有哪些打卡景点?'}, {q:'校园绿化做得好吗?'}, {q:'有湖泊或花园吗?'}, {q:'有特色建筑吗?'}, {q:'校园大不大?适合散步吗?'}, {q:'有哪些适合拍照的地方?'}, {q:'有室外自习空间吗?'}, {q:'校园晚上灯光如何?'}, {q:'有没有露天表演广场?'}, {q:'有樱花或银杏树吗?'}]},
|
|
40
|
+
{category:'招生流程',data: [{q:'什么时候开始填志愿?'}, {q:'招生简章在哪查看?'}, {q:'是否有预估分数线?'}, {q:'如何获取往年录取线?'}, {q:'能否跨省报考?'}, {q:'有哪些专项招生计划?'}, {q:'报志愿前可参观校园吗?'}, {q:'有无强基计划?'}, {q:'是否有面试环节?'}, {q:'报名后多久公布录取?'}]},
|
|
41
|
+
{category:'住宿生活',data: [{q:'几人一间宿舍?'}, {q:'宿舍有空调吗?'}, {q:'洗衣机是否自助?'}, {q:'有独立卫生间吗?'}, {q:'能否申请调换宿舍?'}, {q:'有晚归门禁吗?'}, {q:'网络信号好吗?'}, {q:'有宿舍文化活动吗?'}, {q:'宿舍能使用电器吗?'}, {q:'有快递柜或驿站吗?'}]},
|
|
42
|
+
{category:'餐饮服务',data: [{q:'食堂饭菜贵不贵?'}, {q:'食堂口味多样吗?'}, {q:'有无清真窗口?'}, {q:'学校周边吃饭方便吗?'}, {q:'是否支持刷脸就餐?'}, {q:'饭卡能否线上充值?'}, {q:'有没有饮品店?'}, {q:'食堂有无特色菜?'}, {q:'吃饭时间是否固定?'}, {q:'是否有外卖平台?'}]},
|
|
43
|
+
{category:'教学资源',data: [{q:'师资力量如何?'}, {q:'有外教课程吗?'}, {q:'是否提供选修课?'}, {q:'教材是否统一购买?'}, {q:'有开放实验室吗?'}, {q:'是否配有学习中心?'}, {q:'有哪些在线课程平台?'}, {q:'图书馆开放时间?'}, {q:'能否跨专业听课?'}, {q:'有无名师公开课?'}]},
|
|
44
|
+
{category:'就业实习',data: [{q:'是否提供实习机会?'}, {q:'就业指导服务如何?'}, {q:'有哪些合作企业?'}, {q:'实习是否计入学分?'}, {q:'就业率有多高?'}, {q:'毕业生去向有哪些?'}, {q:'能否推荐实习单位?'}, {q:'是否有职业规划课?'}, {q:'能考研继续深造吗?'}, {q:'就业双选会多不多?'}]},
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
const contentStyle = {
|
|
27
49
|
width:'100%',
|
|
28
50
|
color: '#fff',
|
|
@@ -305,6 +327,7 @@ export class ToolDialogV2 extends React.Component {
|
|
|
305
327
|
sessionId: uuidv4(), // 当前激活的对话对应的sessionId
|
|
306
328
|
dropDownList: [],
|
|
307
329
|
currentFlow: {},
|
|
330
|
+
knowledgeInfo: {...knowledgeInfo},
|
|
308
331
|
};
|
|
309
332
|
|
|
310
333
|
componentWillMount() {
|
|
@@ -352,6 +375,18 @@ export class ToolDialogV2 extends React.Component {
|
|
|
352
375
|
getFlowInfo = async () => {
|
|
353
376
|
const res = await getFlowInfo(this.props.hostUrl, this.props.appId, api_key);
|
|
354
377
|
if(res.status === 200 && typeof res.data !== "string") {
|
|
378
|
+
// 查询知识库相关信息用作预置问题
|
|
379
|
+
// getKnowledgeInfo(this.props.hostUrl, this.props.appId, api_key).then(res => {
|
|
380
|
+
// if (res.status === 200 && typeof res.data !== "string") {
|
|
381
|
+
// if (!isEmpty(knowledgeInfo)) {
|
|
382
|
+
// this.setState({ knowledgeInfo: res.data })
|
|
383
|
+
// } else if (!isEmpty(this.props.knowledgeInfo)) {
|
|
384
|
+
// this.setState({ knowledgeInfo: this.props.knowledgeInfo })
|
|
385
|
+
// }
|
|
386
|
+
// }
|
|
387
|
+
// }).catch(e => {
|
|
388
|
+
// console.log(e);
|
|
389
|
+
// });
|
|
355
390
|
this.setState({ currentFlow: res.data });
|
|
356
391
|
}
|
|
357
392
|
return res.data;
|
|
@@ -401,7 +436,8 @@ export class ToolDialogV2 extends React.Component {
|
|
|
401
436
|
renderCustomDropDown=(dropDownList)=>{
|
|
402
437
|
const { title } = this.props;
|
|
403
438
|
const { currentFlow } = this.state;
|
|
404
|
-
return <TabSelector
|
|
439
|
+
return <TabSelector
|
|
440
|
+
handleRowClick={(word)=> {
|
|
405
441
|
if(!this.isActiveMessage()){
|
|
406
442
|
this.setState({ dropDownList: [] });
|
|
407
443
|
this.handleSendMessage(word);
|
|
@@ -409,7 +445,10 @@ export class ToolDialogV2 extends React.Component {
|
|
|
409
445
|
message.destroy();
|
|
410
446
|
message.info("请等待回复结束后再发送")
|
|
411
447
|
}
|
|
412
|
-
}}
|
|
448
|
+
}}
|
|
449
|
+
welcomeWords={`Hi,欢迎使用${currentFlow.name||title},您可以这样问我:`}
|
|
450
|
+
dataList={this.state.knowledgeInfo?.question || []}
|
|
451
|
+
/>
|
|
413
452
|
}
|
|
414
453
|
|
|
415
454
|
render() {
|
|
@@ -435,7 +474,7 @@ export class ToolDialogV2 extends React.Component {
|
|
|
435
474
|
modalWidth,
|
|
436
475
|
isShowReadIcon = false,
|
|
437
476
|
signUrl,
|
|
438
|
-
|
|
477
|
+
knowledgeInfo,
|
|
439
478
|
} = this.props;
|
|
440
479
|
const { currentFlow = {} } = this.state;
|
|
441
480
|
return (
|
|
@@ -536,11 +575,22 @@ export class ToolDialogV2 extends React.Component {
|
|
|
536
575
|
<div className={'p_toolRightToRight'}>
|
|
537
576
|
<Carousel autoplay={{ dotDuration: true }} autoplaySpeed={4000} className={'p_carousel'}>
|
|
538
577
|
{
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
<
|
|
542
|
-
|
|
543
|
-
|
|
578
|
+
(this.state.knowledgeInfo?.media||[]).map(item => {
|
|
579
|
+
if (item.a_type==='image') {
|
|
580
|
+
return <div key={item.a}>
|
|
581
|
+
<Image
|
|
582
|
+
preview={{
|
|
583
|
+
mask: <div><EyeOutlined style={{marginRight:'4px'}}/>预览</div>
|
|
584
|
+
}}
|
|
585
|
+
style={contentStyle} src={item.a}
|
|
586
|
+
/>
|
|
587
|
+
</div>
|
|
588
|
+
}else if(item.a_type==='video'){
|
|
589
|
+
return <div key={item.a}>
|
|
590
|
+
<video style={contentStyle} src={item.a} controls={true}/>
|
|
591
|
+
</div>
|
|
592
|
+
}
|
|
593
|
+
})
|
|
544
594
|
}
|
|
545
595
|
</Carousel>
|
|
546
596
|
<div style={{ height: '1px', background: '#E5E5E5',marginTop: '25px' }}/>
|
|
@@ -553,6 +603,7 @@ export class ToolDialogV2 extends React.Component {
|
|
|
553
603
|
handleRowClick={(word) => {
|
|
554
604
|
this.handleClick(word)
|
|
555
605
|
}}
|
|
606
|
+
dataList={this.state.knowledgeInfo?.question || []}
|
|
556
607
|
/>
|
|
557
608
|
</div>
|
|
558
609
|
</div>
|