diff --git a/app/api/sessions/[id]/chat/route.ts b/app/api/sessions/[id]/chat/route.ts new file mode 100644 index 0000000..33961c5 --- /dev/null +++ b/app/api/sessions/[id]/chat/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; + +const BACKEND = process.env.HERMES_BACKEND_URL || "http://127.0.0.1:8643"; + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const body = await req.json(); + try { + const res = await fetch(BACKEND + "/api/sessions/" + id + "/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + 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 }); + } +} diff --git a/app/globals.css b/app/globals.css index 13ce3d9..87020c4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,49 +2,84 @@ :root { --safe-bottom: env(safe-area-inset-bottom, 0px); + --accent: #7c6ef0; + --accent-dim: rgba(124, 110, 240, 0.12); + --bg: #111113; + --bg-raised: #1a1a1f; + --bg-hover: #222228; + --border: rgba(255, 255, 255, 0.06); + --text-1: #ececef; + --text-2: #8e8e93; + --text-3: #48484a; } body { - background: #0a0a0f; - color: #f0f0f0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + background: var(--bg); + color: var(--text-1); + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif; -webkit-font-smoothing: antialiased; overscroll-behavior: none; } -/* Glass card */ -.glass { - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px; +/* Card - solid, not glass */ +.card { + background: var(--bg-raised); + border-radius: 14px; + border: 1px solid var(--border); } -.glass-hover:active { - background: rgba(255, 255, 255, 0.07); +.card-hover:active { + background: var(--bg-hover); } /* Scrollbar */ -::-webkit-scrollbar { width: 4px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; } +::-webkit-scrollbar { width: 0; } -/* Safe area padding for bottom nav */ +/* Safe area */ .pb-safe { padding-bottom: calc(4.5rem + var(--safe-bottom)); } /* Platform badges */ -.badge-cli { background: rgba(245,158,11,0.15); color: #f59e0b; } -.badge-telegram { background: rgba(56,189,248,0.15); color: #38bdf8; } -.badge-discord { background: rgba(139,92,246,0.15); color: #8b5cf6; } -.badge-whatsapp { background: rgba(34,197,94,0.15); color: #22c55e; } -.badge-web { background: rgba(168,85,247,0.15); color: #a78bfa; } +.badge { + font-size: 10px; + font-weight: 600; + padding: 2px 8px; + border-radius: 100px; + letter-spacing: 0.02em; +} +.badge-cli { background: rgba(255,255,255,0.06); color: #8e8e93; } +.badge-telegram { background: rgba(0,136,204,0.12); color: #29b6f6; } +.badge-discord { background: rgba(88,101,242,0.12); color: #7289da; } +.badge-whatsapp { background: rgba(37,211,102,0.12); color: #25d366; } +.badge-web { background: rgba(124,110,240,0.12); color: #7c6ef0; } -/* Pulse dot animation */ -@keyframes pulse-dot { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } +/* Accent button */ +.btn-accent { + background: var(--accent); + color: white; + border: none; + border-radius: 10px; + font-weight: 600; + font-size: 14px; + padding: 10px 16px; + transition: opacity 0.15s; } -.animate-pulse-dot { - animation: pulse-dot 2s ease-in-out infinite; +.btn-accent:active { opacity: 0.7; } +.btn-accent:disabled { opacity: 0.3; } + +/* Divider */ +.divider { + height: 1px; + background: var(--border); + margin: 0 -16px; +} + +/* Section label */ +.section-label { + font-size: 13px; + font-weight: 600; + color: var(--text-2); + text-transform: uppercase; + letter-spacing: 0.04em; } diff --git a/app/history/[id]/page.tsx b/app/history/[id]/page.tsx index 267bfe7..1504e44 100644 --- a/app/history/[id]/page.tsx +++ b/app/history/[id]/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { use } from "react"; -import { ArrowLeft, MessageSquare, Wrench } from "lucide-react"; +import { use, useState, useRef, useEffect } from "react"; +import { ArrowLeft, Send } from "lucide-react"; import Link from "next/link"; import { useSession, useMessages } from "@/lib/hooks"; import { timeAgo, formatTokens, platformBadgeClass } from "@/lib/utils"; @@ -10,17 +10,18 @@ import { MessageBubble } from "@/components/MessageBubble"; export default function SessionReplayPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); const { data: sessionData } = useSession(id); - const { data: messagesData, isLoading } = useMessages(id); + const { data: messagesData, isLoading, refetch } = useMessages(id); + const [input, setInput] = useState(""); + const [sending, setSending] = useState(false); + const inputRef = useRef(null); const session = sessionData?.session; const messages = messagesData?.items || []; - // Group tool results with their preceding assistant tool_calls const visibleMessages = messages.filter( (m) => m.role === "user" || m.role === "assistant" - ); + ).reverse(); // newest first - // Collect tool results keyed by tool_call_id const toolResults = new Map(); for (const m of messages) { if (m.role === "tool" && m.tool_call_id) { @@ -31,53 +32,94 @@ export default function SessionReplayPage({ params }: { params: Promise<{ id: st } } + const sendMessage = async () => { + if (!input.trim() || sending) return; + setSending(true); + try { + await fetch("/api/sessions/" + id + "/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message: input.trim() }), + }); + setInput(""); + // Poll for response + setTimeout(() => refetch(), 2000); + setTimeout(() => refetch(), 5000); + setTimeout(() => refetch(), 10000); + } catch {} + setSending(false); + }; + return ( -
+
{/* Header */} -
- - +
+ +
-

- {session?.title || `Session ${id.slice(0, 8)}`} -

+
+ {session?.title || "Session " + id.slice(0, 8)} +
{session && ( -
- +
+ {session.source} - {session.message_count} msgs - {formatTokens(session.input_tokens + session.output_tokens)} tok - {timeAgo(session.started_at)} + + {session.message_count} msgs · {formatTokens(session.input_tokens + session.output_tokens)} · {timeAgo(session.started_at)} +
)}
- {/* Messages */} + {/* Message input */} +
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && sendMessage()} + placeholder="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 - newest first */} {isLoading ? (
{[...Array(4)].map((_, i) => ( -
-
-
+
+
+
))}
) : ( -
+
{visibleMessages.map((m) => ( - + ))} {visibleMessages.length === 0 && ( -
+

Empty session -

+

)}
)} diff --git a/app/history/page.tsx b/app/history/page.tsx index 1781046..b6d7e04 100644 --- a/app/history/page.tsx +++ b/app/history/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Clock, Search, X } from "lucide-react"; +import { Search, X } from "lucide-react"; import { useSessions, useSessionSearch } from "@/lib/hooks"; import { SessionCard } from "@/components/SessionCard"; @@ -19,59 +19,44 @@ export default function HistoryPage() { const [searching, setSearching] = useState(false); const { data: sessionsData, isLoading } = useSessions(30, filter || undefined); - const { data: searchData, isFetching: searchLoading } = useSessionSearch( - searching ? query : "" - ); - + const { data: searchData, isFetching: searchLoading } = useSessionSearch(searching ? query : ""); const showSearch = searching && query.length >= 2; return (
-
-
- -

History

-
-
+

History

- {/* Search bar */}
- + { setQuery(e.target.value); setSearching(true); }} onFocus={() => setSearching(true)} placeholder="Search sessions..." - className="w-full pl-9 pr-9 py-2.5 rounded-xl text-sm bg-white/[0.04] border border-white/[0.08] text-zinc-200 placeholder-zinc-600 outline-none focus:border-white/[0.15] transition-colors" + className="w-full pl-9 pr-9 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)" }} /> {query && ( - )}
- {/* Search results */} {showSearch ? (
-
- {searchLoading ? "Searching..." : `${searchData?.count || 0} results`} +
+ {searchLoading ? "Searching..." : (searchData?.count || 0) + " results"}
-
+
{searchData?.results?.map((r) => ( - -
- ) : ( <> - {/* Platform filter */}
{PLATFORMS.map((p) => (
- {/* Session list */} {isLoading ? ( -
+
{[...Array(5)].map((_, i) => ( -
-
-
+
+
+
))}
) : ( -
+
{sessionsData?.items?.map((s) => ( ))} {sessionsData?.items?.length === 0 && ( -
- No sessions found -
+

No sessions found

)}
)} diff --git a/app/memory/page.tsx b/app/memory/page.tsx index 9e44be6..9266b95 100644 --- a/app/memory/page.tsx +++ b/app/memory/page.tsx @@ -1,13 +1,13 @@ "use client"; import { useState } from "react"; -import { Brain, Plus } from "lucide-react"; +import { Plus } from "lucide-react"; import { useMemory, useAddMemory, usePatchMemory, useDeleteMemory } from "@/lib/hooks"; import { MemoryEntry } from "@/components/MemoryEntry"; const TABS = [ - { id: "memory", label: "Agent", color: "#f59e0b" }, - { id: "user", label: "User", color: "#38bdf8" }, + { id: "memory", label: "Agent" }, + { id: "user", label: "User" }, ] as const; export default function MemoryPage() { @@ -22,10 +22,8 @@ export default function MemoryPage() { const target = data?.targets?.find((t) => t.target === tab); const entries = target?.entries || []; const usage = target?.usage || ""; - const pctMatch = usage.match(/(\d+)%/); const pct = pctMatch ? parseInt(pctMatch[1]) : 0; - const barColor = pct > 85 ? "#ef4444" : pct > 65 ? "#f59e0b" : "#38bdf8"; const handleAdd = () => { if (!newText.trim()) return; @@ -34,41 +32,28 @@ export default function MemoryPage() { }); }; - const handlePatch = (oldText: string, newContent: string) => { - patchMutation.mutate({ target: tab, old_text: oldText, content: newContent }); - }; - - const handleDelete = (oldText: string) => { - deleteMutation.mutate({ target: tab, old_text: oldText }); - }; - return (
-
- -

Memory

-
+

Memory

- {/* Tabs */} -
+
{TABS.map((t) => (
- {/* Usage bar */} {usage && ( -
-
- {tab === "memory" ? "Agent memory" : "User profile"} - {usage} -
-
+
+
85 ? "#ff3b30" : "var(--accent)" }} />
+ {usage}
)} - {/* Add form */} {adding && ( -
+