"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import dynamic from "next/dynamic"; import { Check, ChevronDown, Info, Pencil, Plus, Trash2, Users, X, } from "lucide-react"; import { deleteWorkflow, getWorkflow, updateWorkflow } from "@/app/lib/mikeApi"; import { ShareWorkflowModal } from "@/app/components/workflows/ShareWorkflowModal"; import { WFEditColumnModal } from "@/app/components/workflows/WFEditColumnModal"; import { WFColumnViewModal } from "@/app/components/workflows/WFColumnViewModal"; import { AddColumnModal } from "@/app/components/tabular/AddColumnModal"; import type { ColumnConfig, Workflow } from "@/app/components/shared/types"; import { BUILT_IN_WORKFLOWS } from "@/app/components/workflows/builtinWorkflows"; import { formatIcon, formatLabel } from "@/app/components/tabular/columnFormat"; import { ConfirmPopup } from "@/app/components/shared/ConfirmPopup"; import { HeaderActionsMenu } from "@/app/components/shared/HeaderActionsMenu"; import { PageHeader } from "@/app/components/shared/PageHeader"; import { WorkflowDetailsModal } from "@/app/components/workflows/WorkflowDetailsModal"; import { useAuth } from "@/contexts/AuthContext"; import { useUserProfile } from "@/contexts/UserProfileContext"; // dynamic import keeps Tiptap (browser-only) out of the SSR bundle const WorkflowPromptEditor = dynamic( () => import("@/app/components/workflows/WorkflowPromptEditor").then( (m) => ({ default: m.WorkflowPromptEditor }), ), { ssr: false }, ); interface Props { id: string; workflowType: Workflow["type"]; } type SaveStatus = "idle" | "saving" | "saved"; type DeleteStatus = "idle" | "loading" | "complete"; const NAME_COL_W = "w-[332px] shrink-0"; // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export function WorkflowDetailPage({ id, workflowType }: Props) { const router = useRouter(); const { user } = useAuth(); const { profile } = useUserProfile(); const stickyCellBg = "bg-[#fafbfc]"; const builtinWorkflow = BUILT_IN_WORKFLOWS.find((w) => w.id === id && w.type === workflowType) ?? null; const isBuiltin = builtinWorkflow !== null; const [workflow, setWorkflow] = useState(null); const [loading, setLoading] = useState(true); const [notFound, setNotFound] = useState(false); const readOnly = isBuiltin || (workflow?.is_system ?? false) || workflow?.allow_edit === false; const canShare = !readOnly && (workflow?.is_owner ?? true); // Editor state const [promptMd, setPromptMd] = useState(""); const [columns, setColumns] = useState([]); // Save status const [saveStatus, setSaveStatus] = useState("idle"); const debounceRef = useRef | null>(null); // Column selection const [selectedColIndices, setSelectedColIndices] = useState([]); // Column modal const [addColumnOpen, setAddColumnOpen] = useState(false); const [editingColumn, setEditingColumn] = useState(null); const [viewingColumn, setViewingColumn] = useState(null); // Share popover const [shareOpen, setShareOpen] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [deleteStatus, setDeleteStatus] = useState("idle"); // Column actions dropdown const [colActionsOpen, setColActionsOpen] = useState(false); const colActionsRef = useRef(null); useEffect(() => { function handleClick(e: MouseEvent) { if (colActionsRef.current && !colActionsRef.current.contains(e.target as Node)) { setColActionsOpen(false); } } if (colActionsOpen) document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [colActionsOpen]); // --------------------------------------------------------------------------- // Load workflow // --------------------------------------------------------------------------- useEffect(() => { if (isBuiltin) { const wf = builtinWorkflow; if (!wf) { setNotFound(true); } else { setWorkflow(wf); setPromptMd(wf.prompt_md ?? ""); setColumns(wf.columns_config ?? []); } setLoading(false); return; } getWorkflow(id) .then((wf) => { if (wf.type !== workflowType) { setNotFound(true); return; } setWorkflow(wf); setPromptMd(wf.prompt_md ?? ""); setColumns( (wf.columns_config ?? []) .slice() .sort((a, b) => a.index - b.index), ); }) .catch(() => setNotFound(true)) .finally(() => setLoading(false)); }, [id, isBuiltin, builtinWorkflow, workflowType]); // --------------------------------------------------------------------------- // Debounced auto-save for prompt // --------------------------------------------------------------------------- const save = useCallback( (newPromptMd: string) => { if (readOnly) return; if (debounceRef.current) clearTimeout(debounceRef.current); setSaveStatus("saving"); debounceRef.current = setTimeout(async () => { try { await updateWorkflow(id, { prompt_md: newPromptMd }); setSaveStatus("saved"); setTimeout(() => setSaveStatus("idle"), 2000); } catch { setSaveStatus("idle"); } }, 800); }, [id, readOnly], ); async function handleWorkflowDetailsSave(values: { title: string }) { if (!workflow || readOnly || !values.title) return; if (values.title === workflow.title) return; const updated = await updateWorkflow(id, { title: values.title }); setWorkflow({ ...updated, shared_by_name: updated.shared_by_name ?? workflow.shared_by_name ?? null, }); } async function handleDeleteWorkflow() { if (!workflow || readOnly || workflow.is_owner === false) return; setDeleteStatus("loading"); try { await deleteWorkflow(id); setDeleteStatus("complete"); setTimeout(() => router.push("/workflows"), 600); } catch { setDeleteStatus("idle"); } } function handlePromptChange(val: string | undefined) { const next = val ?? ""; setPromptMd(next); save(next); } // --------------------------------------------------------------------------- // Column save // --------------------------------------------------------------------------- async function saveColumns(next: ColumnConfig[]) { if (readOnly) return; setSaveStatus("saving"); try { const updated = await updateWorkflow(id, { columns_config: next }); setWorkflow(updated); setSaveStatus("saved"); setTimeout(() => setSaveStatus("idle"), 2000); } catch { setSaveStatus("idle"); } } function handleColumnsAdded(added: ColumnConfig[]) { const next = [ ...columns, ...added.map((c, i) => ({ ...c, index: columns.length + i })), ]; setColumns(next); saveColumns(next); setAddColumnOpen(false); } function handleColumnSaved(updated: ColumnConfig) { const next = columns.map((c) => c.index === updated.index ? updated : c, ); setColumns(next); saveColumns(next); setEditingColumn(null); } // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- if (loading) { return (
router.push("/workflows"), title: "Back to Workflows", }, { loading: true, skeletonClassName: "w-40" }, ]} />
{workflowType === "tabular" ? ( ) : ( )}
); } if (notFound || !workflow) { return (

Workflow not found.

); } return (
{/* Page header */} router.push("/workflows"), title: "Back to Workflows", }, { label: ( {workflow.title} ), }, ]} actions={[ saveStatus !== "idle" ? { type: "custom", render: ( {saveStatus === "saved" ? ( ) : null} {saveStatus === "saving" ? "Saving…" : "Saved"} ), } : null, canShare ? { onClick: () => setShareOpen(true), title: "Open workflow people", iconOnly: true, icon: , } : null, !readOnly ? { type: "custom", render: ( setDetailsOpen(true), }, { label: "Workflow Details", icon: Info, onSelect: () => setDetailsOpen(true), }, { label: "Delete", icon: Trash2, variant: "danger", disabled: workflow.is_owner === false, onSelect: () => { setDeleteStatus("idle"); setDeleteOpen(true); }, }, ]} /> ), } : null, ]} /> setDetailsOpen(false)} onSave={handleWorkflowDetailsSave} onShareWorkflow={() => { setDetailsOpen(false); setShareOpen(true); }} /> {shareOpen && ( setShareOpen(false)} /> )} void handleDeleteWorkflow()} onCancel={() => { if (deleteStatus === "loading") return; setDeleteOpen(false); setDeleteStatus("idle"); }} /> {/* Body */}
{workflow.type === "assistant" ? ( /* ── Assistant: WYSIWYG editor ── */
) : ( /* ── Tabular: Column table ── */
{/* Toolbar */} {!readOnly && (
{selectedColIndices.length > 0 && (
{colActionsOpen && (
)}
)}
)} {readOnly && (
Read-only
)}
{/* Table header */}
{columns.length > 0 && ( 0 && selectedColIndices.length === columns.length} ref={(el) => { if (el) el.indeterminate = selectedColIndices.length > 0 && selectedColIndices.length < columns.length; }} onChange={() => setSelectedColIndices(selectedColIndices.length === columns.length ? [] : columns.map((c) => c.index))} className="h-2.5 w-2.5 rounded border-gray-200 cursor-pointer accent-black" /> )} Column Title
Format
Prompt
{!readOnly &&
}
{/* Rows */}
{columns.length === 0 ? (

Columns

Add columns to define what this tabular review workflow extracts from each document.

{!readOnly && ( )}
) : ( columns.map((col) => { const FormatIcon = formatIcon(col.format ?? "text"); const isChecked = selectedColIndices.includes(col.index); return (
readOnly ? setViewingColumn(col) : setEditingColumn(col)} className="group flex items-center h-10 pr-3 md:pr-10 border-b border-gray-50 hover:bg-gray-100 cursor-pointer transition-colors" >
setSelectedColIndices((prev) => prev.includes(col.index) ? prev.filter((i) => i !== col.index) : [...prev, col.index])} onClick={(e) => e.stopPropagation()} className="h-2.5 w-2.5 shrink-0 rounded border-gray-200 cursor-pointer accent-black" /> {col.name}
{formatLabel(col.format ?? "text")}
{col.prompt}
{!readOnly && (
)}
); }) )}
)}
{/* Read-only column view modal */} {viewingColumn && ( setViewingColumn(null)} /> )} {/* Add column modal */} setAddColumnOpen(false)} onAdd={handleColumnsAdded} /> {/* Edit column modal */} {editingColumn && ( setEditingColumn(null)} onSave={handleColumnSaved} onDelete={() => { const next = columns .filter((c) => c.index !== editingColumn.index) .map((c, i) => ({ ...c, index: i })); setColumns(next); saveColumns(next); setEditingColumn(null); }} /> )}
); } function AssistantWorkflowEditorSkeleton() { return (
); } function TabularWorkflowEditorSkeleton() { return ( <>
{[1, 2, 3, 4, 5].map((i) => (
))}
); }