220 lines
6.5 KiB
TypeScript
220 lines
6.5 KiB
TypeScript
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",
|
|
},
|
|
});
|
|
}
|