158 lines
4.5 KiB
TypeScript
158 lines
4.5 KiB
TypeScript
"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,
|
|
};
|
|
}
|