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"
|
"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 (
|
return (
|
||||||
<div className="space-y-6 max-w-5xl mx-auto w-full">
|
<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="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>
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<h2 className="text-lg font-semibold mb-3">Tiger's Brain</h2>
|
||||||
{sections.map(({ title, href, icon: Icon, description }) => (
|
<div className="grid grid-cols-4 gap-3">
|
||||||
<a key={title} href={href} className="contents">
|
{sections.map(s => (
|
||||||
<Card className="bg-card/40 p-5 hover:bg-card/60 hover:border-primary/30 transition-colors cursor-pointer">
|
<a key={s.title} href={s.href} className="p-4 rounded-lg border hover:bg-card/50 text-center">
|
||||||
<div className="flex items-start gap-3 mb-2">
|
<s.icon className="h-6 w-6 mx-auto mb-2" />
|
||||||
<div className="shrink-0 h-9 w-9 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center">
|
<div className="font-medium">{s.title}</div>
|
||||||
<Icon className="h-4 w-4 text-primary" />
|
<div className="text-xs text-muted-foreground">{s.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>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
))}
|
||||||
|
</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