Commit d5be7bb1383fefba2990d4ffd8f31e8b96dcf68b

Authored by chenxt
1 parent 12faefcd

ai

src/mobile/Ai/AiChatStyles.css
@@ -460,6 +460,8 @@ body { @@ -460,6 +460,8 @@ body {
460 background-color: #f3f3f3; 460 background-color: #f3f3f3;
461 z-index: 210; 461 z-index: 210;
462 left: 50%; 462 left: 50%;
463 - top: 10%; 463 + top: 2%;
464 transform: translateX(-50%); 464 transform: translateX(-50%);
  465 + padding: 10px;
  466 + overflow-y: auto;
465 } 467 }
src/mobile/Ai/AiChatStyles.less
@@ -483,7 +483,9 @@ body { @@ -483,7 +483,9 @@ body {
483 background-color: #f3f3f3; 483 background-color: #f3f3f3;
484 z-index: 210; 484 z-index: 210;
485 left: 50%; 485 left: 50%;
486 - top: 10%; 486 + top: 2%;
487 transform: translateX(-50%); 487 transform: translateX(-50%);
  488 + padding: 10px;
  489 + overflow-y: auto;
488 } 490 }
489 } 491 }
490 \ No newline at end of file 492 \ No newline at end of file
src/mobile/Ai/newAi.jsx
1 import React, { useState, useEffect, useRef, useCallback } from 'react'; 1 import React, { useState, useEffect, useRef, useCallback } from 'react';
2 import ReactMarkdown from 'react-markdown'; 2 import ReactMarkdown from 'react-markdown';
3 import remarkGfm from 'remark-gfm'; 3 import remarkGfm from 'remark-gfm';
4 -import { Button } from "antd-mobile";  
5 -import { LocationOutline, PhoneFill } from "antd-mobile-icons"; 4 +import { PhoneFill, LocationOutline } from "antd-mobile-icons";
6 import './AiChatStyles.less'; 5 import './AiChatStyles.less';
7 - 6 +import { Toast, Input, Tabs, Selector, Grid, Image, Button, Checkbox, Switch, Dialog, Radio, Space, CenterPopup, ImageViewer, Collapse, CapsuleTabs } from "antd-mobile";
  7 +import VConsole from 'vconsole';
  8 +let vConsole;
8 const ChatInterface = () => { 9 const ChatInterface = () => {
9 // ==================== 状态管理 ==================== 10 // ==================== 状态管理 ====================
10 const [sessionId, setSessionId] = useState(''); 11 const [sessionId, setSessionId] = useState('');
@@ -16,13 +17,17 @@ const ChatInterface = () => { @@ -16,13 +17,17 @@ const ChatInterface = () => {
16 const [currentModel, setCurrentModel] = useState('general'); 17 const [currentModel, setCurrentModel] = useState('general');
17 const [chatHistory, setChatHistory] = useState([]); 18 const [chatHistory, setChatHistory] = useState([]);
18 const [welcomeContent, setWelcomeContent] = useState(''); 19 const [welcomeContent, setWelcomeContent] = useState('');
  20 + vConsole = new VConsole();
  21 +
19 22
20 // 语音输入状态 23 // 语音输入状态
21 - const [isVoiceModel, setIsVoiceModel] = useState(false);  
22 const [isRecording, setIsRecording] = useState(false); 24 const [isRecording, setIsRecording] = useState(false);
  25 + const [isRecordingModel, setIsRecordingModel] = useState(false);
23 const [isWsConnected, setIsWsConnected] = useState(false); 26 const [isWsConnected, setIsWsConnected] = useState(false);
24 const [isVoiceMode, setIsVoiceMode] = useState(false); 27 const [isVoiceMode, setIsVoiceMode] = useState(false);
25 const [recordingDuration, setRecordingDuration] = useState(0); 28 const [recordingDuration, setRecordingDuration] = useState(0);
  29 + const [isFlushing, setIsFlushing] = useState(false);
  30 + const silenceTimeoutRef = useRef(null); // 静默超时定时器
26 31
27 const messagesEndRef = useRef(null); 32 const messagesEndRef = useRef(null);
28 const inputRef = useRef(null); 33 const inputRef = useRef(null);
@@ -87,15 +92,30 @@ const ChatInterface = () => { @@ -87,15 +92,30 @@ const ChatInterface = () => {
87 ws.onmessage = (event) => { 92 ws.onmessage = (event) => {
88 try { 93 try {
89 const res = JSON.parse(event.data); 94 const res = JSON.parse(event.data);
90 - if (res.code === 0 && (res.msg === "success" || res.msg === "flush_success")) {  
91 - if (res.text && res.text.trim()) { 95 + if (res.code === 0) {
  96 + // 处理普通识别结果
  97 + if ((res.msg === "success" || res.msg === "partial") && res.text?.trim()) {
  98 + if (wsRef.current) {
  99 + wsRef.current.hasReceivedSpeech = true;
  100 + }
92 setInputValue(prev => { 101 setInputValue(prev => {
93 const separator = prev && !prev.endsWith(' ') ? ' ' : ''; 102 const separator = prev && !prev.endsWith(' ') ? ' ' : '';
94 - const newValue = prev ? `${prev}${separator}${res.text}` : res.text;  
95 - console.log('语音识别结果:', res.text, '更新后:', newValue);  
96 - return newValue; 103 + return prev ? `${prev}${separator}${res.text}` : res.text;
97 }); 104 });
98 } 105 }
  106 +
  107 + // 👇 新增:处理 flush 完成
  108 + if (res.msg === "flush_success") {
  109 + console.log("Flush 完成,语音识别结束");
  110 + // 延迟一点确保所有消息处理完毕
  111 + setTimeout(() => {
  112 + setIsFlushing(false);
  113 + // 只有在语音模式下才清空(避免干扰手动输入)
  114 + if (isVoiceMode) {
  115 + setInputValue('');
  116 + }
  117 + }, 100);
  118 + }
99 } 119 }
100 } catch (e) { 120 } catch (e) {
101 console.error("WebSocket消息解析失败:", e); 121 console.error("WebSocket消息解析失败:", e);
@@ -161,20 +181,18 @@ const ChatInterface = () => { @@ -161,20 +181,18 @@ const ChatInterface = () => {
161 // 开始录音 181 // 开始录音
162 const startRecording = async () => { 182 const startRecording = async () => {
163 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { 183 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
164 - alert("浏览器不支持麦克风采集"); 184 + Toast.show('浏览器不支持麦克风');
165 return; 185 return;
166 } 186 }
167 187
168 try { 188 try {
169 - // 先连接WebSocket  
170 if (!isWsConnected) { 189 if (!isWsConnected) {
171 connectWebSocket(); 190 connectWebSocket();
172 await new Promise(resolve => setTimeout(resolve, 1000)); 191 await new Promise(resolve => setTimeout(resolve, 1000));
173 } 192 }
174 193
175 - // 检查 WebSocket 是否已连接  
176 if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { 194 if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
177 - alert("语音识别服务未连接,请重试"); 195 + Toast.show('语音服务未连接');
178 return; 196 return;
179 } 197 }
180 198
@@ -183,7 +201,6 @@ const ChatInterface = () => { @@ -183,7 +201,6 @@ const ChatInterface = () => {
183 inputNodeRef.current = audioContextRef.current.createMediaStreamSource(stream); 201 inputNodeRef.current = audioContextRef.current.createMediaStreamSource(stream);
184 scriptProcessorRef.current = audioContextRef.current.createScriptProcessor(2048, 1, 1); 202 scriptProcessorRef.current = audioContextRef.current.createScriptProcessor(2048, 1, 1);
185 203
186 - // 使用 ref 来检查录音状态,避免闭包问题  
187 scriptProcessorRef.current.onaudioprocess = (event) => { 204 scriptProcessorRef.current.onaudioprocess = (event) => {
188 if (!isRecordingRef.current) return; 205 if (!isRecordingRef.current) return;
189 const inputData = event.inputBuffer.getChannelData(0); 206 const inputData = event.inputBuffer.getChannelData(0);
@@ -197,19 +214,34 @@ const ChatInterface = () => { @@ -197,19 +214,34 @@ const ChatInterface = () => {
197 inputNodeRef.current.connect(scriptProcessorRef.current); 214 inputNodeRef.current.connect(scriptProcessorRef.current);
198 scriptProcessorRef.current.connect(audioContextRef.current.destination); 215 scriptProcessorRef.current.connect(audioContextRef.current.destination);
199 216
200 - // 关键:先设置 ref,再设置 state 217 + // 👇 关键:重置标志 + 启动静默检测
  218 + let hasReceivedSpeech = false; // 闭包变量,记录是否收到语音
  219 +
  220 + // 保存到 ref,供 onmessage 使用
  221 + wsRef.current.hasReceivedSpeech = false;
  222 +
  223 + // 设置 3 秒静默超时
  224 + silenceTimeoutRef.current = setTimeout(() => {
  225 + if (!wsRef.current?.hasReceivedSpeech) {
  226 + console.log('3秒内未检测到语音,自动停止录音');
  227 + Toast.show('未检测到语音,请重新尝试');
  228 + handleSendMessage()
  229 + }
  230 + }, 3000);
  231 +
  232 + // 更新录音状态
201 isRecordingRef.current = true; 233 isRecordingRef.current = true;
202 setIsRecording(true); 234 setIsRecording(true);
203 setIsVoiceMode(true); 235 setIsVoiceMode(true);
204 setRecordingDuration(0); 236 setRecordingDuration(0);
205 237
206 - // 开始计时  
207 recordingTimerRef.current = setInterval(() => { 238 recordingTimerRef.current = setInterval(() => {
208 setRecordingDuration(prev => prev + 1); 239 setRecordingDuration(prev => prev + 1);
209 }, 1000); 240 }, 1000);
  241 +
210 } catch (e) { 242 } catch (e) {
211 console.error("录音启动失败:", e); 243 console.error("录音启动失败:", e);
212 - alert("录音启动失败:" + e.message); 244 + Toast.show('录音启动失败:' + (e.message || '未知错误'));
213 isRecordingRef.current = false; 245 isRecordingRef.current = false;
214 setIsRecording(false); 246 setIsRecording(false);
215 } 247 }
@@ -218,10 +250,15 @@ const ChatInterface = () => { @@ -218,10 +250,15 @@ const ChatInterface = () => {
218 // 停止录音 250 // 停止录音
219 const stopRecording = useCallback(() => { 251 const stopRecording = useCallback(() => {
220 console.log("停止录音"); 252 console.log("停止录音");
  253 + // 👇 清理静默超时
  254 + if (silenceTimeoutRef.current) {
  255 + clearTimeout(silenceTimeoutRef.current);
  256 + silenceTimeoutRef.current = null;
  257 + }
221 isRecordingRef.current = false; 258 isRecordingRef.current = false;
222 setIsRecording(false); 259 setIsRecording(false);
223 setIsVoiceMode(false); 260 setIsVoiceMode(false);
224 - 261 + setIsFlushing(true);
225 if (recordingTimerRef.current) { 262 if (recordingTimerRef.current) {
226 clearInterval(recordingTimerRef.current); 263 clearInterval(recordingTimerRef.current);
227 recordingTimerRef.current = null; 264 recordingTimerRef.current = null;
@@ -242,6 +279,9 @@ const ChatInterface = () => { @@ -242,6 +279,9 @@ const ChatInterface = () => {
242 279
243 // 发送刷新指令获取最终结果 280 // 发送刷新指令获取最终结果
244 sendCommand("flush"); 281 sendCommand("flush");
  282 + setTimeout(() => {
  283 + setInputValue('')
  284 + }, 500);
245 }, [sendCommand]); 285 }, [sendCommand]);
246 286
247 // 切换录音状态(点击按钮) 287 // 切换录音状态(点击按钮)
@@ -406,17 +446,17 @@ const ChatInterface = () => { @@ -406,17 +446,17 @@ const ChatInterface = () => {
406 } catch (error) { 446 } catch (error) {
407 console.error('请求失败:', error); 447 console.error('请求失败:', error);
408 const errorMessage = ` 448 const errorMessage = `
409 - 抱歉,请求出现错误:${error.message} 449 +抱歉,请求出现错误:${error.message}
410 450
411 - **可能的原因:**  
412 - 1. Spring Boot 后端服务未启动  
413 - 2. API 接口路径不正确  
414 - 3. 网络连接问题 451 +**可能的原因:**
  452 +1. Spring Boot 后端服务未启动
  453 +2. API 接口路径不正确
  454 +3. 网络连接问题
415 455
416 - **检查步骤:**  
417 - 1. 确保后端服务在端口 8099 运行  
418 - 2. 检查浏览器控制台查看详细错误  
419 - 3. 刷新页面重试 456 +**检查步骤:**
  457 +1. 确保后端服务在端口 8099 运行
  458 +2. 检查浏览器控制台查看详细错误
  459 +3. 刷新页面重试
420 `; 460 `;
421 addMessage(errorMessage, 'ai', true); 461 addMessage(errorMessage, 'ai', true);
422 } finally { 462 } finally {
@@ -465,15 +505,32 @@ const ChatInterface = () => { @@ -465,15 +505,32 @@ const ChatInterface = () => {
465 const handleModelChange = (e) => { 505 const handleModelChange = (e) => {
466 setCurrentModel(e.target.value); 506 setCurrentModel(e.target.value);
467 }; 507 };
468 - // 录音弹窗  
469 - const handlePhone = () => {  
470 - setIsVoiceModel(true)  
471 - startRecording();  
472 - }  
473 -  
474 // ==================== 渲染 ==================== 508 // ==================== 渲染 ====================
475 return ( 509 return (
476 <div className="ai-chat-container"> 510 <div className="ai-chat-container">
  511 + {/* 头部 */}
  512 + {/* <div className="chat-header">
  513 + <div className="header-left">
  514 + <h1>小羚羊Ai-agent智能体</h1>
  515 + <p>AI 印刷助手</p>
  516 + </div>
  517 + <div className="header-right">
  518 + <select
  519 + className="model-selector"
  520 + value={currentModel}
  521 + onChange={handleModelChange}
  522 + >
  523 + <option value="process">小羚羊印刷行业大模型</option>
  524 + <option value="general">qwen2.5:14b</option>
  525 + </select>
  526 + <button
  527 + className="model-selectors"
  528 + onClick={handleClearChat}
  529 + >
  530 + 清空对话
  531 + </button>
  532 + </div>
  533 + </div> */}
477 <Button 534 <Button
478 className="model-Button" 535 className="model-Button"
479 onClick={handleClearChat} 536 onClick={handleClearChat}
@@ -557,7 +614,7 @@ const ChatInterface = () =&gt; { @@ -557,7 +614,7 @@ const ChatInterface = () =&gt; {
557 {/* 输入区域 */} 614 {/* 输入区域 */}
558 <div className="input-section"> 615 <div className="input-section">
559 {/* 语音模式提示 - 仅在录音时显示 */} 616 {/* 语音模式提示 - 仅在录音时显示 */}
560 - {/* {isRecording && ( 617 + {isRecording && (
561 <div className="voice-mode-indicator"> 618 <div className="voice-mode-indicator">
562 <div className="voice-wave"> 619 <div className="voice-wave">
563 <span></span> 620 <span></span>
@@ -576,7 +633,7 @@ const ChatInterface = () =&gt; { @@ -576,7 +633,7 @@ const ChatInterface = () =&gt; {
576 取消 633 取消
577 </button> 634 </button>
578 </div> 635 </div>
579 - )} */} 636 + )}
580 637
581 <div className="input-wrapper"> 638 <div className="input-wrapper">
582 <input 639 <input
@@ -599,8 +656,10 @@ const ChatInterface = () =&gt; { @@ -599,8 +656,10 @@ const ChatInterface = () =&gt; {
599 disabled={isLoading} 656 disabled={isLoading}
600 readOnly={isRecording} 657 readOnly={isRecording}
601 /> 658 />
602 - <PhoneFill className='input-icon' onClick={handlePhone} />  
603 - 659 + <PhoneFill className='input-icon' onClick={() => {
  660 + setIsRecordingModel(true)
  661 + startRecording()
  662 + }} />
604 <LocationOutline className='input-icon' onClick={handleSendMessage} 663 <LocationOutline className='input-icon' onClick={handleSendMessage}
605 disabled={isLoading || isRecording} /> 664 disabled={isLoading || isRecording} />
606 {/* 语音按钮 - 点击切换录音状态 */} 665 {/* 语音按钮 - 点击切换录音状态 */}
@@ -618,9 +677,9 @@ const ChatInterface = () =&gt; { @@ -618,9 +677,9 @@ const ChatInterface = () =&gt; {
618 <span className="voice-text">点击录音</span> 677 <span className="voice-text">点击录音</span>
619 </> 678 </>
620 )} 679 )}
621 - </button> */} 680 + </button>
622 681
623 - {/* <button 682 + <button
624 className={`send-button ${isLoading ? 'disabled' : ''}`} 683 className={`send-button ${isLoading ? 'disabled' : ''}`}
625 onClick={handleSendMessage} 684 onClick={handleSendMessage}
626 disabled={isLoading || isRecording} 685 disabled={isLoading || isRecording}
@@ -631,44 +690,33 @@ const ChatInterface = () =&gt; { @@ -631,44 +690,33 @@ const ChatInterface = () =&gt; {
631 </div> 690 </div>
632 </div> 691 </div>
633 </div> 692 </div>
  693 +
634 { 694 {
635 - isVoiceModel ?  
636 - <div className='phone-model phone-zhezhao'> 695 + isRecordingModel ?
  696 + <div className='phone-model'>
637 <div className='phone-zhezhao'></div> 697 <div className='phone-zhezhao'></div>
638 <div className='phone-content'> 698 <div className='phone-content'>
639 -  
640 - </div>  
641 - {/* 语音模式提示 - 仅在录音时显示 */}  
642 - {isRecording && (  
643 - <div className="voice-mode-indicator">  
644 - <div className="voice-wave">  
645 - <span></span>  
646 - <span></span>  
647 - <span></span>  
648 - <span></span>  
649 - <span></span> 699 + {inputValue.trim() && (
  700 + <div className="message user-message">
  701 + <div className="message-bubble user-bubble">
  702 + <div className="message-content">
  703 + {inputValue}
  704 + </div>
  705 + <div className="message-meta">
  706 + <span className="message-time">{getCurrentTime()}</span>
  707 + </div>
  708 + </div>
650 </div> 709 </div>
651 - <span className="voice-text">  
652 - 正在录音 {formatDuration(recordingDuration)}  
653 - </span>  
654 - <button  
655 - className="voice-cancel-btn"  
656 - onClick={cancelRecording}  
657 - >  
658 - 取消  
659 - </button>  
660 - </div>  
661 - )}  
662 - <div className='phone-phone' onClick={() => {  
663 - stopRecording()  
664 - setIsVoiceModel(false)  
665 - }}>  
666 - <PhoneFill color='red' /> 710 + )}
  711 + </div>
  712 + <div className='phone-phone'>
  713 + <PhoneFill color='red' onClick={() => {
  714 + setIsRecordingModel(false)
  715 + stopRecording()
  716 + }} />
667 </div> 717 </div>
668 -  
669 </div> : '' 718 </div> : ''
670 } 719 }
671 -  
672 </div> 720 </div>
673 ); 721 );
674 }; 722 };
src/utils/config.js
@@ -9,7 +9,7 @@ const API = process.env.API; @@ -9,7 +9,7 @@ const API = process.env.API;
9 const bHttps = false; 9 const bHttps = false;
10 export const webSite = { 10 export const webSite = {
11 faceAddress: isDev ? '//km5cjx.gnway.cc:36867/xlyFace' : '//' + location.host + '/xlyFace', 11 faceAddress: isDev ? '//km5cjx.gnway.cc:36867/xlyFace' : '//' + location.host + '/xlyFace',
12 - ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//118.178.19.35:9198/xlyEntry_saas/' : '//' + location.host + '/xlyEntry/', 12 + ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//118.178.19.35:9198/xlyEntry_saas/' : '//118.178.19.35:9198/xlyEntry_saas/',
13 13
14 // ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//118.178.19.35:8088/xlyEntry/' : '//' + location.host + '/xlyEntry/', 14 // ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//118.178.19.35:8088/xlyEntry/' : '//' + location.host + '/xlyEntry/',
15 // ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//km5cjx.gnway.cc:36867/xlyEntry/' : '//' + location.host + '/xlyEntry/', 15 // ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//km5cjx.gnway.cc:36867/xlyEntry/' : '//' + location.host + '/xlyEntry/',