2026-03-12 12:47:56 +08:00
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
2026-04-17 09:36:13 +08:00
|
|
|
|
import { Settings2, Zap, Mic, MessageSquare, History, Plus, Film } from 'lucide-react';
|
2026-03-12 12:47:56 +08:00
|
|
|
|
import VoicePanel from './components/VoicePanel';
|
|
|
|
|
|
import ChatPanel from './components/ChatPanel';
|
|
|
|
|
|
import SettingsPanel from './components/SettingsPanel';
|
|
|
|
|
|
import { getVoiceConfig } from './services/voiceApi';
|
2026-03-13 13:06:46 +08:00
|
|
|
|
import SessionHistoryPanel from './components/SessionHistoryPanel';
|
2026-04-17 09:36:13 +08:00
|
|
|
|
import VideoPanel from './components/VideoPanel';
|
2026-03-12 12:47:56 +08:00
|
|
|
|
|
|
|
|
|
|
export default function App() {
|
|
|
|
|
|
const [showSettings, setShowSettings] = useState(false);
|
2026-03-13 13:06:46 +08:00
|
|
|
|
const [showHistory, setShowHistory] = useState(false);
|
2026-03-12 12:47:56 +08:00
|
|
|
|
const [voiceConfig, setVoiceConfig] = useState(null);
|
|
|
|
|
|
// 'voice' | 'chat'
|
|
|
|
|
|
const [mode, setMode] = useState('voice');
|
|
|
|
|
|
// 统一会话 ID(贯穿语音和文字模式,数据库用此 ID 关联所有消息)
|
|
|
|
|
|
const [currentSessionId, setCurrentSessionId] = useState(null);
|
|
|
|
|
|
// 语音转文字的交接数据(保留兼容)
|
|
|
|
|
|
const [handoff, setHandoff] = useState(null);
|
|
|
|
|
|
// 文字聊天消息(用于切回语音时注入上下文)
|
|
|
|
|
|
const [chatMessages, setChatMessages] = useState([]);
|
|
|
|
|
|
const [settings, setSettings] = useState({
|
2026-04-17 09:36:13 +08:00
|
|
|
|
modelVersion: 'O',
|
2026-03-12 12:47:56 +08:00
|
|
|
|
speaker: 'zh_female_vv_jupiter_bigtts',
|
|
|
|
|
|
enableWebSearch: false,
|
|
|
|
|
|
});
|
2026-04-17 09:36:13 +08:00
|
|
|
|
// 文字对话引擎: 'coze' (原方舟 HTTP/SSE) | 's2s' (S2S WebSocket, event 501)
|
|
|
|
|
|
const [textEngine, setTextEngine] = useState(() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return localStorage.getItem('bigwo_text_engine') || 'coze';
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return 'coze';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
const toggleTextEngine = useCallback(() => {
|
|
|
|
|
|
setTextEngine((prev) => {
|
|
|
|
|
|
const next = prev === 'coze' ? 's2s' : 'coze';
|
|
|
|
|
|
try { localStorage.setItem('bigwo_text_engine', next); } catch (_) {}
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
2026-03-12 12:47:56 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
getVoiceConfig()
|
|
|
|
|
|
.then(setVoiceConfig)
|
|
|
|
|
|
.catch((err) => console.warn('Failed to load config:', err));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 语音通话结束后,无缝切换到文字对话(携带同一个 sessionId)
|
|
|
|
|
|
const handleVoiceEnd = useCallback((data) => {
|
|
|
|
|
|
if (data?.sessionId) {
|
|
|
|
|
|
const sid = data.sessionId;
|
|
|
|
|
|
setCurrentSessionId(sid);
|
|
|
|
|
|
setHandoff({
|
|
|
|
|
|
sessionId: sid,
|
|
|
|
|
|
subtitles: data.subtitles || [],
|
|
|
|
|
|
});
|
|
|
|
|
|
setMode('chat');
|
|
|
|
|
|
console.log(`[App] Voice→Chat, sessionId=${sid}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 从文字模式返回语音模式(使用同一个 sessionId,数据库已有完整历史)
|
|
|
|
|
|
const handleBackToVoice = useCallback(() => {
|
|
|
|
|
|
console.log(`[App] Chat→Voice, sessionId=${currentSessionId}`);
|
|
|
|
|
|
setMode('voice');
|
|
|
|
|
|
}, [currentSessionId]);
|
|
|
|
|
|
|
2026-03-13 13:06:46 +08:00
|
|
|
|
// 切换到文字模式(复用已有 sessionId,没有时新建)
|
2026-03-12 12:47:56 +08:00
|
|
|
|
const handleStartChat = useCallback(() => {
|
2026-03-13 13:06:46 +08:00
|
|
|
|
const sid = currentSessionId || `chat_${Date.now().toString(36)}`;
|
|
|
|
|
|
setCurrentSessionId(sid);
|
2026-03-12 12:47:56 +08:00
|
|
|
|
setHandoff({
|
2026-03-13 13:06:46 +08:00
|
|
|
|
sessionId: sid,
|
2026-03-12 12:47:56 +08:00
|
|
|
|
subtitles: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
setMode('chat');
|
2026-03-13 13:06:46 +08:00
|
|
|
|
console.log(`[App] Switch to chat, sessionId=${sid}`);
|
|
|
|
|
|
}, [currentSessionId]);
|
|
|
|
|
|
|
|
|
|
|
|
// 语音会话创建时同步 sessionId 到 App 状态
|
|
|
|
|
|
const handleSessionCreated = useCallback((sessionId) => {
|
|
|
|
|
|
if (sessionId && sessionId !== currentSessionId) {
|
|
|
|
|
|
setCurrentSessionId(sessionId);
|
|
|
|
|
|
console.log(`[App] Voice session synced: ${sessionId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [currentSessionId]);
|
|
|
|
|
|
|
|
|
|
|
|
// 新建会话:重置所有状态
|
|
|
|
|
|
const handleNewSession = useCallback(() => {
|
|
|
|
|
|
setCurrentSessionId(null);
|
|
|
|
|
|
setHandoff(null);
|
|
|
|
|
|
setChatMessages([]);
|
|
|
|
|
|
setMode('voice');
|
|
|
|
|
|
console.log('[App] New session created');
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 从历史记录中选择会话
|
|
|
|
|
|
const handleSelectSession = useCallback((session) => {
|
|
|
|
|
|
const sid = session.id;
|
|
|
|
|
|
setCurrentSessionId(sid);
|
|
|
|
|
|
setChatMessages([]);
|
|
|
|
|
|
// 根据会话最后的模式决定打开方式,默认用文字模式查看历史
|
|
|
|
|
|
setHandoff({
|
|
|
|
|
|
sessionId: sid,
|
|
|
|
|
|
subtitles: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
setMode('chat');
|
|
|
|
|
|
console.log(`[App] Selected session: ${sid}, mode: ${session.mode}`);
|
2026-03-12 12:47:56 +08:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<header className="border-b border-slate-700/50 bg-slate-900/80 backdrop-blur-sm sticky top-0 z-30">
|
|
|
|
|
|
<div className="max-w-5xl mx-auto px-3 sm:px-4 h-14 flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center">
|
|
|
|
|
|
<Zap className="w-4 h-4 text-white" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="text-sm font-semibold text-white leading-tight">
|
2026-04-17 09:36:13 +08:00
|
|
|
|
{mode === 'voice' ? '语音通话' : mode === 'chat' ? '文字对话' : 'AI 视频'}
|
2026-03-12 12:47:56 +08:00
|
|
|
|
</h1>
|
|
|
|
|
|
<p className="text-[11px] text-slate-400 leading-tight">
|
|
|
|
|
|
{mode === 'voice'
|
2026-03-13 13:06:46 +08:00
|
|
|
|
? '直连 S2S 语音 · ChatTTSText'
|
2026-04-17 09:36:13 +08:00
|
|
|
|
: mode === 'chat'
|
|
|
|
|
|
? (textEngine === 's2s'
|
|
|
|
|
|
? 'S2S 文字 · event 501 ChatTextQuery'
|
|
|
|
|
|
: (handoff?.subtitles?.length > 0
|
|
|
|
|
|
? '语音转接 · 上下文已延续'
|
|
|
|
|
|
: '方舟 LLM · Function Calling'))
|
|
|
|
|
|
: 'Seedance · AI 视频生成'}
|
2026-03-12 12:47:56 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
2026-03-13 13:06:46 +08:00
|
|
|
|
{/* History button */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setShowHistory(true)}
|
|
|
|
|
|
className="p-2 rounded-lg hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors mr-1"
|
|
|
|
|
|
title="会话历史"
|
|
|
|
|
|
>
|
|
|
|
|
|
<History className="w-4 h-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{/* New session button */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleNewSession}
|
|
|
|
|
|
className="p-2 rounded-lg hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors"
|
|
|
|
|
|
title="新建会话"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="w-4 h-4" />
|
|
|
|
|
|
</button>
|
2026-03-12 12:47:56 +08:00
|
|
|
|
{/* Mode toggle buttons */}
|
|
|
|
|
|
<div className="flex items-center bg-slate-800/60 rounded-lg border border-slate-700/40 p-0.5 mr-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => { if (mode !== 'voice') handleBackToVoice(); }}
|
|
|
|
|
|
className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs transition-all ${
|
|
|
|
|
|
mode === 'voice'
|
|
|
|
|
|
? 'bg-violet-500/20 text-violet-300 font-medium'
|
|
|
|
|
|
: 'text-slate-500 hover:text-slate-300'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Mic className="w-3 h-3" /> 语音
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => { if (mode !== 'chat') handleStartChat(); }}
|
|
|
|
|
|
className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs transition-all ${
|
|
|
|
|
|
mode === 'chat'
|
|
|
|
|
|
? 'bg-violet-500/20 text-violet-300 font-medium'
|
|
|
|
|
|
: 'text-slate-500 hover:text-slate-300'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MessageSquare className="w-3 h-3" /> 文字
|
|
|
|
|
|
</button>
|
2026-04-17 09:36:13 +08:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setMode('video')}
|
|
|
|
|
|
className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs transition-all ${
|
|
|
|
|
|
mode === 'video'
|
|
|
|
|
|
? 'bg-violet-500/20 text-violet-300 font-medium'
|
|
|
|
|
|
: 'text-slate-500 hover:text-slate-300'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Film className="w-3 h-3" /> 视频
|
|
|
|
|
|
</button>
|
2026-03-12 12:47:56 +08:00
|
|
|
|
</div>
|
2026-04-17 09:36:13 +08:00
|
|
|
|
{mode === 'chat' && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={toggleTextEngine}
|
|
|
|
|
|
className={`px-2 py-1 rounded-md text-[11px] font-medium transition-colors border ${
|
|
|
|
|
|
textEngine === 's2s'
|
|
|
|
|
|
? 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30'
|
|
|
|
|
|
: 'bg-slate-700/40 text-slate-300 border-slate-600/40'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
title="切换文字对话引擎 (S2S 走 event 501, Coze 走 HTTP/SSE)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{textEngine === 's2s' ? 'S2S' : 'Coze'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-03-12 12:47:56 +08:00
|
|
|
|
{mode === 'voice' && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setShowSettings(!showSettings)}
|
|
|
|
|
|
className="p-2 rounded-lg hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors"
|
|
|
|
|
|
title="语音设置"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Settings2 className="w-5 h-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main className="max-w-5xl mx-auto px-4 py-6">
|
|
|
|
|
|
{/* Settings Panel */}
|
|
|
|
|
|
{showSettings && mode === 'voice' && (
|
|
|
|
|
|
<SettingsPanel
|
|
|
|
|
|
settings={settings}
|
|
|
|
|
|
onChange={setSettings}
|
|
|
|
|
|
voiceConfig={voiceConfig}
|
|
|
|
|
|
onClose={() => setShowSettings(false)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{mode === 'voice' ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Voice Panel */}
|
2026-03-13 13:06:46 +08:00
|
|
|
|
<VoicePanel settings={settings} onVoiceEnd={handleVoiceEnd} chatHistory={chatMessages} sessionId={currentSessionId} onSessionCreated={handleSessionCreated} />
|
2026-03-12 12:47:56 +08:00
|
|
|
|
|
|
|
|
|
|
{/* Architecture Info */}
|
|
|
|
|
|
<div className="mt-6 p-4 rounded-xl bg-slate-800/40 border border-slate-700/40">
|
2026-03-13 13:06:46 +08:00
|
|
|
|
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">RTC 直路由语音架构</h3>
|
2026-03-12 12:47:56 +08:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
|
|
|
|
|
|
<div className="p-3 rounded-lg bg-slate-700/30 border border-slate-600/30">
|
2026-03-13 13:06:46 +08:00
|
|
|
|
<div className="text-emerald-400 font-medium mb-1">上行链路</div>
|
|
|
|
|
|
<div className="text-slate-400">浏览器 RTC 麦克风 → 房间字幕/消息 → 后端前置路由</div>
|
2026-03-12 12:47:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="p-3 rounded-lg bg-slate-700/30 border border-slate-600/30">
|
2026-03-13 13:06:46 +08:00
|
|
|
|
<div className="text-amber-400 font-medium mb-1">应答链路</div>
|
|
|
|
|
|
<div className="text-slate-400">知识库/工具结果 → ExternalTextToSpeech → 语音播报</div>
|
2026-03-12 12:47:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="p-3 rounded-lg bg-slate-700/30 border border-slate-600/30">
|
2026-03-13 13:06:46 +08:00
|
|
|
|
<div className="text-violet-400 font-medium mb-1">当前目标</div>
|
|
|
|
|
|
<div className="text-slate-400">彻底绕开原生链纯 S2S 抢答,保证知识库结果能播报</div>
|
2026-03-12 12:47:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
2026-04-17 09:36:13 +08:00
|
|
|
|
) : mode === 'chat' ? (
|
2026-03-12 12:47:56 +08:00
|
|
|
|
/* Chat Panel */
|
|
|
|
|
|
handoff && (
|
|
|
|
|
|
<ChatPanel
|
2026-04-17 09:36:13 +08:00
|
|
|
|
key={`${handoff.sessionId}-${textEngine}`}
|
2026-03-12 12:47:56 +08:00
|
|
|
|
sessionId={handoff.sessionId}
|
|
|
|
|
|
voiceSubtitles={handoff.subtitles}
|
|
|
|
|
|
settings={settings}
|
|
|
|
|
|
onBack={handleBackToVoice}
|
|
|
|
|
|
onMessagesChange={setChatMessages}
|
2026-04-17 09:36:13 +08:00
|
|
|
|
useS2S={textEngine === 's2s'}
|
|
|
|
|
|
playAudioReply={false}
|
2026-03-12 12:47:56 +08:00
|
|
|
|
/>
|
|
|
|
|
|
)
|
2026-04-17 09:36:13 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
/* Video Panel */
|
|
|
|
|
|
<VideoPanel />
|
2026-03-12 12:47:56 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</main>
|
2026-03-13 13:06:46 +08:00
|
|
|
|
|
|
|
|
|
|
{/* Session History Sidebar */}
|
|
|
|
|
|
{showHistory && (
|
|
|
|
|
|
<SessionHistoryPanel
|
|
|
|
|
|
currentSessionId={currentSessionId}
|
|
|
|
|
|
onSelectSession={handleSelectSession}
|
|
|
|
|
|
onNewSession={handleNewSession}
|
|
|
|
|
|
onClose={() => setShowHistory(false)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-03-12 12:47:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|