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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user