From 1de87476a39c430213741f4bc1ea83f9fa7a55a3 Mon Sep 17 00:00:00 2001 From: Vivek Arya Date: Sun, 21 Jun 2026 22:20:34 +0530 Subject: [PATCH 1/2] feat: per-message response time badge --- backend/app/routes/chat.py | 8 ++++++-- frontend/src/components/chat/ChatPanel.tsx | 2 +- frontend/src/components/chat/MessageBubble.tsx | 5 +++++ frontend/src/store/chat-store.ts | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/app/routes/chat.py b/backend/app/routes/chat.py index 8bfd8c61..b9cef48e 100644 --- a/backend/app/routes/chat.py +++ b/backend/app/routes/chat.py @@ -452,18 +452,19 @@ def event_stream(): top_k=payload.top_k, chat_history=chat_history, ): - yield chunk - # Parse to accumulate full answer for history try: if chunk.startswith("data: "): data = json.loads(chunk[6:].strip()) + if data.get("type") == "done": + continue # We will yield our own done event with response time if data.get("type") == "token": full_answer += data.get("data", "") elif data.get("type") == "sources": sources = data.get("data", []) except Exception: pass + yield chunk # Save assistant response to history from app.database import SessionLocal @@ -472,6 +473,9 @@ def event_stream(): _save_message(save_db, user.id, payload.document_id, "assistant", full_answer, sources, session_id=session_id) finally: save_db.close() + + elapsed_ms = round((time.perf_counter() - started_at) * 1000) + yield f"data: {json.dumps({'type': 'done', 'response_time_ms': elapsed_ms})}\n\n" finally: record_query_response_time(time.perf_counter() - started_at) diff --git a/frontend/src/components/chat/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx index 59e58094..cdbaa259 100644 --- a/frontend/src/components/chat/ChatPanel.tsx +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -235,7 +235,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { } else if (event.type === "done") { setMessages((prev) => prev.map((m) => - m.id === assistantId ? { ...m, isStreaming: false } : m + m.id === assistantId ? { ...m, isStreaming: false, response_time_ms: event.response_time_ms } : m ) ); } diff --git a/frontend/src/components/chat/MessageBubble.tsx b/frontend/src/components/chat/MessageBubble.tsx index 392b4b8e..68f632b7 100644 --- a/frontend/src/components/chat/MessageBubble.tsx +++ b/frontend/src/components/chat/MessageBubble.tsx @@ -318,6 +318,11 @@ export default function MessageBubble({ message }: Props) { > + {message.response_time_ms && ( + + ⚡ {(message.response_time_ms / 1000).toFixed(1)}s + + )} )} diff --git a/frontend/src/store/chat-store.ts b/frontend/src/store/chat-store.ts index 19c5468a..ca609788 100644 --- a/frontend/src/store/chat-store.ts +++ b/frontend/src/store/chat-store.ts @@ -27,6 +27,7 @@ export interface ChatMsg { sources: SourceChunk[]; feedback?: "up" | "down" | null; isStreaming?: boolean; + response_time_ms?: number; } export interface ChatSession { From c5c1c70c3659ec4c189b6adf94ca88c81a3e8cb0 Mon Sep 17 00:00:00 2001 From: Vivek Arya Date: Sun, 21 Jun 2026 22:31:38 +0530 Subject: [PATCH 2/2] fix: cast SSE event type for response_time_ms to satisfy TypeScript --- frontend/src/components/chat/ChatPanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/chat/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx index 88136846..ddd30963 100644 --- a/frontend/src/components/chat/ChatPanel.tsx +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -449,7 +449,9 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { } else if (event.type === "done") { setMessages((prev) => prev.map((m) => - m.id === assistantId ? { ...m, isStreaming: false, response_time_ms: event.response_time_ms } : m, + m.id === assistantId + ? { ...m, isStreaming: false, response_time_ms: (event as { type: string; response_time_ms?: number }).response_time_ms } + : m, ), ); }