import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { IssueWorkProduct } from "@paperclipai/shared"; import { issuesApi } from "../api/issues"; import { queryKeys } from "../lib/queryKeys"; import { MarkdownBody } from "./MarkdownBody"; import { cn } from "../lib/utils"; import { FileText, ExternalLink, GitBranch, GitCommit, Globe, Server, Package, Loader2, ArrowLeft, X, CheckCircle2, XCircle, RotateCcw, } from "lucide-react"; import { Button } from "@/components/ui/button"; interface ArtifactsPanelProps { taskId: string; isAgentWorking?: boolean; /** Open the document viewer directly to a specific doc */ openDocKey?: string | null; openDocTitle?: string | null; onClearOpenDoc?: () => void; /** Approval callbacks — called from the document viewer */ onApprove?: () => void; onReject?: () => void; } type FilterValue = "all" | "in_progress" | "for_review" | "completed"; const FILTERS: Array<{ label: string; value: FilterValue }> = [ { label: "All", value: "all" }, { label: "In Progress", value: "in_progress" }, { label: "For Review", value: "for_review" }, { label: "Completed", value: "completed" }, ]; function matchesFilter(wp: IssueWorkProduct, filter: FilterValue): boolean { if (filter === "all") return true; if (filter === "in_progress") return wp.status === "active" || wp.status === "draft"; if (filter === "for_review") return wp.status === "ready_for_review"; if (filter === "completed") return wp.status === "approved" || wp.status === "merged"; return true; } function typeIcon(type: string) { switch (type) { case "document": return FileText; case "pull_request": return GitBranch; case "branch": return GitBranch; case "commit": return GitCommit; case "preview_url": return Globe; case "runtime_service": return Server; case "artifact": return Package; default: return FileText; } } function statusBadge(status: string) { switch (status) { case "active": case "draft": return { label: "In Progress", className: "bg-blue-500/10 text-blue-600 dark:text-blue-400" }; case "ready_for_review": return { label: "For Review", className: "bg-amber-500/10 text-amber-600 dark:text-amber-400" }; case "approved": case "merged": return { label: "Completed", className: "bg-green-500/10 text-green-600 dark:text-green-400" }; case "changes_requested": return { label: "Changes Requested", className: "bg-orange-500/10 text-orange-600 dark:text-orange-400" }; case "failed": return { label: "Failed", className: "bg-red-500/10 text-red-600 dark:text-red-400" }; default: return { label: status, className: "bg-muted text-muted-foreground" }; } } export function ArtifactsPanel({ taskId, isAgentWorking, openDocKey, openDocTitle, onClearOpenDoc, onApprove, onReject }: ArtifactsPanelProps) { const [filter, setFilter] = useState("all"); const [viewingDoc, setViewingDoc] = useState<{ key: string; title: string } | null>(null); const { data: workProducts, isLoading } = useQuery({ queryKey: queryKeys.issues.workProducts(taskId), queryFn: () => issuesApi.listWorkProducts(taskId), refetchInterval: 5000, }); // Open doc from parent (e.g. clicking plan link in chat) const effectiveViewingDoc = openDocKey ? { key: openDocKey, title: openDocTitle ?? "Document" } : viewingDoc; const handleBack = () => { setViewingDoc(null); onClearOpenDoc?.(); }; // Find the work product for the currently viewed doc to know its status const viewedWorkProduct = effectiveViewingDoc ? (workProducts ?? []).find((wp) => wp.title === effectiveViewingDoc.title) : null; const filtered = (workProducts ?? []).filter((wp) => matchesFilter(wp, filter)); // Document viewer if (effectiveViewingDoc) { return ( ); } return (

Artifacts

{/* Filter chips */}
{FILTERS.map((f) => ( ))}
{/* Work products list */}
{isLoading ? (
Loading...
) : filtered.length === 0 ? (

{workProducts?.length === 0 ? "Your team's deliverables and plans will appear here as they're produced." : "No artifacts match this filter."}

) : (
{filtered.map((wp) => { const Icon = typeIcon(wp.type); const badge = statusBadge(wp.status); const isDraft = wp.status === "draft" || wp.status === "active"; const showGenerating = isDraft && isAgentWorking; return ( ); })}
)}
); } function DocumentViewer({ taskId, docKey, title, onBack, status, reviewState, onApprove, onReject, }: { taskId: string; docKey: string; title: string; onBack: () => void; status: string | null; reviewState: string | null; onApprove?: () => void; onReject?: () => void; }) { const { data: doc, isLoading, error } = useQuery({ queryKey: queryKeys.issues.documents(taskId), queryFn: () => issuesApi.getDocument(taskId, docKey), }); const needsAction = status === "ready_for_review" || reviewState === "needs_board_review"; const isApproved = status === "approved" || reviewState === "approved"; const isRejected = status === "changes_requested" || reviewState === "changes_requested"; return (

{title}

{isLoading ? (
Loading document...
) : error ? (

Document not available yet.

) : doc?.body ? (
{doc.body}
) : (

Document is empty.

)}
{/* Sticky action footer */} {needsAction && (

This document needs your review.

)} {isApproved && (

Approved — hire tasks created

)} {isRejected && (

Changes requested — CEO is revising

)}
); }