From cae5df8e1e4a28efc1014f049c65fb8fe7dc7c20 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 14 May 2026 21:51:38 +0100 Subject: [PATCH 01/19] Surface quality warnings and track warning analytics --- components/dashboard/assets-manager.tsx | 23 ++++++++++++++++++- lib/services/generations/runner.ts | 30 ++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/components/dashboard/assets-manager.tsx b/components/dashboard/assets-manager.tsx index 21e0ec7..4f1d653 100644 --- a/components/dashboard/assets-manager.tsx +++ b/components/dashboard/assets-manager.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useTransition } from "react"; -import { Download, FileArchive, ImageIcon, PencilLine, RefreshCw, Sparkles } from "lucide-react"; +import { AlertTriangle, Download, FileArchive, ImageIcon, PencilLine, RefreshCw, Sparkles } from "lucide-react"; import type { AssetRecord, GenerationRecord } from "@/types/project"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -69,6 +69,14 @@ export function AssetsManager({ const heroAsset = assets.find((asset) => asset.asset_type.includes("hero")) ?? assets[0]; const listingAssets = assets.filter((asset) => asset.asset_type.includes("listing")); + function warningMessages(asset: AssetRecord) { + const metadata = asset.metadata_json as { quality_report?: { issues?: Array<{ severity?: string; message?: string }> } } | null; + const issues = metadata?.quality_report?.issues || []; + return issues + .filter((issue) => issue.severity === "warning" && typeof issue.message === "string") + .map((issue) => issue.message as string); + } + return (
{!canDownloadFull ? ( @@ -179,6 +187,19 @@ export function AssetsManager({ {(asset.metadata_json as { render_source?: string } | null)?.render_source === "mistral_image_generation" ? "Mistral image generated" : "Template fallback"}

+ {warningMessages(asset).length ? ( +
+

+ + Quality warnings +

+
+ {warningMessages(asset).map((message, index) => ( +

{message}

+ ))} +
+
+ ) : null}
diff --git a/lib/services/generations/runner.ts b/lib/services/generations/runner.ts index c0916b0..aeda008 100644 --- a/lib/services/generations/runner.ts +++ b/lib/services/generations/runner.ts @@ -101,6 +101,7 @@ export async function runGenerationForProject(project: ProjectRecord, uploads: U const zip = new JSZip(); const renderSources: Record = {}; const qualityFailures: Array<{ assetType: string; issues: string[] }> = []; + const qualityWarnings: QualityFailureDetail[] = []; for (const [index, asset] of deterministicAssets.entries()) { let renderSource: "mistral_image_generation" | "deterministic_template" = "mistral_image_generation"; @@ -125,6 +126,14 @@ export async function runGenerationForProject(project: ProjectRecord, uploads: U continue; } + for (const issue of qualityReport.issues.filter((item) => item.severity === "warning")) { + qualityWarnings.push({ + asset_type: asset.asset_type, + code: issue.code, + message: issue.message + }); + } + try { fullPng = await generateMistralAssetWithRetry({ plan: safePlan, @@ -203,8 +212,27 @@ export async function runGenerationForProject(project: ProjectRecord, uploads: U await supabase.storage.from(ASSET_BUCKET).upload(zipPath, zipBuffer, { contentType: "application/zip", upsert: true }); const { data: zipUrl } = supabase.storage.from(ASSET_BUCKET).getPublicUrl(zipPath); - await supabase.from("generations").update({ status: "completed", style_json: { ...safePlan, zip_url: zipUrl.publicUrl, render_sources: renderSources } }).eq("id", generation.id); + await supabase + .from("generations") + .update({ + status: "completed", + style_json: { ...safePlan, zip_url: zipUrl.publicUrl, render_sources: renderSources, quality_warnings: qualityWarnings } + }) + .eq("id", generation.id); await supabase.from("projects").update({ status: "completed" }).eq("id", project.id); + if (qualityWarnings.length) { + await trackEvent({ + userId: project.user_id, + projectId: project.id, + eventType: "quality_warning", + metadata: { + generationId: generation.id, + projectName: project.name, + warning_codes: qualityWarnings.map((item) => item.code), + warning_assets: qualityWarnings.map((item) => item.asset_type) + } + }); + } await trackEvent({ userId: project.user_id, projectId: project.id, From 6ad4876c4dbc05ecab6b24a567575f194e8e8db3 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 15 May 2026 06:58:58 +0100 Subject: [PATCH 02/19] Fix missing quality warning type for deploy build --- lib/services/generations/runner.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/services/generations/runner.ts b/lib/services/generations/runner.ts index aeda008..b3a2fcc 100644 --- a/lib/services/generations/runner.ts +++ b/lib/services/generations/runner.ts @@ -15,6 +15,12 @@ const ASSET_BUCKET = process.env.STORAGE_BUCKET_ASSETS || "launchpix-assets"; const MISTRAL_ASSET_TIMEOUT_MS = 45_000; const MISTRAL_RENDER_ATTEMPTS = 2; +type QualityFailureDetail = { + asset_type: string; + code: string; + message: string; +}; + function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } From 14dcddaf979a2ea2543ccdfa269b7a34b2a9b5fd Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 16 May 2026 03:07:41 +0100 Subject: [PATCH 03/19] Add structured quality report to generation panel --- components/dashboard/generate-panel.tsx | 83 ++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/components/dashboard/generate-panel.tsx b/components/dashboard/generate-panel.tsx index 564b887..f61c17f 100644 --- a/components/dashboard/generate-panel.tsx +++ b/components/dashboard/generate-panel.tsx @@ -6,7 +6,13 @@ import { useRouter } from "next/navigation"; import { AlertCircle, CheckCircle2, Clock3, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; -type Gen = { id: string; status: string; error_message?: string | null } | null; +type QualityIssue = { asset_type: string; code: string; message: string }; +type Gen = { + id: string; + status: string; + error_message?: string | null; + style_json?: { quality_failures?: QualityIssue[]; quality_warnings?: QualityIssue[] } | null; +} | null; const statusLabel: Record = { queued: "Queued for processing", @@ -28,6 +34,32 @@ export function GeneratePanel({ projectId, ready, missing, credits }: { projectI const currentStatus = generation?.status || "idle"; const busy = ["queued", "analyzing", "generating_copy", "rendering_assets"].includes(currentStatus); + const qualityFailures = generation?.style_json?.quality_failures || []; + const qualityWarnings = generation?.style_json?.quality_warnings || []; + + function fixActionHref(code: string) { + if (code.includes("screenshot")) return `/dashboard/projects/new?projectId=${projectId}&step=2`; + return `/dashboard/projects/new?projectId=${projectId}&step=1`; + } + + function fixActionLabel(code: string) { + if (code.includes("screenshot")) return "Upload screenshots"; + if (code.includes("headline") || code.includes("subheadline") || code.includes("callout") || code.includes("cta")) return "Edit project brief"; + if (code.includes("contrast")) return "Adjust style/color"; + return "Review project details"; + } + + const groupedFailures = qualityFailures.reduce>((acc, item) => { + const key = `${item.asset_type}::${item.code}`; + acc[key] = acc[key] ? [...acc[key], item] : [item]; + return acc; + }, {}); + + const groupedWarnings = qualityWarnings.reduce>((acc, item) => { + const key = `${item.asset_type}::${item.code}`; + acc[key] = acc[key] ? [...acc[key], item] : [item]; + return acc; + }, {}); async function fetchState() { const res = await fetch(`/api/generations/${projectId}`); @@ -161,7 +193,54 @@ export function GeneratePanel({ projectId, ready, missing, credits }: { projectI Sign in again ) : null} - {generation?.status === "failed" && !needsCredits && !sessionExpired ? ( + {generation?.status === "failed" && qualityFailures.length > 0 ? ( +
+

Quality checks blocked export. Fix these and regenerate:

+
+ {Object.entries(groupedFailures) + .slice(0, 6) + .map(([key, failures]) => { + const [assetType, code] = key.split("::"); + const first = failures[0]; + return ( +
+
+

{assetType.replaceAll("_", " ")}

+ {code} +
+

{first.message}

+ + {fixActionLabel(code)} + +
+ ); + })} +
+
+ ) : null} + {generation?.status === "completed" && qualityWarnings.length > 0 ? ( +
+

Export ready, but quality warnings were detected:

+
+ {Object.entries(groupedWarnings) + .slice(0, 4) + .map(([key, warnings]) => { + const [assetType, code] = key.split("::"); + const first = warnings[0]; + return ( +
+
+

{assetType.replaceAll("_", " ")}

+ {code} +
+

{first.message}

+
+ ); + })} +
+
+ ) : null} + {generation?.status === "failed" && !needsCredits && !sessionExpired && qualityFailures.length === 0 ? (

If quality checks fail, shorten copy lines, keep callouts concise, and ensure screenshots are uploaded.

) : null}
From 336975042360a3fb2e7203e9ac18eecf22e20216 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 05:36:36 +0100 Subject: [PATCH 04/19] Rebrand LaunchPix for Talocode and API-first usage --- README.md | 158 ++++++++++++++++++------------------------------------ 1 file changed, 53 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 12c52b8..7827298 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,74 @@ -# LaunchPix +# Talocode LaunchPix -LaunchPix is a Mistral-assisted asset generator for product launches. -It turns raw screenshots into polished listing visuals, promo tiles, and hero banners. +Talocode LaunchPix is an API-first, open-source launch visual engine. +It turns product screenshots into listing frames, promo tiles, and hero banners with deterministic fallback rendering. -## Design system -- `DESIGN.md` is the canonical design brain for the product UI. -- `docs/design-md/google-designmd-spec.md` is a local copy of the Google DESIGN.md specification. -- `docs/design-md/README.md` explains how to use both files in this repo. +## Repository +- Canonical repo: `https://github.com/talocode/launchpix` -## Tech stack -- Next.js App Router + TypeScript -- Tailwind CSS + reusable UI primitives -- Supabase (Auth, Postgres, Storage) -- Mistral structured planning and image generation -- Deterministic SVG -> PNG fallback rendering (`@resvg/resvg-js`) -- Lemon Squeezy credit-pack billing and webhook fulfillment - -## Core product flow -1. Sign in -2. Create project and upload screenshots -3. Generate structured asset plan via Mistral -4. Generate image assets through a Mistral image-generation agent -5. Preview/download assets while credits remain +## Product direction +- API first: developer endpoints live under `/api/v1/*`. +- Open source core: code is public, but API usage requires `LAUNCHPIX_API_KEY`. +- Credits model: users start with free credits, then buy one-time credit packs. -## Pricing model implemented -- Every account starts with 300 credits. -- Existing accounts are raised to at least 300 credits by `0004_credit_balance_model.sql`. -- Billing is credit based, not subscription based. -- Users buy one-time Lemon Squeezy credit packs after exhausting their included credits. - -## Required environment variables +## Core stack +- Next.js App Router + TypeScript +- Supabase (Postgres, Storage) +- Mistral (planning + image generation) +- Lemon Squeezy (credit-pack checkout + webhook fulfillment) +- Resend (transactional email) + +## API authentication +Every `/api/v1/*` request must include: +- `x-launchpix-api-key: ` +- `x-launchpix-user-id: ` + +Supported alternatives: +- `x-api-key` +- `Authorization: Bearer ` + +## Developer API endpoints +- `GET /api/v1/projects` +- `POST /api/v1/projects` +- `GET /api/v1/projects/:projectId/generate` +- `POST /api/v1/projects/:projectId/generate` + +## Environment variables See `.env.example`. -Minimum required: + +Critical keys: - `NEXT_PUBLIC_APP_URL` - `NEXT_PUBLIC_SUPABASE_URL` - `NEXT_PUBLIC_SUPABASE_ANON_KEY` - `SUPABASE_SERVICE_ROLE_KEY` - `DATABASE_URL` +- `NEXTAUTH_SECRET` +- `GOOGLE_CLIENT_ID` +- `GOOGLE_CLIENT_SECRET` - `MISTRAL_API_KEY` -- `MISTRAL_MODEL_VISION` - `MISTRAL_MODEL_TEXT` +- `MISTRAL_MODEL_VISION` - `MISTRAL_IMAGE_MODEL` - `MISTRAL_IMAGE_AGENT_ID` (optional) -- `LEMON_SQUEEZY_API_KEY` -- `LEMON_SQUEEZY_STORE_ID` -- `LEMON_SQUEEZY_WEBHOOK_SECRET` -- `LEMON_SQUEEZY_STARTER_CREDITS_VARIANT_ID` -- `LEMON_SQUEEZY_CREATOR_CREDITS_VARIANT_ID` -- `LEMON_SQUEEZY_STUDIO_CREDITS_VARIANT_ID` - -Optional for Supabase CLI workflows: -- `SUPABASE_ACCESS_TOKEN` -- `SUPABASE_DB_PASSWORD` +- `LAUNCHPIX_API_KEY` +- `LEMON_SQUEEZY_*` +- `RESEND_API_KEY` ## Local setup -1. Copy env: - - `cp .env.example .env.local` -2. Install dependencies: - - `npm install` -4. Apply database migrations using one of these options: - - Supabase CLI: `npx supabase db push --linked` - - or run the SQL files in `supabase/migrations/` in order -5. Start dev server: - - `npm run dev` +1. Copy env file: `cp .env.example .env.local` +2. Install: `npm install` +3. Apply DB migrations: `npx supabase db push --linked` +4. Start app: `npm run dev` -Recommended validation commands: +Validation: - `npm run typecheck` - `npm run build` -## Supabase notes -- Enable email auth. -- Ensure storage buckets exist: - - `project-uploads-raw` - - `project-uploads-normalized` - - `launchpix-assets` -- Apply RLS policies from migrations. -- If you use the Supabase CLI, link the project first with `npx supabase link`. - -## Mistral notes -- Mistral is used for structured product/copy/layout planning. -- Final image assets are generated through a Mistral Agent with the built-in `image_generation` tool. -- Planning default model: `mistral-small-2506` (configurable via env). -- Image generation default model: `mistral-medium-latest` (configurable via `MISTRAL_IMAGE_MODEL`). -- `MISTRAL_IMAGE_AGENT_ID` can point to a pre-created image-generation agent. If it is omitted, LaunchPix creates an agent at runtime. -- If Mistral image generation fails, LaunchPix falls back to deterministic SVG -> PNG rendering so generation does not hard-fail. - -## Lemon Squeezy notes -- Checkout init: `POST /api/billing/checkout` -- Verification: purchases are fulfilled by webhook after Lemon Squeezy confirms the order -- Webhook: `POST /api/billing/webhook` -- Configure Lemon Squeezy webhook URL to point to `/api/billing/webhook`. -- Select the `order_created` event for credit fulfillment. -- Create three Lemon Squeezy variants and map them to the variant ID env vars in `.env.example`. - -## Commands -- `npm run dev` -- `npm run lint` -- `npm run typecheck` -- `npm run build` -- `npm run video:studio` -- `npm run video:render` -- `npm run video:render:chrome` on Windows if Remotion cannot download Chrome Headless Shell - -## Demo video -- The Remotion demo composition lives in `remotion/`. -- Rendered output is written to `output/launchpix-demo.mp4`. -- The video explains the core LaunchPix story: project brief, screenshot uploads, Mistral planning, image generation, fallback rendering, exports, credits, and billing. - -## Deployment notes -- Set all env vars in hosting provider. -- `NEXT_PUBLIC_APP_URL` must be set in the hosting provider's production environment to your live domain; `.env.local` is only used locally. -- Use HTTPS and production callback URLs for Lemon Squeezy. -- Auth confirmation and billing redirects are built from `NEXT_PUBLIC_APP_URL`, so production must not point this to localhost. -- Keep `package-lock.json` committed so CI and hosting builds install the same dependency tree. -- Confirm webhook signature secret matches deployment env. - -## Netlify notes -- Build command: `npm run build` -- Install command: `npm install` or `npm ci` -- The app relies on `@resvg/resvg-js` during server rendering, so the current `next.config.ts` must be preserved in deployments. +## Render deployment +- Render config is in [`render.yaml`](/C:/Users/Hp/Documents/Github/LaunchPix/render.yaml). +- Build command: `npm ci && npm run build` +- Start command: `npm run start` +- Set all required env vars in Render dashboard. -## Known MVP constraints -- Rate limiting is lightweight (in-memory). -- Credit packs are one-time purchases; subscription renewal automation is intentionally not used. -- Visual templates are production-capable baseline and can be expanded further. +## Legacy note +Previous Netlify-specific deployment notes were removed in favor of Render as the primary target. From b0196730fc636e51e59afe174fd084774702ca5f Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 05:37:42 +0100 Subject: [PATCH 05/19] Add Talocode and API-first env vars --- .env.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.env.example b/.env.example index 0d9f73a..ba78b00 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,8 @@ # Public app URL used for auth/billing callbacks NEXT_PUBLIC_APP_URL=http://localhost:3000 +NEXTAUTH_SECRET= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= # Supabase client config NEXT_PUBLIC_SUPABASE_URL= @@ -20,6 +23,7 @@ MISTRAL_IMAGE_MODEL=mistral-medium-latest # Optional: pre-created Mistral agent with the image_generation tool enabled. # If omitted, LaunchPix creates one at runtime. MISTRAL_IMAGE_AGENT_ID= +LAUNCHPIX_API_KEY= # Lemon Squeezy credit billing LEMON_SQUEEZY_API_KEY= @@ -28,6 +32,9 @@ LEMON_SQUEEZY_WEBHOOK_SECRET= LEMON_SQUEEZY_STARTER_CREDITS_VARIANT_ID= LEMON_SQUEEZY_CREATOR_CREDITS_VARIANT_ID= LEMON_SQUEEZY_STUDIO_CREDITS_VARIANT_ID= +RESEND_API_KEY= +RESEND_FROM_EMAIL= +RESEND_WEBHOOK_SECRET= # Storage buckets STORAGE_BUCKET_ASSETS=launchpix-assets From 73ccd1b3a95512713b3f23973dc6ee39624a9f93 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 05:38:05 +0100 Subject: [PATCH 06/19] Rebrand metadata for Talocode LaunchPix --- app/layout.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index b389919..1861fc2 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,22 +4,22 @@ import "./globals.css"; import { ThemeProvider } from "@/components/ui/theme-provider"; export const metadata: Metadata = { - title: "LaunchPix", - description: "Turn raw screenshots into polished launch visuals in minutes.", + title: "Talocode LaunchPix", + description: "API-first, open-source launch asset generation for developer teams.", icons: { icon: "/icon.svg" }, openGraph: { - title: "LaunchPix", - description: "Deterministic AI-assisted launch visuals for product teams.", - url: "https://launchpix.app", - siteName: "LaunchPix", + title: "Talocode LaunchPix", + description: "API-first launch visual generation with deterministic fallbacks.", + url: "https://launchpix.talocode.com", + siteName: "Talocode LaunchPix", type: "website" }, twitter: { card: "summary_large_image", - title: "LaunchPix", - description: "Turn raw screenshots into polished launch visuals in minutes." + title: "Talocode LaunchPix", + description: "Open-source launch visuals API for product and growth teams." } }; From 770969173c3af40938657b70ea7bd6496036bcc9 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 05:38:25 +0100 Subject: [PATCH 07/19] Rebrand homepage metadata for Talocode LaunchPix --- app/page.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index ec299e4..5b15b18 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,17 +4,17 @@ import { LandingSections } from "@/components/marketing/landing-sections"; import { MarketingFooter } from "@/components/marketing/footer"; export const metadata: Metadata = { - title: "LaunchPix | Turn unfinished screenshots into launch-ready visuals", - description: "Generate store-ready screenshot packs, promo tiles, and hero banners from raw product captures.", + title: "Talocode LaunchPix | API-first launch visual generation", + description: "Open-source API for launch-ready screenshot packs, promo tiles, and hero banners.", openGraph: { - title: "LaunchPix", - description: "Turn unfinished screenshots into launch-ready visual packs.", - url: "https://launchpix.app" + title: "Talocode LaunchPix", + description: "Build launch visual workflows on a developer-first API.", + url: "https://launchpix.talocode.com" }, twitter: { card: "summary_large_image", - title: "LaunchPix", - description: "Preview launch-ready screenshot packs before production export." + title: "Talocode LaunchPix", + description: "API-first launch visual generation for product teams." } }; From c93a898b5801a37990061118b5c130a8ea5f9c96 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 05:38:51 +0100 Subject: [PATCH 08/19] Shift app chrome to Talocode monochrome palette --- app/globals.css | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/globals.css b/app/globals.css index f7776f1..74c8a9c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -10,24 +10,24 @@ --muted: 220 24% 96%; --muted-foreground: 220 10% 42%; --border: 220 18% 88%; - --primary: 252 74% 58%; + --primary: 221 83% 53%; --primary-foreground: 0 0% 100%; - --accent: 214 70% 56%; - --ring: 252 74% 58%; + --accent: 217 33% 20%; + --ring: 221 83% 53%; } .dark { - --background: 222 58% 3%; + --background: 0 0% 3%; --foreground: 218 42% 97%; - --card: 222 36% 6%; + --card: 0 0% 6%; --card-foreground: 218 42% 97%; - --muted: 222 28% 10%; + --muted: 0 0% 9%; --muted-foreground: 219 18% 68%; - --border: 222 20% 16%; - --primary: 252 74% 58%; + --border: 0 0% 14%; + --primary: 221 83% 53%; --primary-foreground: 0 0% 100%; - --accent: 214 70% 56%; - --ring: 252 74% 58%; + --accent: 0 0% 18%; + --ring: 221 83% 53%; } * { @@ -62,11 +62,11 @@ body { @layer components { .surface { - @apply rounded-[24px] border border-slate-200 bg-white shadow-[0_28px_80px_-58px_rgba(15,23,42,0.3)] dark:border-white/[0.08] dark:bg-[#070b12] dark:shadow-[0_28px_80px_-58px_rgba(0,0,0,0.9)]; + @apply rounded-[24px] border border-slate-200 bg-white shadow-[0_28px_80px_-58px_rgba(15,23,42,0.3)] dark:border-white/[0.08] dark:bg-[#0a0a0a] dark:shadow-[0_28px_80px_-58px_rgba(0,0,0,0.9)]; } .surface-muted { - @apply rounded-[20px] border border-slate-200 bg-slate-50 dark:border-white/[0.07] dark:bg-[#0b111c]; + @apply rounded-[20px] border border-slate-200 bg-slate-50 dark:border-white/[0.07] dark:bg-[#141414]; } .app-shell { From 9a5e8fcb14f1fd7b172ec217557e459cb20a2822 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 05:39:10 +0100 Subject: [PATCH 09/19] Use Talocode brand asset in logo --- components/brand/logo.tsx | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/components/brand/logo.tsx b/components/brand/logo.tsx index 40b6c88..903f859 100644 --- a/components/brand/logo.tsx +++ b/components/brand/logo.tsx @@ -1,39 +1,20 @@ import { cn } from "@/lib/utils"; +import Image from "next/image"; export function LaunchPixLogo({ className, markClassName }: { className?: string; markClassName?: string }) { return ( - + - LaunchPix - Launch asset studio + Talocode LaunchPix + Open-source launch API ); From cde22a90b5fa31b3b3bdb25d87c23de909b0f1a2 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 06:53:06 +0100 Subject: [PATCH 10/19] Flatten shared buttons to monochrome Talocode styling --- components/ui/button.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ui/button.tsx b/components/ui/button.tsx index e0726de..a473026 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -8,8 +8,8 @@ const buttonVariants = cva( { variants: { variant: { - default: "bg-[#5b5ff7] text-white shadow-[0_18px_44px_-30px_rgba(91,95,247,0.85)] hover:bg-[#686cf8]", - outline: "border border-slate-200 bg-white text-slate-900 hover:border-slate-300 hover:bg-slate-50 dark:border-white/[0.1] dark:bg-[#070b12] dark:text-slate-100 dark:hover:border-white/[0.16] dark:hover:bg-[#0d1320]", + default: "bg-[#050505] text-white shadow-[0_18px_44px_-30px_rgba(0,0,0,0.85)] hover:bg-[#141414]", + outline: "border border-slate-200 bg-white text-slate-900 hover:border-slate-300 hover:bg-slate-50 dark:border-white/[0.12] dark:bg-[#050505] dark:text-white dark:hover:border-white/[0.2] dark:hover:bg-[#111111]", ghost: "text-slate-700 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-300 dark:hover:bg-white/5 dark:hover:text-white" }, size: { From 538e7631c0ba2a57c16e0d659a9eedb80a3a90e9 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 06:56:12 +0100 Subject: [PATCH 11/19] Add LaunchPix API key enforcement --- lib/api-key.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 lib/api-key.ts diff --git a/lib/api-key.ts b/lib/api-key.ts new file mode 100644 index 0000000..0499951 --- /dev/null +++ b/lib/api-key.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; + +const API_KEY_HEADER = "x-launchpix-api-key"; + +export function hasValidLaunchPixApiKey(request: Request): boolean { + const configuredKey = process.env.LAUNCHPIX_API_KEY; + if (!configuredKey) return false; + + const headerKey = request.headers.get(API_KEY_HEADER) ?? request.headers.get("x-api-key"); + const bearer = request.headers.get("authorization"); + const bearerKey = bearer?.toLowerCase().startsWith("bearer ") ? bearer.slice(7).trim() : null; + + const providedKey = headerKey ?? bearerKey; + return Boolean(providedKey && providedKey === configuredKey); +} + +export function requireLaunchPixApiKey(request: Request): NextResponse | null { + if (hasValidLaunchPixApiKey(request)) return null; + + return NextResponse.json( + { + error: "Missing or invalid API key. Get your key and send it as x-launchpix-api-key or Authorization: Bearer ." + }, + { status: 401 } + ); +} From 1f67d08bb74992f023b94f12c84838149fa5e3b7 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 06:56:24 +0100 Subject: [PATCH 12/19] Add API user id helper for developer endpoints --- lib/api-user.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 lib/api-user.ts diff --git a/lib/api-user.ts b/lib/api-user.ts new file mode 100644 index 0000000..eef2bf5 --- /dev/null +++ b/lib/api-user.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; + +export function getApiUserId(request: Request): string | null { + const headerUserId = request.headers.get("x-launchpix-user-id") ?? request.headers.get("x-user-id"); + if (!headerUserId) return null; + const userId = headerUserId.trim(); + if (!userId) return null; + return userId; +} + +export function requireApiUserId(request: Request): { userId: string } | { response: NextResponse } { + const userId = getApiUserId(request); + if (!userId) { + return { + response: NextResponse.json( + { + error: "Missing user id. Send x-launchpix-user-id with the owner UUID." + }, + { status: 400 } + ) + }; + } + + return { userId }; +} From b14d84460f95060ba93100f2676043dfc23ac894 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 06:56:46 +0100 Subject: [PATCH 13/19] Add API-first project endpoints --- app/api/v1/projects/route.ts | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 app/api/v1/projects/route.ts diff --git a/app/api/v1/projects/route.ts b/app/api/v1/projects/route.ts new file mode 100644 index 0000000..be990cf --- /dev/null +++ b/app/api/v1/projects/route.ts @@ -0,0 +1,62 @@ +import { NextResponse } from "next/server"; +import { requireLaunchPixApiKey } from "@/lib/api-key"; +import { requireApiUserId } from "@/lib/api-user"; +import { createProjectSchema } from "@/lib/validation/project"; +import { createSupabaseServerClient } from "@/lib/supabase/server"; + +export async function GET(request: Request) { + const unauthorized = requireLaunchPixApiKey(request); + if (unauthorized) return unauthorized; + + const userResult = requireApiUserId(request); + if ("response" in userResult) return userResult.response; + + const supabase = await createSupabaseServerClient(); + const { data, error } = await supabase + .from("projects") + .select("id,name,product_type,platform,status,created_at,updated_at") + .eq("user_id", userResult.userId) + .order("updated_at", { ascending: false }); + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ projects: data ?? [] }); +} + +export async function POST(request: Request) { + const unauthorized = requireLaunchPixApiKey(request); + if (unauthorized) return unauthorized; + + const userResult = requireApiUserId(request); + if ("response" in userResult) return userResult.response; + + const body = await request.json().catch(() => null); + const parsed = createProjectSchema.safeParse({ ...body, userId: userResult.userId }); + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid payload." }, { status: 400 }); + } + + const supabase = await createSupabaseServerClient(); + const payload = { + user_id: userResult.userId, + name: parsed.data.name, + product_type: parsed.data.productType, + platform: parsed.data.platform, + description: parsed.data.description, + audience: parsed.data.audience, + website_url: parsed.data.websiteUrl || null, + primary_color: parsed.data.primaryColor, + style_preset: parsed.data.stylePreset, + status: "ready" + }; + + const { data: project, error } = await supabase.from("projects").insert(payload).select("*").single(); + if (error || !project) return NextResponse.json({ error: error?.message ?? "Unable to create project." }, { status: 500 }); + + const { data: draft } = await supabase.from("generations").select("id").eq("project_id", project.id).eq("status", "draft").maybeSingle(); + if (!draft) { + const { error: generationError } = await supabase.from("generations").insert({ project_id: project.id, status: "draft" }); + if (generationError) return NextResponse.json({ error: generationError.message }, { status: 500 }); + } + + return NextResponse.json({ project }, { status: 201 }); +} From 8b828d2567706bb96a092e8ec17aaa58a0b1f4d1 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 07:13:32 +0100 Subject: [PATCH 14/19] Add API documentation page --- app/docs/api/page.tsx | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/docs/api/page.tsx diff --git a/app/docs/api/page.tsx b/app/docs/api/page.tsx new file mode 100644 index 0000000..6897c67 --- /dev/null +++ b/app/docs/api/page.tsx @@ -0,0 +1,44 @@ +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; +import { TopNav } from "@/components/marketing/top-nav"; +import { MarketingFooter } from "@/components/marketing/footer"; +import { Button } from "@/components/ui/button"; + +export default function ApiDocsPage() { + return ( + <> + +
+
+

API first

+

Talocode LaunchPix Developer API

+

+ Talocode LaunchPix is open source and API-first. To use production endpoints, request `LAUNCHPIX_API_KEY` and pass it with each request. +

+
+
+

Auth headers

+

`x-launchpix-api-key: <your-key>`

+

`x-launchpix-user-id: <owner-uuid>`

+
+
+

Endpoints

+

`GET /api/v1/projects`

+

`POST /api/v1/projects`

+

`POST /api/v1/projects/:projectId/generate`

+
+
+
+ +
+
+
+ + + ); +} From 4e4f46661251e5caeb924654449827512cc900f2 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 07:23:46 +0100 Subject: [PATCH 15/19] Add Talocode asset directory --- public/assets/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/assets/.gitkeep diff --git a/public/assets/.gitkeep b/public/assets/.gitkeep new file mode 100644 index 0000000..e69de29 From 430b5604303080324de3ae0ef53b49bd0c8db0ed Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 07:26:58 +0100 Subject: [PATCH 16/19] Add Talocode logo asset --- public/assets/talocode-logo.svg | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 public/assets/talocode-logo.svg diff --git a/public/assets/talocode-logo.svg b/public/assets/talocode-logo.svg new file mode 100644 index 0000000..f440329 --- /dev/null +++ b/public/assets/talocode-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + From b6f457d0c1e200636d7873d58dcce22ed8d2d0a4 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 07:33:25 +0100 Subject: [PATCH 17/19] Add Talocode banner asset --- public/assets/talocode-banner.svg | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 public/assets/talocode-banner.svg diff --git a/public/assets/talocode-banner.svg b/public/assets/talocode-banner.svg new file mode 100644 index 0000000..bab1b25 --- /dev/null +++ b/public/assets/talocode-banner.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Talocode + Open-source AI tools for builders. + + + Creators + Founders + Engineers + Businesses + + From d5b01914ffa0963e806fd0d87b7fa937fb503b9a Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 07:34:18 +0100 Subject: [PATCH 18/19] Add Render deployment config --- render.yaml | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 render.yaml diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..0c39424 --- /dev/null +++ b/render.yaml @@ -0,0 +1,61 @@ +services: + - type: web + name: talocode-launchpix + runtime: node + plan: starter + rootDir: . + buildCommand: npm ci && npm run build + startCommand: npm run start + healthCheckPath: / + autoDeploy: true + envVars: + - key: NODE_VERSION + value: 20 + - key: NEXT_TELEMETRY_DISABLED + value: "1" + - key: NEXT_PUBLIC_APP_URL + sync: false + - key: NEXT_PUBLIC_SUPABASE_URL + sync: false + - key: NEXT_PUBLIC_SUPABASE_ANON_KEY + sync: false + - key: SUPABASE_SERVICE_ROLE_KEY + sync: false + - key: DATABASE_URL + sync: false + - key: NEXTAUTH_SECRET + sync: false + - key: GOOGLE_CLIENT_ID + sync: false + - key: GOOGLE_CLIENT_SECRET + sync: false + - key: MISTRAL_API_KEY + sync: false + - key: MISTRAL_MODEL_TEXT + sync: false + - key: MISTRAL_MODEL_VISION + sync: false + - key: MISTRAL_IMAGE_MODEL + sync: false + - key: MISTRAL_IMAGE_AGENT_ID + sync: false + - key: LAUNCHPIX_API_KEY + sync: false + - key: LEMON_SQUEEZY_API_KEY + sync: false + - key: LEMON_SQUEEZY_STORE_ID + sync: false + - key: LEMON_SQUEEZY_WEBHOOK_SECRET + sync: false + - key: LEMON_SQUEEZY_STARTER_CREDITS_VARIANT_ID + sync: false + - key: LEMON_SQUEEZY_CREATOR_CREDITS_VARIANT_ID + sync: false + - key: LEMON_SQUEEZY_STUDIO_CREDITS_VARIANT_ID + sync: false + - key: RESEND_API_KEY + sync: false + - key: RESEND_FROM_EMAIL + sync: false + - key: RESEND_WEBHOOK_SECRET + sync: false From 700614a37fe88fdd1e5c5c79ce888b335edc5284 Mon Sep 17 00:00:00 2001 From: Abdulmuiz Adeyemo Date: Wed, 3 Jun 2026 07:38:51 +0100 Subject: [PATCH 19/19] Add API-first generation endpoint --- .../v1/projects/[projectId]/generate/route.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 app/api/v1/projects/[projectId]/generate/route.ts diff --git a/app/api/v1/projects/[projectId]/generate/route.ts b/app/api/v1/projects/[projectId]/generate/route.ts new file mode 100644 index 0000000..2e2299c --- /dev/null +++ b/app/api/v1/projects/[projectId]/generate/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import { requireLaunchPixApiKey } from "@/lib/api-key"; +import { requireApiUserId } from "@/lib/api-user"; +import { getProjectOverview } from "@/lib/services/projects/queries"; +import { runGenerationForProject } from "@/lib/services/generations/runner"; +import { getLatestGeneration } from "@/lib/services/generations/queries"; +import { allowGenerationAttempt } from "@/lib/services/access/rate-limit"; + +export async function GET(request: Request, { params }: { params: Promise<{ projectId: string }> }) { + const unauthorized = requireLaunchPixApiKey(request); + if (unauthorized) return unauthorized; + + const userResult = requireApiUserId(request); + if ("response" in userResult) return userResult.response; + + const { projectId } = await params; + const { project } = await getProjectOverview(projectId, userResult.userId); + const generation = await getLatestGeneration(project.id); + return NextResponse.json({ generation }); +} + +export async function POST(request: Request, { params }: { params: Promise<{ projectId: string }> }) { + const unauthorized = requireLaunchPixApiKey(request); + if (unauthorized) return unauthorized; + + const userResult = requireApiUserId(request); + if ("response" in userResult) return userResult.response; + + if (!allowGenerationAttempt(userResult.userId)) return NextResponse.json({ error: "Too many generation attempts. Please wait and retry." }, { status: 429 }); + + const { projectId } = await params; + const { project, uploads } = await getProjectOverview(projectId, userResult.userId); + if (!uploads.length) return NextResponse.json({ error: "At least one screenshot is required." }, { status: 400 }); + + const { generationId } = await runGenerationForProject(project, uploads); + return NextResponse.json({ generationId }, { status: 201 }); +}