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 = { 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, 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, scores: item.dimensions as unknown as Record, 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", }, }); }