import { useState, useEffect, useRef, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { useAppPath } from '@core/hooks/useAppPath' import { apiFetch } from '@core/lib/api' import { RichTextEditor } from '@core/components/ui/RichTextEditor' import { Button } from '@core/components/ui/button' import { Input } from '@core/components/ui/input' import { Badge } from '@core/components/ui/badge' import { Skeleton } from '@core/components/ui/skeleton' import { ScrollArea } from '@core/components/ui/scroll-area' import { Plus, BookOpen, Edit } from 'lucide-react' interface Article { id: string title: string description?: string status?: string data?: Record created_at: string } const STATUS_GROUPS: { key: string; label: string }[] = [ { key: 'draft', label: 'Draft' }, { key: 'review', label: 'Under Review' }, { key: 'published', label: 'Published' }, { key: 'deprecated', label: 'Deprecated' }, { key: 'archived', label: 'Archived' }, { key: 'restricted', label: 'Restricted' }, ] const KB_TYPE_LABELS: Record = { article: 'Article', care_guide: 'Care Guide', process_guide: 'Process Guide', code_chunk: 'Code', api_reference: 'API Ref', troubleshooting: 'Troubleshooting', faq: 'FAQ', policy: 'Policy', tutorial: 'Tutorial', } const PRIORITY_COLORS: Record = { low: 'secondary', medium: 'secondary', high: 'default', critical: 'destructive', emergency: 'destructive', } export default function KBPage() { const navigate = useNavigate() const appPath = useAppPath() const [articles, setArticles] = useState([]) const [searchResults, setSearchResults] = useState(null) const [loading, setLoading] = useState(true) const [searching, setSearching] = useState(false) const [search, setSearch] = useState('') const [selected, setSelected] = useState
(null) const [statusFilter, setStatusFilter] = useState('all') const debounceRef = useRef>() // Load all articles on mount useEffect(() => { apiFetch('/api/admin-data?action=list&entity=items&type_slug=kb_article&limit=500') .then(r => r.json()) .then(j => setArticles(Array.isArray(j?.data) ? j.data : Array.isArray(j) ? j : [])) .catch(() => setArticles([])) .finally(() => setLoading(false)) }, []) // Debounced vector search const runSearch = useCallback(async (q: string) => { if (q.trim().length < 2) { setSearchResults(null); return } setSearching(true) try { const res = await apiFetch('/api/custom_kb-embeddings?action=search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q.trim(), limit: 15 }), }) const json = await res.json() const results = json.data || json || [] setSearchResults(Array.isArray(results) ? results : []) } catch { setSearchResults(null) // Fall back to client-side filter } finally { setSearching(false) } }, []) const handleSearch = (val: string) => { setSearch(val) clearTimeout(debounceRef.current) if (!val || val.trim().length < 2) { setSearchResults(null) return } debounceRef.current = setTimeout(() => runSearch(val), 400) } // Use vector results when available, otherwise client-side filter const displayArticles = searchResults ?? articles const filtered = displayArticles.filter(a => { const matchesStatus = statusFilter === 'all' || (a.status || 'draft') === statusFilter return matchesStatus }) const grouped = STATUS_GROUPS.map(sg => ({ ...sg, items: filtered.filter(a => (a.status || 'draft') === sg.key), })).filter(g => g.items.length > 0) return (
{/* Left panel */}
handleSearch(e.target.value)} className="flex-1 h-8" />
{/* Status filter */}
{STATUS_GROUPS.map(sg => ( ))}
{loading ? (
{Array.from({ length: 5 }).map((_, i) => )}
) : filtered.length === 0 ? (

{search ? 'No articles match.' : 'No articles yet.'}

) : (
{grouped.map(group => (

{group.label} ({group.items.length})

{group.items.map(a => ( ))}
))}
)}
{/* Right preview panel */}
{!selected ? (

Select an article to preview

) : ( <>

{selected.title}

{selected.status || 'draft'} {selected.data?.kb_type && ( {KB_TYPE_LABELS[selected.data.kb_type] || selected.data.kb_type} )} {selected.data?.priority && ( {selected.data.priority} )} {selected.data?.category && ( {selected.data.category} )} {new Date(selected.created_at).toLocaleDateString()}
{selected.description ? ( ) : (

No content yet. Click Edit to add content.

)}
)}
) }