From a01d1f73681bcf6dfcdfae110dad509f3a6ba4a3 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 12 May 2026 13:02:59 +0000 Subject: [PATCH] feat: add user-level knowledge page to dashboard Add a new Knowledge page at /ui/knowledge that lists all cross-project and global knowledge entries shared across projects. Previously these entries were only visible via search or mixed into project-specific views. - Add ltm.crossProject() query function in core - Add pageUserKnowledge() with filterable/sortable table showing category, title, source project, confidence, and last updated - Add Knowledge nav link between Dashboard and Search - Update breadcrumb on knowledge detail to link back to /ui/knowledge for cross-project/global entries - Redirect to /ui/knowledge after deleting cross-project/global entries --- packages/core/src/ltm.ts | 11 ++++++ packages/gateway/src/ui.ts | 70 ++++++++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/packages/core/src/ltm.ts b/packages/core/src/ltm.ts index 0ed7d7a..d53662a 100644 --- a/packages/core/src/ltm.ts +++ b/packages/core/src/ltm.ts @@ -418,6 +418,17 @@ export function all(): KnowledgeEntry[] { .all() as KnowledgeEntry[]; } +/** Return all cross-project and global (user-level) knowledge entries. */ +export function crossProject(): KnowledgeEntry[] { + return db() + .query( + `SELECT ${KNOWLEDGE_COLS} FROM knowledge + WHERE (project_id IS NULL OR cross_project = 1) AND confidence > 0.2 + ORDER BY confidence DESC, updated_at DESC`, + ) + .all() as KnowledgeEntry[]; +} + // LIKE-based fallback for when FTS5 fails unexpectedly. function searchLike(input: { query: string; diff --git a/packages/gateway/src/ui.ts b/packages/gateway/src/ui.ts index f12b6b6..06b0530 100644 --- a/packages/gateway/src/ui.ts +++ b/packages/gateway/src/ui.ts @@ -319,6 +319,7 @@ function layout(title: string, body: string): string { @@ -573,18 +574,68 @@ function pageProject(projectId: string): string | null { return layout(project.name ?? "Project", body); } +function pageUserKnowledge(): string { + const entries = ltm.crossProject(); + + let body = breadcrumb([ + { label: "Dashboard", href: "/ui" }, + { label: "Knowledge" }, + ]); + body += `

User Knowledge (${entries.length})

`; + + if (!entries.length) { + body += `

No cross-project or global knowledge entries found. These are created automatically when the curator identifies knowledge worth sharing across projects.

`; + return layout("User Knowledge", body); + } + + // Category breakdown stats + const cats: Record = {}; + for (const e of entries) { + cats[e.category] = (cats[e.category] || 0) + 1; + } + body += `
+
Total
${entries.length}
`; + for (const [cat, count] of Object.entries(cats).sort((a, b) => b[1] - a[1])) { + body += `
${esc(cat)}
${count}
`; + } + body += `
`; + + body += `
+ + `; + for (const e of entries) { + const projName = e.project_id ? projectName(e.project_id) : null; + const projDisplay = e.project_id + ? `${esc(projName ?? "(unknown)")}` + : "(global)"; + body += ` + + + + + + `; + } + body += `
CategoryTitleSource ProjectConfidenceUpdated
${badge(e.category)}${esc(truncate(e.title, 60))}${projDisplay}${e.confidence.toFixed(2)}${timeAgo(e.updated_at)}
`; + + return layout("User Knowledge", body); +} + function pageKnowledge(id: string): string | null { const entry = ltm.get(id); if (!entry) return null; const projName = entry.project_id ? projectName(entry.project_id) : null; + const isCrossOrGlobal = entry.cross_project || !entry.project_id; let body = breadcrumb([ { label: "Dashboard", href: "/ui" }, - ...(entry.project_id - ? [{ label: projName ?? "Project", href: `/ui/projects/${entry.project_id}` }] - : []), - { label: "Knowledge" }, + ...(isCrossOrGlobal + ? [{ label: "Knowledge", href: "/ui/knowledge" }] + : entry.project_id + ? [{ label: projName ?? "Project", href: `/ui/projects/${entry.project_id}` }] + : []), + { label: truncate(entry.title, 40) }, ]); body += `

${esc(entry.title)}

`; @@ -1084,6 +1135,11 @@ export async function handleUIRequest( : htmlResponse(layout("Not Found", `

Project not found

`), 404); } + // User knowledge list (cross-project + global entries) + if (pathname === "/ui/knowledge") { + return htmlResponse(pageUserKnowledge()); + } + // Knowledge detail const knowledgeMatch = matchRoute(pathname, "/ui/knowledge/:id"); if (knowledgeMatch) { @@ -1138,8 +1194,10 @@ export async function handleUIRequest( if (delKnowledge) { const entry = ltm.get(delKnowledge.id); data.deleteKnowledge(delKnowledge.id); - const projectIdVal = entry?.project_id; - return redirect(projectIdVal ? `/ui/projects/${projectIdVal}` : "/ui"); + if (entry?.cross_project || !entry?.project_id) { + return redirect("/ui/knowledge"); + } + return redirect(`/ui/projects/${entry.project_id}`); } // Delete session