dudu MVP:五端语音输入法初始提交
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s

- 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>
This commit is contained in:
wangjia
2026-06-12 00:38:37 +08:00
commit 40760aa884
252 changed files with 40789 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
// 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 });