From 9855b92605940b8e2e0f39ab6e529373a53e2d86 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 11:20:08 -0700 Subject: [PATCH] Add Slide Factory mode workbench --- apps/web/src/main.tsx | 723 +++++++++++++++++++++++++++++++++++----- apps/web/src/styles.css | 120 ++++++- 2 files changed, 763 insertions(+), 80 deletions(-) diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 382bbf8..8351f72 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,8 +1,25 @@ -import React, { DragEvent, useMemo, useRef, useState } from "react"; +import React, { DragEvent, useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; -import { Download, ExternalLink, FileText, Image as ImageIcon, Loader2, Paperclip, Upload, WandSparkles, X } from "lucide-react"; +import { + Download, + ExternalLink, + FileText, + Image as ImageIcon, + Layers, + LayoutTemplate, + Loader2, + Palette, + Paperclip, + Pencil, + Save, + Upload, + WandSparkles, + X +} from "lucide-react"; import "./styles.css"; +type Mode = "template-build" | "deck-build" | "template-edit" | "deck-edit"; + type DeckResult = { id: string; title: string; @@ -16,6 +33,16 @@ type DeckResult = { }; }; +type DesignTemplate = { + id: string; + name: string; + description: string; + sourceName?: string; + colors: string[]; + updatedAt: string; + locked?: boolean; +}; + const initialContent = `# Customer Discovery Notes Paste notes, transcript excerpts, or brief content here. @@ -24,16 +51,111 @@ Paste notes, transcript excerpts, or brief content here. - Build an executive narrative. - Render a PPTX for review.`; +const starterTemplate: DesignTemplate = { + id: "incorta", + name: "Incorta Starter", + description: + "White executive deck, orange brand accent, restrained blue secondary accents, Aptos typography, compact business density, fixed title/body/two-column/roadmap layouts.", + colors: ["#ffffff", "#f05a28", "#1e6bff", "#172033", "#596579"], + updatedAt: "2026-06-09T00:00:00.000Z", + locked: true +}; + +const modeTabs: Array<{ id: Mode; label: string; icon: React.ElementType }> = [ + { id: "template-build", label: "Build Template", icon: LayoutTemplate }, + { id: "deck-build", label: "Build Slide / Deck", icon: Layers }, + { id: "template-edit", label: "Edit Template", icon: Palette }, + { id: "deck-edit", label: "Edit Slide / Deck", icon: Pencil } +]; + function App() { + const [activeMode, setActiveMode] = useState("template-build"); + const [templates, setTemplates] = useState([starterTemplate]); + const [selectedTemplateId, setSelectedTemplateId] = useState(starterTemplate.id); + const [content, setContent] = useState(initialContent); const [instructions, setInstructions] = useState("Create a concise executive deck."); const [file, setFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [dragging, setDragging] = useState(false); + + const [templateName, setTemplateName] = useState("New design standard"); + const [templateDescription, setTemplateDescription] = useState( + "Executive presentation system with clear hierarchy, reusable slide sections, consistent spacing, and restrained visual emphasis." + ); + const [templateFile, setTemplateFile] = useState(null); + const [templatePreviewUrl, setTemplatePreviewUrl] = useState(null); + const [templateDragging, setTemplateDragging] = useState(false); + const [editTemplateId, setEditTemplateId] = useState(starterTemplate.id); + const [editTemplateName, setEditTemplateName] = useState(starterTemplate.name); + const [editTemplateDescription, setEditTemplateDescription] = useState(starterTemplate.description); + const [result, setResult] = useState(null); + const [notice, setNotice] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); + const fileInputRef = useRef(null); + const templateInputRef = useRef(null); + + useEffect(() => { + try { + const stored = window.localStorage.getItem("slideFactory.designTemplates.v1"); + if (!stored) return; + const parsed = JSON.parse(stored) as DesignTemplate[]; + const customTemplates = parsed.filter((template) => template.id !== starterTemplate.id); + setTemplates([starterTemplate, ...customTemplates]); + } catch { + setTemplates([starterTemplate]); + } + }, []); + + useEffect(() => { + const customTemplates = templates.filter((template) => !template.locked); + window.localStorage.setItem("slideFactory.designTemplates.v1", JSON.stringify(customTemplates)); + }, [templates]); + + const selectedTemplate = useMemo( + () => templates.find((template) => template.id === selectedTemplateId) || starterTemplate, + [selectedTemplateId, templates] + ); + + const editableTemplate = useMemo( + () => templates.find((template) => template.id === editTemplateId) || starterTemplate, + [editTemplateId, templates] + ); + + const activeTemplatePreview = useMemo(() => { + if (activeMode === "template-build") { + return { + id: "draft-template", + name: templateName.trim() || "Untitled design standard", + description: templateDescription.trim(), + sourceName: templateFile?.name, + colors: starterTemplate.colors, + updatedAt: new Date().toISOString() + }; + } + + if (activeMode === "template-edit") { + return { + ...editableTemplate, + name: editTemplateName.trim() || editableTemplate.name, + description: editTemplateDescription.trim() + }; + } + + return selectedTemplate; + }, [ + activeMode, + editableTemplate, + editTemplateDescription, + editTemplateName, + selectedTemplate, + templateDescription, + templateFile, + templateName + ]); const sourceLabel = useMemo(() => { if (file) return `${file.name} · ${formatBytes(file.size)}`; @@ -41,6 +163,11 @@ function App() { return chars ? `${chars.toLocaleString()} characters pasted` : "No source selected"; }, [content, file]); + const templateSourceLabel = useMemo(() => { + if (templateFile) return `${templateFile.name} · ${formatBytes(templateFile.size)}`; + return "No design source selected"; + }, [templateFile]); + function setSourceFile(nextFile: File | null) { if (previewUrl) URL.revokeObjectURL(previewUrl); setFile(nextFile); @@ -53,13 +180,32 @@ function App() { } } - function onDrop(event: DragEvent) { + function setTemplateSourceFile(nextFile: File | null) { + if (templatePreviewUrl) URL.revokeObjectURL(templatePreviewUrl); + setTemplateFile(nextFile); + setNotice(null); + setError(null); + if (nextFile?.type.startsWith("image/")) { + setTemplatePreviewUrl(URL.createObjectURL(nextFile)); + } else { + setTemplatePreviewUrl(null); + } + } + + function onSourceDrop(event: DragEvent) { event.preventDefault(); setDragging(false); const dropped = event.dataTransfer.files?.[0]; if (dropped) setSourceFile(dropped); } + function onTemplateDrop(event: DragEvent) { + event.preventDefault(); + setTemplateDragging(false); + const dropped = event.dataTransfer.files?.[0]; + if (dropped) setTemplateSourceFile(dropped); + } + async function onPaste(event: React.ClipboardEvent) { const image = Array.from(event.clipboardData.files).find((item) => item.type.startsWith("image/")); if (image) { @@ -68,9 +214,78 @@ function App() { } } + function saveTemplate() { + const name = templateName.trim() || "Untitled design standard"; + const template: DesignTemplate = { + id: `template-${Date.now()}`, + name, + description: templateDescription.trim(), + sourceName: templateFile?.name, + colors: starterTemplate.colors, + updatedAt: new Date().toISOString() + }; + setTemplates((current) => [starterTemplate, template, ...current.filter((item) => item.id !== starterTemplate.id)]); + setSelectedTemplateId(template.id); + setEditTemplateId(template.id); + setEditTemplateName(template.name); + setEditTemplateDescription(template.description); + setNotice(`${template.name} saved`); + } + + function loadEditableTemplate(templateId: string) { + const template = templates.find((item) => item.id === templateId) || starterTemplate; + setEditTemplateId(template.id); + setEditTemplateName(template.name); + setEditTemplateDescription(template.description); + setNotice(null); + setError(null); + } + + function updateTemplate() { + if (editableTemplate.locked) { + const copy: DesignTemplate = { + ...editableTemplate, + id: `template-${Date.now()}`, + name: `${editTemplateName.trim() || editableTemplate.name} Copy`, + description: editTemplateDescription.trim(), + locked: false, + updatedAt: new Date().toISOString() + }; + setTemplates((current) => [starterTemplate, copy, ...current.filter((item) => item.id !== starterTemplate.id)]); + setSelectedTemplateId(copy.id); + setEditTemplateId(copy.id); + setEditTemplateName(copy.name); + setNotice(`${copy.name} saved`); + return; + } + + setTemplates((current) => + current.map((template) => + template.id === editTemplateId + ? { + ...template, + name: editTemplateName.trim() || template.name, + description: editTemplateDescription.trim(), + updatedAt: new Date().toISOString() + } + : template + ) + ); + setNotice(`${editTemplateName.trim() || editableTemplate.name} updated`); + } + + function removeEditableTemplate() { + if (editableTemplate.locked) return; + setTemplates((current) => current.filter((template) => template.id !== editableTemplate.id)); + setSelectedTemplateId(starterTemplate.id); + loadEditableTemplate(starterTemplate.id); + setNotice(`${editableTemplate.name} removed`); + } + async function createDeck() { setBusy(true); setError(null); + setNotice(null); setResult(null); try { const response = file ? await createDeckFromFile(file) : await createDeckFromText(); @@ -84,12 +299,23 @@ function App() { } } + function designAwareInstructions() { + return [ + instructions, + "", + `Selected design template: ${selectedTemplate.name}.`, + selectedTemplate.description + ] + .filter(Boolean) + .join("\n"); + } + function createDeckFromFile(sourceFile: File) { const formData = new FormData(); formData.set("source", sourceFile); formData.set("style", "incorta"); formData.set("audience", "executives"); - formData.set("instructions", instructions); + formData.set("instructions", designAwareInstructions()); return fetch("/api/decks/from-source", { method: "POST", body: formData @@ -102,116 +328,455 @@ function App() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ style: "incorta", - instructions, + instructions: designAwareInstructions(), input: { type: "markdown", content } }) }); } + const title = modeTabs.find((mode) => mode.id === activeMode)?.label || "Slide Factory"; + return (

Slide Factory

-

Source to deck

+

{title}

Google Slides workflow - + {activeMode === "template-build" && ( + + )} + {activeMode === "deck-build" && ( + + )} + {activeMode === "template-edit" && ( + + )} + {activeMode === "deck-edit" && ( + + )}
+ +
- - -
- {sourceLabel} - {file && ( - - )} -
- - {previewUrl && ( -
- -
)} - - -