"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(INITIAL_PROGRESS); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [isStreaming, setIsStreaming] = useState(false); const [comparisonId, setComparisonId] = useState(null); const [comparisonSlug, setComparisonSlug] = useState(null); const abortControllerRef = useRef(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, }; }