"use client"; import type { CSSProperties, MouseEvent, WheelEvent } from "react"; import { useEffect, useRef, useState } from "react"; import type { DesktopMouseButton, DesktopStreamErrorStatus, DesktopStreamReadyStatus, SandboxAgent } from "sandbox-agent"; type ConnectionState = "connecting" | "ready" | "closed" | "error"; export type DesktopViewerClient = Pick; export interface DesktopViewerProps { client: DesktopViewerClient; className?: string; style?: CSSProperties; imageStyle?: CSSProperties; height?: number | string; showStatusBar?: boolean; onConnect?: (status: DesktopStreamReadyStatus) => void; onDisconnect?: () => void; onError?: (error: DesktopStreamErrorStatus | Error) => void; } const shellStyle: CSSProperties = { display: "flex", flexDirection: "column", overflow: "hidden", border: "1px solid rgba(15, 23, 42, 0.14)", borderRadius: 14, background: "linear-gradient(180deg, rgba(248, 250, 252, 0.96) 0%, rgba(226, 232, 240, 0.92) 100%)", boxShadow: "0 20px 40px rgba(15, 23, 42, 0.08)", }; const statusBarStyle: CSSProperties = { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "10px 14px", borderBottom: "1px solid rgba(15, 23, 42, 0.08)", background: "rgba(255, 255, 255, 0.78)", color: "#0f172a", fontSize: 12, lineHeight: 1.4, }; const viewportStyle: CSSProperties = { position: "relative", display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden", background: "radial-gradient(circle at top, rgba(14, 165, 233, 0.18), transparent 45%), linear-gradient(180deg, #0f172a 0%, #111827 100%)", }; const videoBaseStyle: CSSProperties = { display: "block", width: "100%", height: "100%", objectFit: "contain", userSelect: "none", }; const hintStyle: CSSProperties = { opacity: 0.66, }; const getStatusColor = (state: ConnectionState): string => { switch (state) { case "ready": return "#15803d"; case "error": return "#b91c1c"; case "closed": return "#b45309"; default: return "#475569"; } }; export const DesktopViewer = ({ client, className, style, imageStyle, height = 480, showStatusBar = true, onConnect, onDisconnect, onError, }: DesktopViewerProps) => { const wrapperRef = useRef(null); const videoRef = useRef(null); const sessionRef = useRef | null>(null); const [connectionState, setConnectionState] = useState("connecting"); const [statusMessage, setStatusMessage] = useState("Starting desktop stream..."); const [hasVideo, setHasVideo] = useState(false); const [resolution, setResolution] = useState<{ width: number; height: number } | null>(null); useEffect(() => { let cancelled = false; setConnectionState("connecting"); setStatusMessage("Connecting to desktop stream..."); setResolution(null); setHasVideo(false); const session = client.connectDesktopStream(); sessionRef.current = session; session.onReady((status) => { if (cancelled) return; setConnectionState("ready"); setStatusMessage("Desktop stream connected."); setResolution({ width: status.width, height: status.height }); onConnect?.(status); }); session.onTrack((stream) => { if (cancelled) return; const video = videoRef.current; if (video) { video.srcObject = stream; void video.play().catch(() => undefined); setHasVideo(true); } }); session.onError((error) => { if (cancelled) return; setConnectionState("error"); setStatusMessage(error instanceof Error ? error.message : error.message); onError?.(error); }); session.onDisconnect(() => { if (cancelled) return; setConnectionState((current) => (current === "error" ? current : "closed")); setStatusMessage((current) => (current === "Desktop stream connected." ? "Desktop stream disconnected." : current)); onDisconnect?.(); }); return () => { cancelled = true; session.close(); sessionRef.current = null; const video = videoRef.current; if (video) { video.srcObject = null; } setHasVideo(false); }; }, [client, onConnect, onDisconnect, onError]); const scalePoint = (clientX: number, clientY: number) => { const video = videoRef.current; if (!video || !resolution) { return null; } const rect = video.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) { return null; } // The video uses objectFit: "contain", so we need to compute the actual // rendered content area within the