// 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 = `None (Direct Concept) ` +
state.whackCards
.map((c) => `#${c.number} [${c.suitLabel}] ${escapeHtml(c.title)} `)
.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 =
`All #Tags ` +
(state.stats.tags || [])
.map((t) => `#${escapeHtml(t.name)} (${t.count}) `)
.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
? ` `
: ""
}
${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 =
`🎲 Random Active Idea ` +
active.map((i) => `[${i.status.toUpperCase()}] ${escapeHtml(i.title)} `).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}
🔍 Zoom Card
${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)}
How does "${escapeHtml(card.title)}" transform or advance this idea?
${
idea
? `
Also add as Action Checklist step
💾 Log Breakthrough to Idea
🖼️ Visualize Twist in Image Mockup Studio
`
: ""
}
✨ Spawn Brand-New Idea from Card #${card.number}
`;
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.promptQuestion)}
⚡ Use in Collider
✨ Spawn Idea
`
)
.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.promptQuestion)}
⚡ Collide
✨ Spark
`
)
.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.elevator_pitch || idea.notes_markdown.slice(0, 140) || "No description yet.")}
${idea.tags.map((t) => `#${escapeHtml(t)} `).join("")}
🔥 This Excites Me More
Inspect
`;
}
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.elevator_pitch || "No elevator pitch yet — open Workbench or deal a Whack card to flesh it out.")}
🚀 Promote to ${nextStage.toUpperCase()}
🖼️ Mock Up Image
💡 Prompt Twist
✓ Keep & Next
📦 Archive
`;
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 = `
${list
.map(
(s, idx) => `
${escapeHtml(s.name)}
${escapeHtml(s.style || "Name")}
${s.tagline ? `
${escapeHtml(s.tagline)} ` : ""}
✓ Use Title
${
isCurrentProductTShirt()
? `
👕 Send to Tee Canvas
`
: ""
}
`
)
.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) => `
${escapeHtml(item.text)}
✕
`
)
.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) => `
`
)
.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 = `
${
mockups.length > 1
? `
${mockups
.slice(0, 5)
.map(
(m) => `
`
)
.join("")}
`
: ""
}
🖼️ Mockup Studio (${mockups.length})
🔍 Zoom
`;
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
✨ Create in Mockup Studio
`;
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
? `
`
: ""
}
${
att.textContent
? `
${escapeHtml(att.textContent)}
`
: ""
}
${
att.sourceUrl
? `
🔗 ${escapeHtml(att.sourceUrl)} `
: ""
}
${
att.imageUrl
? `
🎨 Send to Mockup Studio
${
isCurrentProductTShirt()
? `
👕 Tee Canvas
`
: ""
}
${
!isCover
? `⭐ Cover `
: `★ Cover `
}
`
: ""
}
🗑️
`;
})
.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 `
${escapeHtml(m.title)}
${escapeHtml(m.modelUsed || "mockup")}
${m.editInstruction ? `
Edited: "${escapeHtml(m.editInstruction)}" ` : ""}
✨ Refine Mockup
${
isCurrentProductTShirt()
? `
👕 Place on Tee Canvas
`
: ""
}
${
!isCover
? `
⭐ Set Cover `
: `
★ Active Cover `
}
⬇️
🗑️
`;
})
.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}
Card #${card.number}: ${escapeHtml(card.title)}
${escapeHtml(card.promptQuestion)}
Your Breakthrough / Answer for "${escapeHtml(idea.title)}"
Also add to Action Checklist
💾 Save Breakthrough
🖼️ Send Twist to Image Mockup Studio
`;
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.8-flash",
models: [
{ id: "gemini-4-argon", label: "Gemini 4 Argon (Medium)" },
{ id: "gemini-3.8-flash", label: "Gemini 3.8 Flash (Medium)" },
{ id: "claude-opus-5.5", label: "Claude Opus 5.5 (Medium)" },
{ id: "claude-sonnet-5.5", label: "Claude Sonnet 5.5 (Medium)" },
{ id: "gpt-oss-120b", label: "GPT-OSS 120B (Medium)" }
]
},
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)" }
]
}
};
async function copyTextToClipboard(text) {
if (!text) return false;
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Fall through to fallback
}
}
try {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand("copy");
textArea.remove();
return successful;
} catch {
return false;
}
}
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) => `${escapeHtml(m.label)} `)
.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();
}
const instructionNote = document.getElementById("dev-handoff-instruction-note");
if (instructionNote) {
instructionNote.innerHTML =
activeDevTarget === "codex"
? "🤖 Submitting opens OpenAI Codex with the prompt preloaded and saves the full spec to your workspace."
: "🛸 Submitting switches focus to Antigravity and copies the complete project specification to your clipboard. Simply press Ctrl+V in Antigravity's chat to start coding.";
}
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) => `[${i.status.toUpperCase()}] ${escapeHtml(i.title)} `)
.join("") + `+ ✨ Create New Spark from Clip `;
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";
}
const instructionNote = document.getElementById("dev-handoff-instruction-note");
if (instructionNote) {
instructionNote.innerHTML =
activeDevTarget === "codex"
? "🤖 Submitting opens OpenAI Codex with the prompt preloaded and saves the full spec to your workspace."
: "🛸 Submitting switches focus to Antigravity and copies the complete project specification to your clipboard. Simply press Ctrl+V in Antigravity's chat to start coding.";
}
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;
const ok = await copyTextToClipboard(text);
showToast(ok ? "📋 Copied full development specification to clipboard!" : "⚠️ Unable to auto-copy; please copy text manually.");
});
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
await copyTextToClipboard(customPrompt);
// 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");
const modelName = handoff.modelLabel || handoff.model;
if (activeDevTarget === "antigravity") {
resultBox.innerHTML = `
🛸 Switched to Google Antigravity!
👉 Ready to develop with ${escapeHtml(modelName)} !
The full specification for ${escapeHtml(state.activeIdea.title)} has been loaded onto your clipboard! Since Antigravity requires starting or selecting your conversation, simply click into Antigravity's chat bar and press Ctrl+V (or Cmd+V ) to paste the prompt and begin.
`;
document.getElementById("btn-copy-dev-spec-again")?.addEventListener("click", async () => {
const reCopied = await copyTextToClipboard(customPrompt);
showToast(reCopied ? "📋 Specification copied to clipboard! Press Ctrl+V in Antigravity." : "⚠️ Please copy manually from the preview below.");
});
} else {
resultBox.innerHTML = `
✅ Ready in ${escapeHtml(handoff.targetLabel)} (${escapeHtml(handoff.model)})
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 spec & launching Antigravity! Paste with Ctrl+V.`);
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();