scaffold: Next.js 15 + Drizzle + Better Auth + OpenAI + Recharts base

This commit is contained in:
Christopher Mayor
2026-04-24 14:29:47 -07:00
parent 858f7264ce
commit d13780931e
45 changed files with 9036 additions and 121 deletions

View 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",
},
});
}