RAG_Backend / frontend /src /components /chat /ChatInterface.jsx
Madhesh4124's picture
Optimize RAG evaluation to unified single LLM call, increase timeouts, and handle 429 quota limits
1d7b287
Raw
History Blame Contribute Delete
11.2 kB
import React from 'react';
import { useEffect, useState } from "react";
import MessageList from "./MessageList";
import InputBox from "./InputBox";
import { BASE_URL } from "../../services/api";
import EvaluationPanel from "../evaluation/EvaluationPanel";
import { getEvaluationReport } from "../../services/api";
export default function ChatInterface({ docId, docIds = [], configId }) {
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const [showEvaluation, setShowEvaluation] = useState(false);
const [selectedAssistantId, setSelectedAssistantId] = useState("");
const [evaluationReport, setEvaluationReport] = useState(null);
const [evaluationError, setEvaluationError] = useState("");
const [deepEvaluationLoading, setDeepEvaluationLoading] = useState(false);
useEffect(() => {
let cancelled = false;
const hydrateHistory = async () => {
if (!docId) {
setMessages([]);
return;
}
try {
const response = await fetch(`${BASE_URL}/api/chat/history/${docId}`, {
method: "GET",
credentials: "include",
});
if (!response.ok) return;
const rows = await response.json();
if (cancelled || !Array.isArray(rows)) return;
const filtered = configId
? rows.filter((item) => String(item.config_id) === String(configId))
: rows;
const hydrated = filtered.map((item) => ({
id: item.id,
role: item.role,
content: item.content || "",
chunks: Array.isArray(item.retrieved_chunks) ? item.retrieved_chunks : [],
timings: null,
status: "",
}));
setMessages(hydrated);
} catch (e) {
console.error("Failed to restore chat history", e);
}
};
void hydrateHistory();
return () => {
cancelled = true;
};
}, [docId, configId]);
useEffect(() => {
const assistantMessages = messages.filter((item) => item.role === "assistant" && item.id);
if (!assistantMessages.length) {
setSelectedAssistantId("");
return;
}
if (!selectedAssistantId || !assistantMessages.some((item) => String(item.id) === String(selectedAssistantId))) {
setSelectedAssistantId(String(assistantMessages[assistantMessages.length - 1].id));
}
}, [messages, selectedAssistantId]);
// Automatic loading removed. Evaluation is now only triggered manually by the user.
const handleSend = async (query) => {
setMessages((prev) => [...prev, { role: "user", content: query }]);
setLoading(true);
let assistantMsg = { id: null, role: "assistant", content: "", chunks: [], timings: null, status: "" };
let streamDone = false;
setMessages((prev) => [...prev, assistantMsg]);
try {
const response = await fetch(`${BASE_URL}/api/chat/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ query, doc_id: docId, doc_ids: docIds, config_id: configId }),
});
if (!response.ok) throw new Error("Stream connection failed");
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 lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data:")) continue;
try {
const data = JSON.parse(trimmed.slice(5).trim());
if (data.type === "status") {
assistantMsg = { ...assistantMsg, status: data.message };
} else if (data.type === "metadata") {
assistantMsg = { ...assistantMsg, chunks: data.chunks, status: "" };
} else if (data.type === "token") {
assistantMsg = { ...assistantMsg, content: assistantMsg.content + data.content, status: "" };
} else if (data.type === "error") {
const details = data.message || "Unknown stream error";
assistantMsg = {
...assistantMsg,
content: `${assistantMsg.content}\n\n[Stream error] ${details}`.trim(),
status: "",
};
streamDone = true;
setLoading(false);
} else if (data.type === "done") {
assistantMsg = { ...assistantMsg, id: data.message_id || assistantMsg.id, status: "" };
streamDone = true;
setLoading(false);
}
setMessages((prev) => {
const next = [...prev];
next[next.length - 1] = { ...assistantMsg };
return next;
});
if (streamDone) {
reader.cancel().catch(() => {});
break;
}
} catch (err) {
console.error("JSON parse error in stream", err);
}
}
if (streamDone) {
break;
}
}
if (!streamDone && buffer.trim()) {
const finalLine = buffer.trim();
if (finalLine.startsWith("data:")) {
try {
const data = JSON.parse(finalLine.slice(5).trim());
if (data.type === "done") {
assistantMsg = { ...assistantMsg, id: data.message_id || assistantMsg.id, status: "" };
setMessages((prev) => {
const next = [...prev];
next[next.length - 1] = { ...assistantMsg };
return next;
});
} else if (data.type === "error") {
const details = data.message || "Unknown stream error";
setMessages((prev) => {
const next = [...prev];
next[next.length - 1] = {
...next[next.length - 1],
content: `${next[next.length - 1].content}\n\n[Stream error] ${details}`.trim(),
status: "",
};
return next;
});
}
} catch (err) {
console.error("JSON parse error in final stream buffer", err);
}
}
}
} catch (e) {
console.error(e);
setMessages((prev) => {
const next = [...prev];
next[next.length - 1] = { ...next[next.length - 1], content: `Error: ${e.message}`, status: "" };
return next;
});
} finally {
if (!streamDone) {
setLoading(false);
}
}
};
const handleReset = async () => {
if (!window.confirm("This will clear ALL conversation history and vectordb indices. Continue?")) return;
try {
setLoading(true);
await fetch(`${BASE_URL}/api/chat/reset`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ doc_id: docId, config_id: configId }),
});
setMessages([]);
alert("System Reset Complete.");
} catch (e) {
alert(`Reset failed: ${e.message}`);
} finally {
setLoading(false);
}
};
const handleRunDeepEvaluation = async () => {
if (!selectedAssistantId) return;
setDeepEvaluationLoading(true);
setEvaluationError("");
try {
const { data } = await getEvaluationReport({ message_id: selectedAssistantId, deep: true });
setEvaluationReport(data);
} catch (error) {
const detail = error?.response?.data?.detail || error?.message || "Failed to run deep evaluation.";
if (String(detail).toLowerCase().includes("timeout")) {
setEvaluationError("Evaluation took too long and was stopped. Please retry later.");
} else {
setEvaluationError(detail);
}
} finally {
setDeepEvaluationLoading(false);
}
};
return (
<div className="flex flex-col h-full">
<div className="mb-2 flex justify-end gap-2">
<button
onClick={() => {
setShowEvaluation(true);
setEvaluationReport(null);
setEvaluationError("");
}}
className="text-xs text-gray-400 hover:text-blue-600 px-2 py-1 rounded hover:bg-gray-100 transition-colors"
title="Open evaluation panel"
>
Evaluation
</button>
<button
onClick={handleReset}
className="text-xs text-gray-400 hover:text-red-600 px-2 py-1 rounded hover:bg-gray-100 transition-colors"
title="Reset session"
>
Reset
</button>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto pr-1">
<MessageList messages={messages} />
{loading && (
<div className="flex justify-start mt-4">
<div className="bg-white border border-gray-200 rounded-2xl rounded-bl-sm px-4 py-3 shadow-sm">
<div className="flex gap-1 items-center">
<div className="w-2 h-2 bg-gray-300 rounded-full animate-bounce" style={{ animationDelay: "0ms" }} />
<div className="w-2 h-2 bg-gray-300 rounded-full animate-bounce" style={{ animationDelay: "150ms" }} />
<div className="w-2 h-2 bg-gray-300 rounded-full animate-bounce" style={{ animationDelay: "300ms" }} />
</div>
</div>
</div>
)}
</div>
{/* Input */}
<InputBox onSend={handleSend} loading={loading} />
<EvaluationPanel
open={showEvaluation}
onClose={() => setShowEvaluation(false)}
title="Chat Evaluation"
report={evaluationReport}
loading={deepEvaluationLoading}
error={evaluationError}
onRunDeepEvaluation={handleRunDeepEvaluation}
deepLoading={deepEvaluationLoading}
selector={messages.filter((item) => item.role === "assistant" && item.id).length > 0 ? (
<label className="block">
<span className="mb-1 block text-xs font-medium text-gray-500">Assistant turn</span>
<select
value={selectedAssistantId}
onChange={(event) => {
setEvaluationReport(null);
setSelectedAssistantId(event.target.value);
}}
className="w-full rounded-xl border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-200"
>
{messages
.map((message, index) => ({ ...message, index }))
.filter((item) => item.role === "assistant" && item.id)
.map((item) => (
<option key={item.id} value={item.id}>
Turn {item.index + 1}: {(item.content || "").slice(0, 72) || "Assistant response"}
</option>
))}
</select>
</label>
) : (
<div className="rounded-xl border border-dashed border-gray-200 px-3 py-3 text-sm text-gray-400">
Ask at least one question to generate evaluation metrics.
</div>
)}
/>
</div>
);
}