feat(generator): add personalized Random Idea button powered by background profile + Whack Pack card + Codex

This commit is contained in:
airent106 committed 2026-10-29 11:59:05 -06:00
1 parent 4c5560a5a1
commit 2b756ab127
5 files changed
+427 -4

No files matched your search

+312
View File
@@ -0,0 +1,312 @@
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) {
throw new Error(`Codex exited (${exitCode}) without returning valid idea 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) {
throw new Error("Gemini completed without returning valid idea 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()
}
]
}
};
}