Files
comparaison/src/hooks/use-comparison-stream.ts

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,
};
}