40760aa884
- 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>
109 lines
5.0 KiB
React
109 lines
5.0 KiB
React
// 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 });
|