import { useState, useEffect } from 'react' import { useParams, 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 { Label } from '@core/components/ui/label' import { Skeleton } from '@core/components/ui/skeleton' import { ArrowLeft, Save } from 'lucide-react' interface ArticleForm { title: string description: string status: string kb_type: string priority: string category: string security_level: string tags: string audience: string[] } const EMPTY: ArticleForm = { title: '', description: '', status: 'draft', kb_type: 'article', priority: 'medium', category: '', security_level: 'internal', tags: '', audience: [], } const AUDIENCE_OPTIONS = [ { value: 'end_user', label: 'End User' }, { value: 'support_agent', label: 'Support Agent' }, { value: 'developer', label: 'Developer' }, { value: 'admin', label: 'Administrator' }, { value: 'manager', label: 'Manager' }, { value: 'ai_system', label: 'AI System' }, { value: 'internal_only', label: 'Internal Only' }, ] function SidebarSelect({ label, value, onChange, options }: { label: string value: string onChange: (v: string) => void options: { value: string; label: string }[] }) { return (
) } export default function KBEditorPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const appPath = useAppPath() const isNew = !id || id === 'new' const [form, setForm] = useState(EMPTY) const [loading, setLoading] = useState(!isNew) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) useEffect(() => { if (isNew) return apiFetch(`/api/admin-data?action=get&entity=items&id=${id}`) .then(r => r.json()) .then(raw => { const j = raw?.data ?? raw if (j) setForm({ title: j.title || '', description: j.description || '', status: j.status || 'draft', kb_type: j.data?.kb_type || 'article', priority: j.data?.priority || 'medium', category: j.data?.category || '', security_level: j.data?.security_level || 'internal', tags: (() => { const t = j.data?.tags; if (Array.isArray(t)) return t.join(', '); if (typeof t === 'string') { try { const p = JSON.parse(t); return Array.isArray(p) ? p.join(', ') : t } catch { return t } } return '' })(), audience: Array.isArray(j.data?.audience) ? j.data.audience : [], }) }) .catch(() => setError('Failed to load article')) .finally(() => setLoading(false)) }, [id, isNew]) const handleSave = async (publish?: boolean) => { if (!form.title.trim()) { setError('Title is required'); return } if (!form.kb_type) { setError('KB Type is required'); return } setSaving(true) setError(null) try { const payload = { title: form.title.trim(), description: form.description, type_slug: 'kb_article', status: publish ? 'published' : form.status, data: { kb_type: form.kb_type, priority: form.priority, category: form.category.trim() || null, security_level: form.security_level, tags: form.tags ? form.tags.split(',').map(t => t.trim()).filter(Boolean) : [], audience: form.audience, }, } let res: Response if (isNew) { res = await apiFetch('/api/admin-data?action=create&entity=items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) } else { res = await apiFetch(`/api/admin-data?entity=items&id=${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) } if (!res.ok) { const e = await res.clone().json().catch(() => ({})); throw new Error(e?.error || 'Save failed') } // Auto-generate embeddings for the saved article const saved = await res.json().catch(() => null) const itemId = isNew ? (saved?.id || saved?.data?.id) : id if (itemId) { try { await apiFetch('/api/custom_kb-embeddings?action=generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_ids: [itemId], vector_types: ['semantic', 'structure'], force_regenerate: true, }), }) } catch (embErr) { console.warn('Embedding generation failed (article saved successfully):', embErr) } } navigate(appPath('/kb')) } catch (e: any) { setError(e.message) } finally { setSaving(false) } } const toggleAudience = (val: string) => { setForm(f => ({ ...f, audience: f.audience.includes(val) ? f.audience.filter(a => a !== val) : [...f.audience, val], })) } if (loading) return (
) return (
{/* Header */}

{isNew ? 'New Article' : 'Edit Article'}

{error && (
{error}
)}
{/* Main content area */}
setForm(f => ({ ...f, title: e.target.value }))} placeholder="Article title" className="text-lg font-medium" />
setForm(f => ({ ...f, description: html }))} placeholder="Write your article content here…" minHeight="400px" />
{/* Sidebar */}
setForm(f => ({ ...f, status: v }))} options={[ { value: 'draft', label: 'Draft' }, { value: 'review', label: 'Under Review' }, { value: 'published', label: 'Published' }, { value: 'deprecated', label: 'Deprecated' }, { value: 'archived', label: 'Archived' }, { value: 'restricted', label: 'Restricted Access' }, ]} /> setForm(f => ({ ...f, kb_type: v }))} options={[ { value: 'article', label: 'Article' }, { value: 'care_guide', label: 'Care Guide' }, { value: 'process_guide', label: 'Process Guide' }, { value: 'code_chunk', label: 'Code Chunk' }, { value: 'api_reference', label: 'API Reference' }, { value: 'troubleshooting', label: 'Troubleshooting' }, { value: 'faq', label: 'FAQ' }, { value: 'policy', label: 'Policy' }, { value: 'tutorial', label: 'Tutorial' }, ]} /> setForm(f => ({ ...f, priority: v }))} options={[ { value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }, { value: 'critical', label: 'Critical' }, { value: 'emergency', label: 'Emergency' }, ]} /> setForm(f => ({ ...f, category: v }))} options={[ { value: '', label: '— None —' }, { value: 'getting_started', label: 'Getting Started' }, { value: 'user_guide', label: 'User Guide' }, { value: 'technical', label: 'Technical' }, { value: 'billing', label: 'Billing' }, { value: 'security', label: 'Security' }, { value: 'integration', label: 'Integration' }, { value: 'troubleshooting', label: 'Troubleshooting' }, { value: 'reference', label: 'Reference' }, { value: 'best_practices', label: 'Best Practices' }, { value: 'policies', label: 'Policies' }, ]} /> setForm(f => ({ ...f, security_level: v }))} options={[ { value: 'public', label: 'Public' }, { value: 'internal', label: 'Internal' }, { value: 'confidential', label: 'Confidential' }, { value: 'restricted', label: 'Restricted' }, ]} />
setForm(f => ({ ...f, tags: e.target.value }))} placeholder="tag1, tag2, tag3" className="text-sm" />

Comma-separated

{AUDIENCE_OPTIONS.map(opt => ( ))}
) }