Add Submit to Antigravity or Codex for Development modal with model picker and deep links
This commit is contained in:
1 parent
b5af03b628
commit
92c5039bcb
6 files changed
+752
-1
No files matched your search
+239
@@ -1847,6 +1847,104 @@ function stageClipModalImage(dataUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
const DEV_TARGETS = {
|
||||
antigravity: {
|
||||
id: "antigravity",
|
||||
label: "🛸 Google Antigravity",
|
||||
defaultModel: "gemini-3.1-pro",
|
||||
models: [
|
||||
{ id: "gemini-3.1-pro", label: "Gemini 3.1 Pro (Recommended — Full-Stack & Architecture)" },
|
||||
{ id: "gemini-3.0-pro", label: "Gemini 3.0 Pro (Deep Reasoning & Complex Refactors)" },
|
||||
{ id: "gemini-3.1-flash", label: "Gemini 3.1 Flash (Fast Iteration & UI Prototyping)" },
|
||||
{ id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash-Lite (Lightweight & Rapid Tasks)" },
|
||||
{ id: "claude-sonnet-4.5", label: "Claude 4.5 Sonnet (via Antigravity Model Selector)" },
|
||||
{ id: "claude-opus-4.5", label: "Claude 4.5 Opus (via Antigravity Model Selector)" }
|
||||
]
|
||||
},
|
||||
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)" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
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) => `<option value="${escapeHtml(m.id)}">${escapeHtml(m.label)}</option>`)
|
||||
.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();
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -1921,6 +2019,7 @@ function bindGlobalEvents() {
|
||||
} 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();
|
||||
}
|
||||
});
|
||||
@@ -2681,6 +2780,146 @@ function bindGlobalEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
// --- 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";
|
||||
}
|
||||
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;
|
||||
await navigator.clipboard.writeText(text);
|
||||
showToast("📋 Copied full development specification to clipboard!");
|
||||
});
|
||||
|
||||
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
|
||||
try {
|
||||
await navigator.clipboard.writeText(customPrompt);
|
||||
} catch {
|
||||
// Ignore clipboard permission errors if non-HTTPS context
|
||||
}
|
||||
|
||||
// 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");
|
||||
resultBox.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;">
|
||||
<strong>✅ Ready in ${escapeHtml(handoff.targetLabel)} (${escapeHtml(handoff.model)})</strong>
|
||||
<div style="display:flex;gap:0.4rem;">
|
||||
<a href="${escapeHtml(handoff.deepLinkUrl)}" class="btn btn-primary btn-xs">
|
||||
🚀 Open ${activeDevTarget === "codex" ? "Codex App" : "Antigravity App"}
|
||||
</a>
|
||||
<a href="${escapeHtml(handoff.specUrl)}" target="_blank" rel="noopener noreferrer" class="btn btn-ghost btn-xs">
|
||||
📄 View SPEC.md
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted-label" style="margin-top:0.25rem;">
|
||||
Full specification has been <strong>copied to your clipboard (<kbd>Ctrl+V</kbd>)</strong> and saved to server workspace <code>${escapeHtml(handoff.slug)}</code>.
|
||||
</p>
|
||||
${
|
||||
handoff.codexExecution?.output
|
||||
? `<pre style="max-height:160px;overflow:auto;background:var(--bg-base);padding:0.5rem;border-radius:6px;font-size:0.75rem;white-space:pre-wrap;margin-top:0.35rem;">${escapeHtml(handoff.codexExecution.output)}</pre>`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
showToast(`🚀 Copied full spec & launching ${handoff.targetLabel} (${handoff.model})!`);
|
||||
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;
|
||||
|
||||
@@ -376,6 +376,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-header-actions">
|
||||
<button type="button" id="btn-open-dev-handoff" class="btn btn-primary btn-sm" title="Submit all Spark info, specs, and mockups to Antigravity or OpenAI Codex for development">
|
||||
🚀 Send to Dev
|
||||
</button>
|
||||
<button type="button" id="drawer-delete-btn" class="btn btn-danger-ghost btn-sm" title="Delete Idea">🗑️</button>
|
||||
<button type="button" id="drawer-close-btn" class="btn btn-ghost btn-sm" title="Close (Esc)">✕ Close</button>
|
||||
</div>
|
||||
@@ -874,6 +877,75 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: SUBMIT SPARK TO ANTIGRAVITY OR CODEX FOR DEVELOPMENT -->
|
||||
<div id="dev-handoff-modal" class="modal-backdrop hidden">
|
||||
<div class="modal-card modal-card-wide">
|
||||
<div class="modal-header">
|
||||
<h3>🚀 Submit Spark for Development</h3>
|
||||
<button type="button" id="btn-close-dev-modal" class="btn btn-ghost btn-sm">✕</button>
|
||||
</div>
|
||||
<form id="dev-handoff-form" class="modal-body">
|
||||
<p class="muted-label">
|
||||
Packages all Spark information (title, pitch, markdown specs, checklist, pros/cons, reference links, and mockup/attachment URLs) and sends it to <strong>Antigravity</strong> or <strong>OpenAI Codex</strong>.
|
||||
</p>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="field-group">
|
||||
<label>1. Target Coding App</label>
|
||||
<div class="dev-target-pills" id="dev-target-pills">
|
||||
<button type="button" class="pill-btn active" data-dev-target="antigravity">
|
||||
🛸 Google Antigravity
|
||||
</button>
|
||||
<button type="button" class="pill-btn" data-dev-target="codex">
|
||||
🤖 OpenAI Codex
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="dev-model-select">2. Select AI Model</label>
|
||||
<select id="dev-model-select" class="filter-select full-width"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dev-options-row">
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="dev-include-homelab" checked />
|
||||
<span>Include Homelab Stack Spec (vloser Docker + Gitea + Traefik + Homepage)</span>
|
||||
</label>
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox" id="dev-move-building" checked />
|
||||
<span>Move Spark to 🛠️ Building stage</span>
|
||||
</label>
|
||||
<label class="filter-checkbox hidden" id="dev-server-codex-wrap">
|
||||
<input type="checkbox" id="dev-run-server-codex" />
|
||||
<span>⚡ Also run autonomous Codex blueprint on server</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-header-row">
|
||||
<label for="dev-spec-preview-textarea">3. Complete Development Specification Payload (Editable)</label>
|
||||
<div style="display:flex;gap:0.35rem;">
|
||||
<button type="button" id="btn-copy-dev-spec" class="btn btn-ghost btn-xs">📋 Copy Prompt</button>
|
||||
<button type="button" id="btn-download-dev-spec" class="btn btn-ghost btn-xs">⬇️ Download SPEC.md</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="dev-spec-preview-textarea" rows="11" class="textarea-input dev-spec-textarea"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="dev-handoff-result-box" class="nb-edit-box hidden"></div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="btn-cancel-dev-modal" class="btn btn-ghost">Cancel</button>
|
||||
<button type="submit" id="btn-submit-dev-handoff" class="btn btn-primary">
|
||||
🚀 Submit & Launch in Antigravity
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: CARD / MOCKUP LIGHTBOX INSPECTOR -->
|
||||
<div id="lightbox-modal" class="modal-backdrop hidden">
|
||||
<div class="lightbox-card">
|
||||
|
||||
@@ -1445,6 +1445,35 @@ body {
|
||||
width: min(680px, 95vw);
|
||||
}
|
||||
|
||||
.modal-card-wide {
|
||||
width: min(760px, 95vw);
|
||||
max-height: 92vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-card-wide .modal-body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dev-target-pills {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dev-options-row {
|
||||
display: flex;
|
||||
gap: 0.65rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dev-spec-textarea {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0.9rem 1.15rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
@@ -416,3 +416,288 @@ export async function generateNameSuggestions({
|
||||
};
|
||||
}
|
||||
|
||||
export const DEV_HANDOFF_TARGETS = {
|
||||
antigravity: {
|
||||
id: "antigravity",
|
||||
label: "🛸 Google Antigravity",
|
||||
protocol: "antigravity://",
|
||||
defaultModel: "gemini-3.1-pro",
|
||||
models: [
|
||||
{ id: "gemini-3.1-pro", label: "Gemini 3.1 Pro (Recommended — Full-Stack & Architecture)" },
|
||||
{ id: "gemini-3.0-pro", label: "Gemini 3.0 Pro (Deep Reasoning & Complex Refactors)" },
|
||||
{ id: "gemini-3.1-flash", label: "Gemini 3.1 Flash (Fast Iteration & UI Prototyping)" },
|
||||
{ id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash-Lite (Lightweight & Rapid Tasks)" },
|
||||
{ id: "claude-sonnet-4.5", label: "Claude 4.5 Sonnet (via Antigravity Model Selector)" },
|
||||
{ id: "claude-opus-4.5", label: "Claude 4.5 Opus (via Antigravity Model Selector)" }
|
||||
]
|
||||
},
|
||||
codex: {
|
||||
id: "codex",
|
||||
label: "🤖 OpenAI Codex",
|
||||
protocol: "codex://threads/new",
|
||||
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)" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export function slugifyIdeaTitle(title, id = "") {
|
||||
const base = String(title || "spark")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.slice(0, 36);
|
||||
return base || `spark-${id || "app"}`;
|
||||
}
|
||||
|
||||
export function buildDevelopmentSpecMarkdown({
|
||||
idea,
|
||||
targetApp = "antigravity",
|
||||
model = "",
|
||||
includeHomelab = true,
|
||||
extraInstructions = "",
|
||||
publicBaseUrl = "https://sparkvault.vloser.us",
|
||||
userBackground = DEFAULT_USER_BACKGROUND
|
||||
}) {
|
||||
const targetMeta = DEV_HANDOFF_TARGETS[targetApp] || DEV_HANDOFF_TARGETS.antigravity;
|
||||
const chosenModel = model || targetMeta.defaultModel;
|
||||
const slug = slugifyIdeaTitle(idea.title, idea.id);
|
||||
|
||||
const lines = [
|
||||
`# 🚀 Development Handoff Specification: ${idea.title}`,
|
||||
``,
|
||||
`- **Target Environment**: ${targetMeta.label}`,
|
||||
`- **Selected AI Model**: \`${chosenModel}\``,
|
||||
`- **Spark ID**: #${idea.id} (\`${slug}\`)`,
|
||||
`- **Category**: \`${idea.category}\` | **Current Stage**: \`${idea.status}\` | **Priority Score**: ${idea.composite_score}/100`,
|
||||
`- **Ratings**: 🔥 Excitement ${idea.rating_excitement}/5 • 🎯 Impact ${idea.rating_impact}/5 • ⚡ Ease ${idea.rating_ease}/5`,
|
||||
`- **Tags**: ${(idea.tags || []).map((t) => `#${t}`).join(", ") || "None"}`,
|
||||
``
|
||||
];
|
||||
|
||||
if (idea.elevator_pitch) {
|
||||
lines.push(`## 💡 Elevator Pitch`, idea.elevator_pitch, ``);
|
||||
}
|
||||
|
||||
if (idea.notes_markdown) {
|
||||
lines.push(`## 📝 Detailed Specs, Brainstorm & Notes`, idea.notes_markdown, ``);
|
||||
}
|
||||
|
||||
if (idea.checklist && idea.checklist.length > 0) {
|
||||
lines.push(
|
||||
`## ✅ Action Checklist & Requirements`,
|
||||
...idea.checklist.map((c) => `- [${c.done ? "x" : " "}] ${c.text}`),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if ((idea.pros && idea.pros.length > 0) || (idea.cons && idea.cons.length > 0)) {
|
||||
lines.push(`## ⚖️ Pros, Value Props & Constraints`);
|
||||
if (idea.pros?.length) {
|
||||
lines.push(`### Why It Works (Pros)`, ...idea.pros.map((p) => `- ✅ ${p}`), ``);
|
||||
}
|
||||
if (idea.cons?.length) {
|
||||
lines.push(`### Risks & Constraints (Cons)`, ...idea.cons.map((c) => `- ⚠️ ${c}`), ``);
|
||||
}
|
||||
}
|
||||
|
||||
if (idea.whack_history && idea.whack_history.length > 0) {
|
||||
lines.push(
|
||||
`## 🃏 Creative Direction & Lateral Twists`,
|
||||
...idea.whack_history.map(
|
||||
(w) => `- **Card #${w.cardNumber} (${w.cardTitle})**: ${w.reflection}`
|
||||
),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (idea.mockups && idea.mockups.length > 0) {
|
||||
lines.push(
|
||||
`## 🖼️ Visual Mockups & Design Assets`,
|
||||
...idea.mockups.map((m) => {
|
||||
const fullUrl = m.imageUrl?.startsWith("http")
|
||||
? m.imageUrl
|
||||
: `${publicBaseUrl}${m.imageUrl}`;
|
||||
return `- **${m.title}** (\`${m.template || "mockup"}\`, generated via \`${m.modelUsed || "AI"}\`): ${fullUrl}${m.prompt ? `\n - Prompt: *${m.prompt}*` : ""}`;
|
||||
}),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (idea.attachments && idea.attachments.length > 0) {
|
||||
lines.push(
|
||||
`## 📎 Attached Reference Notes & Images`,
|
||||
...idea.attachments.map((a) => {
|
||||
const imgPart = a.imageUrl
|
||||
? ` — Image: ${a.imageUrl.startsWith("http") ? a.imageUrl : `${publicBaseUrl}${a.imageUrl}`}`
|
||||
: "";
|
||||
const srcPart = a.sourceUrl ? ` (Source: ${a.sourceUrl})` : "";
|
||||
const txtPart = a.textContent ? `\n > ${a.textContent.replace(/\n/g, "\n > ")}` : "";
|
||||
return `- **[${a.type.toUpperCase()}] ${a.title}**${imgPart}${srcPart}${txtPart}`;
|
||||
}),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (idea.links && idea.links.length > 0) {
|
||||
lines.push(
|
||||
`## 🔗 Reference Links`,
|
||||
...idea.links.map((l) => `- [${l.title || l.url}](${l.url})`),
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (userBackground) {
|
||||
lines.push(`## 👤 Creator Background & Preferences`, userBackground, ``);
|
||||
}
|
||||
|
||||
if (includeHomelab) {
|
||||
lines.push(
|
||||
`## 🛠️ Target Architecture & Homelab Deployment Requirements`,
|
||||
`1. Build a clean, self-contained full-stack web application (or service) for **${idea.title}** (\`${slug}\`).`,
|
||||
`2. Create/push a Gitea repository at \`https://gitea.vloser.us/chrisf/${slug}\`.`,
|
||||
`3. Deploy via Docker Compose on \`vloser\` (\`/home/chrisf/docker/${slug}\`) with persistent storage under \`/home/chrisf/docker/appdata/${slug}\`.`,
|
||||
`4. Expose via Traefik dynamic config at \`https://${slug}.vloser.us\` and add an entry with a custom SVG icon to Homepage (\`/home/chrisf/docker/appdata/homepage/services.yaml\`).`,
|
||||
`5. Include mobile-responsive UI styling and automated verification tests.`,
|
||||
``
|
||||
);
|
||||
}
|
||||
|
||||
if (extraInstructions && extraInstructions.trim()) {
|
||||
lines.push(`## 🎯 Additional Development Instructions`, extraInstructions.trim(), ``);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export async function executeDevelopmentHandoff({
|
||||
idea,
|
||||
targetApp = "antigravity",
|
||||
model = "",
|
||||
customPrompt = "",
|
||||
includeHomelab = true,
|
||||
extraInstructions = "",
|
||||
executeServerCodex = false,
|
||||
publicBaseUrl = "https://sparkvault.vloser.us",
|
||||
userBackground = DEFAULT_USER_BACKGROUND,
|
||||
workspacesRootDir
|
||||
}) {
|
||||
const targetMeta = DEV_HANDOFF_TARGETS[targetApp] || DEV_HANDOFF_TARGETS.antigravity;
|
||||
const chosenModel = model || targetMeta.defaultModel;
|
||||
const slug = slugifyIdeaTitle(idea.title, idea.id);
|
||||
|
||||
const compiledSpec =
|
||||
customPrompt && customPrompt.trim()
|
||||
? customPrompt.trim()
|
||||
: buildDevelopmentSpecMarkdown({
|
||||
idea,
|
||||
targetApp,
|
||||
model: chosenModel,
|
||||
includeHomelab,
|
||||
extraInstructions,
|
||||
publicBaseUrl,
|
||||
userBackground
|
||||
});
|
||||
|
||||
let workspaceDir = "";
|
||||
if (workspacesRootDir) {
|
||||
workspaceDir = path.join(workspacesRootDir, `${idea.id}-${slug}`);
|
||||
fs.mkdirSync(workspaceDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(workspaceDir, "SPEC.md"), compiledSpec, "utf8");
|
||||
fs.writeFileSync(
|
||||
path.join(workspaceDir, "spark-context.json"),
|
||||
JSON.stringify({ idea, targetApp, model: chosenModel, createdAt: new Date().toISOString() }, null, 2),
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
const specUrl = `${publicBaseUrl}/api/ideas/${idea.id}/spec.md?target=${encodeURIComponent(targetApp)}&model=${encodeURIComponent(chosenModel)}`;
|
||||
|
||||
// Build OS Deep Link URL for the corresponding desktop application
|
||||
let deepLinkUrl = "";
|
||||
let cliCommand = "";
|
||||
|
||||
if (targetApp === "codex") {
|
||||
// Keep URL prompt within safe Windows protocol command-line limits (~1600 chars)
|
||||
const compactPrompt =
|
||||
compiledSpec.length <= 1500
|
||||
? compiledSpec
|
||||
: `${compiledSpec.slice(0, 1350)}\n\n[Full specification copied to clipboard & hosted at: ${specUrl}]`;
|
||||
const codexUrl = new URL("codex://threads/new");
|
||||
codexUrl.searchParams.set("mode", "codex");
|
||||
codexUrl.searchParams.set("prompt", compactPrompt);
|
||||
deepLinkUrl = codexUrl.toString();
|
||||
cliCommand = `codex -m ${chosenModel} "Read SPEC.md and implement ${idea.title}"`;
|
||||
} else {
|
||||
// Antigravity desktop deep link + CLI command
|
||||
deepLinkUrl = `antigravity://new?model=${encodeURIComponent(chosenModel)}&sparkId=${idea.id}`;
|
||||
cliCommand = `agy --model ${chosenModel}`;
|
||||
}
|
||||
|
||||
let codexExecution = null;
|
||||
if (targetApp === "codex" && executeServerCodex && isCodexConfigured()) {
|
||||
try {
|
||||
const reportFile = workspaceDir
|
||||
? path.join(workspaceDir, "CODEX_PLAN.md")
|
||||
: path.join(os.tmpdir(), `codex-plan-${idea.id}-${Date.now()}.md`);
|
||||
const args = [
|
||||
"exec",
|
||||
"-m",
|
||||
chosenModel,
|
||||
"--skip-git-repo-check",
|
||||
"-o",
|
||||
reportFile,
|
||||
`You are an autonomous senior engineer. Review the following SparkVault project specification and produce a complete implementation blueprint, file structure, and starter code plan:\n\n${compiledSpec}`
|
||||
];
|
||||
if (workspaceDir) {
|
||||
args.splice(4, 0, "-C", workspaceDir);
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn("codex", args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: { ...process.env }
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("close", () => resolve());
|
||||
});
|
||||
|
||||
if (fs.existsSync(reportFile)) {
|
||||
const planText = fs.readFileSync(reportFile, "utf8").trim();
|
||||
if (planText) {
|
||||
codexExecution = {
|
||||
ran: true,
|
||||
model: chosenModel,
|
||||
output: planText.slice(0, 6000)
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
codexExecution = {
|
||||
ran: false,
|
||||
error: err.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
targetApp,
|
||||
targetLabel: targetMeta.label,
|
||||
model: chosenModel,
|
||||
slug,
|
||||
specMarkdown: compiledSpec,
|
||||
specUrl,
|
||||
deepLinkUrl,
|
||||
cliCommand,
|
||||
workspaceDir,
|
||||
codexExecution,
|
||||
submittedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+101
-1
@@ -19,8 +19,11 @@ import {
|
||||
} from "./nano_banana.js";
|
||||
import {
|
||||
DEFAULT_USER_BACKGROUND,
|
||||
DEV_HANDOFF_TARGETS,
|
||||
generatePersonalizedRandomIdea,
|
||||
generateNameSuggestions
|
||||
generateNameSuggestions,
|
||||
buildDevelopmentSpecMarkdown,
|
||||
executeDevelopmentHandoff
|
||||
} from "./idea_generator.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -257,6 +260,103 @@ export function createAppServer({
|
||||
return sendJson(res, 200, result);
|
||||
}
|
||||
|
||||
// 3c. GET /api/ideas/:id/spec.md (Download / Fetch Compiled Development Spec)
|
||||
const specMdMatch = pathname.match(/^\/api\/ideas\/(\d+)\/spec\.md$/);
|
||||
if (method === "GET" && specMdMatch) {
|
||||
const id = Number(specMdMatch[1]);
|
||||
const idea = store.getIdea(id);
|
||||
if (!idea) return sendJson(res, 404, { error: "Idea not found" });
|
||||
|
||||
const targetApp = parsedUrl.searchParams.get("target") || "antigravity";
|
||||
const model = parsedUrl.searchParams.get("model") || "";
|
||||
const includeHomelab = parsedUrl.searchParams.get("includeHomelab") !== "false";
|
||||
const userBackground =
|
||||
store.getSetting("user_background", DEFAULT_USER_BACKGROUND) || DEFAULT_USER_BACKGROUND;
|
||||
const host = req.headers.host || "sparkvault.vloser.us";
|
||||
const proto = req.headers["x-forwarded-proto"] || "https";
|
||||
const publicBaseUrl = `${proto}://${host}`;
|
||||
|
||||
const md = buildDevelopmentSpecMarkdown({
|
||||
idea,
|
||||
targetApp,
|
||||
model,
|
||||
includeHomelab,
|
||||
publicBaseUrl,
|
||||
userBackground
|
||||
});
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/markdown; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="spark-${idea.id}-spec.md"`
|
||||
});
|
||||
res.end(md);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3d. POST /api/ideas/:id/dev-handoff (Submit Idea to Antigravity or Codex for Development)
|
||||
const devHandoffMatch = pathname.match(/^\/api\/ideas\/(\d+)\/dev-handoff$/);
|
||||
if (method === "POST" && devHandoffMatch) {
|
||||
const id = Number(devHandoffMatch[1]);
|
||||
const idea = store.getIdea(id);
|
||||
if (!idea) return sendJson(res, 404, { error: "Idea not found" });
|
||||
|
||||
const body = await readJsonBody(req);
|
||||
const targetApp = body.targetApp === "codex" ? "codex" : "antigravity";
|
||||
const model = String(body.model || "").trim();
|
||||
const customPrompt = String(body.customPrompt || "").trim();
|
||||
const extraInstructions = String(body.extraInstructions || "").trim();
|
||||
const includeHomelab = body.includeHomelab !== false;
|
||||
const moveStatusToBuilding = body.moveStatusToBuilding !== false;
|
||||
const executeServerCodex = Boolean(body.executeServerCodex);
|
||||
|
||||
const userBackground =
|
||||
store.getSetting("user_background", DEFAULT_USER_BACKGROUND) || DEFAULT_USER_BACKGROUND;
|
||||
const host = req.headers.host || "sparkvault.vloser.us";
|
||||
const proto = req.headers["x-forwarded-proto"] || "https";
|
||||
const publicBaseUrl = `${proto}://${host}`;
|
||||
const workspacesRootDir = path.join(path.dirname(mockupsDir), "dev_workspaces");
|
||||
|
||||
const handoff = await executeDevelopmentHandoff({
|
||||
idea,
|
||||
targetApp,
|
||||
model,
|
||||
customPrompt,
|
||||
includeHomelab,
|
||||
extraInstructions,
|
||||
executeServerCodex,
|
||||
publicBaseUrl,
|
||||
userBackground,
|
||||
workspacesRootDir
|
||||
});
|
||||
|
||||
// Record the handoff on the Spark and optionally move to "building"
|
||||
const handoffAttachment = {
|
||||
id: `dev-${Date.now()}`,
|
||||
type: "note",
|
||||
title: `🚀 Submitted to ${handoff.targetLabel} (${handoff.model})`,
|
||||
textContent: handoff.codexExecution?.output
|
||||
? `Submitted to ${handoff.targetLabel} using model ${handoff.model}.\n\n--- CODEX IMPLEMENTATION PLAN ---\n${handoff.codexExecution.output}`
|
||||
: `Submitted full Spark specification to ${handoff.targetLabel} using model \`${handoff.model}\`.`,
|
||||
sourceUrl: handoff.specUrl,
|
||||
imageUrl: "",
|
||||
createdAt: handoff.submittedAt
|
||||
};
|
||||
|
||||
const patch = {
|
||||
attachments: [handoffAttachment, ...(idea.attachments || [])]
|
||||
};
|
||||
if (moveStatusToBuilding && (idea.status === "spark" || idea.status === "exploring")) {
|
||||
patch.status = "building";
|
||||
}
|
||||
const updatedIdea = store.updateIdea(id, patch);
|
||||
|
||||
return sendJson(res, 200, {
|
||||
idea: updatedIdea,
|
||||
handoff,
|
||||
devTargets: DEV_HANDOFF_TARGETS
|
||||
});
|
||||
}
|
||||
|
||||
// 4. GET /api/whack-cards
|
||||
if (method === "GET" && pathname === "/api/whack-cards") {
|
||||
const allIdeas = store.listIdeas({});
|
||||
|
||||
@@ -246,5 +246,31 @@ test("Suggest Names endpoint returns 6 creative name and tagline suggestions for
|
||||
assert.ok(data.suggestions[0].tagline);
|
||||
});
|
||||
|
||||
test("Development Handoff endpoint compiles full Spark spec and submits to Antigravity or Codex with model picker", async () => {
|
||||
const specRes = await fetch(`${baseUrl}/api/ideas/1/spec.md?target=codex&model=gpt-5.6-sol`);
|
||||
assert.equal(specRes.status, 200);
|
||||
const specText = await specRes.text();
|
||||
assert.match(specText, /Development Handoff Specification/);
|
||||
assert.match(specText, /gpt-5\.6-sol/);
|
||||
|
||||
const handoffRes = await fetch(`${baseUrl}/api/ideas/1/dev-handoff`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
targetApp: "codex",
|
||||
model: "gpt-5.6-sol",
|
||||
includeHomelab: true,
|
||||
moveStatusToBuilding: true
|
||||
})
|
||||
});
|
||||
assert.equal(handoffRes.status, 200);
|
||||
const handoffData = await handoffRes.json();
|
||||
assert.equal(handoffData.handoff.targetApp, "codex");
|
||||
assert.equal(handoffData.handoff.model, "gpt-5.6-sol");
|
||||
assert.match(handoffData.handoff.deepLinkUrl, /^codex:\/\/threads\/new/);
|
||||
assert.equal(handoffData.idea.status, "building");
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user