// 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, 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(); } 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 key configured (${data.apiKeySource}: ${data.maskedKey})` : "No key stored yet — paste your Gemini API key to enable live Nano Banana 2.1 generation."; } const modelSel = document.getElementById("settings-model-select"); if (modelSel && data.nanoBananaModel) { modelSel.value = data.nanoBananaModel; } } 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-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 & NANO BANANA STUDIO --- async function openIdeaDrawer(id, initialTab = "details") { const data = await apiFetch(`/api/ideas/${id}`); state.activeIdeaId = id; state.activeIdea = data.idea; state.nbEditParentMockup = null; 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) { topInput.value = state.activeIdea.title.slice(0, 26).toUpperCase(); } if (bottomInput && !bottomInput.value) { bottomInput.value = state.activeIdea.tags[0] ? `#${state.activeIdea.tags[0].toUpperCase()}` : "SPARKVAULT STUDIO"; } renderCompositorCanvas(); if (initialTab === "whack" && !state.drawerWhackCard) { await drawCardForActiveIdea(); } } function closeIdeaDrawer() { state.activeIdeaId = null; state.activeIdea = null; document.getElementById("drawer-backdrop").classList.add("hidden"); document.getElementById("idea-drawer").classList.add("hidden"); } 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 }); }); }); // Mockup Gallery renderDrawerMockupGallery(idea); // Whack History renderDrawerWhackHistory(idea); } 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 with 🍌 Nano Banana 2.1 or save a layout from the T-Shirt Compositor!

    `; 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 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"); 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 document.getElementById("btn-mobile-filter-toggle")?.addEventListener("click", () => { const bar = document.getElementById("global-filter-bar"); bar?.classList.toggle("mobile-filters-open"); }); 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; loadIdeas(); }); document.getElementById("filter-category")?.addEventListener("change", (e) => { state.filters.category = e.target.value; loadIdeas(); }); document.getElementById("filter-tag")?.addEventListener("change", (e) => { state.filters.tag = e.target.value; loadIdeas(); }); document.getElementById("filter-has-mockup")?.addEventListener("change", (e) => { state.filters.hasMockup = e.target.checked; 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("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("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 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 = "🎨 Generating high-res AI mockup via OpenAI Codex (takes ~15–25s)..."; try { const res = await apiFetch(`/api/ideas/${state.activeIdeaId}/mockups/nano-banana`, { method: "POST", body: JSON.stringify({ 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 Nano Banana mockup to gallery!"; showToast("🍌 Nano Banana 2.1 generated & saved your 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 payload = { nanoBananaModel: modelSel.value }; 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 Nano Banana 2.1 settings!"); await loadSettings(); }); // Lightbox Close document.getElementById("btn-close-lightbox")?.addEventListener("click", () => { document.getElementById("lightbox-modal")?.classList.add("hidden"); }); } initApp();