Auto-refresh active sessions, collapsible skill categories, platform toolsets in config, search highlighting

This commit is contained in:
Hermes
2026-04-09 21:38:33 -05:00
parent d86cbea6a5
commit 61e512ffe5
4 changed files with 83 additions and 20 deletions
+20
View File
@@ -13,6 +13,7 @@ export default function ConfigPage() {
const memory = cfg.memory as Record<string, unknown> | undefined; const memory = cfg.memory as Record<string, unknown> | undefined;
const display = cfg.display as Record<string, unknown> | undefined; const display = cfg.display as Record<string, unknown> | undefined;
const tts = cfg.tts as Record<string, unknown> | undefined; const tts = cfg.tts as Record<string, unknown> | undefined;
const platformToolsets = cfg.platform_toolsets as Record<string, string[]> | undefined;
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -58,6 +59,25 @@ export default function ConfigPage() {
["Skin", String(display?.skin || "default")], ["Skin", String(display?.skin || "default")],
...(tts ? [["TTS", String(tts.provider || "—")] as [string, string]] : []), ...(tts ? [["TTS", String(tts.provider || "—")] as [string, string]] : []),
]} /> ]} />
{platformToolsets && Object.keys(platformToolsets).length > 0 && (
<div className="card p-4">
<div className="section-label mb-3">Platform Toolsets</div>
<div className="space-y-3">
{Object.entries(platformToolsets).map(([platform, tools]) => (
<div key={platform}>
<div className="text-[12px] font-medium mb-1" style={{ color: "var(--text-2)" }}>{platform}</div>
<div className="flex flex-wrap gap-1">
{(tools as string[]).map((t) => (
<span key={t} className="text-[11px] px-2 py-0.5 rounded-md font-mono" style={{ background: "var(--bg-hover)", color: "var(--text-3)" }}>
{t}
</span>
))}
</div>
</div>
))}
</div>
</div>
)}
</div> </div>
)} )}
</div> </div>
+11 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { use, useState, useRef } from "react"; import { use, useState, useRef, useEffect } from "react";
import { ArrowLeft, Send, Loader2, Eye, RefreshCw, Trash2 } from "lucide-react"; import { ArrowLeft, Send, Loader2, Eye, RefreshCw, Trash2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useSession, useMessages } from "@/lib/hooks"; import { useSession, useMessages } from "@/lib/hooks";
@@ -37,6 +37,16 @@ export default function SessionReplayPage({ params }: { params: Promise<{ id: st
const session = sessionData?.session; const session = sessionData?.session;
const messages = messagesData?.items || []; const messages = messagesData?.items || [];
// Auto-refresh active sessions every 5s
useEffect(() => {
if (!session || session.ended_at) return;
const interval = setInterval(() => {
qc.invalidateQueries({ queryKey: ["messages", id, msgLimit] });
qc.invalidateQueries({ queryKey: ["session", id] });
}, 5000);
return () => clearInterval(interval);
}, [session, id, msgLimit, qc]);
// Filter based on verbosity // Filter based on verbosity
let visibleMessages = messages.filter((m) => { let visibleMessages = messages.filter((m) => {
if (m.role === "user") return true; if (m.role === "user") return true;
+11 -1
View File
@@ -14,6 +14,12 @@ const PLATFORMS = [
{ id: "web", label: "Web" }, { id: "web", label: "Web" },
] as const; ] as const;
function highlightMatch(text: string, query: string): string {
if (!query) return text;
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return text.replace(new RegExp("(" + escaped + ")", "gi"), '<mark style="background:var(--accent-dim);color:var(--accent);border-radius:2px;padding:0 2px">$1</mark>');
}
export default function HistoryPage() { export default function HistoryPage() {
const [filter, setFilter] = useState<string | null>(null); const [filter, setFilter] = useState<string | null>(null);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@@ -68,7 +74,11 @@ export default function HistoryPage() {
<div className="text-[12px] mb-0.5" style={{ color: "var(--text-2)" }}> <div className="text-[12px] mb-0.5" style={{ color: "var(--text-2)" }}>
{r.session_title || r.session_id.slice(0, 8)} {r.session_title || r.session_id.slice(0, 8)}
</div> </div>
<p className="text-[13px] line-clamp-2" style={{ color: "var(--text-1)" }}>{r.content}</p> <p className="text-[13px] line-clamp-2" style={{ color: "var(--text-1)" }}
dangerouslySetInnerHTML={{
__html: highlightMatch(r.content, query),
}}
/>
</div> </div>
</a> </a>
))} ))}
+30 -7
View File
@@ -2,13 +2,14 @@
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { ChevronRight, Search, X } from "lucide-react"; import { ChevronRight, ChevronDown, Search, X } from "lucide-react";
import { useSkills } from "@/lib/hooks"; import { useSkills } from "@/lib/hooks";
import type { SkillSummary } from "@/lib/types"; import type { SkillSummary } from "@/lib/types";
export default function SkillsPage() { export default function SkillsPage() {
const { data, isLoading } = useSkills(); const { data, isLoading } = useSkills();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
const skills = data?.skills || []; const skills = data?.skills || [];
const categories = [...new Set(skills.map((s) => s.category || "uncategorized"))].sort(); const categories = [...new Set(skills.map((s) => s.category || "uncategorized"))].sort();
@@ -21,6 +22,14 @@ export default function SkillsPage() {
.map((cat) => ({ category: cat, skills: filtered.filter((s) => (s.category || "uncategorized") === cat) })) .map((cat) => ({ category: cat, skills: filtered.filter((s) => (s.category || "uncategorized") === cat) }))
.filter((g) => g.skills.length > 0); .filter((g) => g.skills.length > 0);
const toggle = (cat: string) => {
setCollapsed((prev) => {
const next = new Set(prev);
next.has(cat) ? next.delete(cat) : next.add(cat);
return next;
});
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -48,11 +57,24 @@ export default function SkillsPage() {
{isLoading ? ( {isLoading ? (
<div className="space-y-2">{[...Array(5)].map((_, i) => <div key={i} className="card p-4 animate-pulse"><div className="h-4 rounded w-3/4" style={{ background: "var(--bg-hover)" }} /></div>)}</div> <div className="space-y-2">{[...Array(5)].map((_, i) => <div key={i} className="card p-4 animate-pulse"><div className="h-4 rounded w-3/4" style={{ background: "var(--bg-hover)" }} /></div>)}</div>
) : ( ) : (
<div className="space-y-5"> <div className="space-y-3">
{grouped.map(({ category, skills }) => ( {grouped.map(({ category, skills }) => {
const isCollapsed = collapsed.has(category) && !search;
return (
<div key={category}> <div key={category}>
<div className="section-label mb-2">{category}</div> <button
{skills.map((s) => ( onClick={() => toggle(category)}
className="flex items-center gap-1.5 w-full text-left mb-1.5 px-1 active:opacity-70"
>
{isCollapsed ? (
<ChevronRight size={14} style={{ color: "var(--text-3)" }} />
) : (
<ChevronDown size={14} style={{ color: "var(--text-3)" }} />
)}
<span className="section-label">{category}</span>
<span className="text-[10px] ml-1" style={{ color: "var(--text-3)" }}>{skills.length}</span>
</button>
{!isCollapsed && skills.map((s) => (
<Link key={s.name} href={"/skills/" + s.name}> <Link key={s.name} href={"/skills/" + s.name}>
<div className="flex items-center gap-3 py-2.5 px-1 border-b border-white/[0.04] active:bg-white/[0.02] transition-colors"> <div className="flex items-center gap-3 py-2.5 px-1 border-b border-white/[0.04] active:bg-white/[0.02] transition-colors">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -64,9 +86,10 @@ export default function SkillsPage() {
</Link> </Link>
))} ))}
</div> </div>
))} );
})}
{grouped.length === 0 && ( {grouped.length === 0 && (
<p className="text-sm py-8 text-center" style={{ color: "var(--text-3)" }}>No skills match "{search}"</p> <p className="text-sm py-8 text-center" style={{ color: "var(--text-3)" }}>No skills match &ldquo;{search}&rdquo;</p>
)} )}
</div> </div>
)} )}