feat: 结束视频后保留对话,支持继续文字聊天

- useVisionSession 新增 stopVideo 回调(停媒体流,保持连接和消息)
- App.tsx 控制区从二态改为三态(initial/video/textOnly)
- 文字对话态显示「重新开始视频」和「结束会话」按钮
- 新增 CSS 样式(video-ended-hint、btn--outline)
- 新增 i18n key(stopVideo/endSession/resumeVideo/video.ended)
- 新增设计方案文档
This commit is contained in:
2026-06-20 13:48:26 +08:00
parent 3dc2015a91
commit ae9a27c300
7 changed files with 385 additions and 4 deletions

View File

@@ -1503,6 +1503,48 @@ body {
color: white;
}
/* ---- Text-only mode (video ended, chat preserved) ---- */
.video-controls__text-only {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
width: 100%;
}
.video-ended-hint {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 4px 0;
}
.video-ended-hint__title {
font-size: 0.85rem;
font-weight: 600;
color: var(--color-text-muted);
}
.video-ended-hint__sub {
font-size: 0.7rem;
color: var(--color-text-muted);
opacity: 0.7;
}
.btn--outline {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text-muted);
}
.btn--outline:hover {
background: var(--color-surface-2);
color: var(--color-text);
border-color: var(--color-surface-3);
}
/* ---- Scene Cards (empty state) ---- */
.scene-cards {

View File

@@ -80,6 +80,7 @@ function AppContent() {
isObserving,
startSession,
stopSession,
stopVideo,
interrupt,
isCameraOn,
isMicOn,
@@ -397,9 +398,9 @@ function AppContent() {
</button>
</div>
</>
) : (
) : isCameraOn ? (
<>
{/* 通话态:核心控制工具栏 */}
{/* 视频通话态:核心控制工具栏 */}
<div className="video-controls__toolbar">
<button
className={`btn btn--ctrl ${isCameraOn ? "btn--ctrl-on" : "btn--ctrl-off"}`}
@@ -430,8 +431,8 @@ function AppContent() {
{tr("controls.interrupt")}
</button>
)}
<button className="btn btn--danger" onClick={stopSession}>
{tr("controls.stop")}
<button className="btn btn--danger" onClick={stopVideo}>
{tr("controls.stopVideo")}
</button>
</div>
{/* 通话态模式切换 */}
@@ -456,6 +457,24 @@ function AppContent() {
</button>
</div>
</>
) : (
<>
{/* 文字对话态:视频已结束 */}
<div className="video-controls__text-only">
<div className="video-ended-hint">
<span className="video-ended-hint__title">📹 {tr("video.ended")}</span>
<span className="video-ended-hint__sub">{tr("video.ended.hint")}</span>
</div>
<div className="video-controls__toolbar">
<button className="btn btn--primary" onClick={startSession}>
{tr("controls.resumeVideo")}
</button>
<button className="btn btn--danger btn--outline" onClick={stopSession}>
{tr("controls.endSession")}
</button>
</div>
</div>
</>
)}
</div>
</div>

View File

@@ -363,6 +363,22 @@ export function useVisionSession(accessToken?: string | null) {
setIsMicOn(false);
}, [stopObserving, stopVAD, stopMic, stopCamera, disconnect]);
/** 结束视频,保留聊天和连接 */
const stopVideo = useCallback(async () => {
stopObserving();
setMode("dialogue");
await stopVAD();
stopMic();
stopCamera();
ttsPlayerRef.current?.stop();
setIsAudioPlaying(false);
setCurrentReply("");
setIsProcessing(false);
setIsCameraOn(false);
setIsMicOn(false);
// 不断开 WebSocket不清空消息、历史、统计
}, [stopObserving, stopVAD, stopMic, stopCamera]);
/** 摄像头开关 */
const toggleCamera = useCallback(async () => {
if (isCameraOn) {
@@ -479,6 +495,7 @@ export function useVisionSession(accessToken?: string | null) {
toggleMode,
startSession,
stopSession,
stopVideo,
interrupt,
isCameraOn,
isMicOn,

View File

@@ -51,6 +51,8 @@ export const enUS: TranslationMap = {
"video.placeholder": "Type in the chat panel to start",
"video.cameraOff": "Camera is off",
"video.cameraOff.hint": "You can type in the chat panel",
"video.ended": "Video Ended",
"video.ended.hint": "You can continue chatting below",
// Controls
"controls.connecting": "Connecting...",
@@ -64,6 +66,9 @@ export const enUS: TranslationMap = {
"controls.observation": "👁️ Observe",
"controls.interrupt": "⏹ Interrupt",
"controls.stop": "End Session",
"controls.stopVideo": "End Video",
"controls.endSession": "End Session",
"controls.resumeVideo": "📹 Resume Video",
// Chat panel
"chat.title": "Chat",

View File

@@ -51,6 +51,8 @@ export const jaJP: TranslationMap = {
"video.placeholder": "右側のチャットに入力して開始",
"video.cameraOff": "カメラがオフです",
"video.cameraOff.hint": "右側のチャットでテキスト対話ができます",
"video.ended": "ビデオ終了",
"video.ended.hint": "下にテキストを入力して会話を続けることができます",
// Controls
"controls.connecting": "接続中...",
@@ -64,6 +66,9 @@ export const jaJP: TranslationMap = {
"controls.observation": "👁️ 観察モード",
"controls.interrupt": "⏹ 中断",
"controls.stop": "対話を終了",
"controls.stopVideo": "ビデオ終了",
"controls.endSession": "セッション終了",
"controls.resumeVideo": "📹 ビデオ再開",
// Chat panel
"chat.title": "チャット",

View File

@@ -51,6 +51,8 @@ export const zhCN: TranslationMap = {
"video.placeholder": "在右侧聊天框输入即可开始对话",
"video.cameraOff": "摄像头未开启",
"video.cameraOff.hint": "可在右侧聊天框打字对话",
"video.ended": "视频已结束",
"video.ended.hint": "您可以继续在下方输入文字对话",
// Controls
"controls.connecting": "连接中...",
@@ -64,6 +66,9 @@ export const zhCN: TranslationMap = {
"controls.observation": "👁️ 观察模式",
"controls.interrupt": "⏹ 打断",
"controls.stop": "结束对话",
"controls.stopVideo": "结束视频",
"controls.endSession": "结束会话",
"controls.resumeVideo": "📹 重新开始视频",
// Chat panel
"chat.title": "对话",