// SparkVault — Frontend Application Logic // Pipeline Board, Rich Workbench, 64-Card Creative Whack Pack & Nano Banana 2.1 Mockup Studio const state = { ideas: [], stats: { total: 0, statusCounts: {}, totalMockups: 0, totalWhacks: 0, tags: [] }, whackCards: [], whackSuits: {}, currentView: "board", gridStatusFilter: "all", filters: { search: "", category: "all", tag: "all", hasMockup: false, sort: "score" }, // Whack Studio state whackSubmode: "collider", colliderCard: null, colliderIdea: null, spreadCards: [], deckSuitFilter: "all", deckSearch: "", // Quick Banner Whack Card quickBannerCard: null, // Review & Arena state reviewMode: "arena", triageQueue: [], triageIndex: 0, arenaPair: [], // Active Drawer Idea activeIdeaId: null, activeIdea: null, drawerTab: "details", drawerWhackCard: null, // Nano Banana Studio state nbGarmentColor: "#F5E8D3", nbGarmentColorName: "Vintage Cream", nbEditParentMockup: null, uploadedOverlayImage: null, stagedDrawerAttachmentDataUrl: null, stagedClipAttachmentDataUrl: null, settings: { hasApiKey: false, apiKeySource: "none", maskedKey: "", nanoBananaModel: "gemini-nano-banana-2.1" } }; const CATEGORY_LABELS = { creative: "🎨 Creative / Merch", project: "🛠️ Project / App", spark: "⚡ Quick Spark", research: "🔬 Research" }; const NEXT_STAGE_MAP = { spark: "exploring", exploring: "building", building: "shipped", shipped: "shipped", archived: "spark" }; function escapeHtml(str) { return String(str ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function renderSimpleMarkdown(md) { if (!md) return '

No notes yet.

'; const lines = String(md).split("\n"); const html = lines .map((line) => { const trimmed = line.trim(); if (trimmed.startsWith("### ")) return `

${escapeHtml(trimmed.slice(4))}

`; if (trimmed.startsWith("## ")) return `

${escapeHtml(trimmed.slice(3))}

`; if (trimmed.startsWith("# ")) return `

${escapeHtml(trimmed.slice(2))}

`; if (trimmed.startsWith("- ")) return `
  • ${formatInlineMd(trimmed.slice(2))}
  • `; if (!trimmed) return "
    "; return `

    ${formatInlineMd(trimmed)}

    `; }) .join(""); return html; } function formatInlineMd(text) { return escapeHtml(text) .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/\*(.+?)\*/g, "$1") .replace(/`(.+?)`/g, "$1"); } function showToast(message) { const container = document.getElementById("toast-container"); const el = document.createElement("div"); el.className = "toast"; el.textContent = message; container.appendChild(el); setTimeout(() => el.remove(), 3400); } async function apiFetch(url, options = {}) { const res = await fetch(url, { headers: { "Content-Type": "application/json", ...(options.headers || {}) }, ...options }); const data = await res.json().catch(() => ({})); if (!res.ok) { const err = new Error(data.error || `Request failed (${res.status})`); err.code = data.code; throw err; } return data; } // --- INITIALIZATION --- async function initApp() { bindGlobalEvents(); await Promise.all([loadSettings(), loadWhackCatalog(), loadIdeas()]); renderCompositorCanvas(); checkWebShareTargetQueryParams(); } async function loadSettings() { try { const data = await apiFetch("/api/settings"); state.settings = data; const dot = document.getElementById("api-key-status-dot"); if (dot) { dot.classList.toggle("online", Boolean(data.hasApiKey)); } const keyStatus = document.getElementById("settings-key-status"); if (keyStatus) { keyStatus.textContent = data.hasApiKey ? `Active provider configured (${data.apiKeySource}: ${data.maskedKey})` : "No Gemini API key stored — OpenAI Codex works automatically via OAuth."; } const modelSel = document.getElementById("settings-model-select"); if (modelSel && data.nanoBananaModel) { modelSel.value = data.nanoBananaModel; } const studioGenSel = document.getElementById("nb-generator-select"); if (studioGenSel && data.nanoBananaModel) { studioGenSel.value = data.nanoBananaModel; } const bgEl = document.getElementById("settings-user-background"); if (bgEl && data.userBackground !== undefined) { bgEl.value = data.userBackground; } } catch (err) { console.warn("Failed to load settings:", err); } } async function loadWhackCatalog() { const data = await apiFetch("/api/whack-cards"); state.whackCards = data.cards || []; state.whackSuits = data.suits || {}; populateWhackTwistSelect(); renderDeckBrowser(); } function populateWhackTwistSelect() { const sel = document.getElementById("nb-whack-twist-select"); if (!sel) return; sel.innerHTML = `` + state.whackCards .map((c) => ``) .join(""); } async function loadIdeas() { const params = new URLSearchParams(); if (state.filters.search) params.set("search", state.filters.search); if (state.filters.category !== "all") params.set("category", state.filters.category); if (state.filters.tag !== "all") params.set("tag", state.filters.tag); if (state.filters.hasMockup) params.set("hasMockup", "true"); params.set("sort", state.filters.sort); const data = await apiFetch(`/api/ideas?${params.toString()}`); state.ideas = data.ideas || []; state.stats = data.stats || state.stats; updateFilterTagOptions(); document.getElementById("stat-mockups-count").textContent = state.stats.totalMockups || 0; renderKanbanBoard(); renderExplorerGrid(); populateColliderIdeaSelect(); if (state.activeIdeaId) { const refreshed = state.ideas.find((i) => i.id === state.activeIdeaId); if (refreshed) { state.activeIdea = refreshed; renderDrawerContent(); } } } function updateFilterTagOptions() { const sel = document.getElementById("filter-tag"); if (!sel) return; const current = state.filters.tag; const optionsHtml = `` + (state.stats.tags || []) .map((t) => ``) .join(""); sel.innerHTML = optionsHtml; if ([...sel.options].some((o) => o.value === current)) { sel.value = current; } } // --- RENDER KANBAN BOARD --- function renderKanbanBoard() { const statuses = ["spark", "exploring", "building", "shipped", "archived"]; for (const st of statuses) { const colEl = document.getElementById(`col-${st}`); const countEl = document.getElementById(`count-${st}`); const mcountEl = document.getElementById(`mcount-${st}`); const list = state.ideas.filter((i) => i.status === st); if (countEl) countEl.textContent = list.length; if (mcountEl) mcountEl.textContent = list.length; if (!colEl) continue; if (list.length === 0) { colEl.innerHTML = `
    Drop ideas here
    `; continue; } colEl.innerHTML = list.map((idea) => renderIdeaCardHtml(idea)).join(""); bindIdeaCardEvents(colEl); } } function renderIdeaCardHtml(idea) { const checklistTotal = idea.checklist?.length || 0; const checklistDone = (idea.checklist || []).filter((c) => c.done).length; const whackCount = idea.whack_history?.length || 0; const nextStage = NEXT_STAGE_MAP[idea.status]; return `
    ${ idea.cover_mockup_url ? `${escapeHtml(idea.title)}` : "" }
    ${CATEGORY_LABELS[idea.category] || idea.category} ⚡ ${idea.composite_score}

    ${idea.pinned ? "📌 " : ""}${escapeHtml(idea.title)}

    ${idea.elevator_pitch ? `

    ${escapeHtml(idea.elevator_pitch)}

    ` : ""} ${ idea.tags.length > 0 ? `
    ${idea.tags.map((t) => `#${escapeHtml(t)}`).join("")}
    ` : "" }
    `; } function bindIdeaCardEvents(container) { container.querySelectorAll(".idea-card").forEach((cardEl) => { const id = Number(cardEl.dataset.ideaId); cardEl.addEventListener("click", (e) => { if (e.target.closest("button")) return; openIdeaDrawer(id, "details"); }); cardEl.addEventListener("dragstart", (e) => { e.dataTransfer.setData("text/plain", String(id)); }); }); container.querySelectorAll(".btn-card-clip").forEach((btn) => { btn.addEventListener("click", (e) => { e.stopPropagation(); openExternalClipModal({ ideaId: Number(btn.dataset.ideaId) }); }); }); container.querySelectorAll(".btn-card-mockup").forEach((btn) => { btn.addEventListener("click", (e) => { e.stopPropagation(); openIdeaDrawer(Number(btn.dataset.ideaId), "mockup"); }); }); container.querySelectorAll(".btn-card-whack").forEach((btn) => { btn.addEventListener("click", (e) => { e.stopPropagation(); openIdeaDrawer(Number(btn.dataset.ideaId), "whack"); }); }); container.querySelectorAll(".btn-card-advance").forEach((btn) => { btn.addEventListener("click", async (e) => { e.stopPropagation(); const id = Number(btn.dataset.ideaId); const next = btn.dataset.next; await apiFetch(`/api/ideas/${id}`, { method: "PATCH", body: JSON.stringify({ status: next }) }); showToast(`Moved idea to ${next.toUpperCase()}`); await loadIdeas(); }); }); } // --- RENDER EXPLORER GRID --- function renderExplorerGrid() { const container = document.getElementById("explorer-grid"); if (!container) return; let items = state.ideas; if (state.gridStatusFilter !== "all") { items = items.filter((i) => i.status === state.gridStatusFilter); } if (items.length === 0) { container.innerHTML = `
    No matching ideas found. Capture a spark above!
    `; return; } container.innerHTML = items.map((idea) => renderIdeaCardHtml(idea)).join(""); bindIdeaCardEvents(container); } // --- CREATIVE WHACK PACK STUDIO --- function populateColliderIdeaSelect() { const sel = document.getElementById("collider-idea-select"); if (!sel) return; const active = state.ideas.filter((i) => i.status !== "archived"); const currentVal = sel.value; sel.innerHTML = `` + active.map((i) => ``).join(""); if (currentVal && [...sel.options].some((o) => o.value === currentVal)) { sel.value = currentVal; } } async function dealIdeaCollider({ forceRandomIdea = false } = {}) { const suit = document.getElementById("collider-suit-select")?.value || ""; const selectedIdeaId = forceRandomIdea ? "" : document.getElementById("collider-idea-select")?.value || ""; const params = new URLSearchParams({ mode: "collider" }); if (suit) params.set("suit", suit); if (selectedIdeaId) params.set("ideaId", selectedIdeaId); const data = await apiFetch(`/api/whack-cards/draw?${params.toString()}`); state.colliderCard = data.card; state.colliderIdea = data.idea; renderColliderStage(); } function renderColliderStage() { const stage = document.getElementById("collider-stage"); if (!stage || !state.colliderCard) return; const card = state.colliderCard; const idea = state.colliderIdea; stage.innerHTML = `
    ${card.suitLabel} • Card #${card.number}
    ${escapeHtml(card.title)}

    ${escapeHtml(card.title)}

    ${escapeHtml(card.roleDescription)}

    COLLIDING WITH YOUR IDEA: ${ idea ? `

    ${escapeHtml(idea.title)} Stage: ${idea.status.toUpperCase()}

    ${escapeHtml(idea.elevator_pitch || "No elevator pitch yet.")}

    ` : `

    Freeform Brainstorm (No active idea selected)

    ` }
    GUIDING WHACK QUESTION (CARD #${card.number}):
    ${escapeHtml(card.promptQuestion)}
    ${ idea ? ` ` : "" }
    `; document.getElementById("img-collider-card")?.addEventListener("click", () => openCardLightbox(card)); document.getElementById("btn-zoom-collider-card")?.addEventListener("click", () => openCardLightbox(card)); document.getElementById("btn-save-collider-whack")?.addEventListener("click", async () => { const reflection = document.getElementById("collider-reflection-input")?.value.trim(); if (!reflection) { showToast("Write a quick reflection or breakthrough first!"); return; } const addChecklistItem = document.getElementById("collider-add-checklist")?.checked; await apiFetch(`/api/ideas/${idea.id}/whack`, { method: "POST", body: JSON.stringify({ cardNumber: card.number, reflection, addChecklistItem }) }); showToast(`Logged Card #${card.number} breakthrough onto "${idea.title}"!`); document.getElementById("collider-reflection-input").value = ""; await Promise.all([loadIdeas(), loadWhackCatalog()]); }); document.getElementById("btn-collider-to-mockup")?.addEventListener("click", async () => { const reflection = document.getElementById("collider-reflection-input")?.value.trim(); await openIdeaDrawer(idea.id, "mockup"); const twistSel = document.getElementById("nb-whack-twist-select"); if (twistSel) twistSel.value = String(card.number); const promptEl = document.getElementById("nb-prompt-input"); if (promptEl) { promptEl.value = reflection ? `${idea.title} — ${reflection} (Inspired by Card #${card.number}: ${card.title})` : `${idea.title} — reimagined using "${card.title}": ${card.promptQuestion}`; } }); document.getElementById("btn-spawn-from-collider")?.addEventListener("click", async () => { const reflection = document.getElementById("collider-reflection-input")?.value.trim(); const title = reflection ? reflection.split("\n")[0].slice(0, 90) : `Spark from Card #${card.number}: ${card.title}`; const created = await apiFetch("/api/ideas", { method: "POST", body: JSON.stringify({ title, elevator_pitch: `Inspired by Creative Whack Pack Card #${card.number} (${card.title}): ${card.promptQuestion}`, inspired_by_card: card.number, tags: ["whack-spark", card.suit] }) }); showToast(`Created new Spark: "${created.idea.title}"`); await Promise.all([loadIdeas(), loadWhackCatalog()]); openIdeaDrawer(created.idea.id, "details"); }); } async function dealFourRoleSpread() { const data = await apiFetch("/api/whack-cards/draw?mode=spread"); state.spreadCards = data.cards || []; const grid = document.getElementById("spread-grid"); if (!grid) return; grid.innerHTML = state.spreadCards .map( (card) => `
    ${card.suitLabel} (#${card.number})
    ${escapeHtml(card.title)}

    ${escapeHtml(card.title)}

    ${escapeHtml(card.promptQuestion)}

    ` ) .join(""); grid.querySelectorAll("img[data-card-num]").forEach((img) => { img.addEventListener("click", () => { const c = state.whackCards.find((x) => x.number === Number(img.dataset.cardNum)); if (c) openCardLightbox(c); }); }); grid.querySelectorAll(".btn-spread-collide").forEach((btn) => { btn.addEventListener("click", () => { const c = state.whackCards.find((x) => x.number === Number(btn.dataset.cardNum)); if (!c) return; state.colliderCard = c; if (!state.colliderIdea && state.ideas.length > 0) { state.colliderIdea = state.ideas[0]; } switchWhackSubmode("collider"); renderColliderStage(); }); }); grid.querySelectorAll(".btn-spread-spawn").forEach((btn) => { btn.addEventListener("click", () => { const c = state.whackCards.find((x) => x.number === Number(btn.dataset.cardNum)); if (c) showQuickWhackBanner(c); }); }); } function renderDeckBrowser() { const grid = document.getElementById("deck-cards-grid"); if (!grid) return; let list = state.whackCards; if (state.deckSuitFilter !== "all") { list = list.filter((c) => c.suit === state.deckSuitFilter); } if (state.deckSearch.trim()) { const q = state.deckSearch.trim().toLowerCase(); list = list.filter( (c) => c.title.toLowerCase().includes(q) || c.promptQuestion.toLowerCase().includes(q) || String(c.number) === q ); } grid.innerHTML = list .map( (card) => `
    #${card.number} ${card.suitLabel} ${card.usageCount > 0 ? `Used ${card.usageCount}x` : ""}
    ${escapeHtml(card.title)} ${escapeHtml(card.title)}

    ${escapeHtml(card.promptQuestion)}

    ` ) .join(""); grid.querySelectorAll("img[data-card-num]").forEach((img) => { img.addEventListener("click", () => { const c = state.whackCards.find((x) => x.number === Number(img.dataset.cardNum)); if (c) openCardLightbox(c); }); }); grid.querySelectorAll(".btn-deck-collide").forEach((btn) => { btn.addEventListener("click", () => { const c = state.whackCards.find((x) => x.number === Number(btn.dataset.cardNum)); if (!c) return; state.colliderCard = c; if (!state.colliderIdea && state.ideas.length > 0) { state.colliderIdea = state.ideas[0]; } switchWhackSubmode("collider"); renderColliderStage(); }); }); grid.querySelectorAll(".btn-deck-spark").forEach((btn) => { btn.addEventListener("click", () => { const c = state.whackCards.find((x) => x.number === Number(btn.dataset.cardNum)); if (c) showQuickWhackBanner(c); }); }); } // --- REVIEW & HEAD-TO-HEAD ARENA --- async function loadReviewSession() { const data = await apiFetch("/api/review/next"); state.triageQueue = data.triageQueue || []; state.triageIndex = 0; state.arenaPair = data.arenaPair || []; renderArenaStage(); renderTriageStage(); } function renderArenaStage() { const container = document.getElementById("arena-stage"); if (!container) return; if (state.arenaPair.length < 2) { container.innerHTML = `
    Need at least 2 active ideas for Head-to-Head Arena. Capture more sparks above!
    `; return; } const [left, right] = state.arenaPair; container.innerHTML = ` ${renderArenaFighterHtml(left, right.id, "left")}
    VS
    ${renderArenaFighterHtml(right, left.id, "right")} `; container.querySelectorAll(".btn-arena-win").forEach((btn) => { btn.addEventListener("click", async () => { const winnerId = Number(btn.dataset.winner); const loserId = Number(btn.dataset.loser); const res = await apiFetch("/api/review/arena", { method: "POST", body: JSON.stringify({ winnerId, loserId }) }); showToast(`🏆 "${res.winner.title}" wins the round! (Score: ${res.winner.composite_score})`); await Promise.all([loadIdeas(), loadReviewSession()]); }); }); container.querySelectorAll(".btn-arena-open").forEach((btn) => { btn.addEventListener("click", () => openIdeaDrawer(Number(btn.dataset.id), "details")); }); } function renderArenaFighterHtml(idea, opponentId) { return `
    ${CATEGORY_LABELS[idea.category] || idea.category} ⚡ Score ${idea.composite_score} • 🏆 ${idea.arena_wins}W
    ${ idea.cover_mockup_url ? `${escapeHtml(idea.title)}` : "" }

    ${escapeHtml(idea.title)}

    ${escapeHtml(idea.elevator_pitch || idea.notes_markdown.slice(0, 140) || "No description yet.")}

    ${idea.tags.map((t) => `#${escapeHtml(t)}`).join("")}
    `; } function renderTriageStage() { const container = document.getElementById("triage-stage"); if (!container) return; const item = state.triageQueue[state.triageIndex]; if (!item) { container.innerHTML = `

    🎉 All Caught Up!

    You've reviewed every active idea in the triage queue.

    `; return; } const nextStage = NEXT_STAGE_MAP[item.status]; container.innerHTML = `
    Stage: ${item.status.toUpperCase()} • ${CATEGORY_LABELS[item.category] || item.category} Card ${state.triageIndex + 1} of ${state.triageQueue.length}
    ${ item.cover_mockup_url ? `${escapeHtml(item.title)}` : "" }

    ${escapeHtml(item.title)}

    ${escapeHtml(item.elevator_pitch || "No elevator pitch yet — open Workbench or deal a Whack card to flesh it out.")}

    `; document.getElementById("btn-triage-promote")?.addEventListener("click", async () => { await apiFetch(`/api/ideas/${item.id}`, { method: "PATCH", body: JSON.stringify({ status: nextStage, last_reviewed_at: new Date().toISOString() }) }); showToast(`Promoted "${item.title}" to ${nextStage.toUpperCase()}`); state.triageIndex++; await loadIdeas(); renderTriageStage(); }); document.getElementById("btn-triage-mockup")?.addEventListener("click", () => { openIdeaDrawer(item.id, "mockup"); }); document.getElementById("btn-triage-whack")?.addEventListener("click", () => { openIdeaDrawer(item.id, "whack"); }); document.getElementById("btn-triage-keep")?.addEventListener("click", async () => { await apiFetch(`/api/ideas/${item.id}`, { method: "PATCH", body: JSON.stringify({ last_reviewed_at: new Date().toISOString() }) }); state.triageIndex++; renderTriageStage(); }); document.getElementById("btn-triage-archive")?.addEventListener("click", async () => { await apiFetch(`/api/ideas/${item.id}`, { method: "PATCH", body: JSON.stringify({ status: "archived", last_reviewed_at: new Date().toISOString() }) }); showToast(`Archived "${item.title}"`); state.triageIndex++; await loadIdeas(); renderTriageStage(); }); } // --- SLIDE-OVER RICH WORKBENCH & IMAGE MOCKUP STUDIO --- function detectDefaultProductTemplate(idea) { if (!idea) return "app-ui"; const textBlob = [ idea.title || "", idea.elevator_pitch || "", ...(idea.tags || []) ].join(" "); if (/\b(left[- ]chest|pocket\s+tee|pocket\s+emblem)\b/i.test(textBlob)) { return "tshirt-left-chest"; } if (/\b(t-?shirt|tee\b|shirt|apparel|crewneck|garment|hoodie|merch|wearable)\b/i.test(textBlob)) { return "tshirt-front"; } const latestMockupTemplate = idea.mockups?.[0]?.template; if (latestMockupTemplate === "tshirt-front" || latestMockupTemplate === "tshirt-left-chest") { return latestMockupTemplate; } if (/\b(sticker|decal|patch|badge)\b/i.test(textBlob) || latestMockupTemplate === "sticker") { return "sticker"; } if (/\b(poster|print|wall\s*art|framed)\b/i.test(textBlob) || latestMockupTemplate === "poster") { return "poster"; } if (idea.category === "creative") { return "isolated-graphic"; } return "app-ui"; } function isCurrentProductTShirt() { const templateSel = document.getElementById("nb-template-select"); const templateVal = templateSel ? templateSel.value : detectDefaultProductTemplate(state.activeIdea); return templateVal === "tshirt-front" || templateVal === "tshirt-left-chest"; } function syncTShirtGeneratorVisibility() { const isTShirt = isCurrentProductTShirt(); const tshirtCard = document.getElementById("tshirt-compositor-card"); const garmentColorGroup = document.getElementById("nb-garment-color-group"); if (tshirtCard) { tshirtCard.classList.toggle("hidden", !isTShirt); } if (garmentColorGroup) { garmentColorGroup.classList.toggle("hidden", !isTShirt); } if (isTShirt) { const surfaceSel = document.getElementById("comp-surface-select"); if (surfaceSel) surfaceSel.value = "tshirt"; const templateVal = document.getElementById("nb-template-select")?.value; const placementSel = document.getElementById("comp-placement-select"); if (placementSel && templateVal === "tshirt-left-chest") { placementSel.value = "left-chest"; } else if (placementSel && templateVal === "tshirt-front" && placementSel.value === "left-chest") { placementSel.value = "center-chest"; } renderCompositorCanvas(); } } async function openIdeaDrawer(id, initialTab = "details") { const data = await apiFetch(`/api/ideas/${id}`); state.activeIdeaId = id; state.activeIdea = data.idea; state.nbEditParentMockup = null; const templateSel = document.getElementById("nb-template-select"); if (templateSel) { templateSel.value = detectDefaultProductTemplate(state.activeIdea); } syncTShirtGeneratorVisibility(); document.getElementById("drawer-backdrop").classList.remove("hidden"); document.getElementById("idea-drawer").classList.remove("hidden"); switchDrawerTab(initialTab); renderDrawerContent(); // Pre-populate compositor text from idea title const topInput = document.getElementById("comp-top-text"); const bottomInput = document.getElementById("comp-bottom-text"); if (topInput) { topInput.value = state.activeIdea.title.slice(0, 26).toUpperCase(); } if (bottomInput) { bottomInput.value = state.activeIdea.tags[0] ? `#${state.activeIdea.tags[0].toUpperCase()}` : "SPARKVAULT STUDIO"; } if (isCurrentProductTShirt()) { renderCompositorCanvas(); } if (initialTab === "whack" && !state.drawerWhackCard) { await drawCardForActiveIdea(); } } function closeIdeaDrawer() { state.activeIdeaId = null; state.activeIdea = null; document.getElementById("drawer-name-suggestions-panel")?.classList.add("hidden"); document.getElementById("drawer-backdrop").classList.add("hidden"); document.getElementById("idea-drawer").classList.add("hidden"); } async function requestNameSuggestionsForActiveIdea() { if (!state.activeIdeaId || !state.activeIdea) return; const panel = document.getElementById("drawer-name-suggestions-panel"); const btn = document.getElementById("btn-suggest-names"); if (!panel) return; panel.classList.remove("hidden"); panel.innerHTML = `
    ✨ Brainstorming 6 creative names & taglines for "${escapeHtml(state.activeIdea.title)}"...
    `; if (btn) btn.disabled = true; try { const res = await apiFetch(`/api/ideas/${state.activeIdeaId}/suggest-names`, { method: "POST" }); const list = res.suggestions || []; panel.innerHTML = `
    ✨ Suggested Names & Taglines (${escapeHtml(res.generatorUsed || "AI")})
    ${list .map( (s, idx) => `
    ${escapeHtml(s.name)} ${escapeHtml(s.style || "Name")}
    ${s.tagline ? `${escapeHtml(s.tagline)}` : ""}
    ${ isCurrentProductTShirt() ? `` : "" }
    ` ) .join("")}
    `; panel.querySelector("#btn-resuggest-names")?.addEventListener("click", requestNameSuggestionsForActiveIdea); panel.querySelector("#btn-close-name-suggestions")?.addEventListener("click", () => { panel.classList.add("hidden"); }); panel.querySelectorAll(".btn-use-suggested-title").forEach((b) => { b.addEventListener("click", async () => { const newTitle = b.dataset.name; const tagline = b.dataset.tagline; const patch = { title: newTitle }; if (tagline && !state.activeIdea.elevator_pitch) { patch.elevator_pitch = tagline; } document.getElementById("drawer-title-input").value = newTitle; await saveActiveIdeaPatch(patch); showToast(`✨ Renamed Spark to "${newTitle}"!`); }); }); panel.querySelectorAll(".btn-use-suggested-tee").forEach((b) => { b.addEventListener("click", () => { const topEl = document.getElementById("comp-top-text"); const botEl = document.getElementById("comp-bottom-text"); if (topEl) topEl.value = b.dataset.name.toUpperCase(); if (botEl && b.dataset.tagline) botEl.value = b.dataset.tagline.toUpperCase(); switchDrawerTab("mockup"); renderCompositorCanvas(); showToast("👕 Loaded name & tagline onto the T-Shirt Compositor!"); }); }); } catch (err) { panel.innerHTML = `
    ⚠️ ${escapeHtml(err.message)}
    `; } finally { if (btn) btn.disabled = false; } } function switchDrawerTab(tabName) { state.drawerTab = tabName; document.querySelectorAll(".drawer-tab").forEach((btn) => { btn.classList.toggle("active", btn.dataset.drawerTab === tabName); }); ["details", "mockup", "whack"].forEach((t) => { const panel = document.getElementById(`drawer-tab-${t}`); if (panel) panel.classList.toggle("hidden", t !== tabName); }); if (tabName === "mockup") { renderCompositorCanvas(); } } function renderDrawerContent() { const idea = state.activeIdea; if (!idea) return; document.getElementById("drawer-title-input").value = idea.title; document.getElementById("drawer-status-select").value = idea.status; document.getElementById("drawer-category-select").value = idea.category; document.getElementById("drawer-pin-btn").textContent = idea.pinned ? "📌 Pinned" : "📌 Pin"; document.getElementById("drawer-score-badge").textContent = `⚡ Score: ${idea.composite_score}`; document.getElementById("drawer-pitch-input").value = idea.elevator_pitch || ""; document.getElementById("drawer-notes-input").value = idea.notes_markdown || ""; document.getElementById("drawer-notes-preview").innerHTML = renderSimpleMarkdown(idea.notes_markdown); // Sliders document.getElementById("slider-excitement").value = idea.rating_excitement; document.getElementById("val-excitement").textContent = idea.rating_excitement; document.getElementById("slider-impact").value = idea.rating_impact; document.getElementById("val-impact").textContent = idea.rating_impact; document.getElementById("slider-ease").value = idea.rating_ease; document.getElementById("val-ease").textContent = idea.rating_ease; // Counts document.getElementById("drawer-mockup-count").textContent = idea.mockups.length; document.getElementById("drawer-whack-count").textContent = idea.whack_history.length; // Tags const tagsEl = document.getElementById("drawer-tags-list"); tagsEl.innerHTML = idea.tags .map( (t) => `#${escapeHtml(t)} ` ) .join(""); tagsEl.querySelectorAll("button[data-remove-tag]").forEach((btn) => { btn.addEventListener("click", async () => { const nextTags = idea.tags.filter((x) => x !== btn.dataset.removeTag); await saveActiveIdeaPatch({ tags: nextTags }); }); }); // Checklist const chkContainer = document.getElementById("drawer-checklist-items"); const doneCount = idea.checklist.filter((c) => c.done).length; document.getElementById("drawer-checklist-progress").textContent = `${doneCount}/${idea.checklist.length} completed`; chkContainer.innerHTML = idea.checklist .map( (item) => `
    ` ) .join(""); chkContainer.querySelectorAll("input[data-chk-id]").forEach((cb) => { cb.addEventListener("change", async () => { const next = idea.checklist.map((c) => (c.id === cb.dataset.chkId ? { ...c, done: cb.checked } : c)); await saveActiveIdeaPatch({ checklist: next }); }); }); chkContainer.querySelectorAll("button[data-del-chk]").forEach((btn) => { btn.addEventListener("click", async () => { const next = idea.checklist.filter((c) => c.id !== btn.dataset.delChk); await saveActiveIdeaPatch({ checklist: next }); }); }); // Pros & Cons renderBulletList("drawer-pros-list", idea.pros, "pros"); renderBulletList("drawer-cons-list", idea.cons, "cons"); // Links const linksEl = document.getElementById("drawer-links-list"); linksEl.innerHTML = idea.links .map( (l) => `
    ${escapeHtml(l.title || l.url)}
    ` ) .join(""); linksEl.querySelectorAll("button[data-del-link]").forEach((btn) => { btn.addEventListener("click", async () => { const next = idea.links.filter((l) => l.id !== btn.dataset.delLink); await saveActiveIdeaPatch({ links: next }); }); }); // Main Spark Thumbnail on Details Tab renderDrawerMainThumbnail(idea); // Mockup Gallery renderDrawerMockupGallery(idea); // External Attachments renderDrawerAttachments(idea); // Whack History renderDrawerWhackHistory(idea); } function renderDrawerMainThumbnail(idea) { const box = document.getElementById("drawer-main-thumb-box"); if (!box || !idea) return; const coverUrl = idea.cover_mockup_url || idea.mockups?.[0]?.imageUrl || ""; const mockups = idea.mockups || []; if (coverUrl) { box.innerHTML = `
    ${escapeHtml(idea.title)} ${ mockups.length > 1 ? `
    ${mockups .slice(0, 5) .map( (m) => ` ${escapeHtml(m.title)} ` ) .join("")}
    ` : "" }
    `; box.querySelector("#spark-main-cover-img")?.addEventListener("click", () => { openImageLightbox(coverUrl, idea.title); }); box.querySelector("#btn-main-zoom-cover")?.addEventListener("click", () => { openImageLightbox(coverUrl, idea.title); }); box.querySelector("#btn-main-open-studio")?.addEventListener("click", () => { switchDrawerTab("mockup"); }); box.querySelectorAll(".spark-mini-thumb").forEach((thumb) => { thumb.addEventListener("click", async () => { await apiFetch(`/api/ideas/${idea.id}/mockups/${thumb.dataset.mkId}/cover`, { method: "PATCH" }); await loadIdeas(); }); }); } else { box.innerHTML = `
    🖼️
    No mockup image yet
    `; box.querySelector("#btn-main-generate-first-mockup")?.addEventListener("click", () => { switchDrawerTab("mockup"); }); } } function renderDrawerAttachments(idea) { const countEl = document.getElementById("drawer-attachments-count"); const listEl = document.getElementById("drawer-attachments-list"); const attachments = idea.attachments || []; if (countEl) countEl.textContent = attachments.length; if (!listEl) return; if (attachments.length === 0) { listEl.innerHTML = `

    No external notes or reference images attached yet. Drop a file, paste a screenshot (Ctrl+V), or enter an external URL above.

    `; return; } listEl.innerHTML = attachments .map((att) => { const isCover = att.imageUrl && idea.cover_mockup_url === att.imageUrl; return `
    ${att.type === "image" ? "🖼️" : "📝"} ${escapeHtml(att.title)} ${new Date(att.createdAt).toLocaleDateString()}
    ${ att.imageUrl ? `${escapeHtml(att.title)}` : "" } ${ att.textContent ? `

    ${escapeHtml(att.textContent)}

    ` : "" } ${ att.sourceUrl ? `🔗 ${escapeHtml(att.sourceUrl)}` : "" }
    `; }) .join(""); listEl.querySelectorAll("img[data-zoom-url]").forEach((img) => { img.addEventListener("click", () => openImageLightbox(img.dataset.zoomUrl, img.alt)); }); listEl.querySelectorAll(".btn-att-to-mockup").forEach((btn) => { btn.addEventListener("click", async () => { const res = await apiFetch(`/api/ideas/${idea.id}/attachments/${btn.dataset.attId}/promote-mockup`, { method: "POST" }); state.activeIdea = res.idea; await loadIdeas(); switchDrawerTab("mockup"); const promoted = state.activeIdea.mockups[0]; if (promoted) { state.nbEditParentMockup = promoted; document.getElementById("nb-edit-mode-box").classList.remove("hidden"); document.getElementById("nb-edit-target-label").textContent = promoted.title; } showToast("Promoted reference image to Mockup Studio for AI refinement!"); }); }); listEl.querySelectorAll(".btn-att-to-canvas").forEach((btn) => { btn.addEventListener("click", () => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { state.uploadedOverlayImage = img; switchDrawerTab("mockup"); renderCompositorCanvas(); showToast("Placed attached image onto the Interactive T-Shirt Canvas!"); }; img.src = btn.dataset.attUrl; }); }); listEl.querySelectorAll(".btn-att-cover").forEach((btn) => { btn.addEventListener("click", async () => { await apiFetch(`/api/ideas/${idea.id}/attachments/${btn.dataset.attId}/cover`, { method: "PATCH" }); showToast("Set attached image as Spark cover!"); await loadIdeas(); }); }); listEl.querySelectorAll(".btn-att-delete").forEach((btn) => { btn.addEventListener("click", async () => { await apiFetch(`/api/ideas/${idea.id}/attachments/${btn.dataset.attId}`, { method: "DELETE" }); showToast("Removed attachment"); await loadIdeas(); }); }); } function renderBulletList(elementId, items, fieldName) { const ul = document.getElementById(elementId); if (!ul) return; ul.innerHTML = items .map( (txt, idx) => `
  • ${escapeHtml(txt)}
  • ` ) .join(""); ul.querySelectorAll("button[data-del-idx]").forEach((btn) => { btn.addEventListener("click", async () => { const idx = Number(btn.dataset.delIdx); const next = items.filter((_, i) => i !== idx); await saveActiveIdeaPatch({ [fieldName]: next }); }); }); } async function saveActiveIdeaPatch(patch) { if (!state.activeIdeaId) return; const res = await apiFetch(`/api/ideas/${state.activeIdeaId}`, { method: "PATCH", body: JSON.stringify(patch) }); state.activeIdea = res.idea; await loadIdeas(); } // --- NANO BANANA MOCKUP GALLERY & ITERATIVE EDITING --- function renderDrawerMockupGallery(idea) { const container = document.getElementById("drawer-mockup-gallery"); if (!container) return; if (!idea.mockups || idea.mockups.length === 0) { container.innerHTML = `

    No mockups saved for this idea yet. Generate one in the Image Mockup Studio!

    `; return; } container.innerHTML = idea.mockups .map((m) => { const isCover = idea.cover_mockup_url === m.imageUrl; return ` `; }) .join(""); container.querySelectorAll("img[data-zoom-url]").forEach((img) => { img.addEventListener("click", () => { openImageLightbox(img.dataset.zoomUrl, img.alt); }); }); container.querySelectorAll(".btn-mk-refine").forEach((btn) => { btn.addEventListener("click", () => { const mk = idea.mockups.find((x) => x.id === btn.dataset.mkId); if (!mk) return; state.nbEditParentMockup = mk; document.getElementById("nb-edit-mode-box").classList.remove("hidden"); document.getElementById("nb-edit-target-label").textContent = mk.title; document.getElementById("nb-edit-instruction-input").focus(); showToast(`Iterative Edit Mode enabled for "${mk.title}"`); }); }); container.querySelectorAll(".btn-mk-load-canvas").forEach((btn) => { btn.addEventListener("click", () => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { state.uploadedOverlayImage = img; renderCompositorCanvas(); showToast("Loaded artwork onto the Interactive T-Shirt Canvas!"); }; img.src = btn.dataset.mkUrl; }); }); container.querySelectorAll(".btn-mk-set-cover").forEach((btn) => { btn.addEventListener("click", async () => { await apiFetch(`/api/ideas/${idea.id}/mockups/${btn.dataset.mkId}/cover`, { method: "PATCH" }); showToast("Updated card cover thumbnail!"); await loadIdeas(); }); }); container.querySelectorAll(".btn-mk-delete").forEach((btn) => { btn.addEventListener("click", async () => { await apiFetch(`/api/ideas/${idea.id}/mockups/${btn.dataset.mkId}`, { method: "DELETE" }); showToast("Deleted mockup variation"); await loadIdeas(); }); }); } // --- INTERACTIVE HTML5 CANVAS T-SHIRT & PRODUCT COMPOSITOR --- function renderCompositorCanvas() { const canvas = document.getElementById("mockup-compositor-canvas"); if (!canvas) return; const ctx = canvas.getContext("2d"); const W = canvas.width; const H = canvas.height; const surface = document.getElementById("comp-surface-select")?.value || "tshirt"; const placement = document.getElementById("comp-placement-select")?.value || "center-chest"; const emblem = document.getElementById("comp-emblem-select")?.value || "mountains"; const topText = (document.getElementById("comp-top-text")?.value || "HIGH ALTITUDE CLUB").toUpperCase(); const bottomText = (document.getElementById("comp-bottom-text")?.value || "EST. 1984 • COLORADO").toUpperCase(); const inkColor = document.getElementById("comp-ink-color")?.value || "#BC4921"; const garmentHex = state.nbGarmentColor || "#F5E8D3"; // Studio background const bgGrad = ctx.createRadialGradient(W / 2, H / 2, 60, W / 2, H / 2, W * 0.65); bgGrad.addColorStop(0, "#232938"); bgGrad.addColorStop(1, "#11141c"); ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, W, H); if (surface === "tshirt") { drawTShirtSilhouette(ctx, W, H, garmentHex); } else if (surface === "sticker") { drawStickerSurface(ctx, W, H, garmentHex); } else { drawPosterSurface(ctx, W, H, garmentHex); } // Determine artwork bounding zone based on placement let artX = W / 2; let artY = H * 0.44; let artScale = 1.0; if (surface === "tshirt") { if (placement === "left-chest") { artX = W * 0.61; // Viewer's right = wearer's left chest pocket zone artY = H * 0.36; artScale = 0.46; } else if (placement === "oversized") { artX = W / 2; artY = H * 0.46; artScale = 1.22; } } ctx.save(); ctx.translate(artX, artY); ctx.scale(artScale, artScale); // Draw optional uploaded or selected image if (state.uploadedOverlayImage) { const img = state.uploadedOverlayImage; const maxDim = 210; const ratio = Math.min(maxDim / img.width, maxDim / img.height); const dw = img.width * ratio; const dh = img.height * ratio; ctx.drawImage(img, -dw / 2, -dh / 2 - 10, dw, dh); } else if (emblem !== "none") { drawVectorEmblem(ctx, emblem, inkColor); } // Draw Screenprint Typography ctx.fillStyle = inkColor; ctx.textAlign = "center"; if (topText) { ctx.font = "900 26px Georgia, serif"; ctx.fillText(topText, 0, -96); } if (bottomText) { ctx.font = "700 16px 'Courier New', monospace"; ctx.fillText(bottomText, 0, 108); } // Decorative vintage rule lines ctx.strokeStyle = inkColor; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(-110, 82); ctx.lineTo(110, 82); ctx.stroke(); ctx.restore(); } function drawTShirtSilhouette(ctx, W, H, garmentHex) { ctx.save(); ctx.shadowColor = "rgba(0, 0, 0, 0.55)"; ctx.shadowBlur = 32; ctx.shadowOffsetY = 14; // Classic Crewneck Tee Vector Path ctx.beginPath(); ctx.moveTo(225, 95); // Left shoulder neck ctx.quadraticCurveTo(340, 132, 455, 95); // Collar front curve ctx.lineTo(545, 130); // Right shoulder ctx.lineTo(625, 255); // Right outer sleeve ctx.lineTo(545, 305); // Right sleeve hem ctx.lineTo(500, 235); // Right armpit ctx.lineTo(500, 595); // Right torso hem ctx.lineTo(180, 595); // Bottom hem ctx.lineTo(180, 235); // Left armpit ctx.lineTo(135, 305); // Left sleeve hem ctx.lineTo(55, 255); // Left outer sleeve ctx.lineTo(135, 130); // Left shoulder ctx.closePath(); ctx.fillStyle = garmentHex; ctx.fill(); // Collar rib & subtle fabric shading ctx.shadowColor = "transparent"; ctx.strokeStyle = "rgba(0,0,0,0.18)"; ctx.lineWidth = 3; ctx.stroke(); // Back neck interior shadow ctx.beginPath(); ctx.moveTo(228, 96); ctx.quadraticCurveTo(340, 114, 452, 96); ctx.quadraticCurveTo(340, 136, 228, 96); ctx.fillStyle = "rgba(0,0,0,0.14)"; ctx.fill(); // Ribbed collar seam ctx.beginPath(); ctx.moveTo(215, 102); ctx.quadraticCurveTo(340, 148, 465, 102); ctx.strokeStyle = "rgba(0,0,0,0.15)"; ctx.lineWidth = 4; ctx.stroke(); ctx.restore(); } function drawStickerSurface(ctx, W, H, fillHex) { ctx.save(); ctx.shadowColor = "rgba(0,0,0,0.6)"; ctx.shadowBlur = 28; // White die-cut vinyl border ctx.beginPath(); ctx.arc(W / 2, H * 0.45, 215, 0, Math.PI * 2); ctx.fillStyle = "#ffffff"; ctx.fill(); ctx.shadowColor = "transparent"; ctx.beginPath(); ctx.arc(W / 2, H * 0.45, 198, 0, Math.PI * 2); ctx.fillStyle = fillHex; ctx.fill(); ctx.lineWidth = 4; ctx.strokeStyle = "#222"; ctx.stroke(); ctx.restore(); } function drawPosterSurface(ctx, W, H, fillHex) { ctx.save(); ctx.shadowColor = "rgba(0,0,0,0.6)"; ctx.shadowBlur = 30; ctx.fillStyle = "#1c1917"; ctx.fillRect(145, 65, 390, 540); ctx.shadowColor = "transparent"; ctx.fillStyle = fillHex; ctx.fillRect(165, 85, 350, 500); ctx.restore(); } function drawVectorEmblem(ctx, emblem, inkColor) { ctx.save(); ctx.strokeStyle = inkColor; ctx.fillStyle = inkColor; ctx.lineWidth = 4; // Outer badge ring ctx.beginPath(); ctx.arc(0, -8, 72, 0, Math.PI * 2); ctx.stroke(); if (emblem === "mountains") { // Sun ctx.beginPath(); ctx.arc(22, -36, 14, 0, Math.PI * 2); ctx.fillStyle = "#EFA51C"; ctx.fill(); // Peaks ctx.fillStyle = inkColor; ctx.beginPath(); ctx.moveTo(-56, 36); ctx.lineTo(-12, -32); ctx.lineTo(18, 12); ctx.lineTo(36, -14); ctx.lineTo(58, 36); ctx.closePath(); ctx.fill(); } else if (emblem === "espresso") { // Cup body ctx.beginPath(); ctx.roundRect(-34, -18, 62, 44, [4, 4, 24, 24]); ctx.fill(); // Handle ctx.beginPath(); ctx.arc(30, 2, 12, -Math.PI / 2, Math.PI / 2); ctx.stroke(); // Steam waves ctx.beginPath(); ctx.moveTo(-14, -28); ctx.quadraticCurveTo(-20, -42, -10, -52); ctx.moveTo(6, -28); ctx.quadraticCurveTo(0, -42, 10, -52); ctx.stroke(); } else if (emblem === "lightbulb") { ctx.beginPath(); ctx.arc(0, -22, 28, 0, Math.PI * 2); ctx.fillStyle = "#EFA51C"; ctx.fill(); ctx.stroke(); ctx.fillStyle = inkColor; ctx.fillRect(-14, 6, 28, 16); } else if (emblem === "compass") { ctx.beginPath(); ctx.moveTo(0, -54); ctx.lineTo(14, -14); ctx.lineTo(54, 0); ctx.lineTo(14, 14); ctx.lineTo(0, 54); ctx.lineTo(-14, 14); ctx.lineTo(-54, 0); ctx.lineTo(-14, -14); ctx.closePath(); ctx.stroke(); } else if (emblem === "lightning") { ctx.beginPath(); ctx.moveTo(8, -52); ctx.lineTo(-24, -4); ctx.lineTo(4, -4); ctx.lineTo(-12, 44); ctx.lineTo(28, -12); ctx.lineTo(0, -12); ctx.closePath(); ctx.fill(); } ctx.restore(); } // --- WHACK THIS IDEA DRAWER TAB --- async function drawCardForActiveIdea() { if (!state.activeIdea) return; const suit = document.getElementById("drawer-whack-suit-select")?.value || ""; const params = new URLSearchParams(); if (suit) params.set("suit", suit); else params.set("status", state.activeIdea.status); const data = await apiFetch(`/api/whack-cards/draw?${params.toString()}`); state.drawerWhackCard = data.card; renderDrawerActiveWhackStage(); } function renderDrawerActiveWhackStage() { const container = document.getElementById("drawer-active-whack-stage"); const card = state.drawerWhackCard; const idea = state.activeIdea; if (!container || !card || !idea) return; container.innerHTML = `
    #${card.number} • ${card.suitLabel} ${escapeHtml(card.title)}

    Card #${card.number}: ${escapeHtml(card.title)}

    ${escapeHtml(card.promptQuestion)}
    `; document.getElementById("drawer-whack-card-img")?.addEventListener("click", () => openCardLightbox(card)); document.getElementById("btn-drawer-save-whack")?.addEventListener("click", async () => { const reflection = document.getElementById("drawer-whack-reflection")?.value.trim(); if (!reflection) { showToast("Enter a reflection first!"); return; } const addChecklistItem = document.getElementById("drawer-whack-add-chk")?.checked; const res = await apiFetch(`/api/ideas/${idea.id}/whack`, { method: "POST", body: JSON.stringify({ cardNumber: card.number, reflection, addChecklistItem }) }); state.activeIdea = res.idea; showToast(`Saved Card #${card.number} breakthrough!`); await Promise.all([loadIdeas(), loadWhackCatalog()]); }); document.getElementById("btn-drawer-whack-to-nb")?.addEventListener("click", () => { const reflection = document.getElementById("drawer-whack-reflection")?.value.trim(); switchDrawerTab("mockup"); const twistSel = document.getElementById("nb-whack-twist-select"); if (twistSel) twistSel.value = String(card.number); const promptEl = document.getElementById("nb-prompt-input"); if (promptEl) { promptEl.value = reflection ? `${idea.title} — ${reflection}` : `${idea.title} (${idea.elevator_pitch || ""}) with twist from "${card.title}"`; } }); } function renderDrawerWhackHistory(idea) { const listEl = document.getElementById("drawer-whack-history-list"); if (!listEl) return; if (!idea.whack_history || idea.whack_history.length === 0) { listEl.innerHTML = `

    No Whack Pack cards applied to this idea yet.

    `; return; } listEl.innerHTML = idea.whack_history .map( (w) => `
    Card #${w.cardNumber}: ${escapeHtml(w.cardTitle)} ${new Date(w.createdAt).toLocaleDateString()}

    ${escapeHtml(w.reflection)}

    ` ) .join(""); } // --- QUICK WHACK BANNER & LIGHTBOX --- function showQuickWhackBanner(card) { state.quickBannerCard = card; const banner = document.getElementById("quick-whack-banner"); banner.classList.remove("hidden"); document.getElementById("qwb-card-img").src = card.imageUrl; const badge = document.getElementById("qwb-suit-badge"); badge.textContent = `${card.suitLabel} #${card.number}`; badge.className = `suit-pill suit-${card.suit}`; document.getElementById("qwb-card-title").textContent = card.title; document.getElementById("qwb-card-question").textContent = card.promptQuestion; } function openCardLightbox(card) { const modal = document.getElementById("lightbox-modal"); modal.classList.remove("hidden"); document.getElementById("lightbox-title").textContent = `Card #${card.number}: ${card.title} (${card.suitLabel})`; document.getElementById("lightbox-img").src = card.imageUrl; document.getElementById("lightbox-meta").innerHTML = `
    ${escapeHtml(card.promptQuestion)}
    `; } function openImageLightbox(url, title = "Mockup Preview") { const modal = document.getElementById("lightbox-modal"); modal.classList.remove("hidden"); document.getElementById("lightbox-title").textContent = title; document.getElementById("lightbox-img").src = url; document.getElementById("lightbox-meta").innerHTML = ""; } function readFileAsDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = reject; reader.readAsDataURL(file); }); } function stageDrawerImage(dataUrl) { state.stagedDrawerAttachmentDataUrl = dataUrl; const row = document.getElementById("drawer-attach-preview-row"); const img = document.getElementById("drawer-attach-preview-img"); if (dataUrl) { if (img) img.src = dataUrl; row?.classList.remove("hidden"); } else { if (img) img.src = ""; row?.classList.add("hidden"); } } function stageClipModalImage(dataUrl) { state.stagedClipAttachmentDataUrl = dataUrl; const row = document.getElementById("clip-preview-row"); const img = document.getElementById("clip-preview-img"); if (dataUrl) { if (img) img.src = dataUrl; row?.classList.remove("hidden"); } else { if (img) img.src = ""; row?.classList.add("hidden"); } } const DEV_TARGETS = { antigravity: { id: "antigravity", label: "🛸 Google Antigravity", defaultModel: "gemini-3.1-pro", models: [ { id: "gemini-3.1-pro", label: "Gemini 3.1 Pro (Recommended — Full-Stack & Architecture)" }, { id: "gemini-3.0-pro", label: "Gemini 3.0 Pro (Deep Reasoning & Complex Refactors)" }, { id: "gemini-3.1-flash", label: "Gemini 3.1 Flash (Fast Iteration & UI Prototyping)" }, { id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash-Lite (Lightweight & Rapid Tasks)" }, { id: "claude-sonnet-4.5", label: "Claude 4.5 Sonnet (via Antigravity Model Selector)" }, { id: "claude-opus-4.5", label: "Claude 4.5 Opus (via Antigravity Model Selector)" } ] }, codex: { id: "codex", label: "🤖 OpenAI Codex", defaultModel: "gpt-5.6-sol", models: [ { id: "gpt-5.6-sol", label: "GPT-5.6-Sol (Recommended — Active Codex Default)" }, { id: "gpt-5.3-codex", label: "GPT-5.3-Codex (Specialized Agentic Coding)" }, { id: "gpt-5.4", label: "GPT-5.4 (Flagship Reasoning & Implementation)" }, { id: "o3", label: "OpenAI o3 (Deep Architecture & Debugging)" }, { id: "codex-mini-latest", label: "Codex Mini (Fast Lightweight Execution)" } ] } }; let activeDevTarget = "antigravity"; function populateDevModelOptions(targetApp) { const meta = DEV_TARGETS[targetApp] || DEV_TARGETS.antigravity; const sel = document.getElementById("dev-model-select"); if (!sel) return; sel.innerHTML = meta.models .map((m) => ``) .join(""); sel.value = meta.defaultModel; } async function refreshDevSpecPreview() { if (!state.activeIdeaId) return; const model = document.getElementById("dev-model-select")?.value || ""; const includeHomelab = document.getElementById("dev-include-homelab")?.checked !== false; const textarea = document.getElementById("dev-spec-preview-textarea"); if (!textarea) return; const params = new URLSearchParams({ target: activeDevTarget, model, includeHomelab: String(includeHomelab) }); try { const res = await fetch(`/api/ideas/${state.activeIdeaId}/spec.md?${params.toString()}`); if (res.ok) { textarea.value = await res.text(); } } catch (err) { console.warn("Failed to load spec preview:", err); } } async function openDevHandoffModal(defaultTarget = "antigravity") { if (!state.activeIdeaId || !state.activeIdea) return; activeDevTarget = defaultTarget; const modal = document.getElementById("dev-handoff-modal"); if (!modal) return; document.querySelectorAll("#dev-target-pills [data-dev-target]").forEach((btn) => { btn.classList.toggle("active", btn.dataset.devTarget === activeDevTarget); }); populateDevModelOptions(activeDevTarget); const serverCodexWrap = document.getElementById("dev-server-codex-wrap"); if (serverCodexWrap) { serverCodexWrap.classList.toggle("hidden", activeDevTarget !== "codex"); } const submitBtn = document.getElementById("btn-submit-dev-handoff"); if (submitBtn) { submitBtn.textContent = activeDevTarget === "codex" ? "🚀 Submit & Open in OpenAI Codex" : "🚀 Submit & Launch in Antigravity"; } const homelabCb = document.getElementById("dev-include-homelab"); if (homelabCb) { homelabCb.checked = state.activeIdea.category === "project" || !isCurrentProductTShirt(); } document.getElementById("dev-handoff-result-box")?.classList.add("hidden"); modal.classList.remove("hidden"); await refreshDevSpecPreview(); } function openExternalClipModal({ ideaId = null, prefillTitle = "", prefillText = "", prefillUrl = "" } = {}) { const modal = document.getElementById("external-clip-modal"); const targetSel = document.getElementById("clip-target-idea"); const newIdeaGroup = document.getElementById("clip-new-idea-group"); if (!modal || !targetSel) return; const activeIdeas = state.ideas.filter((i) => i.status !== "archived"); targetSel.innerHTML = activeIdeas .map((i) => ``) .join("") + ``; if (ideaId && activeIdeas.some((i) => i.id === Number(ideaId))) { targetSel.value = String(ideaId); newIdeaGroup?.classList.add("hidden"); } else if (state.activeIdeaId && activeIdeas.some((i) => i.id === Number(state.activeIdeaId))) { targetSel.value = String(state.activeIdeaId); newIdeaGroup?.classList.add("hidden"); } else if (prefillTitle || activeIdeas.length === 0) { targetSel.value = "new"; newIdeaGroup?.classList.remove("hidden"); } else { targetSel.value = String(activeIdeas[0].id); newIdeaGroup?.classList.add("hidden"); } document.getElementById("clip-new-idea-title").value = prefillTitle || ""; document.getElementById("clip-title-input").value = prefillTitle || ""; document.getElementById("clip-text-input").value = prefillText || ""; document.getElementById("clip-source-url").value = prefillUrl || ""; document.getElementById("clip-external-img-url").value = ""; stageClipModalImage(null); modal.classList.remove("hidden"); } function checkWebShareTargetQueryParams() { const params = new URLSearchParams(window.location.search); const clipTitle = params.get("title") || params.get("clipTitle") || ""; const clipText = params.get("text") || params.get("clipText") || ""; const clipUrl = params.get("url") || params.get("clipUrl") || ""; const ideaId = params.get("ideaId") ? Number(params.get("ideaId")) : null; if (clipTitle || clipText || clipUrl) { openExternalClipModal({ ideaId, prefillTitle: clipTitle, prefillText: clipText, prefillUrl: clipUrl }); window.history.replaceState({}, document.title, window.location.pathname); } } function switchWhackSubmode(mode) { state.whackSubmode = mode; document.querySelectorAll("[data-whack-mode]").forEach((btn) => { btn.classList.toggle("active", btn.dataset.whackMode === mode); }); ["collider", "spread", "browser"].forEach((m) => { document.getElementById(`whack-submode-${m}`)?.classList.toggle("hidden", m !== mode); }); } // --- GLOBAL EVENT BINDINGS --- function bindGlobalEvents() { // Keyboard shortcuts: Ctrl+K for Quick Capture, Esc to close modals/drawer window.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); document.getElementById("quick-capture-input")?.focus(); } else if (e.key === "Escape") { document.getElementById("lightbox-modal")?.classList.add("hidden"); document.getElementById("settings-modal")?.classList.add("hidden"); document.getElementById("dev-handoff-modal")?.classList.add("hidden"); closeIdeaDrawer(); } }); // Quick Capture Form document.getElementById("quick-capture-form")?.addEventListener("submit", async (e) => { e.preventDefault(); const input = document.getElementById("quick-capture-input"); const category = document.getElementById("quick-capture-category").value; const raw = input.value.trim(); if (!raw) return; const payload = { title: raw, category }; if (state.quickBannerCard) { payload.inspired_by_card = state.quickBannerCard.number; payload.elevator_pitch = `Sparked by Card #${state.quickBannerCard.number} (${state.quickBannerCard.title})`; } const res = await apiFetch("/api/ideas", { method: "POST", body: JSON.stringify(payload) }); input.value = ""; showToast(`✨ Captured "${res.idea.title}" in Spark!`); await Promise.all([loadIdeas(), loadWhackCatalog()]); }); // Quick Whack Prompt button document.getElementById("btn-quick-whack-prompt")?.addEventListener("click", async () => { const data = await apiFetch("/api/whack-cards/draw"); if (data.card) showQuickWhackBanner(data.card); }); document.getElementById("btn-qwb-redraw")?.addEventListener("click", async () => { const data = await apiFetch("/api/whack-cards/draw"); if (data.card) showQuickWhackBanner(data.card); }); document.getElementById("btn-qwb-close")?.addEventListener("click", () => { state.quickBannerCard = null; document.getElementById("quick-whack-banner").classList.add("hidden"); }); document.getElementById("btn-qwb-use")?.addEventListener("click", () => { const input = document.getElementById("quick-capture-input"); if (input && state.quickBannerCard) { input.placeholder = `Idea inspired by #${state.quickBannerCard.number} (${state.quickBannerCard.title})...`; input.focus(); } }); document.getElementById("qwb-card-img")?.addEventListener("click", () => { if (state.quickBannerCard) openCardLightbox(state.quickBannerCard); }); // Theme Toggle document.getElementById("btn-theme-toggle")?.addEventListener("click", () => { const html = document.documentElement; const next = html.getAttribute("data-theme") === "dark" ? "paper" : "dark"; html.setAttribute("data-theme", next); document.getElementById("btn-theme-toggle").textContent = next === "dark" ? "☀️ Theme" : "🌙 Theme"; }); // Export Dropdown document.getElementById("btn-export-menu")?.addEventListener("click", () => { document.getElementById("export-dropdown-menu")?.classList.toggle("hidden"); }); // Main View Tabs document.querySelectorAll(".view-tab").forEach((tab) => { tab.addEventListener("click", async () => { const view = tab.dataset.view; state.currentView = view; document.querySelectorAll(".view-tab").forEach((t) => t.classList.toggle("active", t === tab)); ["board", "grid", "whack", "review"].forEach((v) => { document.getElementById(`view-${v}`)?.classList.toggle("hidden", v !== view); }); if (view === "whack" && !state.colliderCard) { await dealIdeaCollider(); } if (view === "review") { await loadReviewSession(); } }); }); // Global Filters function syncFilterControlsUI() { const hasActiveFilter = Boolean(state.filters.search.trim()) || state.filters.category !== "all" || state.filters.tag !== "all" || Boolean(state.filters.hasMockup); document.getElementById("btn-clear-filters")?.classList.toggle("hidden", !hasActiveFilter); const mobileBtn = document.getElementById("btn-mobile-filter-toggle"); if (mobileBtn) { mobileBtn.textContent = hasActiveFilter ? "🔍 Filters • Active" : "🔍 Filters"; } } document.getElementById("btn-mobile-filter-toggle")?.addEventListener("click", () => { const bar = document.getElementById("global-filter-bar"); const isOpen = bar?.classList.toggle("mobile-filters-open"); document.getElementById("btn-mobile-filter-toggle")?.classList.toggle("btn-primary", Boolean(isOpen)); if (isOpen) { document.getElementById("filter-search")?.focus(); } }); document.getElementById("btn-clear-filters")?.addEventListener("click", () => { state.filters.search = ""; state.filters.category = "all"; state.filters.tag = "all"; state.filters.hasMockup = false; const searchEl = document.getElementById("filter-search"); const catEl = document.getElementById("filter-category"); const tagEl = document.getElementById("filter-tag"); const mkEl = document.getElementById("filter-has-mockup"); if (searchEl) searchEl.value = ""; if (catEl) catEl.value = "all"; if (tagEl) tagEl.value = "all"; if (mkEl) mkEl.checked = false; syncFilterControlsUI(); loadIdeas(); }); document.querySelectorAll("#mobile-stage-switcher .stage-tab").forEach((tab) => { tab.addEventListener("click", () => { const stage = tab.dataset.mobileStage; document.querySelectorAll("#mobile-stage-switcher .stage-tab").forEach((t) => t.classList.toggle("active", t === tab) ); document.getElementById("kanban-board")?.setAttribute("data-mobile-active-stage", stage); }); }); document.getElementById("filter-search")?.addEventListener("input", (e) => { state.filters.search = e.target.value; syncFilterControlsUI(); loadIdeas(); }); document.getElementById("filter-category")?.addEventListener("change", (e) => { state.filters.category = e.target.value; syncFilterControlsUI(); loadIdeas(); }); document.getElementById("filter-tag")?.addEventListener("change", (e) => { state.filters.tag = e.target.value; syncFilterControlsUI(); loadIdeas(); }); document.getElementById("filter-has-mockup")?.addEventListener("change", (e) => { state.filters.hasMockup = e.target.checked; syncFilterControlsUI(); loadIdeas(); }); document.getElementById("filter-sort")?.addEventListener("change", (e) => { state.filters.sort = e.target.value; loadIdeas(); }); // Kanban Drag & Drop document.querySelectorAll(".kanban-dropzone").forEach((zone) => { zone.addEventListener("dragover", (e) => { e.preventDefault(); zone.classList.add("drag-over"); }); zone.addEventListener("dragleave", () => zone.classList.remove("drag-over")); zone.addEventListener("drop", async (e) => { e.preventDefault(); zone.classList.remove("drag-over"); const ideaId = Number(e.dataTransfer.getData("text/plain")); const targetStatus = zone.dataset.status; if (!ideaId || !targetStatus) return; await apiFetch(`/api/ideas/${ideaId}`, { method: "PATCH", body: JSON.stringify({ status: targetStatus }) }); showToast(`Moved idea to ${targetStatus.toUpperCase()}`); await loadIdeas(); }); }); // Explorer Grid Status Pills document.querySelectorAll("#grid-status-pills .pill-btn").forEach((btn) => { btn.addEventListener("click", () => { state.gridStatusFilter = btn.dataset.status; document.querySelectorAll("#grid-status-pills .pill-btn").forEach((b) => b.classList.toggle("active", b === btn)); renderExplorerGrid(); }); }); document.getElementById("btn-new-idea-modal")?.addEventListener("click", async () => { const res = await apiFetch("/api/ideas", { method: "POST", body: JSON.stringify({ title: "New Concept Spark", category: "creative" }) }); await loadIdeas(); openIdeaDrawer(res.idea.id, "details"); }); // Whack Studio Sub-mode Tabs document.querySelectorAll("[data-whack-mode]").forEach((btn) => { btn.addEventListener("click", async () => { const mode = btn.dataset.whackMode; switchWhackSubmode(mode); if (mode === "spread" && state.spreadCards.length === 0) { await dealFourRoleSpread(); } }); }); document.getElementById("btn-deal-collider")?.addEventListener("click", () => dealIdeaCollider({ forceRandomIdea: false })); document.getElementById("btn-collider-random-both")?.addEventListener("click", () => dealIdeaCollider({ forceRandomIdea: true })); document.getElementById("btn-deal-spread")?.addEventListener("click", () => dealFourRoleSpread()); document.querySelectorAll("#deck-suit-filters .pill-btn").forEach((btn) => { btn.addEventListener("click", () => { state.deckSuitFilter = btn.dataset.suit; document.querySelectorAll("#deck-suit-filters .pill-btn").forEach((b) => b.classList.toggle("active", b === btn)); renderDeckBrowser(); }); }); document.getElementById("deck-search-input")?.addEventListener("input", (e) => { state.deckSearch = e.target.value; renderDeckBrowser(); }); // Review Mode Tabs document.querySelectorAll("[data-review-mode]").forEach((btn) => { btn.addEventListener("click", () => { const mode = btn.dataset.reviewMode; state.reviewMode = mode; document.querySelectorAll("[data-review-mode]").forEach((b) => b.classList.toggle("active", b === btn)); document.getElementById("review-submode-arena")?.classList.toggle("hidden", mode !== "arena"); document.getElementById("review-submode-triage")?.classList.toggle("hidden", mode !== "triage"); }); }); document.getElementById("btn-skip-arena")?.addEventListener("click", () => loadReviewSession()); // Drawer Events document.getElementById("drawer-backdrop")?.addEventListener("click", closeIdeaDrawer); document.getElementById("drawer-close-btn")?.addEventListener("click", closeIdeaDrawer); document.querySelectorAll(".drawer-tab").forEach((btn) => { btn.addEventListener("click", async () => { const tab = btn.dataset.drawerTab; switchDrawerTab(tab); if (tab === "whack" && !state.drawerWhackCard) { await drawCardForActiveIdea(); } }); }); document.getElementById("drawer-title-input")?.addEventListener("change", (e) => { saveActiveIdeaPatch({ title: e.target.value }); }); document.getElementById("btn-suggest-names")?.addEventListener("click", requestNameSuggestionsForActiveIdea); document.getElementById("btn-comp-suggest-names")?.addEventListener("click", requestNameSuggestionsForActiveIdea); document.getElementById("drawer-status-select")?.addEventListener("change", (e) => { saveActiveIdeaPatch({ status: e.target.value }); }); document.getElementById("drawer-category-select")?.addEventListener("change", (e) => { saveActiveIdeaPatch({ category: e.target.value }); }); document.getElementById("drawer-pin-btn")?.addEventListener("click", () => { if (!state.activeIdea) return; saveActiveIdeaPatch({ pinned: !state.activeIdea.pinned }); }); document.getElementById("drawer-delete-btn")?.addEventListener("click", async () => { if (!state.activeIdeaId) return; await apiFetch(`/api/ideas/${state.activeIdeaId}`, { method: "DELETE" }); showToast("Idea deleted"); closeIdeaDrawer(); await loadIdeas(); }); document.getElementById("drawer-pitch-input")?.addEventListener("change", (e) => { saveActiveIdeaPatch({ elevator_pitch: e.target.value }); }); document.getElementById("drawer-notes-input")?.addEventListener("change", (e) => { saveActiveIdeaPatch({ notes_markdown: e.target.value }); }); document.getElementById("btn-toggle-md-preview")?.addEventListener("click", () => { const textarea = document.getElementById("drawer-notes-input"); const preview = document.getElementById("drawer-notes-preview"); preview.innerHTML = renderSimpleMarkdown(textarea.value); textarea.classList.toggle("hidden"); preview.classList.toggle("hidden"); }); ["excitement", "impact", "ease"].forEach((metric) => { const slider = document.getElementById(`slider-${metric}`); slider?.addEventListener("input", (e) => { document.getElementById(`val-${metric}`).textContent = e.target.value; }); slider?.addEventListener("change", (e) => { saveActiveIdeaPatch({ [`rating_${metric}`]: Number(e.target.value) }); }); }); document.getElementById("drawer-add-tag-form")?.addEventListener("submit", async (e) => { e.preventDefault(); if (!state.activeIdea) return; const input = document.getElementById("drawer-new-tag-input"); const val = input.value.replace(/^#/, "").trim().toLowerCase(); if (!val) return; input.value = ""; const next = Array.from(new Set([...state.activeIdea.tags, val])); await saveActiveIdeaPatch({ tags: next }); }); document.getElementById("drawer-add-checklist-form")?.addEventListener("submit", async (e) => { e.preventDefault(); if (!state.activeIdea) return; const input = document.getElementById("drawer-new-checklist-input"); const text = input.value.trim(); if (!text) return; input.value = ""; const next = [...state.activeIdea.checklist, { id: `chk-${Date.now()}`, text, done: false }]; await saveActiveIdeaPatch({ checklist: next }); }); document.getElementById("drawer-add-pro-form")?.addEventListener("submit", async (e) => { e.preventDefault(); if (!state.activeIdea) return; const input = document.getElementById("drawer-new-pro-input"); const text = input.value.trim(); if (!text) return; input.value = ""; await saveActiveIdeaPatch({ pros: [...state.activeIdea.pros, text] }); }); document.getElementById("drawer-add-con-form")?.addEventListener("submit", async (e) => { e.preventDefault(); if (!state.activeIdea) return; const input = document.getElementById("drawer-new-con-input"); const text = input.value.trim(); if (!text) return; input.value = ""; await saveActiveIdeaPatch({ cons: [...state.activeIdea.cons, text] }); }); document.getElementById("drawer-add-link-form")?.addEventListener("submit", async (e) => { e.preventDefault(); if (!state.activeIdea) return; const titleEl = document.getElementById("drawer-link-title"); const urlEl = document.getElementById("drawer-link-url"); if (!urlEl.value.trim()) return; const next = [ ...state.activeIdea.links, { id: `lnk-${Date.now()}`, title: titleEl.value.trim() || urlEl.value.trim(), url: urlEl.value.trim() } ]; titleEl.value = ""; urlEl.value = ""; await saveActiveIdeaPatch({ links: next }); }); // --- NANO BANANA MOCKUP STUDIO EVENTS --- document.querySelectorAll("#nb-color-swatches .color-swatch").forEach((sw) => { sw.addEventListener("click", () => { document.querySelectorAll("#nb-color-swatches .color-swatch").forEach((s) => s.classList.remove("active")); sw.classList.add("active"); state.nbGarmentColor = sw.dataset.color; state.nbGarmentColorName = sw.dataset.name; document.getElementById("nb-selected-color-label").textContent = `${sw.dataset.name} (${sw.dataset.color})`; renderCompositorCanvas(); }); }); document.getElementById("btn-studio-auto-prompt")?.addEventListener("click", () => { if (!state.activeIdea) return; const idea = state.activeIdea; const twistNum = document.getElementById("nb-whack-twist-select")?.value; const twistCard = twistNum ? state.whackCards.find((c) => c.number === Number(twistNum)) : null; let built = `${idea.title}`; if (idea.elevator_pitch) built += ` — ${idea.elevator_pitch}`; if (twistCard) { built += `. Creative twist from Card #${twistCard.number} (${twistCard.title}): ${twistCard.promptQuestion}`; } document.getElementById("nb-prompt-input").value = built; }); document.getElementById("btn-nb-clear-edit")?.addEventListener("click", () => { state.nbEditParentMockup = null; document.getElementById("nb-edit-mode-box").classList.add("hidden"); document.getElementById("nb-edit-instruction-input").value = ""; }); document.getElementById("nb-template-select")?.addEventListener("change", () => { syncTShirtGeneratorVisibility(); if (state.activeIdea) { renderDrawerMockupGallery(state.activeIdea); renderDrawerAttachments(state.activeIdea); } }); document.getElementById("nb-generator-select")?.addEventListener("change", async (e) => { const chosenModel = e.target.value; state.settings.nanoBananaModel = chosenModel; const settingsSel = document.getElementById("settings-model-select"); if (settingsSel) settingsSel.value = chosenModel; await apiFetch("/api/settings", { method: "PUT", body: JSON.stringify({ nanoBananaModel: chosenModel }) }); showToast(`Switched Image Mockup Generator to ${e.target.options[e.target.selectedIndex].text.split("—")[0].trim()}`); }); document.getElementById("btn-generate-nano-banana")?.addEventListener("click", async () => { if (!state.activeIdeaId) return; const btn = document.getElementById("btn-generate-nano-banana"); const statusLabel = document.getElementById("nb-generation-status"); const selectedModel = document.getElementById("nb-generator-select")?.value || state.settings.nanoBananaModel || "codex-image-gen"; const template = document.getElementById("nb-template-select").value; const whackCardNumber = document.getElementById("nb-whack-twist-select").value; const prompt = document.getElementById("nb-prompt-input").value.trim(); const editInstruction = document.getElementById("nb-edit-instruction-input").value.trim(); btn.disabled = true; statusLabel.textContent = selectedModel === "codex-image-gen" ? "🤖 Generating high-res PNG via OpenAI Codex (takes ~15–25s)..." : `✨ Generating mockup via ${selectedModel}...`; try { const res = await apiFetch(`/api/ideas/${state.activeIdeaId}/mockups/nano-banana`, { method: "POST", body: JSON.stringify({ model: selectedModel, template, garmentColor: state.nbGarmentColor, garmentColorName: state.nbGarmentColorName, whackCardNumber: whackCardNumber ? Number(whackCardNumber) : null, prompt, editInstruction: state.nbEditParentMockup ? editInstruction : "", parentMockupId: state.nbEditParentMockup ? state.nbEditParentMockup.id : null }) }); state.activeIdea = res.idea; state.nbEditParentMockup = null; document.getElementById("nb-edit-mode-box").classList.add("hidden"); document.getElementById("nb-edit-instruction-input").value = ""; statusLabel.textContent = `✓ Saved new mockup (${res.mockup.modelUsed}) to gallery!`; showToast("🖼️ Generated & saved your new mockup!"); await loadIdeas(); } catch (err) { statusLabel.textContent = `⚠️ ${err.message}`; if (err.code === "MISSING_API_KEY") { document.getElementById("settings-modal")?.classList.remove("hidden"); } else { showToast(err.message); } } finally { btn.disabled = false; } }); // Compositor controls ["comp-surface-select", "comp-placement-select", "comp-emblem-select", "comp-top-text", "comp-bottom-text", "comp-ink-color"].forEach((id) => { const el = document.getElementById(id); el?.addEventListener("input", renderCompositorCanvas); el?.addEventListener("change", renderCompositorCanvas); }); document.getElementById("comp-upload-art")?.addEventListener("change", (e) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { const img = new Image(); img.onload = () => { state.uploadedOverlayImage = img; renderCompositorCanvas(); }; img.src = ev.target.result; }; reader.readAsDataURL(file); }); document.getElementById("btn-clear-uploaded-art")?.addEventListener("click", () => { state.uploadedOverlayImage = null; const fileEl = document.getElementById("comp-upload-art"); if (fileEl) fileEl.value = ""; renderCompositorCanvas(); }); document.getElementById("btn-save-canvas-mockup")?.addEventListener("click", async () => { if (!state.activeIdeaId) return; const canvas = document.getElementById("mockup-compositor-canvas"); const imageDataUrl = canvas.toDataURL("image/png"); const surface = document.getElementById("comp-surface-select").value; const placement = document.getElementById("comp-placement-select").value; const topText = document.getElementById("comp-top-text").value; const bottomText = document.getElementById("comp-bottom-text").value; const res = await apiFetch(`/api/ideas/${state.activeIdeaId}/mockups/canvas`, { method: "POST", body: JSON.stringify({ title: `${state.nbGarmentColorName} ${surface === "tshirt" ? "Tee" : surface} (${placement})`, template: surface === "tshirt" ? (placement === "left-chest" ? "tshirt-left-chest" : "tshirt-front") : surface, garmentColor: state.nbGarmentColor, garmentColorName: state.nbGarmentColorName, placement, topText, bottomText, imageDataUrl }) }); state.activeIdea = res.idea; showToast("📸 Saved T-Shirt / Product Mockup to Idea Gallery!"); await loadIdeas(); }); document.getElementById("btn-download-canvas-png")?.addEventListener("click", () => { const canvas = document.getElementById("mockup-compositor-canvas"); const link = document.createElement("a"); link.download = `sparkvault-mockup-${Date.now()}.png`; link.href = canvas.toDataURL("image/png"); link.click(); }); // Drawer Whack Card button document.getElementById("btn-drawer-draw-whack")?.addEventListener("click", drawCardForActiveIdea); // Settings Modal document.getElementById("btn-open-settings")?.addEventListener("click", () => { document.getElementById("settings-modal")?.classList.remove("hidden"); }); document.getElementById("btn-close-settings")?.addEventListener("click", () => { document.getElementById("settings-modal")?.classList.add("hidden"); }); document.getElementById("btn-cancel-settings")?.addEventListener("click", () => { document.getElementById("settings-modal")?.classList.add("hidden"); }); document.getElementById("settings-form")?.addEventListener("submit", async (e) => { e.preventDefault(); const keyInput = document.getElementById("settings-api-key"); const modelSel = document.getElementById("settings-model-select"); const bgInput = document.getElementById("settings-user-background"); const payload = { nanoBananaModel: modelSel.value, userBackground: bgInput ? bgInput.value.trim() : undefined }; if (keyInput.value.trim()) { payload.geminiApiKey = keyInput.value.trim(); } await apiFetch("/api/settings", { method: "PUT", body: JSON.stringify(payload) }); keyInput.value = ""; document.getElementById("settings-modal")?.classList.add("hidden"); showToast("✓ Saved SparkVault & AI Generator settings!"); await loadSettings(); }); // Personalized Random Idea Button document.getElementById("btn-generate-random-idea")?.addEventListener("click", async () => { const btn = document.getElementById("btn-generate-random-idea"); if (!btn) return; const origText = btn.innerHTML; btn.disabled = true; btn.innerHTML = "🎲 Generating..."; showToast("🎲 Inventing a fresh idea tailored to your background..."); try { const catSel = document.getElementById("quick-capture-category")?.value || "any"; const res = await apiFetch("/api/ideas/generate-random", { method: "POST", body: JSON.stringify({ category: catSel === "spark" ? "any" : catSel }) }); await Promise.all([loadIdeas(), loadWhackCatalog()]); showToast(`✨ Created "${res.idea.title}"!`); openIdeaDrawer(res.idea.id, "details"); } catch (err) { showToast(`⚠️ ${err.message}`); } finally { btn.disabled = false; btn.innerHTML = origText; } }); // Lightbox Close document.getElementById("btn-close-lightbox")?.addEventListener("click", () => { document.getElementById("lightbox-modal")?.classList.add("hidden"); }); // --- EXTERNAL ATTACHMENT & CLIPBOARD PASTE / DROP EVENTS --- document.getElementById("btn-open-external-clip")?.addEventListener("click", () => { openExternalClipModal(); }); document.getElementById("btn-close-clip-modal")?.addEventListener("click", () => { document.getElementById("external-clip-modal")?.classList.add("hidden"); }); document.getElementById("btn-cancel-clip-modal")?.addEventListener("click", () => { document.getElementById("external-clip-modal")?.classList.add("hidden"); }); document.getElementById("clip-target-idea")?.addEventListener("change", (e) => { document.getElementById("clip-new-idea-group")?.classList.toggle("hidden", e.target.value !== "new"); }); // File input & Drag-and-Drop in Drawer document.getElementById("drawer-attach-file-input")?.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (!file) return; const dataUrl = await readFileAsDataUrl(file); stageDrawerImage(dataUrl); const titleEl = document.getElementById("drawer-attach-title"); if (titleEl && !titleEl.value) titleEl.value = file.name.replace(/\.[^.]+$/, ""); }); document.getElementById("btn-drawer-clear-staged-img")?.addEventListener("click", () => { stageDrawerImage(null); }); const drawerDropzone = document.getElementById("drawer-attachment-dropzone"); if (drawerDropzone) { drawerDropzone.addEventListener("dragover", (e) => { e.preventDefault(); drawerDropzone.classList.add("drag-over"); }); drawerDropzone.addEventListener("dragleave", () => drawerDropzone.classList.remove("drag-over")); drawerDropzone.addEventListener("drop", async (e) => { e.preventDefault(); drawerDropzone.classList.remove("drag-over"); const file = e.dataTransfer?.files?.[0]; if (file && file.type.startsWith("image/")) { const dataUrl = await readFileAsDataUrl(file); stageDrawerImage(dataUrl); const titleEl = document.getElementById("drawer-attach-title"); if (titleEl && !titleEl.value) titleEl.value = file.name.replace(/\.[^.]+$/, ""); } }); } document.getElementById("btn-drawer-save-attachment")?.addEventListener("click", async () => { if (!state.activeIdeaId) return; const title = document.getElementById("drawer-attach-title")?.value.trim() || ""; const urlInput = document.getElementById("drawer-attach-url")?.value.trim() || ""; const textContent = document.getElementById("drawer-attach-text")?.value.trim() || ""; const appendToNotes = document.getElementById("drawer-attach-append-notes")?.checked || false; const setAsCover = document.getElementById("drawer-attach-set-cover")?.checked || false; const isImageExt = /\.(png|jpe?g|webp|gif|svg)(\?.*)?$/i.test(urlInput); const payload = { title, textContent, appendToNotes, setAsCover, imageDataUrl: state.stagedDrawerAttachmentDataUrl || "", externalImageUrl: !state.stagedDrawerAttachmentDataUrl && isImageExt ? urlInput : "", sourceUrl: urlInput }; try { const res = await apiFetch(`/api/ideas/${state.activeIdeaId}/attachments`, { method: "POST", body: JSON.stringify(payload) }); state.activeIdea = res.idea; document.getElementById("drawer-attach-title").value = ""; document.getElementById("drawer-attach-url").value = ""; document.getElementById("drawer-attach-text").value = ""; stageDrawerImage(null); showToast("📎 Attached to Spark!"); await loadIdeas(); } catch (err) { showToast(err.message); } }); // File input & Drag-and-Drop in External Clip Modal document.getElementById("clip-file-input")?.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (!file) return; const dataUrl = await readFileAsDataUrl(file); stageClipModalImage(dataUrl); const titleEl = document.getElementById("clip-title-input"); if (titleEl && !titleEl.value) titleEl.value = file.name.replace(/\.[^.]+$/, ""); }); document.getElementById("btn-clip-clear-img")?.addEventListener("click", () => { stageClipModalImage(null); }); const clipDropzone = document.getElementById("clip-dropzone"); if (clipDropzone) { clipDropzone.addEventListener("dragover", (e) => { e.preventDefault(); clipDropzone.classList.add("drag-over"); }); clipDropzone.addEventListener("dragleave", () => clipDropzone.classList.remove("drag-over")); clipDropzone.addEventListener("drop", async (e) => { e.preventDefault(); clipDropzone.classList.remove("drag-over"); const file = e.dataTransfer?.files?.[0]; if (file && file.type.startsWith("image/")) { const dataUrl = await readFileAsDataUrl(file); stageClipModalImage(dataUrl); } }); } document.getElementById("external-clip-form")?.addEventListener("submit", async (e) => { e.preventDefault(); const targetVal = document.getElementById("clip-target-idea").value; const newIdeaTitle = document.getElementById("clip-new-idea-title").value.trim(); const title = document.getElementById("clip-title-input").value.trim(); const externalImageUrl = document.getElementById("clip-external-img-url").value.trim(); const textContent = document.getElementById("clip-text-input").value.trim(); const sourceUrl = document.getElementById("clip-source-url").value.trim(); const appendToNotes = document.getElementById("clip-append-notes").checked; const setAsCover = document.getElementById("clip-set-cover").checked; try { let targetIdeaId = targetVal === "new" ? null : Number(targetVal); if (!targetIdeaId) { const created = await apiFetch("/api/ideas", { method: "POST", body: JSON.stringify({ title: newIdeaTitle || title || "Clipped External Spark", category: "spark" }) }); targetIdeaId = created.idea.id; } await apiFetch(`/api/ideas/${targetIdeaId}/attachments`, { method: "POST", body: JSON.stringify({ title: title || "External Clip", imageDataUrl: state.stagedClipAttachmentDataUrl || "", externalImageUrl, textContent, sourceUrl, appendToNotes, setAsCover }) }); document.getElementById("external-clip-modal")?.classList.add("hidden"); stageClipModalImage(null); showToast("📎 Attached external note/image to Spark!"); await loadIdeas(); } catch (err) { showToast(err.message); } }); // --- SUBMIT TO ANTIGRAVITY OR CODEX FOR DEVELOPMENT EVENTS --- document.getElementById("btn-open-dev-handoff")?.addEventListener("click", () => { openDevHandoffModal(activeDevTarget || "antigravity"); }); document.getElementById("btn-close-dev-modal")?.addEventListener("click", () => { document.getElementById("dev-handoff-modal")?.classList.add("hidden"); }); document.getElementById("btn-cancel-dev-modal")?.addEventListener("click", () => { document.getElementById("dev-handoff-modal")?.classList.add("hidden"); }); document.querySelectorAll("#dev-target-pills [data-dev-target]").forEach((btn) => { btn.addEventListener("click", async () => { activeDevTarget = btn.dataset.devTarget; document.querySelectorAll("#dev-target-pills [data-dev-target]").forEach((b) => b.classList.toggle("active", b === btn) ); populateDevModelOptions(activeDevTarget); document.getElementById("dev-server-codex-wrap")?.classList.toggle("hidden", activeDevTarget !== "codex"); const submitBtn = document.getElementById("btn-submit-dev-handoff"); if (submitBtn) { submitBtn.textContent = activeDevTarget === "codex" ? "🚀 Submit & Open in OpenAI Codex" : "🚀 Submit & Launch in Antigravity"; } await refreshDevSpecPreview(); }); }); document.getElementById("dev-model-select")?.addEventListener("change", refreshDevSpecPreview); document.getElementById("dev-include-homelab")?.addEventListener("change", refreshDevSpecPreview); document.getElementById("btn-copy-dev-spec")?.addEventListener("click", async () => { const text = document.getElementById("dev-spec-preview-textarea")?.value || ""; if (!text) return; await navigator.clipboard.writeText(text); showToast("📋 Copied full development specification to clipboard!"); }); document.getElementById("btn-download-dev-spec")?.addEventListener("click", () => { if (!state.activeIdeaId) return; const model = document.getElementById("dev-model-select")?.value || ""; const includeHomelab = document.getElementById("dev-include-homelab")?.checked !== false; window.open( `/api/ideas/${state.activeIdeaId}/spec.md?target=${encodeURIComponent(activeDevTarget)}&model=${encodeURIComponent(model)}&includeHomelab=${includeHomelab}`, "_blank" ); }); document.getElementById("dev-handoff-form")?.addEventListener("submit", async (e) => { e.preventDefault(); if (!state.activeIdeaId) return; const submitBtn = document.getElementById("btn-submit-dev-handoff"); const resultBox = document.getElementById("dev-handoff-result-box"); const model = document.getElementById("dev-model-select")?.value || ""; const customPrompt = document.getElementById("dev-spec-preview-textarea")?.value || ""; const includeHomelab = document.getElementById("dev-include-homelab")?.checked !== false; const moveStatusToBuilding = document.getElementById("dev-move-building")?.checked !== false; const executeServerCodex = activeDevTarget === "codex" && Boolean(document.getElementById("dev-run-server-codex")?.checked); if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = executeServerCodex ? "🤖 Running Codex Blueprint & Submitting..." : "🚀 Packaging & Launching..."; } try { // 1. Copy full specification to clipboard so it's immediately ready to paste (Ctrl+V) in Antigravity or Codex try { await navigator.clipboard.writeText(customPrompt); } catch { // Ignore clipboard permission errors if non-HTTPS context } // 2. Persist handoff package & workspace on server const res = await apiFetch(`/api/ideas/${state.activeIdeaId}/dev-handoff`, { method: "POST", body: JSON.stringify({ targetApp: activeDevTarget, model, customPrompt, includeHomelab, moveStatusToBuilding, executeServerCodex }) }); state.activeIdea = res.idea; const handoff = res.handoff; if (resultBox) { resultBox.classList.remove("hidden"); resultBox.innerHTML = `
    ✅ Ready in ${escapeHtml(handoff.targetLabel)} (${escapeHtml(handoff.model)})
    🚀 Open ${activeDevTarget === "codex" ? "Codex App" : "Antigravity App"} 📄 View SPEC.md

    Full specification has been copied to your clipboard (Ctrl+V) and saved to server workspace ${escapeHtml(handoff.slug)}.

    ${ handoff.codexExecution?.output ? `
    ${escapeHtml(handoff.codexExecution.output)}
    ` : "" } `; } showToast(`🚀 Copied full spec & launching ${handoff.targetLabel} (${handoff.model})!`); await loadIdeas(); renderDrawerContent(); // 3. Trigger the OS deep link to open Antigravity or Codex Desktop App if (handoff.deepLinkUrl) { window.location.href = handoff.deepLinkUrl; } } catch (err) { showToast(`⚠️ ${err.message}`); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = activeDevTarget === "codex" ? "🚀 Submit & Open in OpenAI Codex" : "🚀 Submit & Launch in Antigravity"; } } }); // Global Clipboard Paste (Ctrl+V) for screenshots & copied images window.addEventListener("paste", async (e) => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.startsWith("image/")) { const file = item.getAsFile(); if (!file) continue; e.preventDefault(); const dataUrl = await readFileAsDataUrl(file); const clipModalOpen = !document.getElementById("external-clip-modal")?.classList.contains("hidden"); const drawerOpen = !document.getElementById("idea-drawer")?.classList.contains("hidden"); if (clipModalOpen) { stageClipModalImage(dataUrl); showToast("📋 Pasted screenshot into Clip dialog!"); } else if (drawerOpen) { stageDrawerImage(dataUrl); switchDrawerTab("details"); showToast("📋 Pasted screenshot into Spark Workbench — click 'Attach to Spark' to save!"); } else { openExternalClipModal(); stageClipModalImage(dataUrl); showToast("📋 Pasted screenshot — pick a Spark to attach it to!"); } break; } } }); } initApp();