fix(ai-assist): prevent HTML-as-JSON crash; add safeJsonFetch + JSON-only API errors
- Created lib/safeJsonFetch.ts helper to safely parse JSON responses - Updated AIAssist.tsx to use safeJsonFetch with proper Content-Type header - Improved app/api/ai-assist/route.ts with requestId tracking and safe body parsing - All API responses now return JSON with proper error messages
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { randomUUID } from "crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
// We'll use the environment variables for provider routing
|
||||
// Schema validation
|
||||
const schema = z.object({
|
||||
request: z.string().min(10),
|
||||
step: z.enum(["plan", "generate", "preview"]).default("plan"),
|
||||
plan: z.any().optional(),
|
||||
code: z.string().optional(),
|
||||
provider: z.string().optional(),
|
||||
model: z.string().optional()
|
||||
request: z.string().min(1),
|
||||
step: z.enum(["plan", "generate", "preview"]).default("plan"),
|
||||
plan: z.any().optional(),
|
||||
code: z.string().optional(),
|
||||
provider: z.string().optional(),
|
||||
model: z.string().optional()
|
||||
});
|
||||
|
||||
const STEPS = {
|
||||
plan: `You are an expert software architect. Create a DETAILED DEVELOPMENT PLAN for the following request: "{request}"
|
||||
plan: `You are an expert software architect. Create a DETAILED DEVELOPMENT PLAN for the following request: "{request}"
|
||||
|
||||
Output ONLY a JSON object:
|
||||
{
|
||||
@@ -27,7 +28,7 @@ Output ONLY a JSON object:
|
||||
"risks": ["Potential blockers"]
|
||||
}`,
|
||||
|
||||
generate: `You are a Senior Vibe Coder. Execute the following approved plan:
|
||||
generate: `You are a Senior Vibe Coder. Execute the following approved plan:
|
||||
Plan: {plan}
|
||||
|
||||
Generate COMPLETE, PRODUCTION-READY code for all files.
|
||||
@@ -42,7 +43,7 @@ Output ONLY a JSON object:
|
||||
"explanation": "How it works"
|
||||
}`,
|
||||
|
||||
preview: `Convert the following code into a single-file interactive HTML preview (Standalone).
|
||||
preview: `Convert the following code into a single-file interactive HTML preview (Standalone).
|
||||
Use Tailwind CDN.
|
||||
|
||||
Code: {code}
|
||||
@@ -51,25 +52,57 @@ Output ONLY valid HTML.`
|
||||
};
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { request, step, plan, code } = schema.parse(body);
|
||||
const requestId = randomUUID();
|
||||
|
||||
let prompt = STEPS[step];
|
||||
prompt = prompt.replace("{request}", request);
|
||||
if (plan) prompt = prompt.replace("{plan}", JSON.stringify(plan));
|
||||
if (code) prompt = prompt.replace("{code}", code);
|
||||
try {
|
||||
// Safe body parsing
|
||||
const body = await req.json().catch(() => null);
|
||||
|
||||
// In a real scenario, this would call the ModelAdapter/Service
|
||||
// For now, we'll return a structure that the frontend can handle,
|
||||
// instructing it to use the existing streaming adapter for the heavy lifting.
|
||||
|
||||
return NextResponse.json({
|
||||
prompt,
|
||||
step,
|
||||
success: true
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
||||
if (!body) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid JSON body", requestId, success: false },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate schema
|
||||
const parseResult = schema.safeParse(body);
|
||||
if (!parseResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid request body",
|
||||
details: parseResult.error.flatten(),
|
||||
requestId,
|
||||
success: false
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { request, step, plan, code } = parseResult.data;
|
||||
|
||||
let prompt = STEPS[step];
|
||||
prompt = prompt.replace("{request}", request);
|
||||
if (plan) prompt = prompt.replace("{plan}", JSON.stringify(plan));
|
||||
if (code) prompt = prompt.replace("{code}", code);
|
||||
|
||||
// Return the prompt for the frontend to use with the streaming adapter
|
||||
return NextResponse.json({
|
||||
prompt,
|
||||
step,
|
||||
requestId,
|
||||
success: true
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(`[ai-assist] requestId=${requestId}`, err);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: err?.message ?? "AI Assist failed",
|
||||
requestId,
|
||||
success: false
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user