Auto-refresh active sessions, collapsible skill categories, platform toolsets in config, search highlighting
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+41
-18
@@ -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,25 +57,39 @@ 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 }) => {
|
||||||
<div key={category}>
|
const isCollapsed = collapsed.has(category) && !search;
|
||||||
<div className="section-label mb-2">{category}</div>
|
return (
|
||||||
{skills.map((s) => (
|
<div key={category}>
|
||||||
<Link key={s.name} href={"/skills/" + s.name}>
|
<button
|
||||||
<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">
|
onClick={() => toggle(category)}
|
||||||
<div className="flex-1 min-w-0">
|
className="flex items-center gap-1.5 w-full text-left mb-1.5 px-1 active:opacity-70"
|
||||||
<div className="text-[14px]" style={{ color: "var(--text-1)" }}>{s.name}</div>
|
>
|
||||||
<div className="text-[12px] line-clamp-1" style={{ color: "var(--text-3)" }}>{s.description}</div>
|
{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}>
|
||||||
|
<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="text-[14px]" style={{ color: "var(--text-1)" }}>{s.name}</div>
|
||||||
|
<div className="text-[12px] line-clamp-1" style={{ color: "var(--text-3)" }}>{s.description}</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={16} style={{ color: "var(--text-3)" }} />
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight size={16} style={{ color: "var(--text-3)" }} />
|
</Link>
|
||||||
</div>
|
))}
|
||||||
</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 “{search}”</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user