import { useState, useEffect, useRef } from 'react'; import { shouldShowRedirectBanner } from '@/lib/handleRedirect'; interface ArticleStreamData { chunk?: string; media_replacement?: { placeholder: string; replacement: string; }; url_redirect?: { requested: string; canonical: string; }; title?: string; } export function useCopilotArticleStream(topic: string) { const [content, setContent] = useState(''); const [error, setError] = useState(null); const [shouldRedirect, setShouldRedirect] = useState(null); const [displayTitle, setDisplayTitle] = useState(null); const bufferRef = useRef(''); const mediaReplacementsRef = useRef>(new Map()); useEffect(() => { if (!topic) return; const controller = new AbortController(); setContent(''); setError(null); setShouldRedirect(null); setDisplayTitle(null); bufferRef.current = ''; mediaReplacementsRef.current.clear(); streamArticle(topic, controller.signal, { onData: (data: ArticleStreamData) => { if (data.chunk) { bufferRef.current += data.chunk; } else if (data.media_replacement) { mediaReplacementsRef.current.set( data.media_replacement.placeholder, data.media_replacement.replacement ); } else if (data.url_redirect) { const { requested, canonical } = data.url_redirect; const redirectUrl = `/t/${canonical}${shouldShowRedirectBanner(requested, canonical) ? `?redirectedFrom=${encodeURIComponent(requested)}` : ''}`; setShouldRedirect(redirectUrl); } else if (data.title) { setDisplayTitle(data.title); } let displayContent = bufferRef.current; const askTagStart = displayContent.lastIndexOf('<>')) { displayContent = displayContent.substring(0, askTagStart); } mediaReplacementsRef.current.forEach((replacement, placeholder) => { displayContent = displayContent.replace(placeholder, replacement); }); setContent(displayContent); }, onError: err => { setError(err.message); }, }); return () => controller.abort(); }, [topic]); return { content, error, shouldRedirect, displayTitle }; } async function streamArticle( topic: string, signal: AbortSignal, callbacks: { onData: (data: ArticleStreamData) => void; onError: (error: Error) => void; } ) { try { const sessionContext = JSON.parse(sessionStorage.getItem('bogam-session') || 'null'); const response = await fetch( `${process.env.PUBLIC_COPILOT_URL || import.meta.env.PUBLIC_COPILOT_URL || 'http://100.70.30.1:8000'}/generate-article-for-topic`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic, sessionContext }), signal, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const events = buffer.split('\n\n'); buffer = events.pop() || ''; for (const event of events) { if (!event.startsWith('data: ')) continue; const data = event.slice(6); if (data === '[DONE]') return; try { const parsed = JSON.parse(data); callbacks.onData(parsed); } catch (e) { console.error('Failed to parse SSE data:', e); } } } } catch (err: unknown) { if (err instanceof Error && err.name !== 'AbortError') { callbacks.onError(err); } } }