diff --git a/apps/web/src/components/review/InitiativeReview.tsx b/apps/web/src/components/review/InitiativeReview.tsx index 5bb0b66..14b4864 100644 --- a/apps/web/src/components/review/InitiativeReview.tsx +++ b/apps/web/src/components/review/InitiativeReview.tsx @@ -6,6 +6,7 @@ import { trpc } from "@/lib/trpc"; import { parseUnifiedDiff } from "./parse-diff"; import { DiffViewer } from "./DiffViewer"; import { ReviewSidebar } from "./ReviewSidebar"; +import { PreviewControls } from "./PreviewControls"; interface InitiativeReviewProps { initiativeId: string; @@ -48,6 +49,44 @@ export function InitiativeReview({ initiativeId, onCompleted }: InitiativeReview { enabled: !!selectedCommit }, ); + // Preview state + const projectsQuery = trpc.getInitiativeProjects.useQuery({ initiativeId }); + const firstProjectId = projectsQuery.data?.[0]?.id ?? null; + + const previewsQuery = trpc.listPreviews.useQuery( + { initiativeId }, + { refetchInterval: 3000 }, + ); + const existingPreview = previewsQuery.data?.find( + (p) => p.initiativeId === initiativeId, + ); + const [activePreviewId, setActivePreviewId] = useState(null); + const previewStatusQuery = trpc.getPreviewStatus.useQuery( + { previewId: activePreviewId ?? existingPreview?.id ?? "" }, + { + enabled: !!(activePreviewId ?? existingPreview?.id), + refetchInterval: 3000, + }, + ); + const preview = previewStatusQuery.data ?? existingPreview; + + const startPreview = trpc.startPreview.useMutation({ + onSuccess: (data) => { + setActivePreviewId(data.id); + toast.success(`Preview running at ${data.url}`); + }, + onError: (err) => toast.error(`Preview failed: ${err.message}`), + }); + + const stopPreview = trpc.stopPreview.useMutation({ + onSuccess: () => { + setActivePreviewId(null); + toast.success("Preview stopped"); + previewsQuery.refetch(); + }, + onError: (err) => toast.error(`Failed to stop: ${err.message}`), + }); + const approveMutation = trpc.approveInitiativeReview.useMutation({ onSuccess: (_data, variables) => { const msg = variables.strategy === "merge_and_push" @@ -87,6 +126,33 @@ export function InitiativeReview({ initiativeId, onCompleted }: InitiativeReview const sourceBranch = diffQuery.data?.sourceBranch ?? ""; const targetBranch = diffQuery.data?.targetBranch ?? ""; + const previewState = firstProjectId && sourceBranch + ? { + status: startPreview.isPending + ? ("building" as const) + : preview?.status === "running" + ? ("running" as const) + : preview?.status === "building" + ? ("building" as const) + : preview?.status === "failed" + ? ("failed" as const) + : ("idle" as const), + url: preview?.url ?? undefined, + onStart: () => + startPreview.mutate({ + initiativeId, + projectId: firstProjectId, + branch: sourceBranch, + }), + onStop: () => { + const id = activePreviewId ?? existingPreview?.id; + if (id) stopPreview.mutate({ previewId: id }); + }, + isStarting: startPreview.isPending, + isStopping: stopPreview.isPending, + } + : null; + return (
{/* Header */} @@ -127,8 +193,9 @@ export function InitiativeReview({ initiativeId, onCompleted }: InitiativeReview
- {/* Right: action buttons */} + {/* Right: preview + action buttons */}
+ {previewState && } +
+ ); + } + + if (preview.status === "failed") { + return ( + + ); + } + + return ( + + ); +} diff --git a/apps/web/src/components/review/ReviewHeader.tsx b/apps/web/src/components/review/ReviewHeader.tsx index 7aed391..d12a5bd 100644 --- a/apps/web/src/components/review/ReviewHeader.tsx +++ b/apps/web/src/components/review/ReviewHeader.tsx @@ -6,11 +6,7 @@ import { FileCode, Plus, Minus, - ExternalLink, Loader2, - Square, - CircleDot, - RotateCcw, ArrowRight, Eye, AlertCircle, @@ -18,6 +14,8 @@ import { } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { PreviewControls } from "./PreviewControls"; +import type { PreviewState } from "./PreviewControls"; import type { FileDiff, ReviewStatus } from "./types"; interface PhaseOption { @@ -25,15 +23,6 @@ interface PhaseOption { name: string; } -interface PreviewState { - status: "idle" | "building" | "running" | "failed"; - url?: string; - onStart: () => void; - onStop: () => void; - isStarting: boolean; - isStopping: boolean; -} - interface ReviewHeaderProps { phases: PhaseOption[]; activePhaseId: string | null; @@ -285,66 +274,3 @@ export function ReviewHeader({ ); } -function PreviewControls({ preview }: { preview: PreviewState }) { - if (preview.status === "building" || preview.isStarting) { - return ( -
- - Building... -
- ); - } - - if (preview.status === "running") { - return ( -
- - - Preview - - - -
- ); - } - - if (preview.status === "failed") { - return ( - - ); - } - - return ( - - ); -}