+
+
+
+
{selectedKb.display_name}
+ {selectedKb.description &&
{selectedKb.description}
}
+
+
+ {/* Tabs */}
+
+ {(['documents', 'versions'] as const).map(tab => (
+
+ ))}
+
+
+ {activeTab === 'documents' ? (
+
+ {/* Upload area */}
+
+
+
Drag & drop files here, or
+
+
PDF, DOCX, PPTX, XLSX, HTML, TXT, MD, PNG, JPG, WebP
+
+
+ {/* Process button + job status */}
+
+
+ {selectedKb.active_spec_version && (
+
+ Active spec: v{selectedKb.active_spec_version} ({selectedKb.active_spec_char_count?.toLocaleString()} chars)
+
+ )}
+
+
+ {/* Processing progress */}
+ {isJobRunning && activeJob && (
+
+
+
+
+ {activeJob.status === 'parsing_documents' ? 'Parsing documents...' :
+ activeJob.status === 'distilling' ? 'Distilling spec with AI...' :
+ 'Starting...'}
+
+
+ {activeJob.status === 'parsing_documents' && (
+
+
0 ? (activeJob.parsed_documents / activeJob.total_documents) * 100 : 0}%` }}
+ />
+
+ )}
+
+ {activeJob.parsed_documents} / {activeJob.total_documents} documents parsed
+
+
+ )}
+
+ {/* Last job result */}
+ {selectedKb.latest_job && !isJobRunning && (
+
+ Last processing:
+ {selectedKb.latest_job.completed_at &&
{formatDate(selectedKb.latest_job.completed_at)}}
+ {selectedKb.latest_job.error_message &&
{selectedKb.latest_job.error_message}
}
+
+ )}
+
+ {/* Documents table */}
+ {selectedKb.source_documents.length > 0 ? (
+
+
+
+
+ | Filename |
+ Size |
+ Uploaded |
+ Uploaded By |
+ Parse Status |
+ |
+
+
+
+ {selectedKb.source_documents.map(doc => (
+
+ | {doc.filename} |
+ {formatBytes(doc.file_size_bytes)} |
+ {formatDate(doc.created_at)} |
+ {doc.uploaded_by_name || '-'} |
+ |
+
+
+ |
+
+ ))}
+
+
+
+ ) : (
+
+ No source documents uploaded yet.
+
+ )}
+
+ ) : (
+ /* Versions tab */
+
+ {/* Compare button */}
+ {selectedForDiff.length === 2 && (
+
+
+
+ )}
+
+ {versions.length > 0 ? (
+
+ ) : (
+
+ No spec versions generated yet.
+
+ )}
+
+ )}
+
+ );
+ }
+
+ // Level 1: Agent list
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
Knowledge Base
+
Manage the AI agent knowledge bases. Upload source documents, process them, and version the resulting specs.
+
+
+
+ {knowledgeBases.map(kb => {
+ const statusColor =
+ kb.latest_job_status === 'failed' ? 'border-red-300 bg-red-50' :
+ kb.latest_job_status === 'completed' ? 'border-grey-300 bg-white' :
+ 'border-grey-300 bg-white';
+
+ return (
+
+ );
+ })}
+
+
+ );
+};
diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx
index 14c330e..7e4485f 100755
--- a/frontend/components/Sidebar.tsx
+++ b/frontend/components/Sidebar.tsx
@@ -7,6 +7,7 @@ import { SettingsIcon } from './icons/SettingsIcon';
import { UserIcon } from './icons/UserIcon';
import { CampaignsIcon } from './icons/CampaignsIcon';
import { AuditIcon } from './icons/AuditIcon';
+import { KnowledgeBaseIcon } from './icons/KnowledgeBaseIcon';
const navigation = [
{ name: 'Home', icon: DashboardIcon },
@@ -15,6 +16,7 @@ const navigation = [
// { name: 'CopyGenAI', icon: CopyGenAIIcon }, // Hidden: Moved to Settings > Beta
{ name: 'Analytics', icon: AnalyticsIcon },
{ name: 'Auditing', icon: AuditIcon },
+ { name: 'Knowledge Base', icon: KnowledgeBaseIcon, adminOnly: true },
{ name: 'Settings', icon: SettingsIcon },
];
@@ -23,9 +25,10 @@ interface SidebarProps {
onNavigate: (viewName: string) => void;
userName?: string;
userEmail?: string;
+ isAdmin?: boolean;
}
-export const Sidebar: React.FC
= ({ activeItem, onNavigate, userName, userEmail }) => {
+export const Sidebar: React.FC = ({ activeItem, onNavigate, userName, userEmail, isAdmin = true }) => {
return (