feat(web): 实时对话 - 第一阶段输入行内联语音录制与浏览器 STT 转写 (#165)

Co-authored-by: zhaoyue <zhaoyue@rongshantech.com>
This commit is contained in:
爱笑的毛毛虫
2026-08-07 15:06:56 +08:00
committed by GitHub
parent abec135d26
commit 8652ee74c6
13 changed files with 1006 additions and 1 deletions

View File

@@ -7,6 +7,7 @@ import { Link } from "react-router";
import { AssetMediaPreview } from "@/components/asset-media-preview";
import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea";
import { VoiceRecordingButton } from "@/components/conversation/voice-recording-button";
import { ModelPicker } from "@/components/model-picker";
import { canvasResourceMentionToken } from "@/lib/canvas/canvas-resource-references";
import { createClientId } from "@/lib/client-id";
@@ -746,6 +747,10 @@ function CreationComposer(props: ComposerProps) {
</div>
<footer className="creation-chat-dock">
<div className="creation-chat-controls">
<VoiceRecordingButton
disabled={props.busy}
onTranscribed={(text) => props.setPrompt(props.prompt.trim() ? `${props.prompt} ${text}` : text)}
/>
<ModePicker mode={props.mode} onModeChange={props.onModeChange} />
<ModelPicker config={props.config} value={props.model} onChange={props.onModelChange} capability={props.mode} className="creation-model-picker" placeholder={`选择${modeLabels[props.mode]}模型`} showSelectedPrice={false} variant="creation" />
{props.mode === "video" || (props.mode === "image" && imageSettingsSupported) ? <GenerationSettingsMenu {...props} /> : null}

View File

@@ -0,0 +1,107 @@
import { useState } from "react";
import { Button, Card, message } from "antd";
import { Mic, Send } from "lucide-react";
import { VoiceRecordingButton } from "@/components/conversation/voice-recording-button";
import { canvasThemes } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
/**
* 语音录制功能测试页面
* 验证输入行内联波形录制和 STT 转写闭环
*/
export default function TestVoiceRecording() {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const [prompt, setPrompt] = useState("");
const [sending, setSending] = useState(false);
const handleTranscribed = (text: string) => {
// 转写结果填入输入框,供用户确认或直接发送
setPrompt((current) => (current.trim() ? `${current} ${text}` : text));
message.success("语音已转写为文字");
};
const handleSubmit = async () => {
if (!prompt.trim()) return;
setSending(true);
try {
// 原型阶段:发送到对话入口由调用方接入;此处仅输出
console.log("发送文本:", prompt);
message.success("发送成功(原型阶段,仅控制台输出)");
setPrompt("");
} finally {
setSending(false);
}
};
return (
<div className="min-h-screen p-8" style={{ background: theme.spatial.surface }}>
<div className="mx-auto max-w-2xl">
<Card
title={
<div className="flex items-center gap-2">
<Mic className="size-5" style={{ color: theme.accent.primary }} />
<span>实时对话功能测试(MVP)</span>
</div>
}
style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border }}
>
<div className="space-y-4">
{/* 文本输入 */}
<div>
<label className="mb-2 block text-sm font-medium" style={{ color: theme.node.text }}>
文本输入(语音转写结果会自动填入)
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="输入消息,或点击实时对话按钮用语音输入..."
className="w-full rounded-lg border px-3 py-2 text-sm outline-none transition focus:border-current"
style={{
background: theme.node.fill,
borderColor: theme.toolbar.border,
color: theme.node.text,
minHeight: 80,
}}
/>
</div>
{/* 音频预览 */}
{/* 控制栏:实时对话按钮(点击后在输入行展开波形录制条) */}
<div className="flex items-center justify-between gap-2">
<div className="flex flex-1 items-center gap-2">
<VoiceRecordingButton onTranscribed={handleTranscribed} />
</div>
<Button
type="primary"
icon={<Send className="size-4" />}
disabled={!prompt.trim()}
loading={sending}
onClick={handleSubmit}
style={{ background: theme.accent.primary, borderColor: theme.accent.primary }}
>
发送
</Button>
</div>
{/* 说明 */}
<div className="rounded-lg border p-3 text-xs" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.muted }}>
<div className="font-semibold" style={{ color: theme.node.text }}>
使用说明:
</div>
<ul className="mt-1 list-inside list-disc space-y-1">
<li>点击麦克风按钮,输入行内展开波形录制条并自动开始录音</li>
<li>波形动画实时显示音量变化</li>
<li>点击停止按钮完成录制,自动上传并转写</li>
<li>转写结果自动填入输入框,可编辑后发送</li>
<li>转写失败时在录制条内提示,可点击麦克风重试</li>
</ul>
</div>
</div>
</Card>
</div>
</div>
);
}