scaffold: Next.js 15 + Drizzle + Better Auth + OpenAI + Recharts base
This commit is contained in:
157
src/hooks/use-comparison-stream.ts
Normal file
157
src/hooks/use-comparison-stream.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import type { ResearchProgress } from "@/lib/types";
|
||||
import type { ComparisonData } from "@/lib/types";
|
||||
|
||||
interface UseComparisonStreamReturn {
|
||||
progress: ResearchProgress;
|
||||
result: ComparisonData | null;
|
||||
error: string | null;
|
||||
isStreaming: boolean;
|
||||
startResearch: (
|
||||
query: string,
|
||||
items: string[],
|
||||
dimensions?: string[]
|
||||
) => void;
|
||||
cancel: () => void;
|
||||
comparisonId: string | null;
|
||||
comparisonSlug: string | null;
|
||||
}
|
||||
|
||||
const INITIAL_PROGRESS: ResearchProgress = {
|
||||
status: "idle",
|
||||
message: "",
|
||||
itemsCompleted: 0,
|
||||
totalItems: 0,
|
||||
currentStep: "",
|
||||
};
|
||||
|
||||
export function useComparisonStream(): UseComparisonStreamReturn {
|
||||
const [progress, setProgress] =
|
||||
useState<ResearchProgress>(INITIAL_PROGRESS);
|
||||
const [result, setResult] = useState<ComparisonData | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [comparisonId, setComparisonId] = useState<string | null>(null);
|
||||
const [comparisonSlug, setComparisonSlug] = useState<string | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
abortControllerRef.current?.abort();
|
||||
abortControllerRef.current = null;
|
||||
setProgress(INITIAL_PROGRESS);
|
||||
setIsStreaming(false);
|
||||
}, []);
|
||||
|
||||
const startResearch = useCallback(
|
||||
async (query: string, items: string[], dimensions?: string[]) => {
|
||||
abortControllerRef.current?.abort();
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
setProgress({
|
||||
status: "researching",
|
||||
message: "Starting research...",
|
||||
itemsCompleted: 0,
|
||||
totalItems: items.length,
|
||||
currentStep: "Initializing",
|
||||
});
|
||||
setResult(null);
|
||||
setError(null);
|
||||
setComparisonId(null);
|
||||
setComparisonSlug(null);
|
||||
setIsStreaming(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/compare", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query, items, dimensions }),
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.error || "Failed to start comparison");
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error("No response body");
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
let currentEvent = "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("event: ")) {
|
||||
currentEvent = line.slice(7).trim();
|
||||
} else if (line.startsWith("data: ") && currentEvent) {
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6));
|
||||
|
||||
if (currentEvent === "progress") {
|
||||
setProgress(data);
|
||||
if (data.status === "failed") {
|
||||
setError(data.message);
|
||||
}
|
||||
} else if (currentEvent === "done") {
|
||||
setComparisonId(data.id);
|
||||
setComparisonSlug(data.slug);
|
||||
if (data.data) {
|
||||
setResult(data.data);
|
||||
}
|
||||
} else if (currentEvent === "error") {
|
||||
setError(data.error);
|
||||
setProgress((prev) => ({
|
||||
...prev,
|
||||
status: "failed",
|
||||
message: data.error,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// skip malformed JSON
|
||||
}
|
||||
currentEvent = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return;
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Unknown error";
|
||||
setError(message);
|
||||
setProgress((prev) => ({
|
||||
...prev,
|
||||
status: "failed",
|
||||
message,
|
||||
}));
|
||||
} finally {
|
||||
setIsStreaming(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
progress,
|
||||
result,
|
||||
error,
|
||||
isStreaming,
|
||||
startResearch,
|
||||
cancel,
|
||||
comparisonId,
|
||||
comparisonSlug,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user