fix: show in kb new model

This commit is contained in:
Vukasin 2025-09-21 16:54:18 +02:00
parent 02da9e2e6d
commit b20f95850f
5 changed files with 432 additions and 1280 deletions

View File

@ -48,7 +48,7 @@ import { ExpandableMarkdownEditor } from '@/components/ui/expandable-markdown-ed
import { AgentModelSelector } from './config/model-selector';
import { AgentToolsConfiguration } from './agent-tools-configuration';
import { AgentMCPConfiguration } from './agent-mcp-configuration';
import { AgentKnowledgeBaseManager } from './knowledge-base/agent-knowledge-base-manager';
import { AgentKnowledgeBaseManager } from './knowledge-base/agent-kb-tree';
import { AgentPlaybooksConfiguration } from './playbooks/agent-playbooks-configuration';
import { AgentTriggersConfiguration } from './triggers/agent-triggers-configuration';
import { ProfilePictureDialog } from './config/profile-picture-dialog';
@ -72,25 +72,25 @@ export function AgentConfigurationDialog({
const router = useRouter();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const { agent, versionData, isViewingOldVersion, isLoading, error } = useAgentVersionData({ agentId });
const updateAgentMutation = useUpdateAgent();
const updateAgentMCPsMutation = useUpdateAgentMCPs();
const exportMutation = useExportAgent();
const [activeTab, setActiveTab] = useState(initialTab);
const [isProfileDialogOpen, setIsProfileDialogOpen] = useState(false);
const [isEditingName, setIsEditingName] = useState(false);
const [editName, setEditName] = useState('');
const nameInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (open && initialTab) {
setActiveTab(initialTab);
}
}, [open, initialTab]);
const [formData, setFormData] = useState({
name: '',
description: '',
@ -105,14 +105,14 @@ export function AgentConfigurationDialog({
icon_color: '#000000',
icon_background: '#e5e5e5',
});
const [originalFormData, setOriginalFormData] = useState(formData);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
if (!agent) return;
let configSource = agent;
if (versionData) {
configSource = {
@ -123,7 +123,7 @@ export function AgentConfigurationDialog({
icon_background: versionData.icon_background || agent.icon_background,
};
}
const newFormData = {
name: configSource.name || '',
description: configSource.description || '',
@ -138,7 +138,7 @@ export function AgentConfigurationDialog({
icon_color: configSource.icon_color || '#000000',
icon_background: configSource.icon_background || '#e5e5e5',
};
setFormData(newFormData);
setOriginalFormData(newFormData);
setEditName(configSource.name || '');
@ -149,14 +149,14 @@ export function AgentConfigurationDialog({
const isNameEditable = !isViewingOldVersion && (restrictions.name_editable !== false) && !isSunaAgent;
const isSystemPromptEditable = !isViewingOldVersion && (restrictions.system_prompt_editable !== false) && !isSunaAgent;
const areToolsEditable = !isViewingOldVersion && (restrictions.tools_editable !== false) && !isSunaAgent;
const hasChanges = useMemo(() => {
return JSON.stringify(formData) !== JSON.stringify(originalFormData);
}, [formData, originalFormData]);
const handleSaveAll = async () => {
if (!hasChanges) return;
setIsSaving(true);
try {
const updateData: any = {
@ -166,20 +166,20 @@ export function AgentConfigurationDialog({
system_prompt: formData.system_prompt,
agentpress_tools: formData.agentpress_tools,
};
if (formData.model !== undefined) updateData.model = formData.model;
if (formData.profile_image_url !== undefined) updateData.profile_image_url = formData.profile_image_url;
if (formData.icon_name !== undefined) updateData.icon_name = formData.icon_name;
if (formData.icon_color !== undefined) updateData.icon_color = formData.icon_color;
if (formData.icon_background !== undefined) updateData.icon_background = formData.icon_background;
if (formData.is_default !== undefined) updateData.is_default = formData.is_default;
const updatedAgent = await updateAgentMutation.mutateAsync(updateData);
const mcpsChanged =
const mcpsChanged =
JSON.stringify(formData.configured_mcps) !== JSON.stringify(originalFormData.configured_mcps) ||
JSON.stringify(formData.custom_mcps) !== JSON.stringify(originalFormData.custom_mcps);
if (mcpsChanged) {
await updateAgentMCPsMutation.mutateAsync({
agentId,
@ -188,17 +188,17 @@ export function AgentConfigurationDialog({
replace_mcps: true
});
}
queryClient.invalidateQueries({ queryKey: ['versions', 'list', agentId] });
queryClient.invalidateQueries({ queryKey: ['agents', 'detail', agentId] });
if (updatedAgent.current_version_id) {
const params = new URLSearchParams(searchParams.toString());
params.delete('version');
const newUrl = params.toString() ? `?${params.toString()}` : window.location.pathname;
router.push(newUrl);
}
setOriginalFormData(formData);
toast.success('Agent configuration saved successfully');
} catch (error) {
@ -215,7 +215,7 @@ export function AgentConfigurationDialog({
setIsEditingName(false);
return;
}
if (!isNameEditable) {
if (isSunaAgent) {
toast.error("Name cannot be edited", {
@ -226,7 +226,7 @@ export function AgentConfigurationDialog({
setIsEditingName(false);
return;
}
setFormData(prev => ({ ...prev, name: editName }));
setIsEditingName(false);
};
@ -240,7 +240,7 @@ export function AgentConfigurationDialog({
}
return;
}
setFormData(prev => ({ ...prev, system_prompt: value }));
};
@ -257,7 +257,7 @@ export function AgentConfigurationDialog({
}
return;
}
setFormData(prev => ({ ...prev, agentpress_tools: tools }));
};
@ -272,12 +272,12 @@ export function AgentConfigurationDialog({
const handleProfileImageChange = (profileImageUrl: string | null) => {
setFormData(prev => ({ ...prev, profile_image_url: profileImageUrl || '' }));
};
const handleIconChange = (iconName: string | null, iconColor: string, iconBackground: string) => {
setFormData(prev => ({
...prev,
icon_name: iconName,
icon_color: iconColor,
setFormData(prev => ({
...prev,
icon_name: iconName,
icon_color: iconColor,
icon_background: iconBackground,
profile_image_url: iconName && prev.profile_image_url ? '' : prev.profile_image_url
}));
@ -286,7 +286,7 @@ export function AgentConfigurationDialog({
const handleExport = () => {
exportMutation.mutate(agentId);
};
const handleClose = (open: boolean) => {
if (!open && hasChanges) {
setFormData(originalFormData);
@ -341,7 +341,7 @@ export function AgentConfigurationDialog({
/>
)}
</button>
<div>
{isEditingName ? (
<div className="flex items-center gap-2">
@ -408,7 +408,7 @@ export function AgentConfigurationDialog({
</DialogDescription>
</div>
</div>
<div className="flex items-center gap-2">
<AgentVersionSwitcher
agentId={agentId}
@ -443,111 +443,111 @@ export function AgentConfigurationDialog({
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as typeof activeTab)} className="flex-1 flex flex-col min-h-0">
<div className='flex items-center justify-center w-full'>
<TabsList className="mt-4 w-[95%] flex-shrink-0">
{tabItems.map((tab) => {
{tabItems.map((tab) => {
const Icon = tab.icon;
return (
<TabsTrigger
<TabsTrigger
key={tab.id}
value={tab.id}
disabled={tab.disabled}
className={cn(
tab.disabled && "opacity-50 cursor-not-allowed"
tab.disabled && "opacity-50 cursor-not-allowed"
)}
>
>
<Icon className="h-4 w-4" />
{tab.label}
</TabsTrigger>
</TabsTrigger>
);
})}
})}
</TabsList>
</div>
<div className="flex-1 overflow-auto">
<TabsContent value="general" className="p-6 mt-0 flex flex-col h-full">
<div className="flex flex-col flex-1 gap-6">
<div className="flex-shrink-0">
<Label className="text-base font-semibold mb-3 block">Model</Label>
<AgentModelSelector
value={formData.model}
onChange={handleModelChange}
disabled={isViewingOldVersion}
variant="default"
/>
</div>
<div className="flex flex-col flex-1 min-h-0">
<Label className="text-base font-semibold mb-3 block">Description</Label>
<Textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
placeholder="Describe what this agent does..."
className="flex-1 resize-none bg-muted/50"
disabled={isViewingOldVersion}
/>
</div>
</div>
</TabsContent>
<TabsContent value="instructions" className="p-6 mt-0 flex flex-col h-full">
<div className="flex flex-col flex-1 min-h-0">
<Label className="text-base font-semibold mb-3 block flex-shrink-0">System Prompt</Label>
<ExpandableMarkdownEditor
value={formData.system_prompt}
onSave={handleSystemPromptChange}
disabled={!isSystemPromptEditable}
placeholder="Define how your agent should behave..."
className="flex-1 h-[90%]"
<TabsContent value="general" className="p-6 mt-0 flex flex-col h-full">
<div className="flex flex-col flex-1 gap-6">
<div className="flex-shrink-0">
<Label className="text-base font-semibold mb-3 block">Model</Label>
<AgentModelSelector
value={formData.model}
onChange={handleModelChange}
disabled={isViewingOldVersion}
variant="default"
/>
</div>
</TabsContent>
<TabsContent value="tools" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentToolsConfiguration
tools={formData.agentpress_tools}
onToolsChange={handleToolsChange}
disabled={!areToolsEditable}
<div className="flex flex-col flex-1 min-h-0">
<Label className="text-base font-semibold mb-3 block">Description</Label>
<Textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
placeholder="Describe what this agent does..."
className="flex-1 resize-none bg-muted/50"
disabled={isViewingOldVersion}
/>
</div>
</div>
</TabsContent>
<TabsContent value="instructions" className="p-6 mt-0 flex flex-col h-full">
<div className="flex flex-col flex-1 min-h-0">
<Label className="text-base font-semibold mb-3 block flex-shrink-0">System Prompt</Label>
<ExpandableMarkdownEditor
value={formData.system_prompt}
onSave={handleSystemPromptChange}
disabled={!isSystemPromptEditable}
placeholder="Define how your agent should behave..."
className="flex-1 h-[90%]"
/>
</TabsContent>
<TabsContent value="integrations" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentMCPConfiguration
configuredMCPs={formData.configured_mcps}
customMCPs={formData.custom_mcps}
onMCPChange={handleMCPChange}
agentId={agentId}
versionData={{
configured_mcps: formData.configured_mcps,
custom_mcps: formData.custom_mcps,
system_prompt: formData.system_prompt,
agentpress_tools: formData.agentpress_tools
}}
saveMode="callback"
isLoading={updateAgentMCPsMutation.isPending}
/>
</TabsContent>
</div>
</TabsContent>
<TabsContent value="knowledge" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentKnowledgeBaseManager agentId={agentId} agentName={formData.name || 'Agent'} />
</TabsContent>
<TabsContent value="tools" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentToolsConfiguration
tools={formData.agentpress_tools}
onToolsChange={handleToolsChange}
disabled={!areToolsEditable}
/>
</TabsContent>
<TabsContent value="integrations" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentMCPConfiguration
configuredMCPs={formData.configured_mcps}
customMCPs={formData.custom_mcps}
onMCPChange={handleMCPChange}
agentId={agentId}
versionData={{
configured_mcps: formData.configured_mcps,
custom_mcps: formData.custom_mcps,
system_prompt: formData.system_prompt,
agentpress_tools: formData.agentpress_tools
}}
saveMode="callback"
isLoading={updateAgentMCPsMutation.isPending}
/>
</TabsContent>
<TabsContent value="playbooks" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentPlaybooksConfiguration agentId={agentId} agentName={formData.name || 'Agent'} />
</TabsContent>
<TabsContent value="knowledge" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentKnowledgeBaseManager agentId={agentId} agentName={formData.name || 'Agent'} />
</TabsContent>
<TabsContent value="triggers" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentTriggersConfiguration agentId={agentId} />
</TabsContent>
<TabsContent value="playbooks" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentPlaybooksConfiguration agentId={agentId} agentName={formData.name || 'Agent'} />
</TabsContent>
<TabsContent value="triggers" className="p-6 mt-0 h-[calc(100vh-16rem)]">
<AgentTriggersConfiguration agentId={agentId} />
</TabsContent>
</div>
</Tabs>
)}
<DialogFooter className="px-6 py-4 border-t bg-background flex-shrink-0">
<Button
variant="outline"
<Button
variant="outline"
onClick={() => handleClose(false)}
disabled={isSaving}
>
Cancel
</Button>
<Button
<Button
onClick={handleSaveAll}
disabled={!hasChanges || isSaving}
>

View File

@ -254,7 +254,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Knowledge Base</h3>
<div className="text-sm text-muted-foreground">
Open knowlage base page to manage content
Open knowledge base page to manage content
</div>
</div>

View File

@ -8,11 +8,11 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import {
Plus,
Edit2,
Trash2,
Clock,
import {
Plus,
Edit2,
Trash2,
Clock,
AlertCircle,
FileText,
Globe,
@ -29,7 +29,11 @@ import {
BookOpen,
PenTool,
X,
ArrowLeft
ArrowLeft,
FolderIcon,
Settings,
Grid,
List
} from 'lucide-react';
import {
AlertDialog,
@ -47,7 +51,7 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
import {
useAgentKnowledgeBaseEntries,
useCreateAgentKnowledgeBaseEntry,
useUpdateKnowledgeBaseEntry,
@ -60,28 +64,51 @@ import { cn, truncateString } from '@/lib/utils';
import { CreateKnowledgeBaseEntryRequest, KnowledgeBaseEntry, UpdateKnowledgeBaseEntryRequest, ProcessingJob } from '@/hooks/react-query/knowledge-base/types';
import { toast } from 'sonner';
import JSZip from 'jszip';
import { createClient } from '@/lib/supabase/client';
import { SharedTreeItem } from '@/components/knowledge-base/shared-kb-tree';
import {
Code2 as SiJavascript,
Code2 as SiTypescript,
Code2 as SiPython,
Code2 as SiReact,
Code2 as SiHtml5,
Code2 as SiCss3,
import {
Code2 as SiJavascript,
Code2 as SiTypescript,
Code2 as SiPython,
Code2 as SiReact,
Code2 as SiHtml5,
Code2 as SiCss3,
FileText as SiJson,
FileText as SiMarkdown,
FileText as SiYaml,
FileText as SiXml,
FileText as FaFilePdf,
FileText as FaFileWord,
FileText as FaFileExcel,
FileImage as FaFileImage,
Archive as FaFileArchive,
FileText as FaFilePdf,
FileText as FaFileWord,
FileText as FaFileExcel,
FileImage as FaFileImage,
Archive as FaFileArchive,
Code as FaFileCode,
FileText as FaFileAlt,
File as FaFile
} from 'lucide-react';
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000';
// Helper function to get auth headers
const getAuthHeaders = async () => {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
return {
'Content-Type': 'application/json',
...(session?.access_token && { 'Authorization': `Bearer ${session.access_token}` })
};
};
interface TreeItem {
id: string;
name: string;
type: 'folder' | 'file';
expanded?: boolean;
children?: TreeItem[];
data?: any;
}
interface AgentKnowledgeBaseManagerProps {
agentId: string;
agentName: string;
@ -103,9 +130,9 @@ interface UploadedFile {
}
const USAGE_CONTEXT_OPTIONS = [
{
value: 'always',
label: 'Always Active',
{
value: 'always',
label: 'Always Active',
icon: Globe,
color: 'bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800'
},
@ -172,7 +199,7 @@ const getFileTypeIcon = (filename: string, mimeType?: string) => {
const getFileIconColor = (filename: string) => {
const extension = filename.split('.').pop()?.toLowerCase();
switch (extension) {
case 'js':
return 'text-yellow-500';
@ -286,7 +313,13 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
const [dragActive, setDragActive] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);
// Tree view state
const [viewMode, setViewMode] = useState<'list' | 'tree'>('list');
const [treeData, setTreeData] = useState<TreeItem[]>([]);
const [selectedEntries, setSelectedEntries] = useState<Set<string>>(new Set());
const [treeLoading, setTreeLoading] = useState(false);
const [formData, setFormData] = useState<CreateKnowledgeBaseEntryRequest>({
name: '',
description: '',
@ -306,23 +339,194 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
useEffect(() => {
if (processingJobsData?.jobs && processingJobsData.jobs.length > 0) {
const hasProcessingJobs = processingJobsData.jobs.some(job => job.status === 'processing');
const hasRecentlyCompleted = processingJobsData.jobs.some(job =>
job.status === 'completed' &&
job.completed_at &&
const hasRecentlyCompleted = processingJobsData.jobs.some(job =>
job.status === 'completed' &&
job.completed_at &&
new Date(job.completed_at).getTime() > Date.now() - 10000 // Completed in last 10 seconds
);
if (hasProcessingJobs || hasRecentlyCompleted) {
const interval = setInterval(() => {
refetchJobs();
refetch();
}, hasProcessingJobs ? 2000 : 5000); // More frequent refresh while processing
return () => clearInterval(interval);
}
}
}, [processingJobsData?.jobs, refetch, refetchJobs]);
// Load tree data when in tree view mode
useEffect(() => {
if (viewMode === 'tree') {
loadTreeData();
}
}, [agentId, viewMode]);
const loadTreeData = async () => {
setTreeLoading(true);
try {
const headers = await getAuthHeaders();
// Load folders
const foldersResponse = await fetch(`${API_URL}/knowledge-base/folders`, { headers });
if (!foldersResponse.ok) {
throw new Error(`Failed to load folders: ${foldersResponse.status}`);
}
const foldersData = await foldersResponse.json();
const folders = Array.isArray(foldersData) ? foldersData : (foldersData.folders || []);
// Load current assignments
let currentAssignments = {};
try {
const assignmentsResponse = await fetch(`${API_URL}/knowledge-base/agents/${agentId}/assignments`, { headers });
if (assignmentsResponse.ok) {
currentAssignments = await assignmentsResponse.json();
}
} catch (assignError) {
console.warn('Failed to load assignments:', assignError);
}
// Build tree structure
const tree: TreeItem[] = [];
const selectedEntrySet = new Set<string>();
// Add enabled entries to selection
Object.entries(currentAssignments).forEach(([entryId, enabled]) => {
if (enabled) {
selectedEntrySet.add(entryId);
}
});
for (const folder of folders) {
// Load entries for this folder
let entriesData = { entries: [] };
try {
const entriesResponse = await fetch(`${API_URL}/knowledge-base/folders/${folder.folder_id}/entries`, { headers });
if (entriesResponse.ok) {
const rawEntriesData = await entriesResponse.json();
if (Array.isArray(rawEntriesData)) {
entriesData = { entries: rawEntriesData };
} else {
entriesData = rawEntriesData;
}
}
} catch (entriesError) {
console.warn(`Failed to load entries for folder ${folder.folder_id}:`, entriesError);
}
const children: TreeItem[] = [];
const entries = entriesData.entries || [];
for (const entry of entries) {
children.push({
id: entry.entry_id,
name: entry.filename,
type: 'file',
data: entry
});
}
tree.push({
id: folder.folder_id,
name: folder.name,
type: 'folder',
expanded: true,
children,
data: folder
});
}
setTreeData(tree);
setSelectedEntries(selectedEntrySet);
} catch (error) {
console.error('Failed to load tree data:', error);
toast.error(`Failed to load knowledge base tree: ${error.message}`);
} finally {
setTreeLoading(false);
}
};
const getFolderSelectionState = (folderId: string) => {
const folder = treeData.find(f => f.id === folderId);
if (!folder?.children || folder.children.length === 0) {
return { selected: false, indeterminate: false };
}
const folderEntryIds = folder.children.map(child => child.id);
const selectedCount = folderEntryIds.filter(id => selectedEntries.has(id)).length;
if (selectedCount === 0) {
return { selected: false, indeterminate: false };
} else if (selectedCount === folderEntryIds.length) {
return { selected: true, indeterminate: false };
} else {
return { selected: false, indeterminate: true };
}
};
const toggleEntrySelection = async (entryId: string) => {
const newSelection = new Set(selectedEntries);
if (newSelection.has(entryId)) {
newSelection.delete(entryId);
} else {
newSelection.add(entryId);
}
setSelectedEntries(newSelection);
await saveAssignments(newSelection);
};
const toggleFolderSelection = async (folderId: string) => {
const folder = treeData.find(f => f.id === folderId);
if (!folder?.children) return;
const folderEntryIds = folder.children.map(child => child.id);
const allSelected = folderEntryIds.every(id => selectedEntries.has(id));
const newSelection = new Set(selectedEntries);
if (allSelected) {
// Deselect all entries in folder
folderEntryIds.forEach(id => newSelection.delete(id));
} else {
// Select all entries in folder
folderEntryIds.forEach(id => newSelection.add(id));
}
setSelectedEntries(newSelection);
await saveAssignments(newSelection);
};
const saveAssignments = async (selectedSet: Set<string>) => {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/agents/${agentId}/assignments`, {
method: 'POST',
headers,
body: JSON.stringify({ entry_ids: Array.from(selectedSet) })
});
if (!response.ok) {
throw new Error('Failed to save assignments');
}
toast.success('Knowledge base access updated');
} catch (error) {
console.error('Failed to save assignments:', error);
toast.error('Failed to save assignments');
}
};
const toggleExpand = (folderId: string) => {
setTreeData(prev => prev.map(folder =>
folder.id === folderId
? { ...folder, expanded: !folder.expanded }
: folder
));
};
const handleDrag = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
@ -337,7 +541,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFileUpload(e.dataTransfer.files);
}
@ -380,7 +584,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim() || !formData.content.trim()) {
return;
}
@ -400,7 +604,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
} else {
await createMutation.mutateAsync({ agentId, data: formData });
}
handleCloseDialog();
} catch (error) {
console.error('Error saving agent knowledge base entry:', error);
@ -429,7 +633,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
const extractZipFile = async (zipFile: File, zipId: string) => {
try {
setUploadedFiles(prev => prev.map(f =>
setUploadedFiles(prev => prev.map(f =>
f.id === zipId ? { ...f, status: 'extracting' } : f
));
@ -443,13 +647,13 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
if (!file.dir && !path.startsWith('__MACOSX/') && !path.includes('/.')) {
const fileName = path.split('/').pop() || path;
const fileExtension = fileName.toLowerCase().substring(fileName.lastIndexOf('.'));
// Only process supported file formats
if (!supportedExtensions.includes(fileExtension)) {
rejectedFiles.push(fileName);
continue;
}
try {
const blob = await file.async('blob');
const extractedFile = new File([blob], fileName);
@ -477,15 +681,15 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
if (rejectedFiles.length > 0) {
message += `. Skipped ${rejectedFiles.length} unsupported files: ${rejectedFiles.slice(0, 5).join(', ')}${rejectedFiles.length > 5 ? '...' : ''}`;
}
toast.success(message);
} catch (error) {
console.error('Error extracting ZIP:', error);
setUploadedFiles(prev => prev.map(f =>
f.id === zipId ? {
...f,
status: 'error',
error: 'Failed to extract ZIP file'
setUploadedFiles(prev => prev.map(f =>
f.id === zipId ? {
...f,
status: 'error',
error: 'Failed to extract ZIP file'
} : f
));
toast.error('Failed to extract ZIP file');
@ -494,39 +698,39 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
const handleFileUpload = async (files: FileList | null) => {
if (!files || files.length === 0) return;
const supportedExtensions = ['.txt', '.pdf', '.docx'];
const newFiles: UploadedFile[] = [];
const rejectedFiles: string[] = [];
for (const file of Array.from(files)) {
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
// Allow ZIP files as they can contain supported formats
if (!supportedExtensions.includes(fileExtension) && fileExtension !== '.zip') {
rejectedFiles.push(file.name);
continue;
}
const fileId = Math.random().toString(36).substr(2, 9);
const uploadedFile: UploadedFile = {
file,
id: fileId,
status: 'pending'
};
newFiles.push(uploadedFile);
// Extract ZIP files to get individual files
if (file.name.toLowerCase().endsWith('.zip')) {
setTimeout(() => extractZipFile(file, fileId), 100);
}
}
if (rejectedFiles.length > 0) {
toast.error(`Unsupported file format(s): ${rejectedFiles.join(', ')}. Only .txt, .pdf, .docx, and .zip files are supported.`);
}
if (newFiles.length > 0) {
setUploadedFiles(prev => [...prev, ...newFiles]);
if (!addDialogOpen) {
@ -537,48 +741,48 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
};
const uploadFiles = async () => {
const filesToUpload = uploadedFiles.filter(f =>
f.status === 'pending' &&
const filesToUpload = uploadedFiles.filter(f =>
f.status === 'pending' &&
(f.isFromZip || !f.file.name.toLowerCase().endsWith('.zip'))
);
let allSuccessful = true;
for (const uploadedFile of filesToUpload) {
try {
setUploadedFiles(prev => prev.map(f =>
setUploadedFiles(prev => prev.map(f =>
f.id === uploadedFile.id ? { ...f, status: 'uploading' as const } : f
));
await uploadMutation.mutateAsync({ agentId, file: uploadedFile.file });
setUploadedFiles(prev => prev.map(f =>
setUploadedFiles(prev => prev.map(f =>
f.id === uploadedFile.id ? { ...f, status: 'success' as const } : f
));
} catch (error) {
allSuccessful = false;
setUploadedFiles(prev => prev.map(f =>
f.id === uploadedFile.id ? {
...f,
status: 'error' as const,
error: error instanceof Error ? error.message : 'Upload failed'
setUploadedFiles(prev => prev.map(f =>
f.id === uploadedFile.id ? {
...f,
status: 'error' as const,
error: error instanceof Error ? error.message : 'Upload failed'
} : f
));
}
}
// Auto-close dialog and show success message if all uploads succeeded
if (allSuccessful && filesToUpload.length > 0) {
// Trigger immediate refetch to show processing jobs
refetchJobs();
setTimeout(() => {
toast.success(
`Successfully uploaded ${filesToUpload.length} file${filesToUpload.length > 1 ? 's' : ''}. ` +
`Knowledge entries will appear below once processing is complete.`
);
handleCloseDialog();
// Trigger another refetch after closing dialog
setTimeout(() => {
refetch();
@ -639,14 +843,14 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
const entries = knowledgeBase?.entries || [];
const processingJobs = processingJobsData?.jobs || [];
const filteredEntries = entries.filter(entry =>
const filteredEntries = entries.filter(entry =>
entry.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
entry.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
(entry.description && entry.description.toLowerCase().includes(searchQuery.toLowerCase()))
);
return (
<div
<div
className="space-y-4"
onDragEnter={handleDrag}
onDragLeave={handleDrag}
@ -674,10 +878,30 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
className="pl-9 h-9"
/>
</div>
<Button onClick={() => handleOpenAddDialog()} size="sm" className="gap-2">
<Plus className="h-4 w-4" />
Add Knowledge
</Button>
<div className="flex items-center gap-2">
<div className="flex items-center border rounded-lg p-1">
<Button
size="sm"
variant={viewMode === 'list' ? 'default' : 'ghost'}
onClick={() => setViewMode('list')}
className="h-7 px-2"
>
<Grid className="h-3 w-3" />
</Button>
<Button
size="sm"
variant={viewMode === 'tree' ? 'default' : 'ghost'}
onClick={() => setViewMode('tree')}
className="h-7 px-2"
>
<List className="h-3 w-3" />
</Button>
</div>
<Button onClick={() => handleOpenAddDialog()} size="sm" className="gap-2">
<Plus className="h-4 w-4" />
Add Knowledge
</Button>
</div>
</div>
{entries.length === 0 ? (
<div className="text-center py-12 px-6 bg-muted/30 rounded-xl border-2 border-dashed border-border">
@ -710,7 +934,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
const contextConfig = getUsageContextConfig(entry.usage_context);
const ContextIcon = contextConfig.icon;
const SourceIcon = getSourceIcon(entry.source_type || 'manual', entry.source_metadata?.filename);
return (
<div
key={entry.entry_id}
@ -731,8 +955,8 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
)}
{entry.source_type && entry.source_type !== 'manual' && (
<Badge variant="outline" className="text-xs flex-shrink-0">
{entry.source_type === 'git_repo' ? 'Git' :
entry.source_type === 'zip_extracted' ? 'ZIP' : 'File'}
{entry.source_type === 'git_repo' ? 'Git' :
entry.source_type === 'zip_extracted' ? 'ZIP' : 'File'}
</Badge>
)}
</div>
@ -757,18 +981,18 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
</div>
</div>
<div className="flex items-center space-x-2 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
<Button
<Button
size="sm"
variant="ghost"
variant="ghost"
className="h-8 w-8 p-0"
onClick={() => handleOpenEditDialog(entry)}
aria-label="Edit knowledge entry"
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
<Button
size="sm"
variant="ghost"
variant="ghost"
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
onClick={() => setDeleteEntryId(entry.entry_id)}
aria-label="Delete knowledge entry"
@ -794,7 +1018,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
{processingJobs.map((job) => {
const StatusIcon = getJobStatusIcon(job.status);
const statusColor = getJobStatusColor(job.status);
return (
<div key={job.job_id} className="flex items-center justify-between p-4 rounded-lg border bg-card">
<div className="flex items-center gap-4">
@ -805,10 +1029,10 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
<div className="flex items-center space-x-2 mb-1">
<h4 className="text-sm font-medium">
{job.job_type === 'file_upload' ? 'File Upload' :
job.job_type === 'git_clone' ? 'Git Repository' : 'Processing'}
job.job_type === 'git_clone' ? 'Git Repository' : 'Processing'}
</h4>
<Badge variant={job.status === 'completed' ? 'default' :
job.status === 'failed' ? 'destructive' : 'secondary'} className="text-xs">
<Badge variant={job.status === 'completed' ? 'default' :
job.status === 'failed' ? 'destructive' : 'secondary'} className="text-xs">
{job.status}
</Badge>
</div>
@ -881,7 +1105,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
)}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto">
{addDialogMode === 'selection' && (
<div className="space-y-6 p-6">
@ -890,7 +1114,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
Choose how you'd like to add knowledge to {agentName}
</p>
</div>
<div className="grid gap-3">
<button
className="flex items-center gap-4 p-4 rounded-lg border bg-card hover:bg-muted/50 transition-colors text-left"
@ -906,7 +1130,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
</p>
</div>
</button>
<button
className="flex items-center gap-4 p-4 rounded-lg border bg-card hover:bg-muted/50 transition-colors text-left"
onClick={() => setAddDialogMode('files')}
@ -944,7 +1168,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
<Label htmlFor="usage_context" className="text-sm font-medium">Usage Context</Label>
<Select
value={formData.usage_context}
onValueChange={(value: 'always' | 'on_request' | 'contextual') =>
onValueChange={(value: 'always' | 'on_request' | 'contextual') =>
setFormData(prev => ({ ...prev, usage_context: value }))
}
>
@ -996,8 +1220,8 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
<Button type="button" variant="outline" onClick={handleCloseDialog}>
Cancel
</Button>
<Button
type="submit"
<Button
type="submit"
disabled={!formData.name.trim() || !formData.content.trim() || createMutation.isPending}
className="gap-2"
>
@ -1024,7 +1248,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
Drag and drop files here or click to browse.<br />
Supports: Documents, Code, ZIP archives
</p>
<Button
<Button
onClick={() => fileInputRef.current?.click()}
variant="outline"
className="gap-2"
@ -1149,10 +1373,10 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
<Button type="button" variant="outline" onClick={handleCloseDialog}>
Cancel
</Button>
<Button
<Button
onClick={uploadFiles}
disabled={uploadMutation.isPending || uploadedFiles.filter(f =>
f.status === 'pending' &&
disabled={uploadMutation.isPending || uploadedFiles.filter(f =>
f.status === 'pending' &&
(f.isFromZip || !f.file.name.toLowerCase().endsWith('.zip'))
).length === 0}
className="gap-2"
@ -1162,8 +1386,8 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
) : (
<Upload className="h-4 w-4" />
)}
Upload Files ({uploadedFiles.filter(f =>
f.status === 'pending' &&
Upload Files ({uploadedFiles.filter(f =>
f.status === 'pending' &&
(f.isFromZip || !f.file.name.toLowerCase().endsWith('.zip'))
).length})
</Button>
@ -1183,7 +1407,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
Edit Knowledge Entry
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto">
<form onSubmit={handleSubmit} className="space-y-6 p-1">
<div className="space-y-2">
@ -1201,7 +1425,7 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
<Label htmlFor="edit-usage_context" className="text-sm font-medium">Usage Context</Label>
<Select
value={formData.usage_context}
onValueChange={(value: 'always' | 'on_request' | 'contextual') =>
onValueChange={(value: 'always' | 'on_request' | 'contextual') =>
setFormData(prev => ({ ...prev, usage_context: value }))
}
>
@ -1253,8 +1477,8 @@ export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledge
<Button type="button" variant="outline" onClick={handleCloseDialog}>
Cancel
</Button>
<Button
type="submit"
<Button
type="submit"
disabled={!formData.name.trim() || !formData.content.trim() || updateMutation.isPending}
className="gap-2"
>

View File

@ -1,533 +0,0 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Checkbox } from '@/components/ui/checkbox';
import { Switch } from '@/components/ui/switch';
import {
FolderIcon,
FileIcon,
PlusIcon,
UploadIcon,
TrashIcon,
Settings,
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
GripVerticalIcon
} from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
import { createClient } from '@/lib/supabase/client';
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
// Helper function to get auth headers
const getAuthHeaders = async () => {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
return {
'Content-Type': 'application/json',
...(session?.access_token && { 'Authorization': `Bearer ${session.access_token}` })
};
};
interface Folder {
folder_id: string;
name: string;
description?: string;
entry_count: number;
created_at: string;
}
interface Entry {
entry_id: string;
filename: string;
summary: string;
file_size: number;
created_at: string;
}
interface AgentKnowledgeBaseManagerProps {
agentId: string;
agentName: string;
}
interface AgentAssignment {
folder_id: string;
enabled: boolean;
file_assignments: { [entryId: string]: boolean };
}
const useKnowledgeFolders = () => {
const [folders, setFolders] = useState<Folder[]>([]);
const [loading, setLoading] = useState(true);
const fetchFolders = async () => {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/folders`, { headers });
if (response.ok) {
const data = await response.json();
setFolders(data.folders || []);
}
} catch (error) {
console.error('Failed to fetch folders:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchFolders();
}, []);
return { folders, loading, refetch: fetchFolders };
};
const useAgentFolders = (agentId: string) => {
const [assignments, setAssignments] = useState<{ [folderId: string]: AgentAssignment }>({});
const [loading, setLoading] = useState(true);
const fetchAssignments = async () => {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/agents/${agentId}/assignments`, { headers });
if (response.ok) {
const data = await response.json();
setAssignments(data);
}
} catch (error) {
console.error('Failed to fetch agent assignments:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (agentId) {
fetchAssignments();
}
}, [agentId]);
return { assignments, setAssignments, loading, refetch: fetchAssignments };
};
export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledgeBaseManagerProps) => {
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
const [allEntries, setAllEntries] = useState<{ [folderId: string]: Entry[] }>({});
const [showCreateFolder, setShowCreateFolder] = useState(false);
const [showUpload, setShowUpload] = useState(false);
const [showAssignments, setShowAssignments] = useState(false);
const [newFolderName, setNewFolderName] = useState('');
const [uploading, setUploading] = useState(false);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
const fileInputRef = useRef<HTMLInputElement>(null);
const { folders, loading: foldersLoading, refetch: refetchFolders } = useKnowledgeFolders();
const { assignments, setAssignments, loading: assignedLoading, refetch: refetchAssigned } = useAgentFolders(agentId);
// Load all entries for all folders when assignments dialog is opened
useEffect(() => {
if (showAssignments && folders.length > 0) {
loadAllEntries();
}
}, [showAssignments, folders]);
const loadAllEntries = async () => {
const entriesData: { [folderId: string]: Entry[] } = {};
for (const folder of folders) {
try {
const response = await fetch(`/api/knowledge-base/folders/${folder.folder_id}/entries`);
if (response.ok) {
const data = await response.json();
entriesData[folder.folder_id] = data.entries || [];
} else {
entriesData[folder.folder_id] = [];
}
} catch (error) {
entriesData[folder.folder_id] = [];
}
}
setAllEntries(entriesData);
// Expand all folders by default so users can see all available content
const allFolderIds = new Set(folders.map(f => f.folder_id));
setExpandedFolders(allFolderIds);
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
};
const handleCreateFolder = async () => {
if (!newFolderName.trim()) return;
try {
const response = await fetch('/api/knowledge-base/folders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newFolderName })
});
if (response.ok) {
toast.success('Folder created successfully');
setNewFolderName('');
setShowCreateFolder(false);
refetchFolders();
} else {
toast.error('Failed to create folder');
}
} catch (error) {
toast.error('Failed to create folder');
}
};
const handleUploadFile = async (files: FileList | null) => {
if (!files || !selectedFolder) return;
setUploading(true);
for (const file of Array.from(files)) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch(`/api/knowledge-base/folders/${selectedFolder}/upload`, {
method: 'POST',
body: formData
});
if (response.ok) {
toast.success(`${file.name} uploaded successfully`);
} else {
toast.error(`Failed to upload ${file.name}`);
}
} catch (error) {
toast.error(`Failed to upload ${file.name}`);
}
}
setUploading(false);
setShowUpload(false);
fetchFolderEntries(selectedFolder);
refetchFolders(); // Update entry counts
};
const fetchFolderEntries = async (folderId: string) => {
try {
const response = await fetch(`/api/knowledge-base/folders/${folderId}/entries`);
if (response.ok) {
const data = await response.json();
setAllEntries(prev => ({ ...prev, [folderId]: data.entries || [] }));
}
} catch (error) {
console.error('Failed to fetch entries:', error);
}
};
const handleFolderSelect = (folderId: string) => {
setSelectedFolder(folderId);
fetchFolderEntries(folderId);
};
const handleDeleteFolder = async (folderId: string) => {
if (!confirm('Are you sure? This will delete all files in the folder.')) return;
try {
const response = await fetch(`/api/knowledge-base/folders/${folderId}`, {
method: 'DELETE'
});
if (response.ok) {
toast.success('Folder deleted');
refetchFolders();
if (selectedFolder === folderId) {
setSelectedFolder(null);
setAllEntries(prev => {
const newEntries = { ...prev };
delete newEntries[folderId];
return newEntries;
});
}
} else {
toast.error('Failed to delete folder');
}
} catch (error) {
toast.error('Failed to delete folder');
}
};
const handleUpdateAssignments = async () => {
try {
const response = await fetch(`/api/knowledge-base/agents/${agentId}/assignments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ assignments })
});
if (response.ok) {
toast.success('Agent knowledge assignments updated');
setShowAssignments(false);
refetchAssigned();
} else {
toast.error('Failed to update assignments');
}
} catch (error) {
toast.error('Failed to update assignments');
}
};
const toggleFolderAssignment = (folderId: string) => {
setAssignments(prev => ({
...prev,
[folderId]: {
folder_id: folderId,
enabled: !prev[folderId]?.enabled,
file_assignments: prev[folderId]?.file_assignments || {}
}
}));
};
const toggleFileAssignment = (folderId: string, entryId: string) => {
setAssignments(prev => ({
...prev,
[folderId]: {
...prev[folderId],
file_assignments: {
...prev[folderId]?.file_assignments,
[entryId]: !prev[folderId]?.file_assignments?.[entryId]
}
}
}));
};
const toggleFolderExpansion = (folderId: string) => {
setExpandedFolders(prev => {
const newSet = new Set(prev);
if (newSet.has(folderId)) {
newSet.delete(folderId);
} else {
newSet.add(folderId);
// Fetch entries when expanding
fetchFolderEntries(folderId);
}
return newSet;
});
};
const isFolderAssigned = (folderId: string) => {
return assignments[folderId]?.enabled || false;
};
const isFileAssigned = (folderId: string, entryId: string) => {
return assignments[folderId]?.file_assignments?.[entryId] || false;
};
if (foldersLoading || assignedLoading) {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-9 w-32" />
</div>
<Skeleton className="h-64" />
</div>
);
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Knowledge Base</h3>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowAssignments(true)}
>
<Settings className="h-4 w-4 mr-2" />
Configure Access
</Button>
<Button size="sm" onClick={() => setShowCreateFolder(true)}>
<PlusIcon className="h-4 w-4 mr-2" />
New Folder
</Button>
</div>
</div>
{/* Agent Current Access Overview */}
<Card>
<CardHeader>
<CardTitle className="text-base">Current Access for {agentName}</CardTitle>
</CardHeader>
<CardContent>
{Object.keys(assignments).length === 0 ? (
<p className="text-sm text-muted-foreground">No knowledge assigned yet</p>
) : (
<div className="space-y-2">
{folders
.filter(folder => isFolderAssigned(folder.folder_id))
.map(folder => (
<div key={folder.folder_id} className="flex items-center gap-2 text-sm">
<CheckIcon className="h-4 w-4 text-green-500" />
<FolderIcon className="h-4 w-4 text-blue-500" />
<span>{folder.name}</span>
<span className="text-muted-foreground">
({Object.values(assignments[folder.folder_id]?.file_assignments || {}).filter(Boolean).length} files)
</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Create Folder Dialog */}
<Dialog open={showCreateFolder} onOpenChange={setShowCreateFolder}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Folder</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder="Folder name"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleCreateFolder()}
/>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setShowCreateFolder(false)}>
Cancel
</Button>
<Button onClick={handleCreateFolder}>Create</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Upload Dialog */}
<Dialog open={showUpload} onOpenChange={setShowUpload}>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Files</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<input
ref={fileInputRef}
type="file"
multiple
accept=".txt,.pdf,.docx"
onChange={(e) => handleUploadFile(e.target.files)}
className="hidden"
/>
<Button
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
className="w-full"
>
<UploadIcon className="h-4 w-4 mr-2" />
{uploading ? 'Uploading...' : 'Select Files'}
</Button>
<p className="text-xs text-muted-foreground text-center">
Supports: .txt, .pdf, .docx files
</p>
</div>
</DialogContent>
</Dialog>
{/* Enhanced Agent Assignment Dialog with Tree View */}
<Dialog open={showAssignments} onOpenChange={setShowAssignments}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Configure Knowledge Access for {agentName}</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto space-y-4">
<p className="text-sm text-muted-foreground">
All folders and files are shown below. Enable the folders and specific files that this agent should have access to:
</p>
<div className="space-y-2">
{folders.map((folder) => (
<div key={folder.folder_id} className="border rounded-lg">
{/* Folder Header */}
<div className="flex items-center gap-3 p-3 bg-muted/20">
<Button
variant="ghost"
size="sm"
onClick={() => toggleFolderExpansion(folder.folder_id)}
className="p-0 h-6 w-6"
>
{expandedFolders.has(folder.folder_id) ?
<ChevronDownIcon className="h-4 w-4" /> :
<ChevronRightIcon className="h-4 w-4" />
}
</Button>
<Switch
checked={isFolderAssigned(folder.folder_id)}
onCheckedChange={() => toggleFolderAssignment(folder.folder_id)}
/>
<GripVerticalIcon className="h-4 w-4 text-muted-foreground" />
<FolderIcon className="h-4 w-4 text-blue-500" />
<div className="flex-1">
<p className="font-medium text-sm">{folder.name}</p>
<p className="text-xs text-muted-foreground">
{folder.entry_count} files
</p>
</div>
</div>
{/* Folder Files (when expanded) */}
{expandedFolders.has(folder.folder_id) && (
<div className="border-t">
{(allEntries[folder.folder_id] || []).length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
No files in this folder
</div>
) : (
(allEntries[folder.folder_id] || []).map((entry) => (
<div
key={entry.entry_id}
className="flex items-center gap-3 p-3 pl-12 hover:bg-muted/10"
>
<Switch
checked={isFileAssigned(folder.folder_id, entry.entry_id)}
onCheckedChange={() => toggleFileAssignment(folder.folder_id, entry.entry_id)}
disabled={!isFolderAssigned(folder.folder_id)}
/>
<FileIcon className="h-4 w-4 text-gray-500" />
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{entry.filename}</p>
<p className="text-xs text-muted-foreground">
{formatFileSize(entry.file_size)}
</p>
</div>
</div>
))
)}
</div>
)}
</div>
))}
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button variant="outline" onClick={() => setShowAssignments(false)}>
Cancel
</Button>
<Button onClick={handleUpdateAssignments}>Save Configuration</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
};

View File

@ -1,539 +0,0 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Checkbox } from '@/components/ui/checkbox';
import { Switch } from '@/components/ui/switch';
import {
FolderIcon,
FileIcon,
PlusIcon,
UploadIcon,
TrashIcon,
Settings,
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
GripVerticalIcon
} from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
import { createClient } from '@/lib/supabase/client';
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
// Helper function to get auth headers
const getAuthHeaders = async () => {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
return {
'Content-Type': 'application/json',
...(session?.access_token && { 'Authorization': `Bearer ${session.access_token}` })
};
};
interface Folder {
folder_id: string;
name: string;
description?: string;
entry_count: number;
created_at: string;
}
interface Entry {
entry_id: string;
filename: string;
summary: string;
file_size: number;
created_at: string;
}
interface AgentKnowledgeBaseManagerProps {
agentId: string;
agentName: string;
}
interface AgentAssignment {
folder_id: string;
enabled: boolean;
file_assignments: { [entryId: string]: boolean };
}
const useKnowledgeFolders = () => {
const [folders, setFolders] = useState<Folder[]>([]);
const [loading, setLoading] = useState(true);
const fetchFolders = async () => {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/folders`, { headers });
if (response.ok) {
const data = await response.json();
setFolders(data.folders || []);
}
} catch (error) {
console.error('Failed to fetch folders:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchFolders();
}, []);
return { folders, loading, refetch: fetchFolders };
};
const useAgentFolders = (agentId: string) => {
const [assignments, setAssignments] = useState<{ [folderId: string]: AgentAssignment }>({});
const [loading, setLoading] = useState(true);
const fetchAssignments = async () => {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/agents/${agentId}/assignments`, { headers });
if (response.ok) {
const data = await response.json();
setAssignments(data);
}
} catch (error) {
console.error('Failed to fetch agent assignments:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (agentId) {
fetchAssignments();
}
}, [agentId]);
return { assignments, setAssignments, loading, refetch: fetchAssignments };
};
export const AgentKnowledgeBaseManager = ({ agentId, agentName }: AgentKnowledgeBaseManagerProps) => {
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
const [allEntries, setAllEntries] = useState<{ [folderId: string]: Entry[] }>({});
const [showCreateFolder, setShowCreateFolder] = useState(false);
const [showUpload, setShowUpload] = useState(false);
const [showAssignments, setShowAssignments] = useState(false);
const [newFolderName, setNewFolderName] = useState('');
const [uploading, setUploading] = useState(false);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
const fileInputRef = useRef<HTMLInputElement>(null);
const { folders, loading: foldersLoading, refetch: refetchFolders } = useKnowledgeFolders();
const { assignments, setAssignments, loading: assignedLoading, refetch: refetchAssigned } = useAgentFolders(agentId);
// Load all entries for all folders when assignments dialog is opened
useEffect(() => {
if (showAssignments && folders.length > 0) {
loadAllEntries();
}
}, [showAssignments, folders]);
const loadAllEntries = async () => {
const entriesData: { [folderId: string]: Entry[] } = {};
for (const folder of folders) {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/folders/${folder.folder_id}/entries`, { headers });
if (response.ok) {
const data = await response.json();
entriesData[folder.folder_id] = data.entries || [];
} else {
entriesData[folder.folder_id] = [];
}
} catch (error) {
entriesData[folder.folder_id] = [];
}
}
setAllEntries(entriesData);
// Expand all folders by default so users can see all available content
const allFolderIds = new Set(folders.map(f => f.folder_id));
setExpandedFolders(allFolderIds);
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
};
const handleCreateFolder = async () => {
if (!newFolderName.trim()) return;
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/folders`, {
method: 'POST',
headers,
body: JSON.stringify({ name: newFolderName })
});
if (response.ok) {
toast.success('Folder created successfully');
setNewFolderName('');
setShowCreateFolder(false);
refetchFolders();
} else {
toast.error('Failed to create folder');
}
} catch (error) {
toast.error('Failed to create folder');
}
};
const handleUploadFile = async (files: FileList | null) => {
if (!files || !selectedFolder) return;
setUploading(true);
try {
for (const file of files) {
const formData = new FormData();
formData.append('file', file);
const headers = await getAuthHeaders();
delete (headers as any)['Content-Type']; // Remove content-type for FormData
const response = await fetch(`${API_URL}/knowledge-base/folders/${selectedFolder}/upload`, {
method: 'POST',
headers,
body: formData
});
if (!response.ok) {
toast.error(`Failed to upload ${file.name}`);
}
}
toast.success('Files uploaded successfully');
fetchFolderEntries(selectedFolder);
refetchFolders(); // Update entry counts
} catch (error) {
toast.error('Failed to upload files');
} finally {
setUploading(false);
setShowUpload(false);
}
};
const fetchFolderEntries = async (folderId: string) => {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/folders/${folderId}/entries`, { headers });
if (response.ok) {
const data = await response.json();
setAllEntries(prev => ({ ...prev, [folderId]: data.entries || [] }));
}
} catch (error) {
console.error('Failed to fetch entries:', error);
}
};
const handleFolderSelect = (folderId: string) => {
setSelectedFolder(folderId);
fetchFolderEntries(folderId);
};
const handleDeleteFolder = async (folderId: string) => {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/folders/${folderId}`, {
method: 'DELETE',
headers
});
if (response.ok) {
toast.success('Folder deleted');
refetchFolders();
if (selectedFolder === folderId) {
setSelectedFolder(null);
setAllEntries(prev => {
const newEntries = { ...prev };
delete newEntries[folderId];
return newEntries;
});
}
} else {
toast.error('Failed to delete folder');
}
} catch (error) {
toast.error('Failed to delete folder');
}
};
const handleUpdateAssignments = async () => {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_URL}/knowledge-base/agents/${agentId}/assignments`, {
method: 'POST',
headers,
body: JSON.stringify({ assignments })
});
if (response.ok) {
toast.success('Agent knowledge assignments updated');
setShowAssignments(false);
refetchAssigned();
} else {
toast.error('Failed to update assignments');
}
} catch (error) {
toast.error('Failed to update assignments');
}
};
const toggleFolderAssignment = (folderId: string) => {
setAssignments(prev => ({
...prev,
[folderId]: {
folder_id: folderId,
enabled: !prev[folderId]?.enabled,
file_assignments: prev[folderId]?.file_assignments || {}
}
}));
};
const toggleFileAssignment = (folderId: string, entryId: string) => {
setAssignments(prev => ({
...prev,
[folderId]: {
...prev[folderId],
file_assignments: {
...prev[folderId]?.file_assignments,
[entryId]: !prev[folderId]?.file_assignments?.[entryId]
}
}
}));
};
const isFolderAssigned = (folderId: string) => {
return assignments[folderId]?.enabled || false;
};
const isFileAssigned = (folderId: string, entryId: string) => {
return assignments[folderId]?.file_assignments?.[entryId] || false;
};
const toggleFolderExpansion = (folderId: string) => {
setExpandedFolders(prev => {
const newSet = new Set(prev);
if (newSet.has(folderId)) {
newSet.delete(folderId);
} else {
newSet.add(folderId);
}
return newSet;
});
};
if (foldersLoading || assignedLoading) {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Skeleton className="h-7 w-32" />
<Skeleton className="h-9 w-32" />
</div>
<Skeleton className="h-64" />
</div>
);
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Knowledge Base</h3>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowAssignments(true)}
>
<Settings className="h-4 w-4 mr-2" />
Configure Access
</Button>
<Button size="sm" onClick={() => setShowCreateFolder(true)}>
<PlusIcon className="h-4 w-4 mr-2" />
New Folder
</Button>
</div>
</div>
{/* Agent Current Access Overview */}
<Card>
<CardHeader>
<CardTitle className="text-base">Current Access for {agentName}</CardTitle>
</CardHeader>
<CardContent>
{Object.keys(assignments).length === 0 ? (
<p className="text-sm text-muted-foreground">No knowledge assigned yet</p>
) : (
<div className="space-y-2">
{folders
.filter(folder => isFolderAssigned(folder.folder_id))
.map(folder => (
<div key={folder.folder_id} className="flex items-center gap-2 text-sm">
<CheckIcon className="h-4 w-4 text-green-500" />
<FolderIcon className="h-4 w-4 text-blue-500" />
<span>{folder.name}</span>
<span className="text-muted-foreground">
({Object.values(assignments[folder.folder_id]?.file_assignments || {}).filter(Boolean).length} files)
</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Create Folder Dialog */}
<Dialog open={showCreateFolder} onOpenChange={setShowCreateFolder}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Folder</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder="Folder name"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleCreateFolder()}
/>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setShowCreateFolder(false)}>
Cancel
</Button>
<Button onClick={handleCreateFolder}>Create</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Upload Dialog */}
<Dialog open={showUpload} onOpenChange={setShowUpload}>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Files</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<input
ref={fileInputRef}
type="file"
multiple
accept=".txt,.pdf,.docx"
onChange={(e) => handleUploadFile(e.target.files)}
className="hidden"
/>
<Button
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
className="w-full"
>
<UploadIcon className="h-4 w-4 mr-2" />
{uploading ? 'Uploading...' : 'Select Files'}
</Button>
<p className="text-xs text-muted-foreground text-center">
Supports: .txt, .pdf, .docx files
</p>
</div>
</DialogContent>
</Dialog>
{/* Enhanced Agent Assignment Dialog with Tree View */}
<Dialog open={showAssignments} onOpenChange={setShowAssignments}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Configure Knowledge Access for {agentName}</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto space-y-4">
<p className="text-sm text-muted-foreground">
All folders and files are shown below. Enable the folders and specific files that this agent should have access to:
</p>
<div className="space-y-2">
{folders.map((folder) => (
<div key={folder.folder_id} className="border rounded-lg">
{/* Folder Header */}
<div className="flex items-center gap-3 p-3 bg-muted/20">
<Button
variant="ghost"
size="sm"
onClick={() => toggleFolderExpansion(folder.folder_id)}
className="p-0 h-6 w-6"
>
{expandedFolders.has(folder.folder_id) ?
<ChevronDownIcon className="h-4 w-4" /> :
<ChevronRightIcon className="h-4 w-4" />
}
</Button>
<Switch
checked={isFolderAssigned(folder.folder_id)}
onCheckedChange={() => toggleFolderAssignment(folder.folder_id)}
/>
<GripVerticalIcon className="h-4 w-4 text-muted-foreground" />
<FolderIcon className="h-4 w-4 text-blue-500" />
<div className="flex-1">
<p className="font-medium text-sm">{folder.name}</p>
<p className="text-xs text-muted-foreground">
{folder.entry_count} files
</p>
</div>
</div>
{/* Folder Files (when expanded) */}
{expandedFolders.has(folder.folder_id) && (
<div className="border-t">
{(allEntries[folder.folder_id] || []).length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
No files in this folder
</div>
) : (
(allEntries[folder.folder_id] || []).map((entry) => (
<div
key={entry.entry_id}
className="flex items-center gap-3 p-3 pl-12 hover:bg-muted/10"
>
<Switch
checked={isFileAssigned(folder.folder_id, entry.entry_id)}
onCheckedChange={() => toggleFileAssignment(folder.folder_id, entry.entry_id)}
disabled={!isFolderAssigned(folder.folder_id)}
/>
<FileIcon className="h-4 w-4 text-gray-500" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{entry.filename}</p>
<p className="text-xs text-muted-foreground truncate">{entry.summary}</p>
<p className="text-xs text-muted-foreground">{formatFileSize(entry.file_size)}</p>
</div>
</div>
))
)}
</div>
)}
</div>
))}
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button variant="outline" onClick={() => setShowAssignments(false)}>
Cancel
</Button>
<Button onClick={handleUpdateAssignments}>
Save Changes
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
};