mirror of https://github.com/kortix-ai/suna.git
fix: show in kb new model
This commit is contained in:
parent
02da9e2e6d
commit
b20f95850f
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue