codexmobile-relay / client /src /components /VoiceDialogPanel.jsx
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
3.34 kB
import { useEffect, useRef } from 'react';
import { Headphones, Loader2, Mic, Volume2, X } from 'lucide-react';
import { voiceDialogStatusLabel } from '../app-voice-utils.js';
export function VoiceDialogPanel({
open,
state,
error,
transcript,
assistantText,
handoffDraft,
onHandoffDraftChange,
onHandoffSubmit,
onHandoffContinue,
onHandoffCancel,
onStart,
onStop,
onClose
}) {
const closeButtonRef = useRef(null);
useEffect(() => {
if (open) {
closeButtonRef.current?.focus();
}
}, [open]);
if (!open) {
return null;
}
const listening = state === 'listening';
const confirmingHandoff = state === 'handoff';
const busy = ['transcribing', 'sending', 'waiting', 'speaking', 'summarizing'].includes(state);
const statusIcon = state === 'speaking'
? <Volume2 size={28} />
: busy
? <Loader2 className="spin" size={28} />
: <Mic size={28} />;
return (
<div className="voice-dialog-backdrop">
<section className="voice-dialog-panel" role="dialog" aria-modal="true" aria-label="语音对话">
<div className="voice-dialog-header">
<span>
<Headphones size={17} />
语音对话
</span>
<button ref={closeButtonRef} type="button" onClick={onClose} aria-label="关闭语音对话">
<X size={18} />
</button>
</div>
<div className={`voice-dialog-orb is-${state}`}>
{statusIcon}
</div>
<div className={`voice-dialog-status ${error ? 'is-error' : ''}`}>
{error || voiceDialogStatusLabel(state)}
</div>
{transcript ? <p className="voice-dialog-line is-user">{transcript}</p> : null}
{assistantText ? <p className="voice-dialog-line is-assistant">{assistantText}</p> : null}
{confirmingHandoff ? (
<div className="voice-dialog-handoff">
<textarea
value={handoffDraft}
onChange={(event) => onHandoffDraftChange(event.target.value)}
rows={8}
aria-label="交给 Codex 的任务"
/>
<div className="voice-dialog-actions voice-dialog-handoff-actions">
<button type="button" className="voice-dialog-secondary" onClick={onHandoffContinue}>
继续补充
</button>
<button type="button" className="voice-dialog-secondary" onClick={onHandoffCancel}>
取消
</button>
<button
type="button"
className="voice-dialog-primary"
onClick={onHandoffSubmit}
disabled={!String(handoffDraft || '').trim()}
>
交给 Codex
</button>
</div>
</div>
) : (
<div className="voice-dialog-actions">
<button
type="button"
className={`voice-dialog-primary ${listening ? 'is-listening' : ''}`}
onClick={listening ? onStop : onStart}
disabled={busy}
>
{listening ? '停止' : '开始'}
</button>
<button type="button" className="voice-dialog-secondary" onClick={onClose}>
结束
</button>
</div>
)}
</section>
</div>
);
}