Commit d5be7bb1383fefba2990d4ffd8f31e8b96dcf68b

Authored by chenxt
1 parent 12faefcd

ai

src/mobile/Ai/AiChatStyles.css
... ... @@ -460,6 +460,8 @@ body {
460 460 background-color: #f3f3f3;
461 461 z-index: 210;
462 462 left: 50%;
463   - top: 10%;
  463 + top: 2%;
464 464 transform: translateX(-50%);
  465 + padding: 10px;
  466 + overflow-y: auto;
465 467 }
... ...
src/mobile/Ai/AiChatStyles.less
... ... @@ -483,7 +483,9 @@ body {
483 483 background-color: #f3f3f3;
484 484 z-index: 210;
485 485 left: 50%;
486   - top: 10%;
  486 + top: 2%;
487 487 transform: translateX(-50%);
  488 + padding: 10px;
  489 + overflow-y: auto;
488 490 }
489 491 }
490 492 \ No newline at end of file
... ...
src/mobile/Ai/newAi.jsx
1 1 import React, { useState, useEffect, useRef, useCallback } from 'react';
2 2 import ReactMarkdown from 'react-markdown';
3 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 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 9 const ChatInterface = () => {
9 10 // ==================== 状态管理 ====================
10 11 const [sessionId, setSessionId] = useState('');
... ... @@ -16,13 +17,17 @@ const ChatInterface = () => {
16 17 const [currentModel, setCurrentModel] = useState('general');
17 18 const [chatHistory, setChatHistory] = useState([]);
18 19 const [welcomeContent, setWelcomeContent] = useState('');
  20 + vConsole = new VConsole();
  21 +
19 22  
20 23 // 语音输入状态
21   - const [isVoiceModel, setIsVoiceModel] = useState(false);
22 24 const [isRecording, setIsRecording] = useState(false);
  25 + const [isRecordingModel, setIsRecordingModel] = useState(false);
23 26 const [isWsConnected, setIsWsConnected] = useState(false);
24 27 const [isVoiceMode, setIsVoiceMode] = useState(false);
25 28 const [recordingDuration, setRecordingDuration] = useState(0);
  29 + const [isFlushing, setIsFlushing] = useState(false);
  30 + const silenceTimeoutRef = useRef(null); // 静默超时定时器
26 31  
27 32 const messagesEndRef = useRef(null);
28 33 const inputRef = useRef(null);
... ... @@ -87,15 +92,30 @@ const ChatInterface = () => {
87 92 ws.onmessage = (event) => {
88 93 try {
89 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 101 setInputValue(prev => {
93 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 120 } catch (e) {
101 121 console.error("WebSocket消息解析失败:", e);
... ... @@ -161,20 +181,18 @@ const ChatInterface = () => {
161 181 // 开始录音
162 182 const startRecording = async () => {
163 183 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
164   - alert("浏览器不支持麦克风采集");
  184 + Toast.show('浏览器不支持麦克风');
165 185 return;
166 186 }
167 187  
168 188 try {
169   - // 先连接WebSocket
170 189 if (!isWsConnected) {
171 190 connectWebSocket();
172 191 await new Promise(resolve => setTimeout(resolve, 1000));
173 192 }
174 193  
175   - // 检查 WebSocket 是否已连接
176 194 if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
177   - alert("语音识别服务未连接,请重试");
  195 + Toast.show('语音服务未连接');
178 196 return;
179 197 }
180 198  
... ... @@ -183,7 +201,6 @@ const ChatInterface = () => {
183 201 inputNodeRef.current = audioContextRef.current.createMediaStreamSource(stream);
184 202 scriptProcessorRef.current = audioContextRef.current.createScriptProcessor(2048, 1, 1);
185 203  
186   - // 使用 ref 来检查录音状态,避免闭包问题
187 204 scriptProcessorRef.current.onaudioprocess = (event) => {
188 205 if (!isRecordingRef.current) return;
189 206 const inputData = event.inputBuffer.getChannelData(0);
... ... @@ -197,19 +214,34 @@ const ChatInterface = () => {
197 214 inputNodeRef.current.connect(scriptProcessorRef.current);
198 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 233 isRecordingRef.current = true;
202 234 setIsRecording(true);
203 235 setIsVoiceMode(true);
204 236 setRecordingDuration(0);
205 237  
206   - // 开始计时
207 238 recordingTimerRef.current = setInterval(() => {
208 239 setRecordingDuration(prev => prev + 1);
209 240 }, 1000);
  241 +
210 242 } catch (e) {
211 243 console.error("录音启动失败:", e);
212   - alert("录音启动失败:" + e.message);
  244 + Toast.show('录音启动失败:' + (e.message || '未知错误'));
213 245 isRecordingRef.current = false;
214 246 setIsRecording(false);
215 247 }
... ... @@ -218,10 +250,15 @@ const ChatInterface = () => {
218 250 // 停止录音
219 251 const stopRecording = useCallback(() => {
220 252 console.log("停止录音");
  253 + // 👇 清理静默超时
  254 + if (silenceTimeoutRef.current) {
  255 + clearTimeout(silenceTimeoutRef.current);
  256 + silenceTimeoutRef.current = null;
  257 + }
221 258 isRecordingRef.current = false;
222 259 setIsRecording(false);
223 260 setIsVoiceMode(false);
224   -
  261 + setIsFlushing(true);
225 262 if (recordingTimerRef.current) {
226 263 clearInterval(recordingTimerRef.current);
227 264 recordingTimerRef.current = null;
... ... @@ -242,6 +279,9 @@ const ChatInterface = () => {
242 279  
243 280 // 发送刷新指令获取最终结果
244 281 sendCommand("flush");
  282 + setTimeout(() => {
  283 + setInputValue('')
  284 + }, 500);
245 285 }, [sendCommand]);
246 286  
247 287 // 切换录音状态(点击按钮)
... ... @@ -406,17 +446,17 @@ const ChatInterface = () => {
406 446 } catch (error) {
407 447 console.error('请求失败:', error);
408 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 461 addMessage(errorMessage, 'ai', true);
422 462 } finally {
... ... @@ -465,15 +505,32 @@ const ChatInterface = () => {
465 505 const handleModelChange = (e) => {
466 506 setCurrentModel(e.target.value);
467 507 };
468   - // 录音弹窗
469   - const handlePhone = () => {
470   - setIsVoiceModel(true)
471   - startRecording();
472   - }
473   -
474 508 // ==================== 渲染 ====================
475 509 return (
476 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 534 <Button
478 535 className="model-Button"
479 536 onClick={handleClearChat}
... ... @@ -557,7 +614,7 @@ const ChatInterface = () =&gt; {
557 614 {/* 输入区域 */}
558 615 <div className="input-section">
559 616 {/* 语音模式提示 - 仅在录音时显示 */}
560   - {/* {isRecording && (
  617 + {isRecording && (
561 618 <div className="voice-mode-indicator">
562 619 <div className="voice-wave">
563 620 <span></span>
... ... @@ -576,7 +633,7 @@ const ChatInterface = () =&gt; {
576 633 取消
577 634 </button>
578 635 </div>
579   - )} */}
  636 + )}
580 637  
581 638 <div className="input-wrapper">
582 639 <input
... ... @@ -599,8 +656,10 @@ const ChatInterface = () =&gt; {
599 656 disabled={isLoading}
600 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 663 <LocationOutline className='input-icon' onClick={handleSendMessage}
605 664 disabled={isLoading || isRecording} />
606 665 {/* 语音按钮 - 点击切换录音状态 */}
... ... @@ -618,9 +677,9 @@ const ChatInterface = () =&gt; {
618 677 <span className="voice-text">点击录音</span>
619 678 </>
620 679 )}
621   - </button> */}
  680 + </button>
622 681  
623   - {/* <button
  682 + <button
624 683 className={`send-button ${isLoading ? 'disabled' : ''}`}
625 684 onClick={handleSendMessage}
626 685 disabled={isLoading || isRecording}
... ... @@ -631,44 +690,33 @@ const ChatInterface = () =&gt; {
631 690 </div>
632 691 </div>
633 692 </div>
  693 +
634 694 {
635   - isVoiceModel ?
636   - <div className='phone-model phone-zhezhao'>
  695 + isRecordingModel ?
  696 + <div className='phone-model'>
637 697 <div className='phone-zhezhao'></div>
638 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 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 717 </div>
668   -
669 718 </div> : ''
670 719 }
671   -
672 720 </div>
673 721 );
674 722 };
... ...
src/utils/config.js
... ... @@ -9,7 +9,7 @@ const API = process.env.API;
9 9 const bHttps = false;
10 10 export const webSite = {
11 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 14 // ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//118.178.19.35:8088/xlyEntry/' : '//' + location.host + '/xlyEntry/',
15 15 // ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//km5cjx.gnway.cc:36867/xlyEntry/' : '//' + location.host + '/xlyEntry/',
... ...