feat(generator): add personalized Random Idea button powered by background profile + Whack Pack card + Codex
This commit is contained in:
1 parent
4c5560a5a1
commit
2b756ab127
5 files changed
+427
-4
No files matched your search
+38
-2
@@ -145,6 +145,10 @@ async function loadSettings() {
|
||||
if (modelSel && data.nanoBananaModel) {
|
||||
modelSel.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);
|
||||
}
|
||||
@@ -2141,7 +2145,11 @@ function bindGlobalEvents() {
|
||||
e.preventDefault();
|
||||
const keyInput = document.getElementById("settings-api-key");
|
||||
const modelSel = document.getElementById("settings-model-select");
|
||||
const payload = { nanoBananaModel: modelSel.value };
|
||||
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();
|
||||
}
|
||||
@@ -2151,10 +2159,38 @@ function bindGlobalEvents() {
|
||||
});
|
||||
keyInput.value = "";
|
||||
document.getElementById("settings-modal")?.classList.add("hidden");
|
||||
showToast("🍌 Saved Nano Banana 2.1 settings!");
|
||||
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("🎲 Drawing a Whack Pack card & inventing an idea from 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}" (via Card #${res.card.number} • ${res.generatorUsed})!`);
|
||||
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");
|
||||
|
||||
+19
-1
@@ -39,8 +39,16 @@
|
||||
<button type="submit" class="btn btn-primary btn-sm">Capture</button>
|
||||
<button
|
||||
type="button"
|
||||
id="btn-open-external-clip"
|
||||
id="btn-generate-random-idea"
|
||||
class="btn btn-secondary btn-sm"
|
||||
title="Generate a personalized random idea tailored to your background & a Whack Pack card"
|
||||
>
|
||||
🎲 Random Idea
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="btn-open-external-clip"
|
||||
class="btn btn-ghost btn-sm"
|
||||
title="Attach external notes, clipboard screenshots, photos, or image URLs to a spark"
|
||||
>
|
||||
📎 Clip / Attach
|
||||
@@ -750,6 +758,16 @@
|
||||
<option value="gemini-3.1-flash-lite-image">gemini-3.1-flash-lite-image — Nano Banana 2 Lite (Ultra-Fast)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="settings-user-background">🎲 Personal Background & Tastes (Used by '🎲 Random Idea')</label>
|
||||
<textarea
|
||||
id="settings-user-background"
|
||||
rows="5"
|
||||
class="textarea-input"
|
||||
placeholder="Describe your interests, aesthetic tastes, homelab stack, apparel preferences..."
|
||||
></textarea>
|
||||
<small class="muted-label">When you click <strong>🎲 Random Idea</strong>, SparkVault combines this profile with a random Creative Whack Pack card to invent a tailored idea.</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="btn-cancel-settings" class="btn btn-ghost">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
+40
-1
@@ -17,6 +17,10 @@ import {
|
||||
generateWithNanoBanana,
|
||||
isCodexConfigured
|
||||
} from "./nano_banana.js";
|
||||
import {
|
||||
DEFAULT_USER_BACKGROUND,
|
||||
generatePersonalizedRandomIdea
|
||||
} from "./idea_generator.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -180,6 +184,36 @@ export function createAppServer({
|
||||
return;
|
||||
}
|
||||
|
||||
// 2b. POST /api/ideas/generate-random (Personalized Random Idea Generator)
|
||||
if (method === "POST" && pathname === "/api/ideas/generate-random") {
|
||||
const body = await readJsonBody(req);
|
||||
const userBackground =
|
||||
body.userBackground ||
|
||||
store.getSetting("user_background", DEFAULT_USER_BACKGROUND) ||
|
||||
DEFAULT_USER_BACKGROUND;
|
||||
const categoryFocus = body.category || "any";
|
||||
const existingTitles = store.listIdeas({}).map((i) => i.title);
|
||||
const savedKey = store.getSetting("gemini_api_key", "") || process.env.GEMINI_API_KEY || "";
|
||||
|
||||
const result = await generatePersonalizedRandomIdea({
|
||||
userBackground,
|
||||
categoryFocus,
|
||||
existingTitles,
|
||||
geminiApiKey: savedKey
|
||||
});
|
||||
|
||||
const created = store.createIdea({
|
||||
...result.ideaPayload,
|
||||
preserveRawTitle: true
|
||||
});
|
||||
|
||||
return sendJson(res, 201, {
|
||||
idea: created,
|
||||
card: result.card,
|
||||
generatorUsed: result.generatorUsed
|
||||
});
|
||||
}
|
||||
|
||||
// 3. GET / PATCH / DELETE /api/ideas/:id
|
||||
const ideaIdMatch = pathname.match(/^\/api\/ideas\/(\d+)$/);
|
||||
if (ideaIdMatch) {
|
||||
@@ -663,6 +697,7 @@ export function createAppServer({
|
||||
? "Authenticated via ~/.codex/auth.json"
|
||||
: "",
|
||||
nanoBananaModel: model,
|
||||
userBackground: store.getSetting("user_background", DEFAULT_USER_BACKGROUND) || DEFAULT_USER_BACKGROUND,
|
||||
availableModels: NANO_BANANA_MODELS,
|
||||
surfacePresets: Object.entries(SURFACE_PRESETS).map(([id, val]) => ({
|
||||
id,
|
||||
@@ -678,6 +713,9 @@ export function createAppServer({
|
||||
if (body.nanoBananaModel !== undefined) {
|
||||
store.setSetting("nano_banana_model", String(body.nanoBananaModel).trim());
|
||||
}
|
||||
if (body.userBackground !== undefined) {
|
||||
store.setSetting("user_background", String(body.userBackground).trim());
|
||||
}
|
||||
const savedKey = store.getSetting("gemini_api_key", "");
|
||||
const envKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || "";
|
||||
const activeKey = savedKey || envKey;
|
||||
@@ -686,7 +724,8 @@ export function createAppServer({
|
||||
saved: true,
|
||||
hasApiKey: Boolean(activeKey) || codexReady,
|
||||
codexReady,
|
||||
nanoBananaModel: store.getSetting("nano_banana_model", "codex-image-gen")
|
||||
nanoBananaModel: store.getSetting("nano_banana_model", "codex-image-gen"),
|
||||
userBackground: store.getSetting("user_background", DEFAULT_USER_BACKGROUND) || DEFAULT_USER_BACKGROUND
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,3 +216,21 @@ test("External notes and images can be attached to a spark and promoted to Mocku
|
||||
assert.equal(promData.idea.mockups[0].title, "Vintage Ski Patch Photo");
|
||||
});
|
||||
|
||||
test("Personalized Random Idea Generator creates a tailored idea from background + Whack Pack card", async () => {
|
||||
const res = await fetch(`${baseUrl}/api/ideas/generate-random`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
category: "creative"
|
||||
})
|
||||
});
|
||||
assert.equal(res.status, 201);
|
||||
const data = await res.json();
|
||||
assert.ok(data.idea && data.idea.id);
|
||||
assert.ok(data.idea.title.length > 5);
|
||||
assert.ok(data.card && data.card.number >= 1 && data.card.number <= 64);
|
||||
assert.equal(data.idea.inspired_by_card, data.card.number);
|
||||
assert.ok(data.idea.whack_history.length >= 1);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user