Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 44 additions & 78 deletions app/api/generate-prompt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}`);
Expand All @@ -40,19 +47,22 @@ 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.";
}

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") {
Expand All @@ -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,
Expand All @@ -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"),
},
],
},
Expand All @@ -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 || "",
);
Comment on lines +164 to +174

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In the Gemini generateContent API response schema, can candidates[0].content.parts contain multiple text parts for one candidate, and should clients concatenate them to reconstruct the full text response?

💡 Result:

Yes, the Gemini generateContent API response schema can contain multiple text parts within candidates.content.parts [1][2][3]. While many simple requests return a single text part, the API may return multiple parts in scenarios such as when the model includes "thinking" blocks, function calls, or other structured content alongside the generated text [2][3]. Clients should not rely solely on convenience accessors (like response.text) if they need to handle all response types robustly, as these accessors often only work for simple, single-part responses and may fail or return incomplete data when multiple parts are present [4][5][6]. To reconstruct the full text response correctly, clients should iterate through the parts array, check for the presence of text, and concatenate all text segments [4][2][3]. Example logic for reconstruction: const fullText = response.candidates.content.parts.filter(part => part.text).map(part => part.text).join(''); [4][2]

Citations:


🏁 Script executed:

cat -n app/api/generate-prompt/route.ts | sed -n '160,180p'

Repository: JasonDebnath001/PromptForge

Length of output: 686


🏁 Script executed:

cat -n app/api/generate-prompt/route.ts | sed -n '150,190p'

Repository: JasonDebnath001/PromptForge

Length of output: 1295


Concatenate all returned Gemini text parts instead of reading only the first part.

At lines 172–174, the code uses only parts[0]?.text, which can truncate valid output when the Gemini API response splits content across multiple parts. The API may return multiple text parts in the content.parts array—for example, when the model includes thinking blocks or other structured content—and clients should concatenate all text segments to reconstruct the full response.

Proposed fix
-    const promptText = cleanText(
-      parsed?.candidates?.[0]?.content?.parts?.[0]?.text || "",
-    );
+    const promptText = cleanText(
+      (parsed?.candidates?.[0]?.content?.parts ?? [])
+        .map((part) => part?.text ?? "")
+        .join(""),
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/generate-prompt/route.ts` around lines 164 - 174, The current code
only uses the first Gemini content part
(parsed?.candidates?.[0]?.content?.parts?.[0]?.text) which can truncate outputs;
update the logic that constructs promptText (and uses cleanText) to concatenate
all parts' text for the first candidate by iterating
parsed?.candidates?.[0]?.content?.parts, extracting each part.text (ignoring
undefined), joining them into a single string, then pass that concatenated
string to cleanText so the full Gemini response is preserved; keep references to
parsed, promptText, cleanText and the content.parts array when making the
change.


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 });
}
}
54 changes: 54 additions & 0 deletions app/build/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Sparkles } from "lucide-react";
import { Navbar } from "@/components/layout/Navbar";

export default function Loading() {
return (
<>
<Navbar />
<main className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6 lg:px-8">
<section className="paper-card relative overflow-hidden rounded-[2.5rem] p-6 sm:p-8">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(178,74,47,0.12),transparent_34%),radial-gradient(circle_at_top_right,rgba(23,19,17,0.08),transparent_30%)]" />
<div className="relative flex flex-col gap-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="h-3 w-28 animate-pulse rounded-full bg-black/10" />
<div className="mt-4 h-12 w-4/5 animate-pulse rounded-[1.25rem] bg-black/10 sm:h-14 sm:w-3/5" />
<div className="mt-4 h-4 w-full max-w-2xl animate-pulse rounded-full bg-black/10" />
<div className="mt-3 h-4 w-5/6 animate-pulse rounded-full bg-black/10" />
</div>

<div className="inline-flex items-center gap-2 self-start rounded-full border border-[color:theme(--color-border)] bg-white/80 px-4 py-3 text-sm font-semibold text-ink">
<Sparkles className="h-4 w-4" />
Forging
</div>
</div>

<div className="rounded-[2rem] border border-[color:theme(--color-border)] bg-white/75 p-5 sm:p-6">
<div className="space-y-5">
<div className="inline-flex items-center gap-2 rounded-full border border-[color:theme(--color-border)] bg-white/80 px-3 py-2 text-xs font-semibold tracking-[0.2em] text-muted uppercase">
<Sparkles className="h-4 w-4" />
Generating
</div>
<div className="space-y-3">
<div className="h-4 w-1/4 animate-pulse rounded-full bg-black/10" />
<div className="h-4 w-5/6 animate-pulse rounded-full bg-black/10" />
<div className="h-4 w-4/5 animate-pulse rounded-full bg-black/10" />
<div className="h-4 w-3/4 animate-pulse rounded-full bg-black/10" />
<div className="h-4 w-2/3 animate-pulse rounded-full bg-black/10" />
</div>
<div className="rounded-[1.5rem] border border-dashed border-[color:theme(--color-border)] bg-white/70 p-4">
<div className="space-y-3">
<div className="h-3 w-2/5 animate-pulse rounded-full bg-black/10" />
<div className="h-3 w-full animate-pulse rounded-full bg-black/10" />
<div className="h-3 w-11/12 animate-pulse rounded-full bg-black/10" />
<div className="h-3 w-4/5 animate-pulse rounded-full bg-black/10" />
</div>
</div>
</div>
</div>
</div>
</section>
</main>
</>
);
}
Loading