From fccc89630e47ab9b660c9927bd9e769a49f97db1 Mon Sep 17 00:00:00 2001 From: Justin Reiners Date: Thu, 26 Mar 2026 12:14:52 -0500 Subject: [PATCH] feat: add AI pet name generator page and API endpoint Client-side generator page with species (dog/cat) and theme selectors submits a job to /api/pet-names POST, then polls GET until completed. API uses nameGeneratorLimiter for rate limiting and delegates to queueNameGenerationJob/getJobStatus from lib/server/ai. Co-Authored-By: Claude Sonnet 4.6 --- .../(public)/name-generator/+page.svelte | 233 ++++++++++++++++++ src/routes/api/pet-names/+server.ts | 54 ++++ 2 files changed, 287 insertions(+) create mode 100644 src/routes/(public)/name-generator/+page.svelte create mode 100644 src/routes/api/pet-names/+server.ts diff --git a/src/routes/(public)/name-generator/+page.svelte b/src/routes/(public)/name-generator/+page.svelte new file mode 100644 index 0000000..044bade --- /dev/null +++ b/src/routes/(public)/name-generator/+page.svelte @@ -0,0 +1,233 @@ + + + + Pet Name Generator + + + +
+
+

Pet Name Generator

+

+ Can't decide on a name? Let our AI help! Powered by local AI — no data leaves our servers. +

+
+
+ +
+
+ +
+
+ +
+ +
+ + +
+
+ + +
+ +
+ {#each themes as t} + + {/each} +
+
+ + + +
+
+ + + {#if error} +
+ {error} +
+ {/if} + + + {#if names.length > 0} +
+

+ Here are your {species} names! +

+
+ {#each names as name} + + {/each} +
+

+ Generated by AI running locally +

+
+ +
+
+ {/if} + + +
+

+ Looking to adopt? Check out our + available pets + — they already have great names! +

+
+
+
diff --git a/src/routes/api/pet-names/+server.ts b/src/routes/api/pet-names/+server.ts new file mode 100644 index 0000000..c7de790 --- /dev/null +++ b/src/routes/api/pet-names/+server.ts @@ -0,0 +1,54 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { queueNameGenerationJob, getJobStatus } from '$lib/server/ai'; +import { nameGeneratorLimiter } from '$lib/server/rate-limit'; + +export const POST: RequestHandler = async ({ request, getClientAddress }) => { + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? getClientAddress(); + + // Rate limit: 10 requests per 5 minutes per IP + if (!nameGeneratorLimiter.check(ip)) { + return json({ error: 'Too many requests. Please slow down.' }, { status: 429 }); + } + + const { species, theme, count } = await request.json(); + + const nameCount = Math.min(Math.max(parseInt(count) || 10, 1), 20); + const animalType = species === 'cat' ? 'cat' : 'dog'; + + const jobId = await queueNameGenerationJob({ + species: animalType, + theme: theme || 'classic', + count: nameCount + }); + + return json({ jobId }); +}; + +export const GET: RequestHandler = async ({ url }) => { + const jobId = parseInt(url.searchParams.get('jobId') || '0'); + if (!jobId || isNaN(jobId)) { + return json({ error: 'Missing or invalid jobId' }, { status: 400 }); + } + + const job = await getJobStatus(jobId); + if (!job) { + return json({ error: 'Job not found' }, { status: 404 }); + } + + if (job.status === 'completed') { + let output = job.output as { names: string[] } | string | null; + if (typeof output === 'string') { + try { output = JSON.parse(output); } catch { output = null; } + } + const names = (output as { names: string[] } | null)?.names || []; + return json({ status: 'completed', names }); + } + + if (job.status === 'failed') { + return json({ status: 'failed', error: job.error || 'Unknown error' }); + } + + // pending or processing + return json({ status: job.status }); +};