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')}
+ ); }