Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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, updateText, stopListening, finishFallbackRecording } =
|
||||
useVoiceSession({
|
||||
active: open && !disabled,
|
||||
onError,
|
||||
});
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
stopListening();
|
||||
onClose();
|
||||
}, [onClose, 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();
|
||||
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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user