+
+ {linkedKBs.length === 0 &&
None linked.
}
+
+ {linkedKBs.map((kb) => (
+ -
+ {kb.knowledge_base_code ? `${kb.knowledge_base_code} · ` : ''}{kb.title}
+
+
+ ))}
+
+
+
+
+
+
+ )}
+
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 8d995aa..660c278 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -35,6 +35,7 @@ export default function Sidebar({ user, onLogout }: Props) {
const links = user ? [
{ to: '/', icon: '📊', label: 'Dashboard' },
{ to: '/projects', icon: '📁', label: 'Projects' },
+ { to: '/knowledge-bases', icon: '📚', label: 'Knowledge Bases' },
{ to: '/proposals', icon: '💡', label: 'Proposals' },
{ to: '/calendar', icon: '📅', label: 'Calendar' },
{ to: '/notifications', icon: '🔔', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') },
diff --git a/src/index.css b/src/index.css
index d99a9b1..11ce3aa 100644
--- a/src/index.css
+++ b/src/index.css
@@ -708,3 +708,127 @@ dd { font-size: .92rem; font-family: 'JetBrains Mono', monospace; }
.main-content { padding: 22px 18px; }
.task-create-grid { grid-template-columns: 1fr; }
}
+
+/* ── Knowledge Base tree (browse + structure edit) ───────────────────── */
+.kb-tree {
+ background: var(--bg-card);
+ border: var(--hair);
+ border-radius: 8px;
+ padding: 16px 18px;
+ max-width: 960px;
+}
+.kb-tree-toolbar { margin-bottom: 14px; }
+
+.kb-node { margin: 0; }
+
+/* Each topic is a self-contained block */
+.kb-topic {
+ border: var(--hair);
+ border-radius: 6px;
+ background: var(--bg-sink);
+ padding: 10px 14px;
+ margin-bottom: 12px;
+}
+.kb-topic:last-child { margin-bottom: 0; }
+
+.kb-node-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+ min-height: 30px;
+}
+.kb-node-title { color: var(--text); font-weight: 600; font-size: .94rem; }
+.kb-topic-title {
+ color: var(--accent);
+ font-family: 'Saira Condensed', sans-serif;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ font-size: 1.05rem;
+ font-weight: 700;
+}
+.kb-category > .kb-node-header > .kb-node-title { color: var(--steel); }
+
+/* Nested children indent under a guide line */
+.kb-children {
+ margin: 6px 0 0 6px;
+ padding-left: 16px;
+ border-left: 2px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.kb-category { padding: 3px 0; }
+
+.kb-fact {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 6px;
+ font-size: .9rem;
+ color: var(--text);
+ border-radius: var(--radius);
+}
+.kb-fact:hover { background: var(--bg-hover); }
+.kb-bullet { color: var(--accent); }
+.kb-fact-text { flex: 1; min-width: 0; word-break: break-word; }
+.kb-row-actions { display: inline-flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-left: auto; }
+/* reveal node actions on hover to keep the tree calm */
+.kb-node-header .kb-row-actions,
+.kb-fact .kb-row-actions { opacity: 0; transition: opacity .12s; }
+.kb-node-header:hover .kb-row-actions,
+.kb-topic:hover > .kb-node-header > .kb-row-actions,
+.kb-fact:hover .kb-row-actions { opacity: 1; }
+.kb-mini-btn {
+ background: transparent;
+ border: var(--hair);
+ color: var(--text-dim);
+ border-radius: var(--radius);
+ padding: 2px 8px;
+ font-size: .78rem;
+ cursor: pointer;
+ transition: .12s;
+ white-space: nowrap;
+}
+.kb-mini-btn:hover { border-color: var(--accent); color: var(--accent); }
+.kb-mini-btn.kb-danger:hover { border-color: var(--danger); color: var(--danger); }
+.kb-mini-btn:disabled { opacity: .5; cursor: default; }
+.kb-inline-form { display: inline-flex; gap: 4px; align-items: center; }
+.kb-inline-form input {
+ background: var(--bg-sink);
+ border: var(--hair);
+ color: var(--text);
+ border-radius: var(--radius);
+ padding: 2px 8px;
+ font-size: .9rem;
+}
+.kb-inline-grow { flex: 1; }
+.kb-inline-grow input { flex: 1; }
+
+/* Project-modal: linked knowledge bases */
+.kb-link-section { border-top: var(--hair); padding-top: 12px; margin-top: 4px; }
+.kb-link-list { list-style: none; padding: 0; margin: 6px 0; display: flex; flex-direction: column; gap: 4px; }
+.kb-link-list li { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: .9rem; }
+.kb-link-add { display: flex; gap: 8px; align-items: center; margin-top: 6px; }
+.kb-link-add select { flex: 1; }
+
+/* KB node edit form (name + description) and description display */
+.kb-edit-form { display: flex; flex-direction: column; gap: 6px; width: 100%; max-width: 560px; }
+.kb-edit-form input,
+.kb-edit-form textarea {
+ background: var(--bg-sink);
+ border: var(--hair);
+ color: var(--text);
+ border-radius: var(--radius);
+ padding: 4px 8px;
+ font: inherit;
+ font-size: .9rem;
+}
+.kb-edit-form textarea { resize: vertical; }
+.kb-node-desc {
+ color: var(--text-dim);
+ font-size: .85rem;
+ margin: 4px 0 2px 22px;
+ white-space: pre-wrap;
+ line-height: 1.45;
+}
diff --git a/src/pages/KnowledgeBaseDetailPage.tsx b/src/pages/KnowledgeBaseDetailPage.tsx
new file mode 100644
index 0000000..47757c5
--- /dev/null
+++ b/src/pages/KnowledgeBaseDetailPage.tsx
@@ -0,0 +1,87 @@
+import { useCallback, useEffect, useState } from 'react'
+import { useNavigate, useParams } from 'react-router-dom'
+import api from '@/services/api'
+import type { KnowledgeBase, KnowledgeBaseTree as Tree } from '@/types'
+import KnowledgeBaseTree from '@/components/KnowledgeBaseTree'
+import KnowledgeBaseFormModal from '@/components/KnowledgeBaseFormModal'
+
+export default function KnowledgeBaseDetailPage() {
+ const { id } = useParams<{ id: string }>()
+ const navigate = useNavigate()
+ const [kb, setKb] = useState
(null)
+ const [tree, setTree] = useState(null)
+ const [showEdit, setShowEdit] = useState(false)
+ const [error, setError] = useState('')
+
+ const loadTree = useCallback(async () => {
+ if (!id) return
+ const { data } = await api.get(`/knowledge-bases/${id}/tree`)
+ setTree(data)
+ }, [id])
+
+ const loadMeta = useCallback(async () => {
+ if (!id) return
+ const { data } = await api.get(`/knowledge-bases/${id}`)
+ setKb(data)
+ }, [id])
+
+ useEffect(() => {
+ setError('')
+ Promise.all([loadMeta(), loadTree()]).catch((err) => {
+ setError(err?.response?.data?.detail || 'Failed to load knowledge base')
+ })
+ }, [loadMeta, loadTree])
+
+ const onDelete = async () => {
+ if (!kb) return
+ if (!confirm(`Delete knowledge base "${kb.title}" and all its content?`)) return
+ try {
+ await api.delete(`/knowledge-bases/${kb.id}`)
+ navigate('/knowledge-bases')
+ } catch (err: any) {
+ alert(err?.response?.data?.detail || 'Failed to delete knowledge base')
+ }
+ }
+
+ if (error) {
+ return (
+
+
+
{error}
+
+ )
+ }
+
+ if (!kb) return Loading…
+
+ return (
+
+
+
+
+
+ 📚 {kb.title}
+ {kb.knowledge_base_code && {kb.knowledge_base_code}}
+
+
+
+
+
+
+
+ {kb.description &&
{kb.description}
}
+
+
setShowEdit(false)}
+ onSaved={async () => { await loadMeta() }}
+ />
+
+
+
Structure
+ {tree && }
+
+
+ )
+}
diff --git a/src/pages/KnowledgeBasesPage.tsx b/src/pages/KnowledgeBasesPage.tsx
new file mode 100644
index 0000000..cf88852
--- /dev/null
+++ b/src/pages/KnowledgeBasesPage.tsx
@@ -0,0 +1,51 @@
+import { useState, useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
+import api from '@/services/api'
+import type { KnowledgeBase } from '@/types'
+import dayjs from 'dayjs'
+import KnowledgeBaseFormModal from '@/components/KnowledgeBaseFormModal'
+
+export default function KnowledgeBasesPage() {
+ const [items, setItems] = useState([])
+ const [showCreate, setShowCreate] = useState(false)
+ const navigate = useNavigate()
+
+ const fetchItems = () => {
+ api.get('/knowledge-bases').then(({ data }) => setItems(data)).catch(() => setItems([]))
+ }
+
+ useEffect(() => { fetchItems() }, [])
+
+ return (
+
+
+
📚 Knowledge Bases ({items.length})
+
+
+
+
setShowCreate(false)}
+ onSaved={() => fetchItems()}
+ />
+
+
+ {items.map((kb) => (
+
navigate(`/knowledge-bases/${kb.knowledge_base_code || kb.id}`)}
+ >
+
{kb.title}
+ {kb.knowledge_base_code &&
{kb.knowledge_base_code}}
+
{kb.description || 'No description'}
+
+ Created {kb.created_at ? dayjs(kb.created_at).format('YYYY-MM-DD') : '—'}
+
+
+ ))}
+ {items.length === 0 &&
No knowledge bases yet. Create one above.
}
+
+
+ )
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index c392391..758c037 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -26,6 +26,51 @@ export interface Project {
created_at: string
}
+export interface KnowledgeBase {
+ id: number
+ knowledge_base_code: string | null
+ title: string
+ description: string | null
+ created_by: number
+ created_at: string | null
+ last_updated_at: string | null
+}
+
+export interface KnowledgeFact {
+ id: number
+ category_id: number | null
+ topic_id: number
+ fact: string
+ last_updated_at: string | null
+}
+
+export interface KnowledgeCategoryNode {
+ id: number
+ name: string
+ parent: number | null
+ topic_id: number
+ description: string | null
+ categories: KnowledgeCategoryNode[]
+ facts: KnowledgeFact[]
+}
+
+export interface KnowledgeTopicNode {
+ id: number
+ topic: string
+ knowledge_base_id: number
+ description: string | null
+ categories: KnowledgeCategoryNode[]
+ facts: KnowledgeFact[]
+}
+
+export interface KnowledgeBaseTree {
+ id: number
+ knowledge_base_code: string | null
+ title: string
+ description: string | null
+ topics: KnowledgeTopicNode[]
+}
+
export interface ProjectMember {
id: number
user_id: number