From 893c74ed38c7b158c7ebd9695f204612b2bf6d07 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 9 Apr 2026 21:15:11 -0500 Subject: [PATCH] Improve chat UX: thinking indicator, pending message, 5min timeout --- app/api/sessions/[id]/chat/route.ts | 10 ++++- app/history/[id]/page.tsx | 57 +++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/app/api/sessions/[id]/chat/route.ts b/app/api/sessions/[id]/chat/route.ts index 33961c5..1ef0172 100644 --- a/app/api/sessions/[id]/chat/route.ts +++ b/app/api/sessions/[id]/chat/route.ts @@ -6,17 +6,23 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const { id } = await params; const body = await req.json(); try { + // Agent chat can take a while — 5 minute timeout + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 300_000); const res = await fetch(BACKEND + "/api/sessions/" + id + "/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), + signal: controller.signal, }); + clearTimeout(timeout); const data = await res.text(); return new NextResponse(data, { status: res.status, headers: { "content-type": "application/json" }, }); - } catch { - return NextResponse.json({ error: "backend unreachable" }, { status: 502 }); + } catch (e) { + const msg = e instanceof Error ? e.message : "backend unreachable"; + return NextResponse.json({ error: msg }, { status: 502 }); } } diff --git a/app/history/[id]/page.tsx b/app/history/[id]/page.tsx index 1504e44..73e931e 100644 --- a/app/history/[id]/page.tsx +++ b/app/history/[id]/page.tsx @@ -1,26 +1,29 @@ "use client"; -import { use, useState, useRef, useEffect } from "react"; -import { ArrowLeft, Send } from "lucide-react"; +import { use, useState, useRef } from "react"; +import { ArrowLeft, Send, Loader2 } from "lucide-react"; import Link from "next/link"; import { useSession, useMessages } from "@/lib/hooks"; import { timeAgo, formatTokens, platformBadgeClass } from "@/lib/utils"; import { MessageBubble } from "@/components/MessageBubble"; +import { useQueryClient } from "@tanstack/react-query"; export default function SessionReplayPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); + const qc = useQueryClient(); const { data: sessionData } = useSession(id); - const { data: messagesData, isLoading, refetch } = useMessages(id); + const { data: messagesData, isLoading } = useMessages(id); const [input, setInput] = useState(""); const [sending, setSending] = useState(false); + const [pendingMsg, setPendingMsg] = useState(null); const inputRef = useRef(null); const session = sessionData?.session; const messages = messagesData?.items || []; - const visibleMessages = messages.filter( - (m) => m.role === "user" || m.role === "assistant" - ).reverse(); // newest first + const visibleMessages = messages + .filter((m) => m.role === "user" || m.role === "assistant") + .reverse(); const toolResults = new Map(); for (const m of messages) { @@ -34,20 +37,23 @@ export default function SessionReplayPage({ params }: { params: Promise<{ id: st const sendMessage = async () => { if (!input.trim() || sending) return; + const msg = input.trim(); + setInput(""); setSending(true); + setPendingMsg(msg); + try { await fetch("/api/sessions/" + id + "/chat", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message: input.trim() }), + body: JSON.stringify({ message: msg }), }); - setInput(""); - // Poll for response - setTimeout(() => refetch(), 2000); - setTimeout(() => refetch(), 5000); - setTimeout(() => refetch(), 10000); } catch {} + + setPendingMsg(null); setSending(false); + qc.invalidateQueries({ queryKey: ["messages", id] }); + qc.invalidateQueries({ queryKey: ["session", id] }); }; return ( @@ -82,7 +88,7 @@ export default function SessionReplayPage({ params }: { params: Promise<{ id: st value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && sendMessage()} - placeholder="Send a message..." + placeholder={sending ? "Hermes is thinking..." : "Send a message..."} className="flex-1 px-4 py-2.5 rounded-xl text-sm outline-none transition-colors" style={{ background: "var(--bg-raised)", @@ -97,7 +103,7 @@ export default function SessionReplayPage({ params }: { params: Promise<{ id: st className="px-3 rounded-xl transition-opacity disabled:opacity-20" style={{ background: "var(--accent)", color: "white" }} > - + {sending ? : } @@ -113,10 +119,31 @@ export default function SessionReplayPage({ params }: { params: Promise<{ id: st ) : (
+ {/* Pending thinking indicator */} + {sending && ( +
+
+
+ + Thinking... +
+
+
+ )} + + {/* Pending user message */} + {pendingMsg && ( +
+
+

{pendingMsg}

+
+
+ )} + {visibleMessages.map((m) => ( ))} - {visibleMessages.length === 0 && ( + {visibleMessages.length === 0 && !sending && (

Empty session