new feature
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { userService } from "./userService";
|
||||
|
||||
export type SSEEvent =
|
||||
| { type: "tool_start"; tool: string }
|
||||
| { type: "tool_end"; tool: string }
|
||||
| { type: "response"; message: string }
|
||||
| { type: "error"; message: string };
|
||||
|
||||
export type SSEEventCallback = (event: SSEEvent) => void;
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
text: string;
|
||||
@@ -112,6 +120,59 @@ class ConversationService {
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async streamQuery(
|
||||
query: string,
|
||||
conversation_id: string,
|
||||
onEvent: SSEEventCallback,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
const response = await userService.fetchWithRefreshToken(
|
||||
`${this.conversationBaseUrl}/stream-query`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ query, conversation_id }),
|
||||
signal,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to stream query");
|
||||
}
|
||||
|
||||
await this._readSSEStream(response, onEvent);
|
||||
}
|
||||
|
||||
private async _readSSEStream(
|
||||
response: Response,
|
||||
onEvent: SSEEventCallback,
|
||||
): Promise<void> {
|
||||
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 parts = buffer.split("\n\n");
|
||||
buffer = parts.pop() ?? "";
|
||||
|
||||
for (const part of parts) {
|
||||
const line = part.trim();
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
const data = line.slice(6);
|
||||
if (data === "[DONE]") return;
|
||||
try {
|
||||
const event = JSON.parse(data) as SSEEvent;
|
||||
onEvent(event);
|
||||
} catch {
|
||||
// ignore malformed events
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const conversationService = new ConversationService();
|
||||
|
||||
@@ -2,13 +2,14 @@ import { useEffect, useState, useRef } from "react";
|
||||
import { conversationService } from "../api/conversationService";
|
||||
import { QuestionBubble } from "./QuestionBubble";
|
||||
import { AnswerBubble } from "./AnswerBubble";
|
||||
import { ToolBubble } from "./ToolBubble";
|
||||
import { MessageInput } from "./MessageInput";
|
||||
import { ConversationList } from "./ConversationList";
|
||||
import catIcon from "../assets/cat.png";
|
||||
|
||||
type Message = {
|
||||
text: string;
|
||||
speaker: "simba" | "user";
|
||||
speaker: "simba" | "user" | "tool";
|
||||
};
|
||||
|
||||
type QuestionAnswer = {
|
||||
@@ -25,6 +26,24 @@ type ChatScreenProps = {
|
||||
setAuthenticated: (isAuth: boolean) => void;
|
||||
};
|
||||
|
||||
const TOOL_MESSAGES: Record<string, string> = {
|
||||
simba_search: "🔍 Searching Simba's records...",
|
||||
web_search: "🌐 Searching the web...",
|
||||
get_current_date: "📅 Checking today's date...",
|
||||
ynab_budget_summary: "💰 Checking budget summary...",
|
||||
ynab_search_transactions: "💳 Looking up transactions...",
|
||||
ynab_category_spending: "📊 Analyzing category spending...",
|
||||
ynab_insights: "📈 Generating budget insights...",
|
||||
obsidian_search_notes: "📝 Searching notes...",
|
||||
obsidian_read_note: "📖 Reading note...",
|
||||
obsidian_create_note: "✏️ Saving note...",
|
||||
obsidian_create_task: "✅ Creating task...",
|
||||
journal_get_today: "📔 Reading today's journal...",
|
||||
journal_get_tasks: "📋 Getting tasks...",
|
||||
journal_add_task: "➕ Adding task...",
|
||||
journal_complete_task: "✔️ Completing task...",
|
||||
};
|
||||
|
||||
export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [answer, setAnswer] = useState<string>("");
|
||||
@@ -170,17 +189,25 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
try {
|
||||
const result = await conversationService.sendQuery(
|
||||
await conversationService.streamQuery(
|
||||
query,
|
||||
selectedConversation.id,
|
||||
(event) => {
|
||||
if (!isMountedRef.current) return;
|
||||
if (event.type === "tool_start") {
|
||||
const friendly =
|
||||
TOOL_MESSAGES[event.tool] ?? `🔧 Using ${event.tool}...`;
|
||||
setMessages((prev) => prev.concat([{ text: friendly, speaker: "tool" }]));
|
||||
} else if (event.type === "response") {
|
||||
setMessages((prev) =>
|
||||
prev.concat([{ text: event.message, speaker: "simba" }]),
|
||||
);
|
||||
} else if (event.type === "error") {
|
||||
console.error("Stream error:", event.message);
|
||||
}
|
||||
},
|
||||
abortController.signal,
|
||||
);
|
||||
setQuestionsAnswers(
|
||||
questionsAnswers.concat([{ question: query, answer: result.response }]),
|
||||
);
|
||||
setMessages(
|
||||
currMessages.concat([{ text: result.response, speaker: "simba" }]),
|
||||
);
|
||||
} catch (error) {
|
||||
// Ignore abort errors (these are intentional cancellations)
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
@@ -348,9 +375,10 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
|
||||
)}
|
||||
|
||||
{messages.map((msg, index) => {
|
||||
if (msg.speaker === "simba") {
|
||||
if (msg.speaker === "tool")
|
||||
return <ToolBubble key={index} text={msg.text} />;
|
||||
if (msg.speaker === "simba")
|
||||
return <AnswerBubble key={index} text={msg.text} />;
|
||||
}
|
||||
return <QuestionBubble key={index} text={msg.text} />;
|
||||
})}
|
||||
{isLoading && <AnswerBubble text="" loading={true} />}
|
||||
|
||||
5
raggr-frontend/src/components/ToolBubble.tsx
Normal file
5
raggr-frontend/src/components/ToolBubble.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export const ToolBubble = ({ text }: { text: string }) => (
|
||||
<div className="text-sm text-gray-500 italic px-3 py-1 self-start">
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
Reference in New Issue
Block a user