feat: Refactor DpmOverlays to use ShowCard for rendering cards and add ToolStatusToast for operation status notifications
- Moved card rendering logic from DpmOverlays to a new ShowCard component for better reusability. - Introduced ToolStatusToast to display real-time operation statuses in the top right corner. - Updated PageHeader to conditionally render credits based on the current path. - Modified PromptPanel to change tool names and update prompt titles. - Enhanced ScreenLayout to include ToolStatusToast. - Updated styles for new components and adjusted existing styles for consistency. - Implemented statusBus utility for dispatching tool status events. - Updated useMqttControl to integrate tool status notifications during navigation and card display actions.
This commit is contained in:
@@ -25,7 +25,7 @@ import { useParkSim } from '../utils/parkData';
|
||||
|
||||
/* ---------- 模型占比(TOKEN 构成演示分布) ---------- */
|
||||
const MODELS = [
|
||||
{ name: 'DeepSeek-V3', value: 38, color: '#4c8dff' },
|
||||
{ name: 'DeepSeek', value: 38, color: '#4c8dff' },
|
||||
{ name: '通义千问', value: 21, color: '#22d3ee' },
|
||||
{ name: '智谱 GLM-4', value: 16, color: '#34d399' },
|
||||
{ name: '豆包', value: 12, color: '#fbbf24' },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Icon from '../components/Icons';
|
||||
import * as api from '../utils/api';
|
||||
import { getApiBase } from '../config';
|
||||
import { Sfx } from '../utils/sounds';
|
||||
@@ -557,6 +558,12 @@ export default function MediaScreen() {
|
||||
)}
|
||||
|
||||
{/* 播放控制已上移页眉(PageHeader 内渲染,媒体区保持纯净全屏) */}
|
||||
|
||||
{/* 左下角:版权/开发 玻璃质感芯片(顶部已移除) */}
|
||||
<div className="ms-credit-chip">
|
||||
<Icon name="copyright" size={12} />
|
||||
<span>云南派音人工智能科技有限公司 · 开发</span>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 页脚:园区概览(与数据大屏一致) */}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { S2sWsRealtimeClient } from '../voice/s2s-ws-client.js';
|
||||
import useVisionDetection, { VISION_DWELL_MS } from '../voice/useVisionDetection.js';
|
||||
import { getApiBase as API_BASE, getVoiceWsUrl } from '../config';
|
||||
import { aiStatus } from '../utils/statusBus';
|
||||
import '../styles/datascreen.css';
|
||||
import './voice-original.css';
|
||||
import './voice-overrides.css';
|
||||
@@ -115,8 +116,11 @@ export default function VoiceAssistant() {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [muted, setMuted] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
// 全局人物识别开关(MQTT vision_set 控制)
|
||||
// 摄像头常开:左下角视频预览保持显示(识别到人也不会自动问候,问候由 greetEnabled 控制)
|
||||
const [visionEnabled, setVisionEnabled] = useState(true);
|
||||
// 主动问候开关:默认关闭「识别到人 → 主动问候」,但摄像头/视频仍常开。
|
||||
// 需要恢复自动问候时改回 true,或用 MQTT 下发开启。
|
||||
const [greetEnabled, setGreetEnabled] = useState(false);
|
||||
|
||||
const clientRef = useRef(null);
|
||||
const audioCtxRef = useRef(null);
|
||||
@@ -133,6 +137,7 @@ export default function VoiceAssistant() {
|
||||
|
||||
const live = LIVE_STATES.has(state);
|
||||
|
||||
|
||||
// ── 自动问候生命周期状态(须在 start 之前定义:start 依赖数组引用 sendGreeting)──
|
||||
const GREETING_TEXT = '您好呀,需要我的帮助吗?';
|
||||
const GREETING_WAIT_MS = 25000; // 问候播完后等待用户回答的超时
|
||||
@@ -224,6 +229,14 @@ export default function VoiceAssistant() {
|
||||
try { args = JSON.parse(argsStr || '{}'); } catch { /* 忽略非法参数 */ }
|
||||
console.warn(`[tools] LLM 调用工具: ${name}`, args);
|
||||
pushMessage('assistant', `🔧 调用工具:${name}`, false);
|
||||
// 右上角状态提示
|
||||
const toolLabel = {
|
||||
control_display: args?.action === 'switch_page' ? '切换页面' : '控制大屏',
|
||||
get_park_overview: '查询园区数据',
|
||||
query_companies: '查询企业名录',
|
||||
get_time: '查询时间',
|
||||
}[name] || name;
|
||||
aiStatus(`正在调用工具:${toolLabel}`, { icon: name === 'control_display' && args?.action === 'switch_page' ? 'page' : 'tool' });
|
||||
let output;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE()}/api/tools/exec`, {
|
||||
@@ -301,10 +314,23 @@ export default function VoiceAssistant() {
|
||||
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
|
||||
});
|
||||
streamRef.current = stream;
|
||||
if (!audioCtxRef.current) audioCtxRef.current = new AudioContext();
|
||||
if (audioCtxRef.current.state === 'suspended') await audioCtxRef.current.resume();
|
||||
// 复用一个仍在运行的上下文;若已关闭(此前被误关)或未创建,则重建。
|
||||
// 注意:client.close() 不再关闭共享上下文,故会话结束(含手势重启)后仍可复用。
|
||||
let _ctx = audioCtxRef.current;
|
||||
if (!_ctx || _ctx.state === 'closed') {
|
||||
_ctx = new AudioContext();
|
||||
audioCtxRef.current = _ctx;
|
||||
}
|
||||
if (_ctx.state === 'suspended') {
|
||||
try { await _ctx.resume(); } catch { /* ignore */ }
|
||||
// 无用户激活(手势)时 resume 可能异步生效,稍等再试一次
|
||||
if (_ctx.state === 'suspended') {
|
||||
await new Promise((r) => setTimeout(r, 80));
|
||||
try { await _ctx.resume(); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
// 基础提示词:从后端拉取(配置 + 园区知识库已由服务端组装);失败用极简兜底
|
||||
// 基础提示词:从后端拉取(配置 + 园区知识库已由服务端组装);失败用极简兜底。
|
||||
let instructions = FALLBACK_INSTRUCTIONS;
|
||||
try {
|
||||
const insRes = await fetch(`${API_BASE()}/api/s2s/instructions`, { cache: 'no-store' });
|
||||
@@ -386,12 +412,20 @@ export default function VoiceAssistant() {
|
||||
if (c) { try { await c.close(); } catch { /* noop */ } }
|
||||
streamRef.current?.getTracks().forEach((t) => t.stop());
|
||||
streamRef.current = null;
|
||||
// 结束会话后丢弃 AudioContext 引用:下次启动一律新建,避免复用可能被
|
||||
// Chromium 自动挂起/关闭的旧上下文(否则麦克风不处理 → 一直「聆听」)。
|
||||
audioCtxRef.current = null;
|
||||
setMuted(false);
|
||||
setErrorMsg('');
|
||||
dismissAllBubbles();
|
||||
setState('idle');
|
||||
}, [dismissAllBubbles]);
|
||||
|
||||
// 离开语音对话页时强制停止会话与麦克风,避免残留触发/占用
|
||||
useEffect(() => {
|
||||
return () => { void stop(); };
|
||||
}, [stop]);
|
||||
|
||||
const toggleMute = useCallback(() => {
|
||||
const c = clientRef.current;
|
||||
if (!c) return;
|
||||
@@ -446,37 +480,18 @@ export default function VoiceAssistant() {
|
||||
});
|
||||
|
||||
// 手势 → 对话控制:举手 toggle —— 无对话则开始,正在对话则结束
|
||||
const onGesture = useCallback((g) => {
|
||||
if (g !== 'raise') return;
|
||||
if (clientRef.current) {
|
||||
void stop();
|
||||
} else {
|
||||
void start();
|
||||
}
|
||||
}, [start, stop]);
|
||||
// 【已按需求关闭】视觉手势识别误判会触发右上角弹窗/误开对话,故这里不再响应手势。
|
||||
const onGesture = useCallback(() => {
|
||||
// 视觉手势已关闭:识别到人脸/手势不再自动开始/结束对话
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
gestureRef.current = onGesture;
|
||||
}, [onGesture]);
|
||||
|
||||
const onVisionTrigger = useCallback(() => {
|
||||
// 仅 idle 允许自动问候:对话中/连接中/其他状态一律不打扰
|
||||
if (voiceStateRef.current !== 'idle') {
|
||||
console.warn('[vision] 自动问候跳过:当前状态', voiceStateRef.current);
|
||||
return;
|
||||
}
|
||||
pendingGreetingRef.current = true;
|
||||
if (clientRef.current) {
|
||||
sendGreeting('');
|
||||
} else {
|
||||
void start(); // start 连接成功后检查 pendingGreetingRef 再发问候
|
||||
}
|
||||
fetch(`${API_BASE()}/api/vision/event`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ event: 'triggered', faces: vision.faces, dwell_ms: VISION_DWELL_MS }),
|
||||
}).catch(() => { /* 上报失败不影响本地问候 */ });
|
||||
}, [vision.faces, start, sendGreeting]);
|
||||
// 主动问候已彻底关闭:识别到人(含停留 10 秒)不再自动问候/开始对话,也不弹任何提示
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
visionTriggerRef.current = onVisionTrigger;
|
||||
|
||||
Reference in New Issue
Block a user