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. + + + + + + + + + + + + I'm naming a... + + species = 'dog'} + class="flex-1 py-3 px-4 rounded-xl text-center font-medium transition cursor-pointer {species === 'dog' + ? 'bg-teal-600 text-white shadow-md' + : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}" + > + Dog + + species = 'cat'} + class="flex-1 py-3 px-4 rounded-xl text-center font-medium transition cursor-pointer {species === 'cat' + ? 'bg-teal-600 text-white shadow-md' + : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}" + > + Cat + + + + + + + Name theme + + {#each themes as t} + theme = t.value} + class="py-2.5 px-3 rounded-lg text-sm font-medium transition cursor-pointer {theme === t.value + ? 'bg-teal-600 text-white shadow-md' + : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}" + > + {t.label} + + {/each} + + + + + + {#if loading} + + + + + + {thinkingMessages[thinkingIndex]} + + {:else} + Generate Names! + {/if} + + + + + + {#if error} + + {error} + + {/if} + + + {#if names.length > 0} + + + Here are your {species} names! + + + {#each names as name} + copyName(name)} + class="group bg-white rounded-xl border border-gray-200 p-4 text-left hover:border-teal-500 hover:shadow-md transition cursor-pointer" + title="Click to copy" + > + {name} + Click to copy + + {/each} + + + Generated by AI running locally + + + + Generate more → + + + + {/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 }); +};
+ Can't decide on a name? Let our AI help! Powered by local AI — no data leaves our servers. +
+ Generated by AI running locally +
+ Looking to adopt? Check out our + available pets + — they already have great names! +