feat(dashboard): knowledge page + Tiger knowledge API

This commit is contained in:
Manohar 2026-06-05 19:49:23 +00:00
parent 598dbdd4a4
commit 2cbb9b3bcf
2 changed files with 259 additions and 76 deletions

View file

@ -0,0 +1,38 @@
import { NextRequest } from "next/server"
const BRIDGE = "http://127.0.0.1:3456"
const TOKEN = "14fb879429386b69beac339bbd98e43011ec29485da17592410da34ed97e0236"
export async function GET(request: NextRequest) {
const url = request.nextUrl.searchParams.get("url") || "/knowledge"
try {
const res = await fetch(`${BRIDGE}${url}`, {
headers: { Authorization: `Bearer ${TOKEN}` }
})
const data = await res.json()
return Response.json(data)
} catch (err) {
return Response.json({ error: (err as Error).message }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
const body = await request.json().catch(() => ({}))
const url = request.nextUrl.searchParams.get("url") || "/knowledge"
try {
const res = await fetch(`${BRIDGE}${url}`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
})
const data = await res.json()
return Response.json(data)
} catch (err) {
return Response.json({ error: (err as Error).message }, { status: 500 })
}
}

View file

@ -1,90 +1,235 @@
"use client" "use client"
/** import { useEffect, useState, useRef } from "react"
* /knowledge Tiger's brain (Phase 1 hub) import dynamic from 'next/dynamic'
* import { Brain, Search, ScrollText, Wrench, Activity, Clock, Network, MessageSquare } from "lucide-react"
* Absorbs four orphans that previously had no sidebar entry:
* /memory SOUL.md, USER.md, IDENTITY.md, MEMORY.md content
* /skills registry of skills
* /activity agent file-write timeline
* /cron scheduled tasks
*
* For Phase 1 this is a hub that links into each existing page. Phase 6
* will fold all four into tabs on this page.
*/
import { Brain, ScrollText, Wrench, Activity, Clock } from "lucide-react" const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false })
import { Card } from "@/components/ui/card"
const sections = [ interface KnowledgeNode {
{ id: string
title: "Memory", type: string
href: "/memory", name: string
icon: ScrollText, description: string
description: }
"Tiger's persistent memory — SOUL.md, USER.md, IDENTITY.md, MEMORY.md. " +
"Edit these files to teach Tiger about you and itself.", interface FeedbackPref {
}, key: string
{ value: string
title: "Skills", }
href: "/skills",
icon: Wrench, interface GraphNode {
description: id: string
"Registry of capabilities Tiger can invoke. Skills are reusable " + name: string
"instruction sets that ship with the openclaw runtime.", type: string
}, val: number
{ }
title: "Activity",
href: "/activity", interface GraphLink {
icon: Activity, source: { id: string }
description: target: { id: string }
"Timeline of every workspace file write across all agents. Useful for " + name: string
"auditing what Tiger and the sub-agents have been doing.", }
},
{
title: "Scheduled jobs",
href: "/cron",
icon: Clock,
description:
"Cron-scheduled agent runs. Things like 'morning digest at 8am' or " +
"'pull RE news daily' live here.",
},
]
export default function KnowledgePage() { export default function KnowledgePage() {
const [nodes, setNodes] = useState<KnowledgeNode[]>([])
const [prefs, setPrefs] = useState<FeedbackPref[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState("")
const [showGraph, setShowGraph] = useState(true)
const graphRef = useRef<any>(null)
const [error, setError] = useState<string | null>(null)
const sections = [
{ title: "Memory", href: "/memory", icon: ScrollText, description: "Tiger's persistent memory" },
{ title: "Skills", href: "/skills", icon: Wrench, description: "Registry of capabilities" },
{ title: "Activity", href: "/activity", icon: Activity, description: "Timeline of events" },
{ title: "Schedule", href: "/cron", icon: Clock, description: "Scheduled tasks" },
]
useEffect(() => {
Promise.all([
fetch("/api/tiger/knowledge").then(r => r.json()),
fetch("/api/tiger/knowledge?url=/feedback/prefer").then(r => r.json())
]).then(([kg, fp]) => {
if (kg?.nodes) setNodes(kg.nodes)
if (fp?.preferences) setPrefs(fp.preferences)
setLoading(false)
}).catch(e => {
setError(e.message)
setLoading(false)
})
}, [])
const filteredNodes = search
? nodes.filter(n => n.name.toLowerCase().includes(search.toLowerCase()))
: nodes
const getTypeColor = (type: string) => {
switch (type) {
case "person": return "#60a5fa"
case "company": return "#4ade80"
case "concept": return "#c084fc"
default: return "#94a3b8"
}
}
const getTypeIcon = (type: string) => {
switch (type) {
case "person": return "👤"
case "company": return "🏢"
case "concept": return "💡"
default: return "📌"
}
}
// Build graph data from nodes
const graphNodes: GraphNode[] = nodes.map(n => ({ id: n.id, name: n.name, type: n.type, val: 10 }))
const graphLinks: GraphLink[] = nodes.flatMap(n =>
nodes.filter(m => m.id !== n.id).slice(0, 1).map(m => ({
source: { id: n.id },
target: { id: m.id },
name: "related"
}))
)
const connections = [
{ from: "Manohar", rel: "works-at", to: "Renew Power" },
{ from: "Manohar", rel: "interested-in", to: "PE/VC" },
{ from: "Renew Power", rel: "competitor", to: "Adani Green" },
]
if (loading) {
return (
<div className="p-6">
<div className="flex items-center gap-2 mb-4">
<Brain className="h-5 w-5" />
<h1 className="text-2xl font-bold">Knowledge</h1>
</div>
<div className="text-muted-foreground">Loading...</div>
</div>
)
}
return ( return (
<div className="space-y-6 max-w-5xl mx-auto w-full"> <div className="p-6 space-y-6">
{/* TOP - Tiger's Brain */}
<div> <div>
<h1 className="text-2xl font-bold flex items-center gap-2"> <div className="flex items-center gap-2 mb-3">
<Brain className="h-6 w-6 text-primary" /> <Brain className="h-5 w-5" />
Knowledge <h1 className="text-2xl font-bold">Knowledge</h1>
</h1> </div>
<p className="text-muted-foreground text-sm">
Tiger's brain — what it remembers, what it can do, what it's done, <h2 className="text-lg font-semibold mb-3">Tiger's Brain</h2>
and what it'll do next. <div className="grid grid-cols-4 gap-3">
</p> {sections.map(s => (
<a key={s.title} href={s.href} className="p-4 rounded-lg border hover:bg-card/50 text-center">
<s.icon className="h-6 w-6 mx-auto mb-2" />
<div className="font-medium">{s.title}</div>
<div className="text-xs text-muted-foreground">{s.description}</div>
</a>
))}
</div>
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> {/* BOTTOM */}
{sections.map(({ title, href, icon: Icon, description }) => ( <div className="grid grid-cols-2 gap-6">
<a key={title} href={href} className="contents"> {/* Graph/List Toggle */}
<Card className="bg-card/40 p-5 hover:bg-card/60 hover:border-primary/30 transition-colors cursor-pointer"> <div className="space-y-3">
<div className="flex items-start gap-3 mb-2"> <div className="flex items-center justify-between">
<div className="shrink-0 h-9 w-9 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center"> <div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-primary" /> <Network className="h-4 w-4" />
<h3 className="text-lg font-semibold">Knowledge Graph</h3>
</div>
<button
onClick={() => setShowGraph(!showGraph)}
className="px-3 py-1 text-sm rounded border hover:bg-card/50"
>
{showGraph ? "Show List" : "Show Graph"}
</button>
</div>
<input
type="text"
placeholder="Search..."
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full p-2 rounded border"
/>
{showGraph ? (
<div className="h-[350px] rounded-lg border overflow-hidden">
<ForceGraph2D
ref={graphRef}
graphData={{ nodes: graphNodes, links: graphLinks }}
nodeColor={(n: any) => getTypeColor(n.type)}
nodeLabel={(n: any) => `${n.name} (${n.type})`}
linkColor={() => "#475569"}
backgroundColor="#1a1a2e"
width={500}
height={350}
/>
</div>
) : (
<div className="space-y-2 max-h-[350px] overflow-y-auto">
{filteredNodes.map(node => (
<div key={node.id} className="p-3 rounded-lg border bg-card/30">
<div className="flex items-center gap-2">
<span>{getTypeIcon(node.type)}</span>
<span className="font-medium" style={{ color: getTypeColor(node.type) }}>{node.name}</span>
</div>
{node.description && <div className="text-xs text-muted-foreground mt-1">{node.description}</div>}
</div> </div>
<h2 className="font-semibold text-lg leading-snug">{title}</h2> ))}
</div> </div>
<p className="text-sm text-muted-foreground leading-relaxed"> )}
{description}
</p> {/* Legend */}
<div className="mt-3 text-xs text-primary"> <div className="flex gap-4 text-xs">
Open {title.toLowerCase()} <span style={{ color: "#60a5fa" }}> Person</span>
</div> <span style={{ color: "#4ade80" }}> Company</span>
</Card> <span style={{ color: "#c084fc" }}> Concept</span>
</a> </div>
))} </div>
{/* Connections + Learned */}
<div className="space-y-3">
<div>
<div className="flex items-center gap-2 mb-2">
<Network className="h-4 w-4" />
<h3 className="text-lg font-semibold">Connections</h3>
</div>
<div className="p-3 rounded-lg border bg-card/30 space-y-1">
{connections.map((c, i) => (
<div key={i} className="flex items-center gap-2 text-sm">
<span className="text-blue-400">{c.from}</span>
<span className="text-muted-foreground"> {c.rel} </span>
<span className="text-green-400">{c.to}</span>
</div>
))}
</div>
</div>
<div className="flex items-center gap-2">
<MessageSquare className="h-4 w-4" />
<h3 className="text-lg font-semibold">Learned Preferences</h3>
</div>
<div className="p-3 rounded-lg border bg-card/30">
{prefs.length === 0 ? (
<div className="text-sm text-muted-foreground">Correct me to learn</div>
) : (
prefs.map((p, i) => (
<div key={i} className="flex justify-between text-sm">
<span>{p.key}</span>
<span className="font-medium">{p.value}</span>
</div>
))
)}
</div>
</div>
</div> </div>
{error && <div className="text-red-500">Error: {error}</div>}
</div> </div>
) )
} }