import { spawn } from "node:child_process"; import { GoogleGenAI } from "@google/genai"; import { drawCards } from "./whack_cards.js"; import { isCodexConfigured } from "./nano_banana.js"; export const DEFAULT_USER_BACKGROUND = `Creative technologist, homelabber, and maker (vloser.us). Interests & Background: - Vintage screenprint apparel, heavyweight crewneck tees (5-inch left-chest emblems & full-back prints), die-cut vinyl stickers, national park & alpine ski/mountain aesthetics, specialty espresso & craft brewing culture. - Self-hosted Docker homelab infrastructure (Traefik, Gitea, Homepage, SQLite, AI agents, local automation, media & art tools). - Tactile hardware & ambient displays (ESP32, E-Ink dashboards, home telemetry, vinyl/audio archives, photography & curated digital art galleries). - Lateral thinking, Roger von Oech's Creative Whack Pack, and playful weekend builds that ship fast.`; export const CURATED_IDEA_TEMPLATES = [ { title: "Alpine Telemetry Club — Heavyweight Left-Chest & Back Print Tee", elevator_pitch: "Vintage 1970s mountain weather-station tee featuring a 5-inch left-chest anemometer badge and a full-back topographic isobar map.", category: "creative", tags: ["tshirt", "merch", "screenprint", "alpine", "weather"], rating_excitement: 5, rating_impact: 4, rating_ease: 5, notes_markdown: "### Apparel & Print Concept\n- **Garment**: 6.5oz Heavyweight Vintage Cream (`#F5E8D3`) or Forest Pine (`#1E3F2B`) crewneck.\n- **Front Print**: 5-inch left-chest circular patch reading `HIGH ALTITUDE TELEMETRY DIVISION • EST. 1978`.\n- **Back Print**: 3-color spot screenprint showing elevation contour lines, barometric pressure curves, and a retro satellite dish icon.", checklist: [ "Mock up 5-inch left-chest emblem in Visual Mockup Studio", "Test Burnt Orange + Espresso Brown + Gold ink palette on Cream tee", "Export print-ready high-res PNG" ], pros: ["Bridges geeky homelab telemetry with classic outdoor heritage apparel", "Works great as a 3-color screenprint"], cons: ["Fine isobar contour lines need careful halftone/stroke weight"] }, { title: "Self-Hosted Homelab Rack Patch & Die-Cut Sticker Series", elevator_pitch: "Collectible merit-badge style vinyl stickers and embroidered patches for homelab milestones ('Zero Downtime Club', 'DNS Survivor', 'Docker Compose Alchemist').", category: "creative", tags: ["sticker", "merch", "homelab", "docker", "illustration"], rating_excitement: 5, rating_impact: 4, rating_ease: 5, notes_markdown: "### Series Concept\n- Design a set of 4 vintage scout/national-park style badges celebrating self-hosting and homelab triumphs:\n 1. **It Was DNS All Along** (owl holding a tangled ethernet cable)\n 2. **Container Ship Captain** (vintage tugboat carrying glowing Docker containers)\n 3. **3-2-1 Backup Ranger** (forest lookout tower with tape reels)\n- Great for laptop lids, server rack doors, or left-chest pocket tees.", checklist: [ "Generate 'It Was DNS All Along' die-cut sticker mockup", "Test white vinyl contour border on dark slate surface", "Try left-chest t-shirt variation" ], pros: ["Instant hit with self-hosting/homelab community", "Low unit cost to print die-cut vinyl runs"], cons: ["Need crisp bold typography that reads well at 3-inch sticker scale"] }, { title: "Sub-Zero Cold Brew & Vinyl Listening Society Crewneck", elevator_pitch: "Retro Japanese jazz-kissa (listening bar) inspired t-shirt combining a spinning 45rpm record adapter with an iced pour-over carafe.", category: "creative", tags: ["tshirt", "merch", "coffee", "vinyl", "retro"], rating_excitement: 4, rating_impact: 4, rating_ease: 5, notes_markdown: "### Design Notes\n- Inspired by Tokyo kissaten hi-fi coffee lounges.\n- **Colorway**: Midnight Navy (`#1B2A4A`) tee with cream and warm amber puff/plastisol ink.\n- **Graphic**: A geometric 45rpm vinyl spindle morphing into ripples of espresso in a ceramic mug.", checklist: [ "Generate Midnight Navy t-shirt mockup in Studio", "Experiment with vintage mono-spaced typography in Canvas Compositor" ], pros: ["Combines two tactile daily rituals (coffee + music)", "Minimalist 2-color screenprint"], cons: ["Dark garment requires opaque cream underbase"] }, { title: "Homelab Service Health Receipt Printer (Thermal Desk Totem)", elevator_pitch: "A retro POS thermal receipt printer on your desk that prints a physical morning 'Daily Dispatch' ticket with server stats, weather, and 1 random SparkVault idea + Whack Pack card.", category: "project", tags: ["hardware", "homelab", "thermal-printer", "automation", "esp32"], rating_excitement: 5, rating_impact: 5, rating_ease: 4, notes_markdown: "### Morning Ritual Hardware\n- Uses a $25 ESC/POS USB or Wi-Fi 58mm thermal receipt printer.\n- Every morning at 7:30 AM (or when a big arcade button is pressed), it queries `sparkvault.vloser.us/api/whack-cards/draw?mode=collider` and `vloser.us` container status.\n- Prints a crisp tear-off physical ticket with:\n - Today's Creative Whack Pack Card & prompt question\n - One stale Spark from your vault to think about over coffee\n - Homelab uptime summary", checklist: [ "Write Node/Python ESC/POS formatter script", "Hook up `/api/whack-cards/draw?mode=collider` endpoint", "Design dithered 1-bit header logo for the thermal roll" ], pros: ["Zero-ink thermal paper is pennies per roll", "Brings digital ideas into the physical world every morning"], cons: ["Thermal paper fades if left in direct sunlight"] }, { title: "AI Frame Curator — Dynamic Art & Mockup Rotator for Wall Display", elevator_pitch: "Self-hosted web frame mode that turns any tablet or wall TV into a curated gallery cycling through your best art, photography, and active SparkVault mockups with QR codes to vote/comment.", category: "project", tags: ["art-gallery", "homelab", "docker", "display", "ui"], rating_excitement: 4, rating_impact: 4, rating_ease: 4, notes_markdown: "### Architecture\n- Lightweight kiosk view with museum-style matte framing, subtle ambient color matching, and museum placard metadata in the corner.\n- Guests or family can scan a tiny corner QR code on the LAN to give a 🔥 vote or drop a note directly into SparkVault.", checklist: [ "Mock up the gallery kiosk UI in Mockup Studio (`app-ui` preset)", "Add museum placard typography overlay", "Deploy behind Traefik on `vloser.us`" ], pros: ["Showcases art & t-shirt mockups passively around the house", "Uses existing homelab stack"], cons: ["Needs wake-lock / auto-dimming schedule for night hours"] }, { title: "Midnight Maintenance Crew — Glow-in-the-Dark Homelab Tee", elevator_pitch: "Vintage utility-worker style t-shirt for late-night server tinkerers: 'MIDNIGHT MAINTENANCE CREW • VLOSER LABS • IF IT AIN'T BROKE, UPGRADE IT'.", category: "creative", tags: ["tshirt", "merch", "homelab", "screenprint", "vintage"], rating_excitement: 5, rating_impact: 4, rating_ease: 5, notes_markdown: "### Garment & Ink\n- **Garment**: Vintage Black (`#1C1B1A`) heavyweight tee.\n- **Graphic**: Retro mascot of a raccoon wearing a headlamp holding a wrench and a glowing fiber-optic patch cable.\n- **Placement**: 5-inch left-chest badge + oversized vintage back graphic.", checklist: [ "Generate raccoon mascot screenprint graphic in Mockup Studio", "Test both Left-Chest and Full-Front placements" ], pros: ["Relatable self-deprecating homelab humor", "Looks awesome on Vintage Black or Heather Grey"], cons: ["Multi-color mascot illustration benefits from DTG or simulated process screenprint"] } ]; function extractJsonObject(rawText) { if (!rawText) return null; const cleaned = String(rawText) .replace(/```json/gi, "") .replace(/```/g, "") .trim(); const match = cleaned.match(/\{[\s\S]*\}/); if (!match) return null; try { return JSON.parse(match[0]); } catch { return null; } } async function generateIdeaViaCodex(prompt) { const { stdout, exitCode } = await new Promise((resolve, reject) => { const child = spawn( "codex", [ "exec", "-m", process.env.CODEX_MODEL || "gpt-5.6-sol", "--skip-git-repo-check", "--json", prompt ], { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } } ); let out = ""; child.stdout.on("data", (d) => { out += d.toString(); }); child.on("error", reject); child.on("close", (code) => resolve({ stdout: out, exitCode: code })); }); let lastAgentText = ""; for (const line of stdout.split("\n")) { const trimmed = line.trim(); if (!trimmed.startsWith("{")) continue; try { const evt = JSON.parse(trimmed); if (evt.type === "item.completed" && evt.item?.type === "agent_message") { lastAgentText = evt.item.text; } } catch { // ignore } } const parsed = extractJsonObject(lastAgentText); if (!parsed || (!parsed.title && !Array.isArray(parsed.suggestions))) { throw new Error(`Codex exited (${exitCode}) without returning valid JSON.`); } return parsed; } async function generateIdeaViaGemini(apiKey, prompt) { const ai = new GoogleGenAI({ apiKey }); const interaction = await ai.interactions.create({ model: "gemini-3.1-flash-lite", input: prompt }); const parsed = extractJsonObject(interaction.output_text); if (!parsed || (!parsed.title && !Array.isArray(parsed.suggestions))) { throw new Error("Gemini completed without returning valid JSON."); } return parsed; } /** * Generates a fresh, personalized idea tailored to the user's background, * existing vault themes, and a randomly drawn Creative Whack Pack card. */ export async function generatePersonalizedRandomIdea({ userBackground = DEFAULT_USER_BACKGROUND, categoryFocus = "any", existingTitles = [], geminiApiKey = "" }) { const card = drawCards({ count: 1 })[0]; const categoryInstruction = categoryFocus && categoryFocus !== "any" ? `Focus specifically on the "${categoryFocus}" category (${ categoryFocus === "creative" ? "a graphic t-shirt, apparel, sticker, poster, or tactile merch design" : categoryFocus === "project" ? "a self-hosted app, homelab tool, hardware/E-Ink build, or creative utility" : categoryFocus }).` : "Pick either a standout graphic t-shirt/merch concept ('creative') OR a clever homelab/hardware/creative-tech project ('project')."; const prompt = [ "You are an imaginative creative partner generating a fresh, specific, high-craft idea for a maker's SparkVault.", `USER BACKGROUND & TASTES:\n${userBackground || DEFAULT_USER_BACKGROUND}`, `CREATIVE WHACK PACK CATALYST CARD DRAWN:\nCard #${card.number} (${card.suitLabel}): "${card.title}" — ${card.promptQuestion}`, existingTitles.length > 0 ? `EXISTING IDEAS IN VAULT (do NOT repeat these, invent something distinctly new):\n- ${existingTitles.slice(0, 20).join("\n- ")}` : "", categoryInstruction, " Do NOT run any shell commands or tools. Reply ONLY with a single raw JSON object (no markdown fences) matching this exact schema:", JSON.stringify({ title: "Catchy, specific idea title", elevator_pitch: "Compelling one-sentence pitch explaining what makes it awesome", category: "creative or project or research or spark", tags: ["tag1", "tag2", "tag3"], notes_markdown: "### Concept & Specs\n- Detailed markdown notes (if apparel: blank color, ink palette, 5-inch left-chest vs full-front; if project: stack, hardware/UI, and why it's fun)", checklist: ["Concrete step 1", "Concrete step 2", "Concrete step 3"], pros: ["Pro 1", "Pro 2"], cons: ["Constraint or risk 1"], whack_reflection: `How Card #${card.number} (${card.title}) shaped this idea` }) ] .filter(Boolean) .join("\n\n"); let rawIdea = null; let generatorUsed = "Curated Personalized Generator"; // 1. Try OpenAI Codex (if authenticated) if (isCodexConfigured()) { try { rawIdea = await generateIdeaViaCodex(prompt); generatorUsed = "OpenAI Codex (gpt-5.6-sol)"; } catch (err) { console.warn("Codex idea generation fallback:", err.message); } } // 2. Try Gemini 3.1 Flash-Lite (works on Free Tier!) if (!rawIdea && geminiApiKey) { try { rawIdea = await generateIdeaViaGemini(geminiApiKey, prompt); generatorUsed = "Gemini 3.1 Flash-Lite"; } catch (err) { console.warn("Gemini idea generation fallback:", err.message); } } // 3. Curated fallback remix if offline / no AI provider if (!rawIdea) { const unused = CURATED_IDEA_TEMPLATES.filter( (t) => !existingTitles.some((ex) => ex.toLowerCase() === t.title.toLowerCase()) ); const pool = unused.length > 0 ? unused : CURATED_IDEA_TEMPLATES; const chosen = pool[Math.floor(Math.random() * pool.length)]; rawIdea = { ...chosen, whack_reflection: `Sparked with Creative Whack Pack Card #${card.number} (${card.title}): ${card.promptQuestion}` }; } return { card, generatorUsed, ideaPayload: { title: rawIdea.title || "Untitled Spark", elevator_pitch: rawIdea.elevator_pitch || "", category: ["creative", "project", "spark", "research"].includes(rawIdea.category) ? rawIdea.category : "creative", status: "spark", rating_excitement: 4, rating_impact: 4, rating_ease: 4, inspired_by_card: card.number, tags: Array.isArray(rawIdea.tags) ? rawIdea.tags : ["ai-spark"], notes_markdown: rawIdea.notes_markdown || "", checklist: (Array.isArray(rawIdea.checklist) ? rawIdea.checklist : []).map((txt, idx) => ({ id: `chk-${Date.now()}-${idx}`, text: typeof txt === "string" ? txt : txt.text || "", done: false })), pros: Array.isArray(rawIdea.pros) ? rawIdea.pros : [], cons: Array.isArray(rawIdea.cons) ? rawIdea.cons : [], whack_history: [ { id: `whack-${Date.now()}`, cardNumber: card.number, cardTitle: card.title, suit: card.suit, suitLabel: card.suitLabel, promptQuestion: card.promptQuestion, reflection: rawIdea.whack_reflection || `Generated using Card #${card.number} (${card.title}) + personal background profile.`, createdAt: new Date().toISOString() } ] } }; } /** * Generates 6 creative name / title & slogan suggestions for a given idea * across distinct naming angles (Punchy Brand, Vintage Club/Badge, Clever/Witty, * Minimalist Editorial, Graphic Slogan, Technical/Maker). */ export async function generateNameSuggestions({ idea, userBackground = DEFAULT_USER_BACKGROUND, geminiApiKey = "" }) { const prompt = [ "You are a sharp creative director, brand namer, and apparel/product copywriter.", `IDEA TO NAME:\n- Current Title: ${idea.title}\n- Elevator Pitch: ${idea.elevator_pitch || ""}\n- Category: ${idea.category}\n- Tags: ${(idea.tags || []).join(", ")}\n- Notes: ${(idea.notes_markdown || "").slice(0, 600)}`, `USER BACKGROUND & TASTES:\n${userBackground || DEFAULT_USER_BACKGROUND}`, "Generate 6 distinct, memorable name/title suggestions for this idea across different naming angles (e.g., Vintage Club/Crest, Punchy Brand Name, Witty/Clever, Minimalist, Screenprint Headline + Sub-tagline, Maker/Project Codename).", "Do NOT run any shell commands or tools. Reply ONLY with a single raw JSON object (no markdown fences) matching this exact schema:", JSON.stringify({ suggestions: [ { name: "Suggested Idea Title or Brand Name", tagline: "Short punchy subtitle, est. line, or one-line hook", style: "Angle badge (e.g. Vintage Club, Punchy Brand, Witty, Minimalist, Tee Slogan, Codename)" } ] }) ].join("\n\n"); let resultObj = null; let generatorUsed = "Smart Name Generator"; if (isCodexConfigured()) { try { resultObj = await generateIdeaViaCodex(prompt); generatorUsed = "OpenAI Codex (gpt-5.6-sol)"; } catch (err) { console.warn("Codex name suggestion fallback:", err.message); } } if (!resultObj && geminiApiKey) { try { resultObj = await generateIdeaViaGemini(geminiApiKey, prompt); generatorUsed = "Gemini 3.1 Flash-Lite"; } catch (err) { console.warn("Gemini name suggestion fallback:", err.message); } } if (!resultObj || !Array.isArray(resultObj.suggestions) || resultObj.suggestions.length === 0) { const coreWords = String(idea.title || "Spark") .replace(/—.*$/, "") .replace(/[^\w\s-]/g, "") .trim(); const shortCore = coreWords.split(/\s+/).slice(0, 3).join(" ") || "Alpine Spark"; resultObj = { suggestions: [ { name: `${shortCore} Society`, tagline: `EST. 1984 • HIGH ALTITUDE DIVISION`, style: "Vintage Club" }, { name: `The ${shortCore} Dept.`, tagline: `FIELD TESTED & SELF-HOSTED`, style: "Heritage Badge" }, { name: `${shortCore} — Heavyweight Edition`, tagline: `BUILT FOR LATE NIGHTS & EARLY SUMMITS`, style: "Apparel / Merch" }, { name: `Project ${shortCore.split(" ")[0] || "Pulse"}`, tagline: `ZERO FRICTION • LOCAL FIRST`, style: "Maker Codename" }, { name: `${shortCore} Co. Outpost`, tagline: `CRAFTED IN COLORADO • NO SHORTCUTS`, style: "Outfitter Crest" }, { name: ` Midnight ${shortCore} Club`, tagline: `IF IT AIN'T BROKE, UPGRADE IT`, style: "Witty / Homelab" } ] }; } return { generatorUsed, suggestions: resultObj.suggestions.slice(0, 6) }; }