feat(dashboard): knowledge page + Tiger knowledge API
This commit is contained in:
parent
598dbdd4a4
commit
2cbb9b3bcf
2 changed files with 259 additions and 76 deletions
38
dashboard/src/app/api/tiger/knowledge/route.ts
Normal file
38
dashboard/src/app/api/tiger/knowledge/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +1,235 @@
|
|||
"use client"
|
||||
|
||||
/**
|
||||
* /knowledge — Tiger's brain (Phase 1 hub)
|
||||
*
|
||||
* 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 { useEffect, useState, useRef } from "react"
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Brain, Search, ScrollText, Wrench, Activity, Clock, Network, MessageSquare } from "lucide-react"
|
||||
|
||||
import { Brain, ScrollText, Wrench, Activity, Clock } from "lucide-react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false })
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: "Memory",
|
||||
href: "/memory",
|
||||
icon: ScrollText,
|
||||
description:
|
||||
"Tiger's persistent memory — SOUL.md, USER.md, IDENTITY.md, MEMORY.md. " +
|
||||
"Edit these files to teach Tiger about you and itself.",
|
||||
},
|
||||
{
|
||||
title: "Skills",
|
||||
href: "/skills",
|
||||
icon: Wrench,
|
||||
description:
|
||||
"Registry of capabilities Tiger can invoke. Skills are reusable " +
|
||||
"instruction sets that ship with the openclaw runtime.",
|
||||
},
|
||||
{
|
||||
title: "Activity",
|
||||
href: "/activity",
|
||||
icon: Activity,
|
||||
description:
|
||||
"Timeline of every workspace file write across all agents. Useful for " +
|
||||
"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.",
|
||||
},
|
||||
]
|
||||
interface KnowledgeNode {
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface FeedbackPref {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface GraphNode {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
val: number
|
||||
}
|
||||
|
||||
interface GraphLink {
|
||||
source: { id: string }
|
||||
target: { id: string }
|
||||
name: string
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-6 max-w-5xl mx-auto w-full">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* TOP - Tiger's Brain */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
<Brain className="h-6 w-6 text-primary" />
|
||||
Knowledge
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Tiger's brain — what it remembers, what it can do, what it's done,
|
||||
and what it'll do next.
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Brain className="h-5 w-5" />
|
||||
<h1 className="text-2xl font-bold">Knowledge</h1>
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg font-semibold mb-3">Tiger's Brain</h2>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{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 className="grid gap-4 md:grid-cols-2">
|
||||
{sections.map(({ title, href, icon: Icon, description }) => (
|
||||
<a key={title} href={href} className="contents">
|
||||
<Card className="bg-card/40 p-5 hover:bg-card/60 hover:border-primary/30 transition-colors cursor-pointer">
|
||||
<div className="flex items-start gap-3 mb-2">
|
||||
<div className="shrink-0 h-9 w-9 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center">
|
||||
<Icon className="h-4 w-4 text-primary" />
|
||||
{/* BOTTOM */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* Graph/List Toggle */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
<h2 className="font-semibold text-lg leading-snug">{title}</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
<div className="mt-3 text-xs text-primary">
|
||||
Open {title.toLowerCase()} →
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex gap-4 text-xs">
|
||||
<span style={{ color: "#60a5fa" }}>● Person</span>
|
||||
<span style={{ color: "#4ade80" }}>● Company</span>
|
||||
<span style={{ color: "#c084fc" }}>● Concept</span>
|
||||
</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>
|
||||
|
||||
{error && <div className="text-red-500">Error: {error}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue