yt-chat-components 1.5.0 → 1.5.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-chat-components",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "main": "build/static/js/bundle.min.js",
5
5
  "module": "build/static/js/bundle.min.js",
6
6
  "types": "build/static/js/index.d.ts",
package/public/index.html CHANGED
@@ -111,7 +111,6 @@
111
111
  host-url="https://ai-api.yuntu.cn"
112
112
  sign-url="https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/smartSchool/appCreator/school/bcyz/user/ai/logo.png"
113
113
  app-id="5f48c683-38d8-430e-8479-17730a605821"
114
- user-info='{"id": "123", "name": "John Doe", "code":"1606451129" }'
115
114
  box-style='{"height":"100%","background":"url(https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/smartSchool/appCreator/school/bcyz/system/portal_bg_blue.png) center top / 100% 100% no-repeat"}'
116
115
  is-show-side-right=false
117
116
  is-show-side-left=true
@@ -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, { useState, useEffect, useRef } from 'react';
2
- import { Button, Avatar, message } from 'antd';
1
+ import React, {useEffect, useRef, useState} from 'react';
2
+ import {Avatar, Button, message} from 'antd';
3
3
  import './index.module.css';
4
- import { callStyle } from './style';
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: (message: ChatMessageType) => void;
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
- source.connect(analyser);
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
 
@@ -37,7 +37,10 @@ for (let i = 1; i <= 68; i++) {
37
37
  }
38
38
 
39
39
  // 根据name生成固定的颜色索引
40
- const getAvatarByName = (name: string) => {
40
+ const getAvatar = (name: string, icon: string) => {
41
+ if(icon){
42
+ return <Avatar shape="square" src={icon} style={{marginRight: 8}}/>
43
+ }
41
44
  const total = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
42
45
  const index = total % avatarImages.length;
43
46
  const img = avatarImages[index];
@@ -315,6 +318,35 @@ export default function ChatMessage({
315
318
  return renderLoading(MessageType.url, 50);
316
319
  }
317
320
  }
321
+ else if (className && className.includes('language-image')) {
322
+ try {
323
+ // 解析代码块内容为 JSON 配置
324
+ const data = JSON.parse(children.toString().trim().replace(/\\n/g, '').replace('\n', ''));
325
+ const {url} = data;
326
+ // 渲染组件
327
+ return <Image
328
+ height={'auto'}
329
+ width={'100%'}
330
+ src={url}
331
+ alt={''}
332
+ fallback="data:image/png;base64,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"
333
+ />
334
+ } catch (error) {
335
+ return renderLoading(MessageType.url, 50);
336
+ }
337
+ }
338
+ else if (className && className.includes('language-video')) {
339
+ try {
340
+ // 解析代码块内容为 JSON 配置
341
+ const data = JSON.parse(children.toString().trim().replace(/\\n/g, '').replace('\n', ''));
342
+ const {url} = data;
343
+
344
+ // 渲染组件
345
+ return <video src={url} autoPlay={true} controls={false}/>
346
+ } catch (error) {
347
+ return renderLoading(MessageType.url, 50);
348
+ }
349
+ }
318
350
  // 默认渲染其他代码块
319
351
  return <code className={className} {...props}>{children}</code>;
320
352
  },
@@ -357,7 +389,7 @@ export default function ChatMessage({
357
389
 
358
390
  const renderBotTextMessage = () => {
359
391
  const items = messageItemList.map((item, index) => {
360
- const {id, thinkMessage, message, loadingMessage, type, name, toolCallInfo, rawInfo} = item;
392
+ const {id, thinkMessage, message, loadingMessage, type, name, icon, toolCallInfo, rawInfo} = item||{};
361
393
  const isLatestItem = index === messageItemList.length - 1;
362
394
  let status = 'success'
363
395
  // 只有在接收信息的时候有 pending 状态,最后的信息才为 pending
@@ -367,7 +399,7 @@ export default function ChatMessage({
367
399
 
368
400
  const resultItem: CustomThoughtChainItem = {
369
401
  title: <div style={{display:"flex", flexDirection: 'row', alignItems: 'center'}}>
370
- {getAvatarByName(name)}
402
+ {getAvatar(name, icon)}
371
403
  <span>{name}</span>
372
404
  </div>,
373
405
  description: toolCallInfo,
@@ -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
  }
@@ -1,8 +1,9 @@
1
1
  .w_send_file_box {
2
- height: 23px;
2
+ height: 100%;
3
+ align-items: end;
4
+ padding-bottom: 16px;
3
5
  display: flex;
4
6
  background: transparent;
5
- align-items: center;
6
7
  justify-content: center;
7
8
  border-radius: 24px;
8
9
  font-size: 12px;
@@ -164,7 +164,7 @@ export default function ChatWindow({
164
164
  let content_ns: string = null
165
165
  let event_latest: string = null
166
166
  const nowAIContentListRef = useRef(nowAIContentList);
167
- const {registerChatMethods} = React.useContext(MethodContext);
167
+ const {registerChatMethods} = React.useContext(MethodContext) || {};
168
168
  let voiceChunks = []; // 临时存储录制的语音片段
169
169
  // 滚动事件处理,选择文件时,文件内容超出显示框时,显示左右箭头
170
170
  const handleScroll = () => {
@@ -206,7 +206,7 @@ export default function ChatWindow({
206
206
  }
207
207
  }
208
208
 
209
- const addMessageItem = ({chunk, id, name, status = null, isThinkChunk = false, loadingMessage = null}) => {
209
+ const addMessageItem = ({chunk, id, name, icon = null, status = null, isThinkChunk = false, loadingMessage = null}) => {
210
210
  // console.log("--- addMessageItem", chunk, status, loadingMessage)
211
211
  setNowAIContentList((prevState) => {
212
212
  const content = {}
@@ -221,6 +221,7 @@ export default function ChatWindow({
221
221
  const messageItem : MessageItem = {
222
222
  id,
223
223
  name,
224
+ icon,
224
225
  type: MessageType.text,
225
226
  toolCallInfo: status,
226
227
  loadingMessage,
@@ -251,6 +252,7 @@ export default function ChatWindow({
251
252
  const newMessageItem: MessageItem = {
252
253
  id: latestMessageItem.id === 'WAIT' ? content_id : latestMessageItem.id,
253
254
  name: latestMessageItem.name,
255
+ icon: latestMessageItem.icon,
254
256
  type: latestMessageItem.type,
255
257
  toolCallInfo: status,
256
258
  loadingMessage,
@@ -259,7 +261,6 @@ export default function ChatWindow({
259
261
 
260
262
  const newState = [...prevState]
261
263
  newState[newState.length - 1] = newMessageItem
262
-
263
264
  nowAIContentListRef.current = newState
264
265
  return newState
265
266
  })
@@ -273,7 +274,7 @@ export default function ChatWindow({
273
274
  getHistoryList();
274
275
  }
275
276
  else if (event == 'token' || event == 't_full_token') {
276
- let { chunk, id, r_id, ns, name, loading_message } = data
277
+ let { chunk, id, r_id, ns, name, icon, loading_message } = data
277
278
  if (chunk.includes('```') && !chunk.startsWith('\n')) {
278
279
  // 确保 ``` 前有换行
279
280
  chunk = '\n' + chunk;
@@ -290,7 +291,7 @@ export default function ChatWindow({
290
291
  // 新建信息
291
292
  else{
292
293
  content_id = id
293
- addMessageItem({chunk, id, name, loadingMessage: loading_message || ''})
294
+ addMessageItem({chunk, id, name, icon, loadingMessage: loading_message || ''})
294
295
  }
295
296
  }
296
297
  // 输出主体没有变化
@@ -309,7 +310,7 @@ export default function ChatWindow({
309
310
  // if (event_latest === 'token' && event === 't_full_token'){
310
311
  // updateMessageItem({chunk, loadingMessage: loading_message || ''})
311
312
  // } else {
312
- // addMessageItem({chunk, id, name, loadingMessage:loading_message})
313
+ // addMessageItem({chunk, id, name, icon, loadingMessage:loading_message})
313
314
  // }
314
315
  // }else{
315
316
  updateMessageItem({chunk, loadingMessage: loading_message || ''})
@@ -328,7 +329,7 @@ export default function ChatWindow({
328
329
  content_ns = ns
329
330
  // 新建信息
330
331
  content_id = id
331
- addMessageItem({chunk, id, name, status:null, isThinkChunk:true, loadingMessage: loading_message || ''})
332
+ addMessageItem({chunk, id, name, icon, status:null, isThinkChunk:true, loadingMessage: loading_message || ''})
332
333
  }else {
333
334
  updateMessageItem({chunk, status:null, isThinkChunk:true, loadingMessage: loading_message || ''})
334
335
  }
@@ -350,7 +351,7 @@ export default function ChatWindow({
350
351
  }else{
351
352
  // 这个时候还没有信息的id,所以 content_id 给特殊值
352
353
  content_id = "WAIT"
353
- addMessageItem({chunk:"", id: "WAIT", name, status})
354
+ addMessageItem({chunk:"", id: "WAIT", name, icon, status})
354
355
  }
355
356
  }
356
357
  // 当前智能体在调用工具
@@ -633,6 +634,15 @@ export default function ChatWindow({
633
634
  // 处理挂断电话
634
635
  const handleHangup = () => {
635
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);
636
646
  };
637
647
 
638
648
  /**
@@ -957,7 +967,6 @@ export default function ChatWindow({
957
967
  }, [sessionId]);
958
968
 
959
969
  useEffect(() => {
960
- console.log("registerChatMethods",registerChatMethods)
961
970
  if (registerChatMethods) {
962
971
  registerChatMethods({
963
972
  isActiveMessage,
@@ -1360,8 +1369,11 @@ export default function ChatWindow({
1360
1369
  flowId={flowId}
1361
1370
  sessionId={sessionId}
1362
1371
  hostUrl={hostUrl}
1363
- onMessage={addMessage}
1364
1372
  userInfo={userInfo}
1373
+ addMessage={e => {
1374
+ // addMessage(e)
1375
+ setDropDownList([])
1376
+ }}
1365
1377
  />
1366
1378
  :
1367
1379
  <div className="cl-middle-container">
@@ -8,6 +8,7 @@ export enum MessageType {
8
8
  export type MessageItem = {
9
9
  id:string,
10
10
  name: string;
11
+ icon: string;
11
12
  thinkMessage: string;
12
13
  message: string;
13
14
  loadingMessage?: string;
@@ -2204,10 +2204,11 @@ export const yt_style = `
2204
2204
  }
2205
2205
 
2206
2206
  .w_send_file_box {
2207
- height: 23px;
2207
+ height: 100%;
2208
+ align-items: end;
2209
+ padding-bottom: 16px;
2208
2210
  display: flex;
2209
2211
  background: transparent;
2210
- align-items: center;
2211
2212
  justify-content: center;
2212
2213
  border-radius: 24px;
2213
2214
  font-size: 12px;
@@ -16,19 +16,17 @@ const defaultMap = {
16
16
  '就业实习': ['是否提供实习机会?', '就业指导服务如何?', '有哪些合作企业?', '实习是否计入学分?', '就业率有多高?', '毕业生去向有哪些?', '能否推荐实习单位?', '是否有职业规划课?', '能考研继续深造吗?', '就业双选会多不多?'],
17
17
  };
18
18
 
19
- const TabSelector = ({ map = defaultMap, customUrl, handleRowClick, welcomeWords, isSimple }) => {
20
- const [activeKey, setActiveKey] = useState(Object.keys(map)[0]);
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 = Object.keys(map);
28
- const values = map[activeKey] || [];
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}`, }}><span
145
- style={{ marginRight: '12px' }}>·</span>{item}
142
+ ellipsis={{ rows: 1, tooltip: `${item.q}`, }}>
143
+ {isSimple && <span style={{ marginRight: '12px' }}>·</span>}
144
+ {item.q}
146
145
  </Typography.Paragraph>
147
- {
148
- !isSimple && <RightOutlined
149
- className={styles.rightArrow}
150
- style={{ float: 'right' }}
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',
@@ -299,25 +321,35 @@ const api_key = 'sk-mniUFDpcWwvNF3iFzssXa6etN-S_Y4AApyHYcBU44L0';
299
321
  export const MethodContext = React.createContext();
300
322
 
301
323
  export class ToolDialogV2 extends React.Component {
302
-
324
+ sessionUserInfo = {};
303
325
  state = {
304
326
  historyList: [], // 历史对话列表
305
327
  sessionId: uuidv4(), // 当前激活的对话对应的sessionId
306
328
  dropDownList: [],
307
329
  currentFlow: {},
330
+ knowledgeInfo: {...knowledgeInfo},
308
331
  };
309
332
 
333
+ componentWillMount() {
334
+ const { userInfo = {} } = this.props;
335
+ const { sessionId } = this.state;
336
+ if (isEmpty(userInfo.code)) {
337
+ const code = sessionId.replaceAll("-", "").slice(2);
338
+ this.sessionUserInfo.code = code;
339
+ this.sessionUserInfo.id = code;
340
+ this.sessionUserInfo.name = code;
341
+ }else{
342
+ this.sessionUserInfo = userInfo;
343
+ }
344
+ }
345
+
310
346
  async componentDidMount() {
311
- this.getHistoryList({},true);
347
+ this.getHistoryList(true);
312
348
  }
313
349
 
314
350
  getCurrentFlowHistory=(flowId)=>{
315
- let { userInfo = {} } = this.props;
316
351
  const { sessionId } = this.state;
317
- if(isEmpty(userInfo.code)) {
318
- userInfo.code = sessionId;
319
- }
320
- const operationId = userInfo['code'] || '';
352
+ const operationId = this.sessionUserInfo['code'] || '';
321
353
  getHistoryList(this.props.hostUrl, flowId, operationId).then((res) => {
322
354
  if(res.status === 200 && typeof res.data !== "string" && typeof res.data !== "string"){
323
355
  this.setState({ historyList: res.data });
@@ -343,6 +375,18 @@ export class ToolDialogV2 extends React.Component {
343
375
  getFlowInfo = async () => {
344
376
  const res = await getFlowInfo(this.props.hostUrl, this.props.appId, api_key);
345
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
+ // });
346
390
  this.setState({ currentFlow: res.data });
347
391
  }
348
392
  return res.data;
@@ -392,7 +436,8 @@ export class ToolDialogV2 extends React.Component {
392
436
  renderCustomDropDown=(dropDownList)=>{
393
437
  const { title } = this.props;
394
438
  const { currentFlow } = this.state;
395
- return <TabSelector handleRowClick={(word)=> {
439
+ return <TabSelector
440
+ handleRowClick={(word)=> {
396
441
  if(!this.isActiveMessage()){
397
442
  this.setState({ dropDownList: [] });
398
443
  this.handleSendMessage(word);
@@ -400,7 +445,10 @@ export class ToolDialogV2 extends React.Component {
400
445
  message.destroy();
401
446
  message.info("请等待回复结束后再发送")
402
447
  }
403
- }} welcomeWords={`Hi,欢迎使用${currentFlow.name||title},您可以这样问我:`}/>
448
+ }}
449
+ welcomeWords={`Hi,欢迎使用${currentFlow.name||title},您可以这样问我:`}
450
+ dataList={this.state.knowledgeInfo?.question || []}
451
+ />
404
452
  }
405
453
 
406
454
  render() {
@@ -426,7 +474,7 @@ export class ToolDialogV2 extends React.Component {
426
474
  modalWidth,
427
475
  isShowReadIcon = false,
428
476
  signUrl,
429
- bannerMap,
477
+ knowledgeInfo,
430
478
  } = this.props;
431
479
  const { currentFlow = {} } = this.state;
432
480
  return (
@@ -464,7 +512,7 @@ export class ToolDialogV2 extends React.Component {
464
512
  <div className="p_historyDialog">
465
513
  <div className="p_historyTitle">
466
514
  <div className="p_dialogTitle">对话列表</div>
467
- <HistoryOutlined style={{color:'#A4A4A4'}}/>
515
+ <Tooltip title={isEmpty(userInfo)?'当前历史记录将会在页面刷新后清除':'历史对话记录'}><HistoryOutlined style={{color:'#A4A4A4'}}/></Tooltip>
468
516
  </div>
469
517
  {this.state.historyList.length > 0 ? (
470
518
  <div className="p_historyList">
@@ -508,7 +556,7 @@ export class ToolDialogV2 extends React.Component {
508
556
  host_url={hostUrl}
509
557
  api_key={api_key}
510
558
  session_id={this.state.sessionId}
511
- userInfo={userInfo}
559
+ userInfo={this.sessionUserInfo}
512
560
  getHistoryList={this.getHistoryList}
513
561
  setDropDownList={(list) => this.setState({ dropDownList: list })}
514
562
  dropDownList={this.state.dropDownList}
@@ -527,11 +575,22 @@ export class ToolDialogV2 extends React.Component {
527
575
  <div className={'p_toolRightToRight'}>
528
576
  <Carousel autoplay={{ dotDuration: true }} autoplaySpeed={4000} className={'p_carousel'}>
529
577
  {
530
- Object.values(bannerMap).map(item => (
531
- <div key={item}>
532
- <img style={contentStyle} src={item}/>
533
- </div>
534
- ))
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
+ })
535
594
  }
536
595
  </Carousel>
537
596
  <div style={{ height: '1px', background: '#E5E5E5',marginTop: '25px' }}/>
@@ -544,6 +603,7 @@ export class ToolDialogV2 extends React.Component {
544
603
  handleRowClick={(word) => {
545
604
  this.handleClick(word)
546
605
  }}
606
+ dataList={this.state.knowledgeInfo?.question || []}
547
607
  />
548
608
  </div>
549
609
  </div>