import { useCallback, useEffect, useMemo, useState } from "react"; import { isLocalWorkspaceHost } from "../../shared"; import type { DeploymentInfo } from "../../document-model"; import type { DeployStatus, PdfActionStatus } from "../workbenchTypes"; import { localMutationHeaders } from "../localMutationRequest"; import { parseDeployError } from "./deploymentStatusModel"; export type WordExportMode = "visual" | "semantic"; export type WordExportOptions = { mode: WordExportMode; pageIndexes?: number[]; }; export interface UseDeploymentWorkbenchOptions { deploymentInfo: DeploymentInfo; // Active Press slug — when present the local PDF export endpoint // tells the CLI to export this Press (open-press pdf . --press ) // instead of defaulting to the first Press. Empty / null means the // workspace has only one Press, or the workbench is at the gallery // root, and the CLI default is correct. pressSlug?: string | null; } export interface DeploymentWorkbench { status: DeployStatus; pdfActionStatus: PdfActionStatus; wordActionStatus: PdfActionStatus; currentDeploymentInfo: DeploymentInfo; localDeployEnabled: boolean; pdfButtonDisabled: boolean; wordButtonDisabled: boolean; handleDeploy: () => Promise; handleOpenWorkbenchPdf: (pageIndexes?: number[]) => void; handleOpenWorkbenchWord: (options?: WordExportOptions) => void; } export function useDeploymentWorkbench({ deploymentInfo, pressSlug = null }: UseDeploymentWorkbenchOptions): DeploymentWorkbench { const [status, setStatus] = useState("idle"); const [pdfActionStatus, setPdfActionStatus] = useState("idle"); const [wordActionStatus, setWordActionStatus] = useState("idle"); const [currentDeploymentInfo, setCurrentDeploymentInfo] = useState(deploymentInfo); const staticPdfHref = currentDeploymentInfo.pdf; const localDeployEnabled = useMemo(() => { if (typeof window === "undefined") return false; return isLocalWorkspaceHost(window.location.hostname); }, []); useEffect(() => { if (!localDeployEnabled) return; let cancelled = false; const query = pressSlug ? `?press=${encodeURIComponent(pressSlug)}` : ""; void fetch(`/__openpress/status${query}`, { cache: "no-store" }) .then(async (response) => response.ok ? response.json() : null) .then((result: LocalDeploymentStatus | null) => { if (cancelled || !result) return; setCurrentDeploymentInfo(localDeploymentInfo(result)); }) .catch(() => {}); return () => { cancelled = true; }; }, [localDeployEnabled, pressSlug]); const pdfButtonDisabled = localDeployEnabled ? pdfActionStatus === "generating" || pdfActionStatus === "opening" : !staticPdfHref; const wordButtonDisabled = !localDeployEnabled || wordActionStatus === "generating" || wordActionStatus === "opening"; const handleDeploy = useCallback(async () => { if (status === "deploying") return; if (currentDeploymentInfo.configured === false) { setStatus("setup"); return; } setStatus("deploying"); try { const requestBody = pressSlug ? { press: pressSlug } : {}; const response = await fetch("/__openpress/deploy", { method: "POST", headers: localMutationHeaders({ "Content-Type": "application/json" }), body: JSON.stringify(requestBody), }); if (response.status === 404 || response.status === 405) { setStatus("unavailable"); return; } if (!response.ok) { const text = await response.text().catch(() => ""); const result = parseDeployError(text); if (result?.deploy_configured === false) { setCurrentDeploymentInfo((info) => ({ ...info, configured: false, adapter: result.deploy_adapter ?? info.adapter, source: result.deploy_source ?? info.source, projectName: result.deploy_project_name ?? info.projectName, setupMessage: result.message ?? info.setupMessage, })); setStatus("setup"); return; } console.error("OpenPress deploy failed", text); setStatus("failed"); return; } const result = (await response.json().catch(() => null)) as { deployed_at?: string; pdf?: string; public_url?: string; } | null; setCurrentDeploymentInfo((info) => ({ online: true, deployedAt: result?.deployed_at ?? new Date().toISOString(), pdf: result?.pdf ?? info.pdf ?? __OPENPRESS_PDF_HREF__, publicUrl: result?.public_url ?? info.publicUrl, dirty: false, })); setStatus("deployed"); setTimeout(() => setStatus("idle"), 3200); } catch (error) { console.error("OpenPress deploy unavailable", error); setStatus("unavailable"); } }, [status, currentDeploymentInfo.configured, pressSlug]); const handleOpenLatestLocalPdf = useCallback(async (pageIndexes?: number[]) => { if (pdfActionStatus === "generating") return; setPdfActionStatus("generating"); try { const requestBody: Record = pressSlug ? { press: pressSlug } : {}; if (pageIndexes && pageIndexes.length > 0) requestBody.pages = pageIndexes; const response = await fetch("/__openpress/local-pdf-export", { method: "POST", headers: localMutationHeaders({ "Content-Type": "application/json" }), body: JSON.stringify(requestBody), }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(text || `Local PDF export failed with status ${response.status}`); } const result = (await response.json().catch(() => null)) as { pdf?: string } | null; const pdfHref = result?.pdf ?? "/__openpress/local-pdf-file"; setPdfActionStatus("opening"); window.setTimeout(() => window.location.assign(pdfHref), 180); } catch (error) { console.error("OpenPress local PDF export failed", error); setPdfActionStatus("failed"); } }, [pdfActionStatus, pressSlug]); const handleOpenLatestLocalWord = useCallback(async (options: WordExportOptions = { mode: "visual" }) => { if (wordActionStatus === "generating" || wordActionStatus === "opening") return; setWordActionStatus("generating"); try { const requestBody: Record = { ...(pressSlug ? { press: pressSlug } : {}), mode: options.mode, }; if (options.mode === "visual" && options.pageIndexes && options.pageIndexes.length > 0) { requestBody.pages = options.pageIndexes; } const response = await fetch("/__openpress/local-word-export", { method: "POST", headers: localMutationHeaders({ "Content-Type": "application/json" }), body: JSON.stringify(requestBody), }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(text || `Local Word export failed with status ${response.status}`); } const result = (await response.json().catch(() => null)) as { word?: string } | null; const wordHref = result?.word ?? "/__openpress/local-word-file"; setWordActionStatus("opening"); window.setTimeout(() => { window.location.assign(wordHref); window.setTimeout(() => setWordActionStatus("idle"), 1200); }, 180); } catch (error) { console.error("OpenPress local Word export failed", error); setWordActionStatus("failed"); } }, [pressSlug, wordActionStatus]); const handleOpenWorkbenchPdf = useCallback((pageIndexes?: number[]) => { if (localDeployEnabled) { void handleOpenLatestLocalPdf(pageIndexes); return; } if (!staticPdfHref) return; window.open(staticPdfHref, "_blank", "noopener,noreferrer"); }, [handleOpenLatestLocalPdf, localDeployEnabled, staticPdfHref]); const handleOpenWorkbenchWord = useCallback((options?: WordExportOptions) => { if (!localDeployEnabled) return; void handleOpenLatestLocalWord(options); }, [handleOpenLatestLocalWord, localDeployEnabled]); return { status, pdfActionStatus, wordActionStatus, currentDeploymentInfo, localDeployEnabled, pdfButtonDisabled, wordButtonDisabled, handleDeploy, handleOpenWorkbenchPdf, handleOpenWorkbenchWord, }; } type LocalDeploymentStatus = { deployed_at?: string; pdf?: string; public_url?: string; dirty?: boolean; deploy_configured?: boolean; deploy_adapter?: string; deploy_source?: string; deploy_project_name?: string | null; deploy_setup_message?: string; }; function localDeploymentInfo(result: LocalDeploymentStatus): DeploymentInfo { const configured = result.deploy_configured !== false; return { online: configured && Boolean(result.deployed_at || result.public_url), deployedAt: result.deployed_at, pdf: result.pdf, publicUrl: result.public_url, dirty: result.dirty === true, configured, adapter: result.deploy_adapter, source: result.deploy_source, projectName: typeof result.deploy_project_name === "string" ? result.deploy_project_name : undefined, setupMessage: result.deploy_setup_message, }; }