fix: compact list pagination and refine mobile call UX
Show ellipsis page controls when lists grow large, and make mobile call start manual with clearer hangup layout and assistant name. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
useVoicePreview,
|
||||
type ChatMessage,
|
||||
} from "@/hooks/use-voice-preview";
|
||||
import { assistantsApi } from "@/lib/api";
|
||||
|
||||
type CallView = "video" | "chat";
|
||||
|
||||
@@ -452,9 +453,25 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
||||
selectCamera: changeCamera,
|
||||
} = camera;
|
||||
const [view, setView] = useState<CallView>("chat");
|
||||
const [assistantName, setAssistantName] = useState("");
|
||||
const connecting = status === "connecting";
|
||||
const inCall = !callEnded && (connecting || status === "connected");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void assistantsApi
|
||||
.get(assistantId)
|
||||
.then((assistant) => {
|
||||
if (!cancelled) setAssistantName(assistant.name.trim());
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setAssistantName("");
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [assistantId]);
|
||||
|
||||
const startCall = useCallback(async () => {
|
||||
await connect({
|
||||
visionEnabled: true,
|
||||
@@ -474,11 +491,6 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
||||
[changeCamera, selectTransportCamera],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void startCall();
|
||||
// 首次打开页面时自动发起通话;hooks 会在卸载时各自释放媒体资源。
|
||||
}, [startCall]);
|
||||
|
||||
const error = previewError || cameraError || photoCapture.error;
|
||||
|
||||
return (
|
||||
@@ -524,12 +536,20 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
||||
? "通话中"
|
||||
: connecting
|
||||
? "正在连接…"
|
||||
: "通话已结束"}
|
||||
: status === "failed"
|
||||
? "连接失败"
|
||||
: "等待开始"}
|
||||
</div>
|
||||
|
||||
{!inCall && (
|
||||
<div className="absolute inset-0 z-[2] flex items-center justify-center px-8">
|
||||
<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-5 text-center">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs tracking-wide text-white/45">AI 助手</p>
|
||||
<h1 className="text-2xl font-medium leading-tight text-white">
|
||||
{assistantName || "助手"}
|
||||
</h1>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -539,7 +559,7 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
||||
className="h-12 gap-2 rounded-full bg-white px-6 text-[#07101a] shadow-2xl hover:bg-white/90"
|
||||
>
|
||||
<Phone size={19} />
|
||||
{status === "failed" ? "重新开始通话" : "开始通话"}
|
||||
{status === "failed" || callEnded ? "重新开始通话" : "开始通话"}
|
||||
</Button>
|
||||
{error && <p className="text-xs leading-5 text-white/65">{error}</p>}
|
||||
</div>
|
||||
@@ -547,26 +567,7 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
||||
)}
|
||||
|
||||
{inCall && (
|
||||
<div className="absolute bottom-[max(1.25rem,env(safe-area-inset-bottom))] left-1/2 z-10 flex -translate-x-1/2 items-center gap-3 min-[380px]:gap-5 landscape:bottom-[max(1rem,env(safe-area-inset-bottom))]">
|
||||
<CallAudioDeviceSelect
|
||||
micValue={selectedDeviceId}
|
||||
micDevices={audioInputs}
|
||||
onMicSelect={selectDevice}
|
||||
outputValue={selectedOutputDeviceId}
|
||||
outputDevices={audioOutputs}
|
||||
onOutputSelect={selectOutputDevice}
|
||||
outputSupported={supportsOutputSelection}
|
||||
disabled={connecting}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={endCall}
|
||||
aria-label="挂断电话"
|
||||
title="挂断电话"
|
||||
className="flex size-14 items-center justify-center rounded-full bg-red-500 text-white shadow-2xl transition-transform hover:scale-105 hover:bg-red-600 active:scale-95 min-[380px]:size-16"
|
||||
>
|
||||
<PhoneOff className="size-6 min-[380px]:size-7" />
|
||||
</button>
|
||||
<div className="absolute bottom-[max(1.25rem,env(safe-area-inset-bottom))] left-1/2 z-10 flex -translate-x-1/2 flex-col items-center gap-3 landscape:bottom-[max(1rem,env(safe-area-inset-bottom))]">
|
||||
{photoCapture.visible && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -583,12 +584,37 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<CallCameraDeviceSelect
|
||||
value={cameraDeviceId}
|
||||
devices={cameraDevices}
|
||||
onSelect={selectCamera}
|
||||
disabled={connecting}
|
||||
/>
|
||||
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 min-[380px]:gap-5">
|
||||
<div className="flex justify-end">
|
||||
<CallAudioDeviceSelect
|
||||
micValue={selectedDeviceId}
|
||||
micDevices={audioInputs}
|
||||
onMicSelect={selectDevice}
|
||||
outputValue={selectedOutputDeviceId}
|
||||
outputDevices={audioOutputs}
|
||||
onOutputSelect={selectOutputDevice}
|
||||
outputSupported={supportsOutputSelection}
|
||||
disabled={connecting}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={endCall}
|
||||
aria-label="挂断电话"
|
||||
title="挂断电话"
|
||||
className="flex size-14 items-center justify-center rounded-full bg-red-500 text-white shadow-2xl transition-transform hover:scale-105 hover:bg-red-600 active:scale-95 min-[380px]:size-16"
|
||||
>
|
||||
<PhoneOff className="size-6 min-[380px]:size-7" />
|
||||
</button>
|
||||
<div className="flex justify-start">
|
||||
<CallCameraDeviceSelect
|
||||
value={cameraDeviceId}
|
||||
devices={cameraDevices}
|
||||
onSelect={selectCamera}
|
||||
disabled={connecting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user