Files
memind/src/components/VoiceInputDialog.tsx
T
john 70492d9eba Add attachment text extraction, auto web news skill, and chat/voice UI updates.
Simplify asset upload temp paths, refresh deploy docs for Aliyun DNS topology, and ship MindSpace content-scan and auth improvements.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-20 15:08:10 +08:00

148 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useVoiceSession } from '../hooks/useVoiceSession';
import { isVoiceInputAvailable } from '../voice/capabilities';
import { VoiceWaveform } from './VoiceWaveform';
export function VoiceInputDialog({
open,
disabled = false,
onClose,
onSend,
onError,
}: {
open: boolean;
disabled?: boolean;
onClose: () => void;
onSend: (text: string) => void;
onError?: (message: string) => void;
}) {
const { phase, text, analyser, liveRecognition, stopListening, finishFallbackRecording, resetSession } =
useVoiceSession({
active: open && !disabled,
onError,
});
const handleClose = useCallback(() => {
stopListening();
resetSession();
onClose();
}, [onClose, resetSession, stopListening]);
useEffect(() => {
if (!open) return undefined;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') handleClose();
};
window.addEventListener('keydown', onKeyDown);
return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener('keydown', onKeyDown);
};
}, [open, handleClose]);
if (!open) return null;
const listening = phase === 'listening';
const transcribing = phase === 'transcribing';
const busy = phase === 'requesting' || transcribing;
const canSend = Boolean(text.trim()) && !busy && !disabled;
const handleSend = () => {
const value = text.trim();
if (!value || disabled) return;
stopListening();
resetSession();
onSend(value);
onClose();
};
const statusText =
phase === 'requesting'
? '正在准备麦克风…'
: listening
? liveRecognition
? '正在聆听,文字会实时显示'
: '正在录音,完成后点击「完成识别」'
: transcribing
? '正在识别…'
: '可编辑识别结果后发送';
const supported = isVoiceInputAvailable();
if (!supported) {
return createPortal(
<div className="voice-dialog-backdrop" role="presentation" onClick={handleClose}>
<section className="voice-dialog" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
<p className="voice-dialog-status"></p>
<div className="voice-dialog-actions">
<button type="button" className="ghost-btn" onClick={handleClose}>
</button>
</div>
</section>
</div>,
document.body,
);
}
return createPortal(
<div className="voice-dialog-backdrop" role="presentation" onClick={handleClose}>
<section
className="voice-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="voice-dialog-title"
onClick={(event) => event.stopPropagation()}
>
<div className="voice-dialog-header">
<div>
<p className="voice-dialog-eyebrow"></p>
<h3 id="voice-dialog-title"></h3>
</div>
<button type="button" className="voice-dialog-close" onClick={handleClose} aria-label="关闭">
</button>
</div>
<VoiceWaveform analyser={analyser} active={listening || phase === 'requesting'} />
<p className="voice-dialog-status" role="status">
{transcribing ? <span className="voice-btn-spinner" aria-hidden="true" /> : null}
{statusText}
</p>
<textarea
className="voice-dialog-input input"
rows={4}
placeholder={listening ? '识别文字会出现在这里…' : '输入或编辑要发送的内容…'}
value={text}
disabled={busy}
onChange={(event) => updateText(event.target.value)}
/>
<div className="voice-dialog-actions">
<button type="button" className="ghost-btn" onClick={handleClose}>
</button>
{!liveRecognition && listening ? (
<button
type="button"
className="ghost-btn"
disabled={transcribing}
onClick={() => void finishFallbackRecording()}
>
</button>
) : null}
<button type="button" className="send-btn" disabled={!canSend} onClick={handleSend}>
</button>
</div>
</section>
</div>,
document.body,
);
}