scaffold: Next.js 15 + Drizzle + Better Auth + OpenAI + Recharts base
This commit is contained in:
219
src/app/api/compare/route.ts
Normal file
219
src/app/api/compare/route.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { runResearch } from "@/lib/llm";
|
||||
import type { ComparisonRequest } from "@/lib/llm/types";
|
||||
import type { ComparisonData } from "@/lib/types";
|
||||
import { db } from "@/lib/db";
|
||||
import { comparisons, comparisonItems } from "@/lib/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
function serializeSSE(event: string, data: unknown): string {
|
||||
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
||||
}
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.slice(0, 200);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body: { query?: string; items?: string[]; dimensions?: string[] } =
|
||||
await request.json();
|
||||
const { query, items, dimensions } = body;
|
||||
|
||||
if (!items || items.length < 2) {
|
||||
return Response.json(
|
||||
{ error: "At least 2 items are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const id = createId();
|
||||
const title = `Comparing ${items.join(" vs ")}`;
|
||||
const slug = `${slugify(title)}-${id.slice(-6)}`;
|
||||
|
||||
await db.insert(comparisons).values({
|
||||
id,
|
||||
title,
|
||||
query: query || null,
|
||||
slug,
|
||||
status: "researching",
|
||||
});
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const researchRequest: ComparisonRequest = {
|
||||
query: query || "",
|
||||
items,
|
||||
dimensions,
|
||||
};
|
||||
|
||||
let itemsCompleted = 0;
|
||||
|
||||
try {
|
||||
for await (const progress of runResearch(researchRequest)) {
|
||||
if (progress.stage === "parsing") {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
serializeSSE("progress", {
|
||||
status: "researching",
|
||||
message: progress.message,
|
||||
itemsCompleted: 0,
|
||||
totalItems: items.length,
|
||||
currentStep: progress.message,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (progress.stage === "researching") {
|
||||
itemsCompleted++;
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
serializeSSE("progress", {
|
||||
status: "researching",
|
||||
message: `Researching ${progress.item}...`,
|
||||
itemsCompleted,
|
||||
totalItems: items.length,
|
||||
currentStep: `Analyzing ${progress.item}`,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (progress.stage === "synthesizing") {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
serializeSSE("progress", {
|
||||
status: "researching",
|
||||
message: progress.message,
|
||||
itemsCompleted: items.length,
|
||||
totalItems: items.length,
|
||||
currentStep: "Synthesizing results",
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (progress.stage === "complete") {
|
||||
const result = progress.result;
|
||||
|
||||
const comparisonData: Omit<ComparisonData, "id" | "userId" | "slug" | "tags" | "isPublic" | "viewCount" | "createdAt" | "updatedAt"> = {
|
||||
title,
|
||||
query: query || "",
|
||||
status: "completed",
|
||||
summary: `${result.summary}\n\nRecommendation: ${result.recommendation}`,
|
||||
items: result.items.map((item) => ({
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
overallScore: item.overallScore,
|
||||
dimensions: item.dimensions,
|
||||
pros: item.pros,
|
||||
cons: item.cons,
|
||||
})),
|
||||
dimensions: result.dimensions,
|
||||
};
|
||||
|
||||
await db
|
||||
.update(comparisons)
|
||||
.set({
|
||||
status: "completed",
|
||||
summary: comparisonData.summary,
|
||||
overallData:
|
||||
comparisonData as unknown as Record<string, unknown>,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(comparisons.id, id));
|
||||
|
||||
for (let i = 0; i < result.items.length; i++) {
|
||||
const item = result.items[i];
|
||||
await db.insert(comparisonItems).values({
|
||||
id: createId(),
|
||||
comparisonId: id,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
researchData: item as unknown as Record<string, unknown>,
|
||||
scores:
|
||||
item.dimensions as unknown as Record<string, unknown>,
|
||||
pros: item.pros,
|
||||
cons: item.cons,
|
||||
order: i,
|
||||
});
|
||||
}
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
serializeSSE("progress", {
|
||||
status: "completed",
|
||||
message: "Research complete!",
|
||||
itemsCompleted: items.length,
|
||||
totalItems: items.length,
|
||||
currentStep: "Done",
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
serializeSSE("done", { id, slug, data: comparisonData })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (progress.stage === "error") {
|
||||
await db
|
||||
.update(comparisons)
|
||||
.set({
|
||||
status: "failed",
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(comparisons.id, id));
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
serializeSSE("progress", {
|
||||
status: "failed",
|
||||
message: progress.error,
|
||||
itemsCompleted,
|
||||
totalItems: items.length,
|
||||
currentStep: "Failed",
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await db
|
||||
.update(comparisons)
|
||||
.set({ status: "failed", updatedAt: new Date() })
|
||||
.where(eq(comparisons.id, id));
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
serializeSSE("progress", {
|
||||
status: "failed",
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
itemsCompleted,
|
||||
totalItems: items.length,
|
||||
currentStep: "Failed",
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user