From a8f4b891a0ccb0125e411d94df2d2cf37db43917 Mon Sep 17 00:00:00 2001 From: baiqing Date: Sat, 2 May 2026 10:38:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(qa):=20QaPanel=20=E5=8A=A0=20qa:level=20?= =?UTF-8?q?=E7=9B=91=E5=90=AC=20+=20LevelBar=20=E7=94=B5=E5=B9=B3=E6=9D=A1?= =?UTF-8?q?=20(closes=20#162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端 coordinator.rs:1743 在 QA 录音时每帧 33ms 推 qa:level 事件, 但 QaPanel 之前只 listen qa:state / qa:dismiss,qa:level 完全无人接 → QA 录音用户看不到电平反馈,不知道麦克风是否收音。 修复: - QaPanel 加 listen('qa:level') 累积 level state(cleanup 加 unlistenLevel) - 新增 LevelBar 组件:4px 高蓝色进度条,宽度 = level × 100% - RecordingHeader / TurnIndicator(recording) 都加 LevelBar 渲染 - 状态切换非 recording 时 setLevel(0) 避免残留 测试: - npm run build ✅ - macOS 实测:QA 录音电平条跟随声音起伏(同 capsule 体验) --- openless-all/app/src/pages/QaPanel.tsx | 49 ++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/openless-all/app/src/pages/QaPanel.tsx b/openless-all/app/src/pages/QaPanel.tsx index d1739439..46321e60 100644 --- a/openless-all/app/src/pages/QaPanel.tsx +++ b/openless-all/app/src/pages/QaPanel.tsx @@ -31,6 +31,8 @@ export function QaPanel() { const [pinned, setPinned] = useState(false); /** 流式 LLM 答案:answer_delta 累积、answer 事件来时清空(最终内容已落到 messages)。 */ const [streamingAnswer, setStreamingAnswer] = useState(''); + /** 录音电平:0..1。后端每帧 33ms 通过 qa:level emit。详见 issue #162。 */ + const [level, setLevel] = useState(0); const tRef = useRef(t); tRef.current = t; @@ -39,6 +41,7 @@ export function QaPanel() { if (!isTauri) return; let unlistenState: (() => void) | undefined; let unlistenDismiss: (() => void) | undefined; + let unlistenLevel: (() => void) | undefined; let cancelled = false; (async () => { try { @@ -54,6 +57,7 @@ export function QaPanel() { setSelectionPreview(''); setErrorMsg(''); setStreamingAnswer(''); + setLevel(0); break; case 'recording': setStatus('recording'); @@ -66,6 +70,7 @@ export function QaPanel() { setSelectionPreview(''); setErrorMsg(''); setStreamingAnswer(''); + setLevel(0); break; case 'answer_delta': // 流式增量。仍保持 thinking 状态——直到 answer 事件落定后才回 idle。 @@ -79,11 +84,13 @@ export function QaPanel() { setErrorMsg(''); // messages 已被上面的 setMessages 落定,清掉流式 buffer 避免和最终气泡重影。 setStreamingAnswer(''); + setLevel(0); break; case 'error': setStatus('error'); setErrorMsg(payload.error ?? tRef.current('qa.error')); setStreamingAnswer(''); + setLevel(0); break; } }); @@ -91,12 +98,18 @@ export function QaPanel() { setPinned(false); void qaWindowDismiss(); }); + // qa:level — 录音电平,节流 ~33ms/帧。详见 issue #162。 + const levelHandle = await listen<{ level: number }>('qa:level', event => { + setLevel(event.payload.level ?? 0); + }); if (cancelled) { stateHandle(); dismissHandle(); + levelHandle(); } else { unlistenState = stateHandle; unlistenDismiss = dismissHandle; + unlistenLevel = levelHandle; } } catch (error) { console.error('[QaPanel] listener setup failed', error); @@ -106,6 +119,7 @@ export function QaPanel() { cancelled = true; unlistenState?.(); unlistenDismiss?.(); + unlistenLevel?.(); }; }, []); @@ -145,11 +159,11 @@ export function QaPanel() {
{messages.length === 0 && status === 'idle' && } {messages.length === 0 && status === 'recording' && ( - + )} {status === 'recording' && messages.length > 0 && ( - + )} {streamingAnswer && ( @@ -248,9 +262,11 @@ function EmptyHint({ t }: { t: ReturnType['t'] }) { function RecordingHeader({ preview, t, + level, }: { preview: string; t: ReturnType['t']; + level: number; }) { const truncated = useMemo(() => truncate(preview, SELECTION_PREVIEW_MAX), [preview]); return ( @@ -267,6 +283,32 @@ function RecordingHeader({ {t('qa.recordingHint')}
+ + + ); +} + +/** QA 录音电平条。后端 qa:level 每帧 ~33ms 推一次 0..1。详见 issue #162。 */ +function LevelBar({ level }: { level: number }) { + const pct = Math.min(100, Math.max(0, level * 100)); + return ( +
+
); } @@ -366,10 +408,12 @@ function TurnIndicator({ kind, preview, t, + level, }: { kind: 'recording' | 'thinking'; preview?: string; t: ReturnType['t']; + level?: number; }) { if (kind === 'recording') { const truncated = preview ? truncate(preview, SELECTION_PREVIEW_MAX) : ''; @@ -387,6 +431,7 @@ function TurnIndicator({ {t('qa.recordingHint')}
+ ); }