yt-chat-components 1.5.7 → 1.5.8

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.
@@ -24,6 +24,7 @@ import {UrlCard} from "./urlCard"
24
24
  import {CustomThoughtChainItem, CustomThoughtChainProps} from "./CustomThoughtChain";
25
25
  import CustomThoughtChain from "./CustomThoughtChain";
26
26
  import {isFunction} from "lodash";
27
+ import {AudioPlayer} from "../audioPlayer/AudioPlayer";
27
28
  let speechSynth = window.speechSynthesis;
28
29
  let utterance = null;
29
30
 
@@ -63,6 +64,7 @@ export default function ChatMessage({
63
64
  key,
64
65
  isShowReadIcon,
65
66
  renderInitNode,
67
+ voice
66
68
  }: ChatMessageType) {
67
69
  const parseFileName = (
68
70
  text: string,
@@ -135,24 +137,33 @@ export default function ChatMessage({
135
137
  }
136
138
  };
137
139
 
138
- /**
139
- * 播放文字
140
- * @param text 文字
141
- */
142
140
  const playVoice = (text)=>{
143
141
  if (isPlay) {
144
- speechSynth.cancel();
145
142
  setIsPlay(false)
146
- }else{
147
- if (text) {
148
- utterance = new SpeechSynthesisUtterance(text);
149
- const voices = speechSynth.getVoices();
150
- utterance.voice = voices[61];
151
- utterance.rate = 1;
152
- utterance.pitch = 1;
153
- speechSynth.speak(utterance);
154
- setIsPlay(true)
143
+ if (window.messageAudioPlayer){
144
+ window.messageAudioPlayer.stop();
145
+ }
146
+ return;
147
+ }
148
+
149
+ if (text) {
150
+ if (window.messageAudioPlayer){
151
+ window.messageAudioPlayer.stop();
152
+ window.messageAudioPlayer = null;
155
153
  }
154
+ console.log('voice = ' + voice)
155
+ window.messageAudioPlayer = new AudioPlayer(voice);
156
+ window.messageAudioPlayer.onReady = () => {
157
+ console.log('已连接到语音合成服务');
158
+ window.messageAudioPlayer.sendRunSynthesis(text)// 尽快发送 StartSynthesis
159
+ };
160
+ window.messageAudioPlayer.onComplete = () => {
161
+ setIsPlay(false);
162
+ };
163
+ window.messageAudioPlayer.onError = (err) => {
164
+ // alert('发生错误:' + err);
165
+ };
166
+ setIsPlay(true)
156
167
  }
157
168
  }
158
169
 
@@ -29,7 +29,8 @@ import {isEmpty, isFunction} from 'lodash';
29
29
  import btn_answer from '../../../assets/aicenter/btn_answer.png';
30
30
  import {MethodContext} from "../../previewDialogV2";
31
31
  import TabSelector from "../../components/TabSelector";
32
-
32
+ import {AudioRecorder} from "./audioRecorder";
33
+ import InputMobile from "./inputMobile/inputMobile";
33
34
 
34
35
  let mediaRecorder = null; // 语音对象,用于录音
35
36
  let recognition = null; // 语音识别对象
@@ -92,6 +93,9 @@ export default function ChatWindow({
92
93
  isTagsHidden,
93
94
  isEnableForV1,
94
95
  questions,
96
+ voice,
97
+ onSmartRobotClick,
98
+ isShowMobileInputArea,
95
99
  }: {
96
100
  is_enable_call:boolean;
97
101
  tags: [];
@@ -143,12 +147,17 @@ export default function ChatWindow({
143
147
  isTagsHidden?: boolean;
144
148
  isEnableForV1?: boolean;
145
149
  questions?: any[];
150
+ voice?: string;
151
+ onSmartRobotClick?: Function;
152
+ isShowMobileInputArea?: boolean;
146
153
  }) {
147
154
  const ref = useRef<HTMLDivElement>(null);
148
155
  const lastMessage = useRef<HTMLDivElement>(null);
149
156
  const [showCallInterface, setShowCallInterface] = useState(false); // 添加通话界面状态
150
157
  const [aiStatus, setAiStatus] = useState(null);
158
+ const inputMobileRef = useRef(null);
151
159
  const inputRef = useRef<HTMLInputElement>(null); /* User input Ref */
160
+ const audioRecorder = useRef(null);
152
161
  /* Initial listener for loss of focus that refocuses User input after a small delay */
153
162
  const [nowAIContentList, setNowAIContentList] = useState<Array<MessageItem>>([]);
154
163
  const [receivingMessage, setReceivingMessage] = useState(false);
@@ -164,7 +173,7 @@ export default function ChatWindow({
164
173
  const isStream = true;//是否流式输出(手动开关)
165
174
  const [recordState, setRecordState] = useState(false); // 录音状态。true为正在录音,false为停止录音
166
175
  const [tagList, setTagList] = useState([]); // 问题标签列表
167
- const {isTitleSideIcon, logoWidth, agentUrl, stopMessageUrl, sendMessageUrl, speakUrl} = baseConfig;
176
+ const {isTitleSideIcon, logoWidth, agentUrl, stopMessageUrl, sendMessageUrl, speakUrl, callUrl} = baseConfig;
168
177
  const [inputContainerHeight, setInputContainerHeight] = useState('120px')
169
178
  let content_id: string = null
170
179
  let content_ns: string = null
@@ -880,6 +889,39 @@ export default function ChatWindow({
880
889
  }
881
890
  };
882
891
 
892
+ const stopRecord = () =>{
893
+ audioRecorder.current.stopRecorder();
894
+ audioRecorder.current = undefined;
895
+ setRecordState(false)
896
+ }
897
+ const startConnectAndRecord = () => {
898
+ if (recordState){
899
+ stopRecord()
900
+ }else {
901
+ setRecordState(true)
902
+ audioRecorder.current = new AudioRecorder();
903
+ audioRecorder.current.startRecorder();
904
+ audioRecorder.current.onTextUpdate = (text: string) => {
905
+ setValue(text);
906
+ if (inputMobileRef.current){
907
+ inputMobileRef.current.handleSetInputText(text);
908
+ }
909
+ if (inputRef.current) {
910
+ inputRef.current.value = text;
911
+ }
912
+ };
913
+ audioRecorder.current.onRecordError = (error: string) => {
914
+ stopRecord()
915
+ };
916
+ audioRecorder.current.onRecordTimeout = () => {
917
+ stopRecord()
918
+ if (inputMobileRef.current){
919
+ inputMobileRef.current.stopRecording();
920
+ }
921
+ };
922
+ }
923
+ };
924
+
883
925
  /**
884
926
  * 输出消息时,滚动到底部
885
927
  */
@@ -968,9 +1010,20 @@ export default function ChatWindow({
968
1010
  </div>
969
1011
  }
970
1012
  <TabSelector
1013
+ isMobile={isMobile}
1014
+ isSimple={isMobile}
1015
+ isShowByPages={true}
1016
+ keyShowSize={4}
1017
+ agentName={window_title}
1018
+ agentUrl={agentUrl}
971
1019
  welcomeWords={`Hi,欢迎使用${window_title},您可以这样问我:`}
972
1020
  handleRowClick={(word) => {
973
- handleSendMessage(word)
1021
+ if(!receivingMessageRef.current){
1022
+ handleSendMessage(word)
1023
+ }else{
1024
+ messageTip.destroy();
1025
+ messageTip.info("请等待回复结束后再发送")
1026
+ }
974
1027
  }}
975
1028
  dataList={questions}
976
1029
  />
@@ -1150,7 +1203,40 @@ export default function ChatWindow({
1150
1203
  return <></>
1151
1204
  }
1152
1205
 
1206
+ const renderInputAreaMobile = () => {
1207
+ return (
1208
+ <InputMobile
1209
+ ref={inputMobileRef}
1210
+ isShowVoiceButton={isShowVoiceButton}
1211
+ inputRef={inputRef}
1212
+ disabled={receivingMessage}
1213
+ placeholder={
1214
+ receivingMessage
1215
+ ? placeholder_sending || '思考中...'
1216
+ : placeholder || '请输入您的问题...'
1217
+ }
1218
+ onValueChange={ value => setValue(value)}
1219
+ onSendClick={() => {
1220
+ if (receivingMessage) {
1221
+ addMessage({
1222
+ messageItemList: [...nowAIContentList],
1223
+ isSend: false,
1224
+ });
1225
+ abortControllerRef.current.abort('disconnect');
1226
+ abortControllerRef.current = new AbortController();
1227
+ } else {
1228
+ handleSendMessage()
1229
+ }
1230
+ }}
1231
+ onStartRecordClick={startConnectAndRecord}
1232
+ onStopRecordClick={stopRecord}
1233
+ onSmartRobotClick={onSmartRobotClick}
1234
+ />
1235
+ )
1236
+ }
1237
+
1153
1238
  const renderInputArea = () => {
1239
+ if (isMobile && isShowMobileInputArea) renderInputAreaMobile();
1154
1240
  let isRender = messages.length === 0
1155
1241
  if (messages.length > 0) {
1156
1242
  const {messageItemList} = messages[messages.length - 1]
@@ -1260,7 +1346,7 @@ export default function ChatWindow({
1260
1346
  <div
1261
1347
  className="w_send_voice_box"
1262
1348
  style={receivingMessage ? {cursor: 'not-allowed'} : {}}
1263
- onClick={startRecord}
1349
+ onClick={startConnectAndRecord}
1264
1350
  >
1265
1351
  <img src={recordState ? soundWavePng : (speakUrl || luyinPng)} style={{width: 35}}
1266
1352
  className={recordState ? "w_recordIng" : ''}></img>
@@ -1275,7 +1361,7 @@ export default function ChatWindow({
1275
1361
  style={receivingMessage ? {cursor: 'not-allowed'} : {}}
1276
1362
  onClick={handleCallButtonClick}
1277
1363
  >
1278
- <img src={phonePng} style={{width: 23}}></img>
1364
+ <img src={(callUrl || phonePng)} style={{width: 35}}></img>
1279
1365
  </div>
1280
1366
  </Tooltip>
1281
1367
  }
@@ -1296,7 +1382,7 @@ export default function ChatWindow({
1296
1382
  style={{
1297
1383
  ...(receivingMessage ? {cursor: 'pointer'} : {}),
1298
1384
  height:'100%',
1299
- padding: '0 13px 16px',
1385
+ padding: '0 13px 16px 0',
1300
1386
  background: 'transparent',
1301
1387
  display: 'flex',
1302
1388
  alignItems: 'end',
@@ -1386,6 +1472,7 @@ export default function ChatWindow({
1386
1472
  </>
1387
1473
  )
1388
1474
  }
1475
+
1389
1476
  return (
1390
1477
  <div
1391
1478
  style={{...chat_window_style, width: width, height: "100%"}}
@@ -1458,6 +1545,7 @@ export default function ChatWindow({
1458
1545
  handleSendMessage={handleSendMessage}
1459
1546
  isShowReadIcon={isShowReadIcon}
1460
1547
  renderInitNode={message.renderInitNode}
1548
+ voice={voice}
1461
1549
  />
1462
1550
  ))
1463
1551
  }
@@ -1476,13 +1564,14 @@ export default function ChatWindow({
1476
1564
  isSend={false}
1477
1565
  handleSendMessage={handleSendMessage}
1478
1566
  isShowReadIcon={isShowReadIcon}
1567
+ voice={voice}
1479
1568
  /> : <ChatMessagePlaceholder bot_message_style={bot_message_style} aiStatus={aiStatus}/>
1480
1569
  )
1481
1570
  }
1482
1571
  <div ref={lastMessage}></div>
1483
1572
  </div>
1484
1573
  {
1485
- renderInputArea()
1574
+ renderInputAreaMobile()
1486
1575
  }
1487
1576
  </div>
1488
1577
  }
@@ -0,0 +1,134 @@
1
+ .m_tool_box {
2
+ position: relative;
3
+ width: 100%;
4
+ height: fit-content;
5
+ display: flex;
6
+ background-color: white;
7
+
8
+ .m_avatar {
9
+ position: absolute;
10
+ left: 10px;
11
+ bottom: 60px;
12
+ /* 头像容器样式 */
13
+ img{
14
+ z-index: 10;
15
+ position: relative;
16
+ }
17
+ }
18
+
19
+ .m_input_box {
20
+ /* 输入框整体容器样式 */
21
+ display: flex;
22
+ align-items: center;
23
+ width: 100%;
24
+ padding: 8px 8px;
25
+ background-color: #f9f9f9;
26
+
27
+ /* 在移动设备上优化触摸体验 */
28
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
29
+ }
30
+
31
+ .m_voiceToggle {
32
+ /* 语音/键盘切换按钮样式 */
33
+ transition: background-color 0.3s ease;
34
+ }
35
+
36
+ .m_center_box {
37
+ margin-left: 8px;
38
+ /* 中间输入区域容器 */
39
+ flex: 1;
40
+ position: relative;
41
+ /* 优化移动端点击区域 */
42
+ -webkit-user-select: auto;
43
+ user-select: auto;
44
+ }
45
+
46
+ .m_clickToTalk {
47
+ /* 点击说话样式 */
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ height: 40px;
52
+ border-radius: 8px;
53
+ background-color: #f0f0f0;
54
+ font-size: 14px;
55
+ color: #666;
56
+ }
57
+
58
+ .m_inputBox-textarea {
59
+ /* 输入框样式 */
60
+ width: 100%;
61
+ font-size: 14px;
62
+ line-height: 20px;
63
+ border: none;
64
+ resize: none;
65
+ overflow-y: hidden;
66
+ background-color: transparent;
67
+
68
+ /* 设置输入法中文输入时的兼容样式 */
69
+ -webkit-box-sizing: border-box;
70
+ -moz-box-sizing: border-box;
71
+ box-sizing: border-box;
72
+
73
+ /* 获得焦点时的样式 */
74
+
75
+ &:focus {
76
+ outline: none;
77
+ }
78
+ }
79
+
80
+ .m_sendButton {
81
+ /* 发送按钮样式 */
82
+ padding: 0px 14px;
83
+ height: 34px;
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ margin-left: 10px;
88
+ border-radius: 8px;
89
+ background-color: #4a90e2;
90
+ transition: background-color 0.3s ease;
91
+
92
+ &:active {
93
+ background-color: #3a80d2;
94
+ }
95
+ }
96
+
97
+ .m_sendButton {
98
+ /* 发送按钮样式 */
99
+ padding: 0px 14px;
100
+ height: 34px;
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ margin-left: 10px;
105
+ border-radius: 8px;
106
+ background-color: #4a90e2;
107
+ transition: background-color 0.3s ease;
108
+
109
+ &:active {
110
+ background-color: #3a80d2;
111
+ }
112
+ }
113
+
114
+ /* 音波动画样式 */
115
+
116
+ .audioWave {
117
+ fill: #fff;
118
+ opacity: 1;
119
+
120
+ &.active {
121
+ animation: wavePulse 1s ease-out infinite;
122
+ }
123
+ }
124
+ }
125
+
126
+ /* 音波动画关键帧 */
127
+ @keyframes wavePulse {
128
+ 0%, 100% {
129
+ transform: scaleY(1);
130
+ }
131
+ 50% {
132
+ transform: scaleY(1.5);
133
+ }
134
+ }
@@ -0,0 +1,170 @@
1
+ import React from "react";// 音波动画组件
2
+ import styles from "./index.module.css";
3
+ import {Input, message} from "antd";
4
+
5
+ class InputMobile extends React.Component {
6
+ constructor(props) {
7
+ super(props);
8
+ this.state = {
9
+ isVoice: false,
10
+ inputText: '',
11
+ isRecording: false,
12
+ isAudioActive: false
13
+ };
14
+ }
15
+
16
+ handleSetInputText = (text) => {
17
+ this.setState({inputText: text});
18
+ };
19
+
20
+ // 处理输入变化
21
+ handleInputChange = (e) => {
22
+ const {onValueChange} = this.props;
23
+ onValueChange(e.target.value);
24
+ this.setState({inputText: e.target.value}, () => {
25
+ // this.autoResizeTextarea();
26
+ });
27
+ }
28
+
29
+ // 开始录音时激活音波动画
30
+ startRecording = () => {
31
+ if (this.props.disabled){
32
+ message.destroy();
33
+ message.warning('对话结束后重试');
34
+ return;
35
+ }
36
+ this.setState({
37
+ isRecording: true,
38
+ isAudioActive: true
39
+ });
40
+ // 可以在这里添加实际录音的初始化逻辑
41
+ }
42
+
43
+ // 停止录音时停止音波动画
44
+ stopRecording = () => {
45
+ const {onStopRecordClick} = this.props;
46
+ onStopRecordClick()
47
+ this.setState({
48
+ isRecording: false,
49
+ isAudioActive: false,
50
+ isVoice: false
51
+ });
52
+ }
53
+
54
+ handleRecordClick = () => {
55
+ const {onStartRecordClick} = this.props;
56
+ if (this.state.isVoice && !this.state.isRecording) {
57
+ this.startRecording();
58
+ onStartRecordClick()
59
+ } else {
60
+ this.stopRecording();
61
+ }
62
+ }
63
+
64
+ render() {
65
+ const {isShowVoiceButton, disabled, placeholder, onSendClick, inputRef, onSmartRobotClick = () => {} } = this.props;
66
+ return (
67
+ <div className={styles.m_tool_box}>
68
+ {/*智能体小人图形固定显示位置*/}
69
+ <div className={styles.m_avatar}>
70
+ <img src={'https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/kai_yuan/common/smartBoy.png'} width={'60px'} style={{zIndex:10}} onClick={onSmartRobotClick}/>
71
+ </div>
72
+ {/*输入框*/}
73
+ <div className={styles.m_input_box} style={{display: 'flex', alignItems: 'center', width: '100%'}}>
74
+ {/*语音键盘切换按钮*/}
75
+ {
76
+ isShowVoiceButton &&
77
+ <div
78
+ onClick={() => this.setState({isVoice: !this.state.isVoice})}
79
+ className={styles.m_voiceToggle}
80
+ >
81
+ <img
82
+ src={this.state.isVoice ?
83
+ 'https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/kai_yuan/common/keyboard.png' :
84
+ 'https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/kai_yuan/common/voice.png'
85
+ }
86
+ width={'30px'}
87
+ height={'30px'}
88
+ />
89
+ </div>
90
+ }
91
+
92
+ {/*输入框 或 点击说话*/}
93
+ <div className={styles.m_center_box} style={{flex: 1, position: 'relative'}}>
94
+ {this.state.isVoice ? (
95
+ <div
96
+ className={styles.m_clickToTalk}
97
+ style={{background: this.state.isRecording ? '#1774FF' : '#f0f0f0'}}
98
+ onClick={this.handleRecordClick}
99
+ >
100
+ {this.state.isRecording ? <AudioWaveAnimation isActive={true}/> : '点击说话'}
101
+ </div>
102
+ ) : (
103
+ <Input
104
+ size={'large'}
105
+ ref={inputRef}
106
+ value={this.state.inputText}
107
+ disabled={disabled}
108
+ onChange={this.handleInputChange}
109
+ maxLength={500}
110
+ placeholder={placeholder}
111
+ className="m_inputBox-textarea"
112
+ style={{overflowY: 'hidden'}} // 默认隐藏滚动条
113
+ />
114
+ )}
115
+ </div>
116
+ {/*发送按钮*/}
117
+ {
118
+ this.state.isVoice && this.state.isRecording ? (
119
+ <div
120
+ className={styles.m_sendButton}
121
+ style={{backgroundColor: 'red'}}
122
+ onClick={this.stopRecording}
123
+ >
124
+ <span style={{color: 'white', fontSize: 14}}>结束</span>
125
+ </div>
126
+ ) : (
127
+ <div className={styles.m_sendButton} onClick={() => {
128
+ onSendClick();
129
+ this.setState({inputText: ''})
130
+ }}>
131
+ <span style={{color: 'white', fontSize: 14}}>{disabled ? '停止' : '开始'}</span>
132
+ </div>
133
+ )
134
+ }
135
+ </div>
136
+ </div>
137
+ )
138
+ }
139
+ }
140
+
141
+ class AudioWaveAnimation extends React.Component {
142
+ render() {
143
+ const { isActive } = this.props;
144
+ return (
145
+ <svg className={`${styles.audioWave} ${isActive ? styles.active : ''}`} width="160" height="20"
146
+ viewBox="0 0 60 20">
147
+ <rect x="1" y="9" width="4" height="2" rx="2" ry="2"/>
148
+ <rect x="7" y="7" width="4" height="4" rx="2" ry="2"/>
149
+ <rect x="13" y="5" width="4" height="6" rx="2" ry="2"/>
150
+ <rect x="19" y="7" width="4" height="4" rx="2" ry="2"/>
151
+ <rect x="25" y="9" width="4" height="2" rx="2" ry="2"/>
152
+ <rect x="31" y="7" width="4" height="4" rx="2" ry="2"/>
153
+ <rect x="37" y="5" width="4" height="6" rx="2" ry="2"/>
154
+ <rect x="43" y="7" width="4" height="4" rx="2" ry="2"/>
155
+ <rect x="49" y="9" width="4" height="2" rx="2" ry="2"/>
156
+ <rect x="1" y="9" width="4" height="2" rx="2" ry="2"/>
157
+ <rect x="7" y="7" width="4" height="4" rx="2" ry="2"/>
158
+ <rect x="13" y="5" width="4" height="6" rx="2" ry="2"/>
159
+ <rect x="19" y="7" width="4" height="4" rx="2" ry="2"/>
160
+ <rect x="25" y="9" width="4" height="2" rx="2" ry="2"/>
161
+ <rect x="31" y="7" width="4" height="4" rx="2" ry="2"/>
162
+ <rect x="37" y="5" width="4" height="6" rx="2" ry="2"/>
163
+ <rect x="43" y="7" width="4" height="4" rx="2" ry="2"/>
164
+ <rect x="49" y="9" width="4" height="2" rx="2" ry="2"/>
165
+ </svg>
166
+ );
167
+ }
168
+ }
169
+
170
+ export default InputMobile;
@@ -53,7 +53,10 @@ export default function ChatWidget({
53
53
  renderCustomDropDown,
54
54
  isTagsHidden,
55
55
  isEnableForV1 = true,
56
- questions
56
+ questions,
57
+ voice,
58
+ onSmartRobotClick,
59
+ isShowMobileInputArea,
57
60
  }: {
58
61
  is_enable_call:boolean,
59
62
  tags: [];
@@ -101,6 +104,10 @@ export default function ChatWidget({
101
104
  isTagsHidden: boolean;
102
105
  isEnableForV1: boolean;
103
106
  questions: any[];
107
+ isTagsHidden: boolean,
108
+ voice?: string;
109
+ onSmartRobotClick?: Function;
110
+ isShowMobileInputArea?: boolean;
104
111
  }) {
105
112
  const [open, setOpen] = useState(start_open);
106
113
  const [messages, setMessages] = useState<ChatMessageType[]>([]);
@@ -178,6 +185,9 @@ export default function ChatWidget({
178
185
  isTagsHidden={isTagsHidden}
179
186
  isEnableForV1={isEnableForV1}
180
187
  questions={questions}
188
+ voice={voice}
189
+ onSmartRobotClick={onSmartRobotClick}
190
+ isShowMobileInputArea={isShowMobileInputArea}
181
191
  />
182
192
  </div>
183
193
  );