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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user