File size: 5,175 Bytes
28cf767
 
 
 
 
60ffeeb
 
 
 
 
28cf767
 
 
 
 
60ffeeb
28cf767
 
 
 
 
 
 
 
 
 
 
 
c293cc8
28cf767
 
4573dd4
 
28cf767
 
 
 
 
c293cc8
28cf767
4573dd4
28cf767
 
 
 
 
 
 
 
 
 
 
 
 
bbab1dd
28cf767
 
 
 
4573dd4
28cf767
 
 
 
bbab1dd
 
 
 
 
28cf767
 
 
bbab1dd
28cf767
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c293cc8
 
28cf767
 
c293cc8
314e0fe
 
 
 
 
 
 
 
 
 
 
28cf767
 
314e0fe
28cf767
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4573dd4
28cf767
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"use client";

import { useCallback, useRef, useState } from "react";
import type { NodeState, StageEvent } from "@/types/pipeline";

const API_URL =
  process.env.NEXT_PUBLIC_API_URL ??
  (typeof window !== "undefined" && window.location.origin !== "http://localhost:3000"
    ? window.location.origin
    : "http://localhost:8001");

// Default idle nodes shown before any query is run
const DEFAULT_NODES: NodeState[] = [
  { id: "intent", label: "Intent Router", status: "idle", detail: "" },
  { id: "rewrite", label: "Query Rewrite", status: "idle", detail: "" },
  { id: "cache", label: "Cache Check", status: "idle", detail: "" },
  { id: "rag_init", label: "Loading RAG Store", status: "idle", detail: "" },
  { id: "retrieval_1", label: "Retrieval", status: "idle", detail: "" },
  { id: "retrieval_2", label: "Retrieval (retry)", status: "idle", detail: "" },
  { id: "retrieval_3", label: "Retrieval (retry 2)", status: "idle", detail: "" },
  { id: "citations", label: "Citation Enforcement", status: "idle", detail: "" },
  { id: "verification", label: "Claim Verification", status: "idle", detail: "" },
  { id: "answer", label: "Answer", status: "idle", detail: "" },
];

export function useQueryStream() {
  const [nodes, setNodes] = useState<NodeState[]>(DEFAULT_NODES);
  const [answer, setAnswer] = useState<string>("");
  const [streamingAnswer, setStreamingAnswer] = useState<string>("");
  const [streaming, setStreaming] = useState(false);
  const [error, setError] = useState<string>("");
  const [followUps, setFollowUps] = useState<string[]>([]);
  const [lastQuestion, setLastQuestion] = useState<string>("");
  const abortRef = useRef<AbortController | null>(null);

  const reset = useCallback(() => {
    setNodes(DEFAULT_NODES.map((n) => ({ ...n, status: "idle", detail: "" })));
    setAnswer("");
    setStreamingAnswer("");
    setError("");
    setFollowUps([]);
  }, []);

  const updateNode = useCallback((event: StageEvent) => {
    setNodes((prev) =>
      prev.map((n) =>
        n.id === event.id
          ? { ...n, label: event.label, status: event.status, detail: event.detail ?? "" }
          : n
      )
    );
  }, []);

  const submit = useCallback(
    async (question: string, mode?: string) => {
      // Cancel any in-flight request
      abortRef.current?.abort();
      abortRef.current = new AbortController();

      setLastQuestion(question);
      reset();
      setStreaming(true);

      try {
        const payload: Record<string, unknown> = { question };
        if (mode) {
          payload.mode = mode;
        }

        const response = await fetch(`${API_URL}/query`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
          signal: abortRef.current.signal,
        });

        if (!response.ok) {
          throw new Error(`Server error: ${response.status}`);
        }

        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: ")) {
              const raw = line.slice(6).trim();
              try {
                const parsed = JSON.parse(raw);
                if (currentEvent === "stage") {
                  updateNode(parsed as StageEvent);
                } else if (currentEvent === "token") {
                  setStreamingAnswer((prev) => prev + (parsed.token ?? ""));
                } else if (currentEvent === "done") {
                  setAnswer(parsed.answer ?? "");
                  setStreamingAnswer("");
                  const f = parsed.follow_up_questions;
                  if (Array.isArray(f)) {
                    setFollowUps(
                      f
                        .map((q: unknown) => (typeof q === "string" ? q.trim() : ""))
                        .filter((q: string) => q.length > 0)
                        .slice(0, 3)
                    );
                  } else {
                    setFollowUps([]);
                  }
                } else if (currentEvent === "error") {
                  setError(parsed.message ?? "Unknown error");
                  setFollowUps([]);
                }
              } catch {
                // malformed JSON — skip
              }
              currentEvent = "";
            }
          }
        }
      } catch (err: unknown) {
        if (err instanceof Error && err.name !== "AbortError") {
          setError(err.message);
        }
      } finally {
        setStreaming(false);
      }
    },
    [reset, updateNode]
  );

  return { nodes, answer, streamingAnswer, streaming, error, submit, reset, followUps, lastQuestion };
}