Update MobileCallPage and useVoicePreview to handle call end state
- Introduce a new state variable `callEnded` in `useVoicePreview` to manage call termination more effectively. - Modify `MobileCallPage` to adjust the `inCall` condition based on `callEnded`, ensuring accurate UI updates during and after calls. - Update rendering logic to display appropriate messages based on call status, enhancing user feedback during call transitions. - Refactor button visibility to only show when in an active call, improving user interface clarity.
This commit is contained in:
@@ -248,6 +248,7 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
selectOutputDevice,
|
selectOutputDevice,
|
||||||
supportsOutputSelection,
|
supportsOutputSelection,
|
||||||
messages,
|
messages,
|
||||||
|
callEnded,
|
||||||
networkQuality,
|
networkQuality,
|
||||||
connect,
|
connect,
|
||||||
replaceVideoStream,
|
replaceVideoStream,
|
||||||
@@ -267,7 +268,7 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const [view, setView] = useState<CallView>("camera");
|
const [view, setView] = useState<CallView>("camera");
|
||||||
const connecting = status === "connecting";
|
const connecting = status === "connecting";
|
||||||
const inCall = connecting || status === "connected";
|
const inCall = !callEnded && (connecting || status === "connected");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoRef.current) videoRef.current.srcObject = cameraStream;
|
if (videoRef.current) videoRef.current.srcObject = cameraStream;
|
||||||
@@ -282,6 +283,7 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
}, [connect, startCamera]);
|
}, [connect, startCamera]);
|
||||||
|
|
||||||
const endCall = useCallback(() => {
|
const endCall = useCallback(() => {
|
||||||
|
setView("camera");
|
||||||
disconnect();
|
disconnect();
|
||||||
stopCamera();
|
stopCamera();
|
||||||
}, [disconnect, stopCamera]);
|
}, [disconnect, stopCamera]);
|
||||||
@@ -300,11 +302,10 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
}, [startCall]);
|
}, [startCall]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === "idle" || status === "failed") {
|
if (callEnded || status === "idle" || status === "failed") {
|
||||||
stopCamera();
|
stopCamera();
|
||||||
setView("camera");
|
|
||||||
}
|
}
|
||||||
}, [status, stopCamera]);
|
}, [callEnded, status, stopCamera]);
|
||||||
|
|
||||||
const error = previewError || cameraError;
|
const error = previewError || cameraError;
|
||||||
|
|
||||||
@@ -341,33 +342,39 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
<span
|
<span
|
||||||
className={[
|
className={[
|
||||||
"h-2 w-2 rounded-full",
|
"h-2 w-2 rounded-full",
|
||||||
status === "connected"
|
!callEnded && status === "connected"
|
||||||
? "animate-pulse bg-emerald-400"
|
? "animate-pulse bg-emerald-400"
|
||||||
: connecting
|
: !callEnded && connecting
|
||||||
? "animate-pulse bg-amber-300"
|
? "animate-pulse bg-amber-300"
|
||||||
: "bg-white/40",
|
: "bg-white/40",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
/>
|
/>
|
||||||
{status === "connected"
|
{callEnded
|
||||||
? "通话中"
|
? "通话已结束"
|
||||||
: connecting
|
: status === "connected"
|
||||||
? "正在连接…"
|
? "通话中"
|
||||||
: "通话已结束"}
|
: connecting
|
||||||
|
? "正在连接…"
|
||||||
|
: "通话已结束"}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
{inCall && (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setView((current) => current === "camera" ? "chat" : "camera")}
|
type="button"
|
||||||
aria-label={view === "camera" ? "显示聊天记录" : "显示摄像头画面"}
|
onClick={() =>
|
||||||
title={view === "camera" ? "聊天记录" : "摄像头画面"}
|
setView((current) => current === "camera" ? "chat" : "camera")
|
||||||
className="absolute right-4 top-[max(1rem,env(safe-area-inset-top))] z-10 flex size-9 items-center justify-center rounded-full border border-white/15 bg-black/40 text-white shadow-lg backdrop-blur-md transition-colors hover:bg-black/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
|
}
|
||||||
>
|
aria-label={view === "camera" ? "显示聊天记录" : "显示摄像头画面"}
|
||||||
{view === "camera" ? (
|
title={view === "camera" ? "聊天记录" : "摄像头画面"}
|
||||||
<MessageSquareText className="size-[18px]" />
|
className="absolute right-4 top-[max(1rem,env(safe-area-inset-top))] z-10 flex size-9 items-center justify-center rounded-full border border-white/15 bg-black/40 text-white shadow-lg backdrop-blur-md transition-colors hover:bg-black/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
|
||||||
) : (
|
>
|
||||||
<Video className="size-[18px]" />
|
{view === "camera" ? (
|
||||||
)}
|
<MessageSquareText className="size-[18px]" />
|
||||||
</button>
|
) : (
|
||||||
|
<Video className="size-[18px]" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{!cameraStream && inCall && view === "camera" && (
|
{!cameraStream && inCall && view === "camera" && (
|
||||||
<div className="absolute inset-0 z-[2] flex items-center justify-center px-8 text-center">
|
<div className="absolute inset-0 z-[2] flex items-center justify-center px-8 text-center">
|
||||||
@@ -391,7 +398,10 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
<div className="flex max-w-sm flex-col items-center gap-4 text-center">
|
<div className="flex max-w-sm flex-col items-center gap-4 text-center">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void startCall()}
|
onClick={() => {
|
||||||
|
setView("camera");
|
||||||
|
void startCall();
|
||||||
|
}}
|
||||||
className="h-12 gap-2 rounded-full bg-white px-6 text-[#07101a] shadow-2xl hover:bg-white/90"
|
className="h-12 gap-2 rounded-full bg-white px-6 text-[#07101a] shadow-2xl hover:bg-white/90"
|
||||||
>
|
>
|
||||||
<Phone size={19} />
|
<Phone size={19} />
|
||||||
|
|||||||
@@ -214,6 +214,9 @@ export function useVoicePreview(
|
|||||||
// 远端(助手 TTS)媒体流:除挂到 <audio> 播放外,也暴露给波形可视化
|
// 远端(助手 TTS)媒体流:除挂到 <audio> 播放外,也暴露给波形可视化
|
||||||
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
|
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
// 后端结束节点/EndFrame 触发的正常结束。与 failed 区分,供独立通话页
|
||||||
|
// 立即收起媒体画面并展示重新开始入口。
|
||||||
|
const [callEnded, setCallEnded] = useState(false);
|
||||||
const [networkQuality, setNetworkQuality] =
|
const [networkQuality, setNetworkQuality] =
|
||||||
useState<NetworkQuality>("unknown");
|
useState<NetworkQuality>("unknown");
|
||||||
// 可选麦克风/扬声器列表与当前选择(空串表示交给浏览器选默认设备)
|
// 可选麦克风/扬声器列表与当前选择(空串表示交给浏览器选默认设备)
|
||||||
@@ -398,6 +401,7 @@ export function useVoicePreview(
|
|||||||
setError(null);
|
setError(null);
|
||||||
setMicWarning(null);
|
setMicWarning(null);
|
||||||
setMessages([]); // 新会话清空上一轮聊天记录
|
setMessages([]); // 新会话清空上一轮聊天记录
|
||||||
|
setCallEnded(false);
|
||||||
endedByServerRef.current = false;
|
endedByServerRef.current = false;
|
||||||
setStatus("connecting");
|
setStatus("connecting");
|
||||||
|
|
||||||
@@ -630,9 +634,12 @@ export function useVoicePreview(
|
|||||||
// 工作流:后端报告当前激活节点,交给画布高亮
|
// 工作流:后端报告当前激活节点,交给画布高亮
|
||||||
onNodeActiveRef.current?.(msg.nodeId);
|
onNodeActiveRef.current?.(msg.nodeId);
|
||||||
} else if (msg?.type === "call-ended") {
|
} else if (msg?.type === "call-ended") {
|
||||||
// 后端走到结束节点、正常收尾:随后的断开按正常结束处理,不报错
|
// 后端已播完结束节点的收尾语,即将排入 EndFrame。此时主动释放
|
||||||
|
// 浏览器侧资源,不依赖后续 WebRTC 状态事件是否及时到达。
|
||||||
endedByServerRef.current = true;
|
endedByServerRef.current = true;
|
||||||
|
setCallEnded(true);
|
||||||
onNodeActiveRef.current?.(null);
|
onNodeActiveRef.current?.(null);
|
||||||
|
disconnect();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* 非 JSON / 未知消息,忽略 */
|
/* 非 JSON / 未知消息,忽略 */
|
||||||
@@ -689,7 +696,14 @@ export function useVoicePreview(
|
|||||||
} finally {
|
} finally {
|
||||||
startingRef.current = false;
|
startingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [assistantId, applyOutputDevice, fail, closeOnRemoteEnd, refreshDevices]);
|
}, [
|
||||||
|
assistantId,
|
||||||
|
applyOutputDevice,
|
||||||
|
fail,
|
||||||
|
closeOnRemoteEnd,
|
||||||
|
disconnect,
|
||||||
|
refreshDevices,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status !== "connected") return;
|
if (status !== "connected") return;
|
||||||
@@ -826,6 +840,7 @@ export function useVoicePreview(
|
|||||||
localStream,
|
localStream,
|
||||||
remoteStream,
|
remoteStream,
|
||||||
messages,
|
messages,
|
||||||
|
callEnded,
|
||||||
networkQuality,
|
networkQuality,
|
||||||
audioInputs,
|
audioInputs,
|
||||||
audioOutputs,
|
audioOutputs,
|
||||||
|
|||||||
Reference in New Issue
Block a user