diff --git a/app/api/generate-prompt/route.ts b/app/api/generate-prompt/route.ts index 5e5f6d2..bc2c0a6 100644 --- a/app/api/generate-prompt/route.ts +++ b/app/api/generate-prompt/route.ts @@ -7,6 +7,8 @@ export const runtime = "nodejs"; export const dynamic = "force-dynamic"; type Brief = { + mode?: "guided" | "simple"; + description?: string; businessName?: string; businessType?: string; topic?: string; @@ -29,6 +31,11 @@ function cleanText(text: string) { function buildBrief(input: Brief) { const lines: string[] = []; + if (input.mode === "simple" && input.description) { + lines.push(`Description: ${input.description}`); + return lines.join("\n"); + } + if (input.businessName) lines.push(`Business name: ${input.businessName}`); if (input.businessType) lines.push(`Business type: ${input.businessType}`); if (input.task) lines.push(`Task: ${input.task}`); @@ -40,6 +47,7 @@ function buildBrief(input: Brief) { if (input.platform) lines.push(`Platform: ${input.platform}`); if (input.keyPoints) lines.push(`Key details: ${input.keyPoints}`); if (input.notes) lines.push(`Extra notes: ${input.notes}`); + if (input.description) lines.push(`Description: ${input.description}`); return lines.length ? lines.join("\n") : "No extra brief supplied."; } @@ -47,12 +55,14 @@ function buildBrief(input: Brief) { export async function POST(request: Request) { try { const rawBody = await request.json().catch(() => null); + const body = (() => { if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) { return null; } const validated: Brief = {}; + for (const [key, value] of Object.entries(rawBody)) { if (value == null) continue; if (typeof value !== "string") { @@ -77,6 +87,7 @@ export async function POST(request: Request) { } const brief = buildBrief(body || {}); + const isSimple = body?.mode === "simple" || !!body?.description; const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent( GEMINI_MODEL, @@ -99,15 +110,20 @@ export async function POST(request: Request) { role: "user", parts: [ { - text: - "You are a senior prompt engineer.\n" + - "Turn the following brief into one polished, ready-to-paste AI prompt.\n" + - "The prompt should be detailed and accurate, but still reasonably short, around 120 to 180 words.\n" + - "Use only the information provided. Do not invent facts.\n" + - "Keep it in plain text.\n" + - "Do not use markdown, lists, headings, or code fences.\n" + - "Write the prompt so it can be pasted into another AI tool and immediately used.\n\n" + + text: [ + "You are a senior prompt engineer.", + isSimple + ? "Turn the user's plain-language request into one polished, ready-to-paste AI prompt." + : "Turn the following brief into one polished, ready-to-paste AI prompt.", + "The prompt should be detailed and accurate, but still reasonably short, around 120 to 180 words.", + "Use only the information provided.", + "Do not invent facts.", + "Keep it in plain text.", + "Do not use markdown, lists, headings, or code fences.", + "Write the prompt so it can be pasted into another AI tool and immediately used.", + "", `Brief:\n${brief}`, + ].join("\n"), }, ], }, @@ -127,97 +143,47 @@ export async function POST(request: Request) { if (!response.ok) { let upstreamError: unknown = rawText; - try { upstreamError = JSON.parse(rawText); } catch { // Keep raw text if it is not JSON. } - const status = - response.status >= 400 && response.status < 500 ? response.status : 502; - - console.error("Gemini request failed", { - status: response.status, - model: GEMINI_MODEL, - upstreamError, - }); - return NextResponse.json( { - error: "Gemini request failed.", - status, - model: GEMINI_MODEL, - ...(process.env.NODE_ENV !== "production" ? { upstreamError } : {}), + error: + typeof upstreamError === "string" + ? upstreamError + : "Gemini returned an error.", + details: upstreamError, }, - { status }, + { status: response.status }, ); } - let data: { + const parsed = JSON.parse(rawText) as { candidates?: Array<{ - content?: { parts?: Array<{ text?: string }> }; + content?: { + parts?: Array<{ text?: string }>; + }; }>; - } | null = null; + }; - try { - data = JSON.parse(rawText); - } catch (parseError) { - console.error("Failed to parse Gemini response", { - error: parseError, - rawText, - }); + const promptText = cleanText( + parsed?.candidates?.[0]?.content?.parts?.[0]?.text || "", + ); + if (!promptText) { return NextResponse.json( - { - error: "Invalid response format from Gemini upstream.", - ...(process.env.NODE_ENV !== "production" - ? { - details: - parseError instanceof Error - ? parseError.message - : String(parseError), - } - : {}), - }, + { error: "No prompt was returned by Gemini." }, { status: 502 }, ); } - const generatedText = - data?.candidates?.[0]?.content?.parts - ?.map((part) => part.text ?? "") - .join("") ?? ""; - - const prompt = cleanText(generatedText); - - return NextResponse.json( - { prompt }, - { - headers: { - "Cache-Control": "no-store", - }, - }, - ); + return NextResponse.json({ prompt: promptText }); } catch (error) { - console.error(error); - - if (error instanceof Error && error.name === "AbortError") { - return NextResponse.json( - { error: "Gemini request timed out." }, - { status: 504 }, - ); - } - - const body: { error: string; details?: string } = { - error: "Unable to generate prompt.", - }; - - if (process.env.NODE_ENV !== "production") { - body.details = - error instanceof Error ? error.message : "Unknown server error"; - } - - return NextResponse.json(body, { status: 500 }); + const message = + error instanceof Error ? error.message : "Unexpected error."; + return NextResponse.json({ error: message }, { status: 500 }); } } diff --git a/app/build/loading.tsx b/app/build/loading.tsx new file mode 100644 index 0000000..483c548 --- /dev/null +++ b/app/build/loading.tsx @@ -0,0 +1,54 @@ +import { Sparkles } from "lucide-react"; +import { Navbar } from "@/components/layout/Navbar"; + +export default function Loading() { + return ( + <> + +
+
+
+
+
+
+
+
+
+
+
+ +
+ + Forging +
+
+ +
+
+
+ + Generating +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + ); +} diff --git a/app/build/page.tsx b/app/build/page.tsx index 2577061..b204d88 100644 --- a/app/build/page.tsx +++ b/app/build/page.tsx @@ -1,16 +1,20 @@ "use client"; -import { Suspense, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; -import { Check, Copy } from "lucide-react"; +import { Check, Copy, Sparkles } from "lucide-react"; import { Navbar } from "@/components/layout/Navbar"; +type Mode = "guided" | "simple"; + type GeneratePromptResponse = { prompt?: string; error?: string; }; type Brief = { + mode: Mode; + description: string; businessName: string; businessType: string; topic: string; @@ -24,7 +28,18 @@ type Brief = { }; function readBrief(searchParams: ReturnType): Brief { + const mode = searchParams.get("mode") === "simple" ? "simple" : "guided"; + let description = searchParams.get("description")?.trim() || ""; + + if (searchParams.get("description_stored") === "true") { + if (typeof window !== "undefined") { + description = window.sessionStorage.getItem("forge_description") || ""; + } + } + return { + mode, + description, businessName: searchParams.get("businessName")?.trim() || "", businessType: searchParams.get("businessType")?.trim() || "", topic: searchParams.get("topic")?.trim() || "", @@ -38,23 +53,6 @@ function readBrief(searchParams: ReturnType): Brief { }; } -function toLabel(value: string) { - return value || "Not specified"; -} - -function briefToCards(brief: Brief) { - return [ - { label: "Business name", value: toLabel(brief.businessName) }, - { label: "Business type", value: toLabel(brief.businessType) }, - { label: "Task", value: toLabel(brief.task) }, - { label: "Topic", value: toLabel(brief.topic) }, - { label: "Audience", value: toLabel(brief.audience) }, - { label: "Tone", value: toLabel(brief.tone) }, - { label: "Length", value: toLabel(brief.outputLength) }, - { label: "Platform", value: toLabel(brief.platform) }, - ]; -} - function BuildPageContent() { const searchParams = useSearchParams(); const brief = useMemo(() => readBrief(searchParams), [searchParams]); @@ -82,7 +80,7 @@ function BuildPageContent() { body: JSON.stringify(brief), }); - const data = await response.json(); + const data: GeneratePromptResponse = await response.json(); if (!response.ok) { throw new Error( @@ -130,91 +128,93 @@ function BuildPageContent() { try { await navigator.clipboard.writeText(generatedPrompt); setCopySuccess(true); - - window.setTimeout(() => { - setCopySuccess(false); - }, 1500); + window.setTimeout(() => setCopySuccess(false), 1500); } catch (err) { console.error(err); } }; - const briefCards = briefToCards(brief); - return ( -
-
-
-
-
-

- Prompt result -

-

- {loading ? "Generating..." : "Ready"} -

-
+ <> + +
+
+
+
+
+
+

+ {brief.mode === "simple" ? "Simple mode" : "Guided mode"} +

+

+ {loading + ? "Forging your prompt..." + : error + ? "The forge cooled down" + : "Your prompt is ready"} +

+

+ {brief.mode === "simple" + ? "A plain-language request went into the kiln. The result below is ready to copy." + : "Your structured brief went into the kiln. The result below is tuned to your details."} +

+
- -
- -
- {briefCards.map((item) => ( -
-

- {item.label} -

-

{item.value}

-
- ))} -
+ )} + {copySuccess ? "Copied" : "Copy prompt"} + +
-
- {loading ? ( -

- Generating your prompt... -

- ) : error ? ( -

{error}

- ) : ( -

- {generatedPrompt} -

- )} +
+ {loading ? ( +
+
+ + Generating +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : error ? ( +
+ {error} +
+ ) : ( +
+                  {generatedPrompt}
+                
+ )} +
-
-
-
+
+
+ ); } export default function BuildPage() { - return ( - <> - - - - - - ); + return ; } diff --git a/app/page.tsx b/app/page.tsx index 47812dd..e7ee302 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,7 +6,9 @@ import { useRouter } from "next/navigation"; import { ArrowRight, Sparkles } from "lucide-react"; import { Navbar } from "@/components/layout/Navbar"; -type FormState = { +type Mode = "guided" | "simple"; + +type GuidedFormState = { businessName: string; businessType: string; topic: string; @@ -19,7 +21,7 @@ type FormState = { notes: string; }; -const INITIAL_STATE: FormState = { +const INITIAL_GUIDED_STATE: GuidedFormState = { businessName: "", businessType: "", topic: "", @@ -82,10 +84,29 @@ const PLATFORM_OPTIONS = [ "Other", ]; -function buildQueryParams(form: FormState) { +function buildQueryParams( + mode: Mode, + guidedForm: GuidedFormState, + description: string, +) { const params = new URLSearchParams(); + params.set("mode", mode); + if (mode === "simple") { + const trimmed = description.trim(); + if (trimmed) { + if (trimmed.length > 2000) { + if (typeof window !== "undefined") { + window.sessionStorage.setItem("forge_description", trimmed); + } + params.set("description_stored", "true"); + } else { + params.set("description", trimmed); + } + } + return params; + } - Object.entries(form).forEach(([key, value]) => { + Object.entries(guidedForm).forEach(([key, value]) => { const trimmed = value.trim(); if (trimmed) { params.set(key, trimmed); @@ -105,55 +126,63 @@ function FieldLabel({ htmlFor?: string; }) { return ( -
- +
+ ); } export default function HomePage() { const router = useRouter(); - const [form, setForm] = useState(INITIAL_STATE); + const [mode, setMode] = useState("guided"); + const [guidedForm, setGuidedForm] = useState(INITIAL_GUIDED_STATE); + const [description, setDescription] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [formError, setFormError] = useState(null); - const formSummary = useMemo(() => { - const pieces = [form.businessName, form.topic, form.task].filter(Boolean); + const guidedSummary = useMemo(() => { + const pieces = [guidedForm.businessName, guidedForm.topic, guidedForm.task].filter(Boolean); return pieces.length ? pieces.join(" · ") : "Build a more tailored prompt"; - }, [form.businessName, form.topic, form.task]); + }, [guidedForm.businessName, guidedForm.topic, guidedForm.task]); + + const simpleSummary = useMemo(() => { + const trimmed = description.trim(); + return trimmed ? trimmed.slice(0, 90) : "Describe the prompt in your own words"; + }, [description]); - const handleChange = ( - event: ChangeEvent< - HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement - >, + const handleGuidedChange = ( + event: ChangeEvent, ) => { const { name, value } = event.target; - setForm((current) => ({ ...current, [name]: value })); + setGuidedForm((current) => ({ ...current, [name]: value })); setFormError(null); }; - const handleSubmit = (event: FormEvent) => { + const handleSubmit = (event: FormEvent) => { event.preventDefault(); - setIsSubmitting(true); - setFormError(null); - try { - const params = buildQueryParams(form); - const query = params.toString(); + if (mode === "simple" && !description.trim()) { + setFormError("Please describe what you need first."); + return; + } - if (!query) { - setFormError( - "Please provide at least one detail before generating a prompt.", - ); + if (mode === "guided") { + const hasAnyValue = Object.values(guidedForm).some((value) => value.trim().length > 0); + if (!hasAnyValue) { + setFormError("Please provide at least one detail before generating a prompt."); return; } + } - router.push(`/build?${query}`); + setIsSubmitting(true); + setFormError(null); + + try { + const params = buildQueryParams(mode, guidedForm, description); + router.push(`/build?${params.toString()}`); } finally { setIsSubmitting(false); } @@ -162,239 +191,256 @@ export default function HomePage() { return ( <> +
+
+
+
+
+ + + Prompt generation with a sharper brief + +
+ + +
+
-
-