'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; import { Loader2, Plus, Trash2, Pencil, RefreshCw, BookOpen } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from '@/components/ui/card'; import { useTranslations } from 'next-intl'; interface DocSummary { id: string; title: string; updated_at: string; } /** Editor target: 'new' when creating, a doc id when editing, null when closed. */ type EditTarget = 'new' | string | null; export function AiKnowledgeCard({ accountId, canEdit, hasEmbeddingsKey, }: { accountId: string | null; canEdit: boolean; hasEmbeddingsKey: boolean; }) { const [docs, setDocs] = useState([]); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(null); const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [saving, setSaving] = useState(false); const [reindexing, setReindexing] = useState(false); const loadedAccountIdRef = useRef(null); const t = useTranslations('Settings.aiKnowledge'); const fetchDocs = useCallback(async () => { setLoading(true); try { const res = await fetch('/api/ai/knowledge'); const data = await res.json(); if (res.ok) setDocs(data.documents ?? []); else toast.error(data.error ?? t('loadFailed')); } catch { toast.error(t('loadFailed')); } finally { setLoading(false); } }, []); useEffect(() => { if (!accountId || loadedAccountIdRef.current === accountId) return; loadedAccountIdRef.current = accountId; void fetchDocs(); }, [accountId, fetchDocs]); const openNew = () => { setEditing('new'); setTitle(''); setContent(''); }; const openEdit = async (id: string) => { try { const res = await fetch(`/api/ai/knowledge/${id}`); const data = await res.json(); if (!res.ok) { toast.error(data.error ?? t('openFailed')); return; } setEditing(id); setTitle(data.title ?? ''); setContent(data.content ?? ''); } catch { toast.error(t('openFailed')); } }; const cancelEdit = () => { setEditing(null); setTitle(''); setContent(''); }; const save = async () => { if (!title.trim() || !content.trim()) { toast.error(t('titleContentRequired')); return; } setSaving(true); try { const isNew = editing === 'new'; const res = await fetch( isNew ? '/api/ai/knowledge' : `/api/ai/knowledge/${editing}`, { method: isNew ? 'POST' : 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: title.trim(), content: content.trim() }), }, ); const data = await res.json(); if (res.ok) { // A 200 with `warning` means saved but indexing degraded. if (data.warning) toast.warning(data.warning); else toast.success(isNew ? t('saveSuccessNew') : t('saveSuccessUpdate')); cancelEdit(); await fetchDocs(); } else { toast.error(data.error ?? t('saveFailed')); } } catch { toast.error(t('saveFailed')); } finally { setSaving(false); } }; const remove = async (id: string) => { try { const res = await fetch(`/api/ai/knowledge/${id}`, { method: 'DELETE' }); if (res.ok) { toast.success(t('removeSuccess')); setDocs((d) => d.filter((x) => x.id !== id)); } else { const data = await res.json(); toast.error(data.error ?? t('removeFailed')); } } catch { toast.error(t('removeFailed')); } }; const reindex = async () => { setReindexing(true); try { const res = await fetch('/api/ai/knowledge/reindex', { method: 'POST' }); const data = await res.json(); if (res.ok && data.success) { toast.success(t('reindexSuccess', { count: data.reindexed })); } else { toast.error(data.error ?? t('reindexFailed')); } } catch { toast.error(t('reindexFailed')); } finally { setReindexing(false); } }; return ( {t('title')} {t('description', { searchType: hasEmbeddingsKey ? t('semanticSearchOn') : t('keywordSearchOn') })} {loading ? (
{t('loading')}
) : ( <> {docs.length === 0 && editing === null && (

{t('noDocs')}

)} {docs.length > 0 && (
    {docs.map((doc) => (
  • {doc.title} {canEdit && ( )}
  • ))}
)} {editing !== null ? (
setTitle(e.target.value)} placeholder={t('editDocTitlePlaceholder')} disabled={saving} />