Refactor AssistantPage and DebugVoicePanel for improved view management

- Remove the SegmentedIconGroup for view selection and streamline view handling in DebugDrawer.
- Introduce DebugVisionWorkspace component to manage video and chat views more effectively.
- Update DebugVoicePanel to accept onViewChange prop for better view state management.
- Enhance video handling with dynamic positioning and drag functionality for the floating video panel.
This commit is contained in:
Xin Wang
2026-07-13 07:18:22 +08:00
parent fd57f34a55
commit a25b149f65

View File

@@ -2243,22 +2243,6 @@ function DebugDrawer({
</SegmentedIconGroup>
</>
)}
<SegmentedIconGroup label="预览视图">
<SegmentedIconButton
selected={view === "chat"}
label="聊天记录"
onClick={() => setView("chat")}
>
<MessageSquareText size={14} />
</SegmentedIconButton>
<SegmentedIconButton
selected={view === "video"}
label="视频流"
onClick={() => setView("video")}
>
<Video size={14} />
</SegmentedIconButton>
</SegmentedIconGroup>
</div>
</div>
<div className="shrink-0 border-b border-hairline px-5 py-2.5">
@@ -2268,6 +2252,7 @@ function DebugDrawer({
</div>
<DebugVoicePanel
view={view}
onViewChange={setView}
showTranscript={showTranscript}
vizStyle={vizStyle}
assistantId={assistantId}
@@ -2596,6 +2581,7 @@ function DebugInputModeButton({
function DebugVoicePanel({
view,
onViewChange,
showTranscript,
vizStyle,
assistantId,
@@ -2607,6 +2593,7 @@ function DebugVoicePanel({
dynamicVariablesError,
}: {
view: DebugView;
onViewChange: (view: DebugView) => void;
showTranscript: boolean;
vizStyle: VizStyle;
assistantId: string | null;
@@ -2672,7 +2659,16 @@ function DebugVoicePanel({
<div className="flex min-h-0 flex-1 flex-col">
{/* 后端 TTS 音频经 WebRTC 媒体流过来,挂这里播放 */}
<audio ref={audioRef} autoPlay playsInline className="hidden" />
{view === "video" ? (
{vision && !showIdleHub ? (
<DebugVisionWorkspace
view={view}
onViewChange={onViewChange}
messages={messages}
recording={recording}
camera={camera}
videoStream={preview.videoStream}
/>
) : view === "video" ? (
showIdleHub ? (
<DebugIdleHub
status={status}
@@ -2941,12 +2937,133 @@ function DebugIdleHub({
);
}
function DebugVisionWorkspace({
view,
onViewChange,
messages,
recording,
camera,
videoStream,
}: {
view: DebugView;
onViewChange: (view: DebugView) => void;
messages: ChatMessage[];
recording: boolean;
camera: CameraPreview;
videoStream: MediaStream | null;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 16, y: 16 });
const positionedRef = useRef(false);
const dragRef = useRef<{
pointerId: number;
startX: number;
startY: number;
originX: number;
originY: number;
moved: boolean;
} | null>(null);
const latestMessage = [...messages]
.reverse()
.find((message) => message.content.trim());
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const rect = container.getBoundingClientRect();
if (!positionedRef.current) {
positionedRef.current = true;
setPosition({ x: Math.max(16, rect.width - 192), y: 16 });
return;
}
setPosition((current) => ({
x: Math.min(Math.max(16, current.x), Math.max(16, rect.width - 192)),
y: Math.min(Math.max(16, current.y), Math.max(16, rect.height - 116)),
}));
}, [view]);
const moveFloatingVideo = useCallback((event: React.PointerEvent<HTMLButtonElement>) => {
const drag = dragRef.current;
const container = containerRef.current;
if (!drag || !container || drag.pointerId !== event.pointerId) return;
const dx = event.clientX - drag.startX;
const dy = event.clientY - drag.startY;
if (Math.abs(dx) + Math.abs(dy) > 4) drag.moved = true;
const rect = container.getBoundingClientRect();
setPosition({
x: Math.min(Math.max(16, drag.originX + dx), Math.max(16, rect.width - 192)),
y: Math.min(Math.max(16, drag.originY + dy), Math.max(16, rect.height - 116)),
});
}, []);
if (view === "video") {
return (
<div ref={containerRef} className="relative flex min-h-0 flex-1 overflow-hidden bg-black">
<DebugVideoPanel camera={camera} streamOverride={videoStream} />
<button
type="button"
onClick={() => onViewChange("chat")}
className="absolute left-4 right-4 top-4 z-10 flex h-16 items-center gap-3 overflow-hidden rounded-2xl border border-white/15 bg-[#07101a]/55 px-4 py-2 text-left text-white shadow-lg backdrop-blur-md transition-colors hover:bg-[#07101a]/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
aria-label="切换到聊天记录"
>
<MessageSquareText size={17} className="shrink-0 text-white/70" />
<span className="line-clamp-2 min-w-0 flex-1 text-xs leading-5 text-white/90">
<span className="mr-1.5 font-medium text-white/55">
{latestMessage?.role === "user" ? "我:" : "助手:"}
</span>
<span>
{latestMessage?.content || "暂无消息,点击返回聊天记录"}
</span>
</span>
</button>
</div>
);
}
return (
<div ref={containerRef} className="relative flex min-h-0 flex-1 overflow-hidden">
<DebugTranscriptPanel messages={messages} recording={recording} />
<button
type="button"
style={{ left: position.x, top: position.y }}
className="absolute z-10 h-[100px] w-44 touch-none overflow-hidden rounded-2xl border border-white/20 bg-black text-left shadow-xl ring-1 ring-black/10 transition-shadow hover:shadow-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label="拖动摄像头浮窗,点击切换到视频流"
onPointerDown={(event) => {
event.currentTarget.setPointerCapture(event.pointerId);
dragRef.current = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
originX: position.x,
originY: position.y,
moved: false,
};
}}
onPointerMove={moveFloatingVideo}
onPointerUp={(event) => {
const drag = dragRef.current;
event.currentTarget.releasePointerCapture(event.pointerId);
dragRef.current = null;
if (drag && !drag.moved) onViewChange("video");
}}
onPointerCancel={() => {
dragRef.current = null;
}}
>
<DebugVideoPanel camera={camera} streamOverride={videoStream} compact />
</button>
</div>
);
}
function DebugVideoPanel({
camera,
streamOverride,
compact = false,
}: {
camera: CameraPreview;
streamOverride?: MediaStream | null;
compact?: boolean;
}) {
const { stream, error, starting, active } = camera;
const effectiveStream = streamOverride ?? stream;
@@ -2961,6 +3078,7 @@ function DebugVideoPanel({
<div
className={[
"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden",
compact ? "h-full w-full" : "",
effectiveActive ? "bg-black" : "bg-canvas-soft",
].join(" ")}
>
@@ -2970,19 +3088,12 @@ function DebugVideoPanel({
playsInline
muted
className={[
"h-full w-full -scale-x-100 object-contain",
"h-full w-full -scale-x-100",
compact ? "object-cover" : "object-contain",
effectiveActive ? "" : "hidden",
].join(" ")}
/>
{effectiveActive ? (
<Badge
variant="secondary"
className="absolute left-3 top-3 gap-1.5 rounded-full border border-hairline bg-card/80 px-2.5 py-1 text-[11px] font-medium text-muted-foreground shadow-none backdrop-blur"
>
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-success" />
</Badge>
) : (
{!effectiveActive ? (
<div className="flex max-w-xs flex-col items-center gap-2 px-6 text-center">
<Video size={28} className="text-muted-soft" />
<div className="text-sm font-medium text-foreground">
@@ -2992,7 +3103,7 @@ function DebugVideoPanel({
<p className="text-xs leading-5 text-muted-foreground">{error}</p>
)}
</div>
)}
) : null}
</div>
);
}