"use client"; import { use, useState, useRef } from "react"; import { ArrowLeft, Send, Loader2, Eye, RefreshCw, Trash2 } 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"; import { useRouter } from "next/navigation"; export type Verbosity = "compact" | "normal" | "full"; const VERBOSITY_OPTIONS: { id: Verbosity; label: string }[] = [ { id: "compact", label: "Compact" }, { id: "normal", label: "Normal" }, { id: "full", label: "Full" }, ]; export default function SessionReplayPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); const qc = useQueryClient(); const router = useRouter(); const [confirmDelete, setConfirmDelete] = useState(false); const [editingTitle, setEditingTitle] = useState(false); const [titleDraft, setTitleDraft] = useState(""); const { data: sessionData } = useSession(id); const [msgLimit, setMsgLimit] = useState(100); const { data: messagesData, isLoading } = useMessages(id, msgLimit); const [input, setInput] = useState(""); const [sending, setSending] = useState(false); const [pendingMsg, setPendingMsg] = useState(null); const [verbosity, setVerbosity] = useState("normal"); const [showVerbosity, setShowVerbosity] = useState(false); const inputRef = useRef(null); const session = sessionData?.session; const messages = messagesData?.items || []; // Filter based on verbosity let visibleMessages = messages.filter((m) => { if (m.role === "user") return true; if (m.role === "assistant") { if (verbosity === "compact") return !!m.content?.trim(); if (verbosity === "normal") return !!m.content?.trim() || !!m.tool_calls; // full: show everything including tool-only and reasoning-only messages return true; } return false; }).reverse(); const toolResults = new Map(); for (const m of messages) { if (m.role === "tool" && m.tool_call_id) { toolResults.set(m.tool_call_id, { name: m.tool_name || "tool", content: m.content || "", }); } } 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: msg }), }); } catch {} setPendingMsg(null); setSending(false); qc.invalidateQueries({ queryKey: ["messages", id] }); qc.invalidateQueries({ queryKey: ["session", id] }); }; const deleteSession = async () => { await fetch("/api/sessions/" + id, { method: "DELETE" }); router.push("/history"); }; return (
{/* Header */}
{editingTitle ? ( setTitleDraft(e.target.value)} onBlur={async () => { if (titleDraft.trim() && titleDraft !== session?.title) { await fetch("/api/sessions/" + id, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: titleDraft.trim() }), }); qc.invalidateQueries({ queryKey: ["session", id] }); } setEditingTitle(false); }} onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} className="text-[15px] font-medium w-full bg-transparent outline-none" style={{ color: "var(--text-1)", borderBottom: "1px solid var(--accent)" }} autoFocus /> ) : (
{ setTitleDraft(session?.title || ""); setEditingTitle(true); }} > {session?.title || "Session " + id.slice(0, 8)}
)} {session && (
{session.source} {session.model && {session.model.split("/").pop()}} {session.model && " · "} {session.message_count} msgs · {formatTokens(session.input_tokens + session.output_tokens)} · {timeAgo(session.started_at)}
)}
{/* Delete */} {confirmDelete ? (
) : ( )} {/* Refresh */} {/* Verbosity toggle */}
{showVerbosity && (
{VERBOSITY_OPTIONS.map((opt) => ( ))}
)}
{/* Message input */}
setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && sendMessage()} 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)", border: "1px solid var(--border)", color: "var(--text-1)", }} disabled={sending} />
{/* Messages */} {isLoading ? (
{[...Array(4)].map((_, i) => (
))}
) : (
{sending && (
Thinking...
)} {pendingMsg && (

{pendingMsg}

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

Empty session

)} {/* Load more (at bottom = older messages since list is reversed) */} {(messagesData?.total || 0) > msgLimit && ( )}
)}
); }