Knowledge base + grouped, reordered sidebar

Adds an in-app Knowledge base (route /help): 15 searchable, expandable how-to articles
with step-by-step guides and examples (concepts, A-to-Z setup, the review inbox, the
handoff + memory, the libraries, analytics, governance, troubleshooting), rendered as
markdown.

Reorganizes the sidebar into UX-ordered groups with section labels — Get started ·
Work (Board/Team/Cartable/Reviews) · Organization (Structure/Org chart/Members) ·
AI & libraries (AI seats/Skills/Agent profiles/Product profiles) · Insights
(Performance/Analytics) · Help (Knowledge base).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-16 08:06:23 +03:30
parent 8a033a2a6f
commit c9be692d52
4 changed files with 449 additions and 6 deletions
+109
View File
@@ -0,0 +1,109 @@
import { useMemo, useState } from 'react'
import { BookOpen, ChevronDown, ChevronRight, Search } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { AppShell } from '@/components/AppShell'
import { Card, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { KB_ARTICLES, KB_CATEGORIES, type KbArticle } from '@/lib/kbArticles'
import '@/components/markdown.css'
export function KnowledgeBasePage() {
const [query, setQuery] = useState('')
const [open, setOpen] = useState<Set<string>>(new Set())
const matches = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return KB_ARTICLES
return KB_ARTICLES.filter((a) =>
`${a.title} ${a.summary} ${a.keywords} ${a.body}`.toLowerCase().includes(q),
)
}, [query])
const grouped = useMemo(() => {
return KB_CATEGORIES.map((category) => ({
category,
articles: matches.filter((a) => a.category === category),
})).filter((g) => g.articles.length > 0)
}, [matches])
const toggle = (id: string) =>
setOpen((s) => {
const next = new Set(s)
next.has(id) ? next.delete(id) : next.add(id)
return next
})
return (
<AppShell>
<div className="mx-auto max-w-3xl p-6">
<header className="mb-5">
<h1 className="flex items-center gap-2 text-2xl font-semibold tracking-tight">
<BookOpen className="size-6" /> Knowledge base
</h1>
<p className="text-sm text-muted-foreground">
How to work with TeamUp concepts, step-by-step guides, and examples.
</p>
</header>
<div className="relative mb-6">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search the knowledge base…"
className="pl-9"
/>
</div>
{grouped.length === 0 && (
<p className="text-sm text-muted-foreground">No articles match {query}.</p>
)}
<div className="flex flex-col gap-6">
{grouped.map((group) => (
<section key={group.category} className="flex flex-col gap-2">
<h2 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{group.category}
</h2>
{group.articles.map((article) => (
<ArticleCard
key={article.id}
article={article}
open={open.has(article.id) || (!!query && matches.length <= 4)}
onToggle={() => toggle(article.id)}
/>
))}
</section>
))}
</div>
</div>
</AppShell>
)
}
function ArticleCard({ article, open, onToggle }: { article: KbArticle; open: boolean; onToggle: () => void }) {
return (
<Card>
<CardContent className="py-0">
<button
type="button"
onClick={onToggle}
className="flex w-full items-start gap-3 py-4 text-left"
>
{open ? <ChevronDown className="mt-0.5 size-4 shrink-0 text-muted-foreground" /> : <ChevronRight className="mt-0.5 size-4 shrink-0 text-muted-foreground" />}
<span className="min-w-0">
<span className="block text-sm font-medium">{article.title}</span>
<span className={cn('block text-xs text-muted-foreground', open && 'sr-only')}>{article.summary}</span>
</span>
</button>
{open && (
<div className="md-prose pb-5 pl-7">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{article.body}</ReactMarkdown>
</div>
)}
</CardContent>
</Card>
)
}