Files
dudu/design/ui_kits/desktop/OverlayDemo.jsx
T
wangjia 40760aa884
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s
dudu MVP:五端语音输入法初始提交
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:38:37 +08:00

109 lines
5.0 KiB
React
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.
// dudu 桌面端 · 识别浮层演示
// 在仿真备忘录窗口上方演示完整 push-to-talk 流程:
// 按住按钮(= 按住 ⌘⇧Space)→ 聆听中 → partial 流式 → 松开 → final 注入输入框
const DEMO_SENTENCES = [
'帮我把这份周报整理一下,重点突出本周的进展和下周的计划。',
'好的,我马上把文件发给你,大概十分钟之内。',
'明天上午十点的会改到下午三点,记得同步给项目组。',
];
function OverlayDemo() {
const { RecognitionOverlay } = window.DuduDesignSystem_2e0172;
const [phase, setPhase] = React.useState('idle'); // idle | listening | streaming
const [finalText, setFinalText] = React.useState('');
const [partialText, setPartialText] = React.useState('');
const [docText, setDocText] = React.useState('');
const [sentenceIdx, setSentenceIdx] = React.useState(0);
const timerRef = React.useRef(null);
const stateRef = React.useRef({});
stateRef.current = { finalText, partialText, sentenceIdx };
const stop = React.useCallback(() => {
clearInterval(timerRef.current);
const { finalText: f, partialText: p, sentenceIdx: idx } = stateRef.current;
const committed = f + p;
if (committed) {
setDocText((d) => d + committed);
setSentenceIdx((idx + 1) % DEMO_SENTENCES.length);
}
setFinalText(''); setPartialText('');
setPhase('idle');
}, []);
const start = React.useCallback(() => {
if (timerRef.current) clearInterval(timerRef.current);
setPhase('listening'); setFinalText(''); setPartialText('');
const sentence = DEMO_SENTENCES[stateRef.current.sentenceIdx];
let i = 0;
setTimeout(() => setPhase((ph) => (ph === 'listening' ? 'streaming' : ph)), 420);
timerRef.current = setInterval(() => {
i += 1;
if (i >= sentence.length) { clearInterval(timerRef.current); setFinalText(sentence); setPartialText(''); return; }
// 模拟流式:句读处定稿(final),其余为 partial
const lastPunct = Math.max(sentence.lastIndexOf('', i), sentence.lastIndexOf('。', i));
setFinalText(sentence.slice(0, lastPunct + 1));
setPartialText(sentence.slice(lastPunct + 1, i));
}, 90);
}, []);
React.useEffect(() => () => clearInterval(timerRef.current), []);
const overlayState = phase === 'streaming' && (finalText || partialText) ? 'text' : 'listening';
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 28 }}>
<div style={{ position: 'relative' }}>
<MacWindow title="备忘录 — 周报草稿" width={560}>
<div style={{ padding: '18px 20px', minHeight: 150 }}>
<div style={{ fontSize: 'var(--text-md)', lineHeight: 'var(--leading-loose)', color: 'var(--text-1)', minHeight: 96 }}>
{docText || <span style={{ color: 'var(--text-3)' }}>光标在这里 在任意应用中按住快捷键即可语音输入</span>}
<span style={{ color: 'var(--accent)', animation: 'dd-caret 1.1s steps(1) infinite' }}></span>
</div>
</div>
</MacWindow>
{phase !== 'idle' && (
<div style={{ position: 'absolute', left: '50%', top: '100%', transform: 'translate(-50%, -22px)', animation: 'dd-rise var(--dur-base) var(--ease-out)' }}>
<RecognitionOverlay
state={overlayState}
finalText={finalText}
partialText={partialText}
hint="松开 ⌘⇧Space 完成输入"
/>
</div>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginTop: phase === 'idle' ? 0 : 40 }}>
<button
type="button"
onPointerDown={start}
onPointerUp={stop}
onPointerLeave={() => phase !== 'idle' && stop()}
style={{
display: 'inline-flex', alignItems: 'center', gap: 8, height: 'var(--control-lg)',
padding: '0 22px', borderRadius: 'var(--radius-full)', border: 'none', cursor: 'pointer',
background: phase === 'idle' ? 'var(--accent)' : 'var(--positive)',
color: 'var(--text-on-accent)', fontSize: 'var(--text-base)', fontWeight: 'var(--weight-semibold)',
fontFamily: 'var(--font-sans)', userSelect: 'none', WebkitUserSelect: 'none',
transition: 'background var(--dur-fast) var(--ease-out)',
}}
>
<DuIcons.Mic size={17} />
{phase === 'idle' ? '按住试一试(模拟按住 ⌘⇧Space)' : '松开完成输入'}
</button>
{docText && (
<button type="button" onClick={() => setDocText('')} style={{
height: 'var(--control-lg)', padding: '0 16px', borderRadius: 'var(--radius-full)',
border: '1px solid var(--border-1)', background: 'var(--surface-card)', color: 'var(--text-2)',
fontSize: 'var(--text-sm)', cursor: 'pointer', fontFamily: 'var(--font-sans)',
}}>清空</button>
)}
</div>
</div>
);
}
Object.assign(window, { OverlayDemo });