move data-source ops to a modal inside build view (#186)

* added data source modal to build view

* fixed scrape url and upload files showing up in the data modal in the build view

* autoupdate data source status

* keep focus in build view on deleting a data source

* removed the rag tab and allow multiple file uploads

* show scrapred urls and uploaded files

* show text in the text data source

* fixed status refreshes when using the detail view of data source

* fix status refresh in the data panel

* cleanup

* fixed review comments on backend changes

* moved the input text field in text data source to the new ui element

* Fix PR review: Replace router.push with data sources modal in agent config

- Add onOpenDataSourcesModal prop to AgentConfig component
- Update 'Go to Data Sources' button to open modal instead of navigating
- Change button text to 'Add Data Source' for clarity
- Use forwardRef and useImperativeHandle to expose modal control
- Remove router dependency from agent_config.tsx

Addresses reviewer comment about using modal instead of route change.

* fixed build issue

* fixed nit comments

* fixed line break
This commit is contained in:
arkml 2025-07-29 14:29:22 +05:30 committed by GitHub
parent 6fd897e569
commit aa95b590e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1387 additions and 71 deletions

View file

@ -1,5 +1,4 @@
'use server'; 'use server';
import { redirect } from "next/navigation";
import { ObjectId, WithId } from "mongodb"; import { ObjectId, WithId } from "mongodb";
import { dataSourcesCollection, dataSourceDocsCollection } from "../lib/mongodb"; import { dataSourcesCollection, dataSourceDocsCollection } from "../lib/mongodb";
import { z } from 'zod'; import { z } from 'zod';
@ -132,8 +131,6 @@ export async function deleteDataSource(projectId: string, sourceId: string) {
version: 1, version: 1,
}, },
}); });
redirect(`/projects/${projectId}/sources`);
} }
export async function toggleDataSource(projectId: string, sourceId: string, active: boolean) { export async function toggleDataSource(projectId: string, sourceId: string, active: boolean) {

View file

@ -6,7 +6,7 @@ export default function Layout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<AppLayout useRag={false} useAuth={true} useBilling={true}> <AppLayout useAuth={true} useBilling={true}>
{children} {children}
</AppLayout> </AppLayout>
); );

View file

@ -6,7 +6,7 @@ export default function Layout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<AppLayout useRag={false} useAuth={true} useBilling={true}> <AppLayout useAuth={true} useBilling={true}>
{children} {children}
</AppLayout> </AppLayout>
); );

View file

@ -23,7 +23,6 @@ import { Info } from "lucide-react";
import { useCopilot } from "../copilot/use-copilot"; import { useCopilot } from "../copilot/use-copilot";
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal"; import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
import { ModelsResponse } from "@/app/lib/types/billing_types"; import { ModelsResponse } from "@/app/lib/types/billing_types";
import { useRouter } from "next/navigation";
import { SectionCard } from "@/components/common/section-card"; import { SectionCard } from "@/components/common/section-card";
// Common section header styles // Common section header styles
@ -49,6 +48,7 @@ export function AgentConfig({
useRag, useRag,
triggerCopilotChat, triggerCopilotChat,
eligibleModels, eligibleModels,
onOpenDataSourcesModal,
}: { }: {
projectId: string, projectId: string,
workflow: z.infer<typeof Workflow>, workflow: z.infer<typeof Workflow>,
@ -63,6 +63,7 @@ export function AgentConfig({
useRag: boolean, useRag: boolean,
triggerCopilotChat: (message: string) => void, triggerCopilotChat: (message: string) => void,
eligibleModels: z.infer<typeof ModelsResponse.shape.agentModels> | "*", eligibleModels: z.infer<typeof ModelsResponse.shape.agentModels> | "*",
onOpenDataSourcesModal?: () => void,
}) { }) {
const [isAdvancedConfigOpen, setIsAdvancedConfigOpen] = useState(false); const [isAdvancedConfigOpen, setIsAdvancedConfigOpen] = useState(false);
const [showGenerateModal, setShowGenerateModal] = useState(false); const [showGenerateModal, setShowGenerateModal] = useState(false);
@ -75,7 +76,6 @@ export function AgentConfig({
const [showRagCta, setShowRagCta] = useState(false); const [showRagCta, setShowRagCta] = useState(false);
const [previousRagSources, setPreviousRagSources] = useState<string[]>([]); const [previousRagSources, setPreviousRagSources] = useState<string[]>([]);
const [billingError, setBillingError] = useState<string | null>(null); const [billingError, setBillingError] = useState<string | null>(null);
const router = useRouter();
const [showSavedBanner, setShowSavedBanner] = useState(false); const [showSavedBanner, setShowSavedBanner] = useState(false);
const { const {
@ -706,11 +706,11 @@ export function AgentConfig({
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
router.push(`/projects/${projectId}/sources`); onOpenDataSourcesModal?.();
}} }}
startContent={<DatabaseIcon className="w-3 h-3" />} startContent={<DatabaseIcon className="w-3 h-3" />}
> >
Go to RAG Sources Add Data Source
</CustomButton> </CustomButton>
</div> </div>
</SelectItem> </SelectItem>

View file

@ -0,0 +1,922 @@
"use client";
import { WithStringId } from "../../../lib/types/types";
import { DataSource } from "../../../lib/types/datasource_types";
import { z } from "zod";
import { XIcon, FileIcon, GlobeIcon, AlertTriangle, CheckCircle, Circle, ExternalLinkIcon, Type, PlusIcon, Edit3Icon, DownloadIcon, Trash2 } from "lucide-react";
import { useState, useEffect, useCallback } from "react";
import { Panel } from "@/components/common/panel-common";
import { Button } from "@/components/ui/button";
import { DataSourceIcon } from "@/app/lib/components/datasource-icon";
import { Tooltip } from "@heroui/react";
import { getDataSource, listDocsInDataSource, deleteDocsFromDataSource, getDownloadUrlForFile, addDocsToDataSource, getUploadUrlsForFilesDataSource } from "@/app/actions/datasource_actions";
import { InputField } from "@/app/lib/components/input-field";
import { DataSourceDoc } from "../../../lib/types/datasource_types";
import { RelativeTime } from "@primer/react";
import { Pagination, Spinner, Button as HeroButton, Textarea as HeroTextarea } from "@heroui/react";
import { useDropzone } from "react-dropzone";
export function DataSourceConfig({
dataSourceId,
handleClose,
onDataSourceUpdate
}: {
dataSourceId: string,
handleClose: () => void,
onDataSourceUpdate?: () => void
}) {
const [dataSource, setDataSource] = useState<WithStringId<z.infer<typeof DataSource>> | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Files-related state
const [files, setFiles] = useState<WithStringId<z.infer<typeof DataSourceDoc>>[]>([]);
const [filesLoading, setFilesLoading] = useState(false);
const [filesPage, setFilesPage] = useState(1);
const [filesTotal, setFilesTotal] = useState(0);
const [projectId, setProjectId] = useState<string>('');
useEffect(() => {
async function loadDataSource() {
try {
setLoading(true);
// Extract projectId from the current URL
const pathParts = window.location.pathname.split('/');
const currentProjectId = pathParts[2]; // /projects/[projectId]/workflow
setProjectId(currentProjectId);
const ds = await getDataSource(currentProjectId, dataSourceId);
setDataSource(ds);
// Load files if it's a files data source
if (ds.data.type === 'files_local' || ds.data.type === 'files_s3') {
await loadFiles(currentProjectId, dataSourceId, 1);
}
// Load URLs if it's a URLs data source
if (ds.data.type === 'urls') {
await loadUrls(currentProjectId, dataSourceId, 1);
}
// Load text content if it's a text data source
if (ds.data.type === 'text') {
await loadTextContent(currentProjectId, dataSourceId);
}
} catch (err) {
console.error('Failed to load data source:', err);
setError('Failed to load data source details');
} finally {
setLoading(false);
}
}
loadDataSource();
}, [dataSourceId]);
// Auto-refresh data source status when it's pending
useEffect(() => {
let ignore = false;
let timeout: NodeJS.Timeout | null = null;
if (!dataSource || !projectId) {
return;
}
if (dataSource.status !== 'pending') {
return;
}
async function refreshStatus() {
if (timeout) {
clearTimeout(timeout);
}
try {
const updatedSource = await getDataSource(projectId, dataSourceId);
if (!ignore) {
setDataSource(updatedSource);
onDataSourceUpdate?.(); // Notify parent of status change
// Continue polling if still pending
if (updatedSource.status === 'pending') {
timeout = setTimeout(refreshStatus, 5000); // Poll every 5 seconds
}
}
} catch (err) {
console.error('Failed to refresh data source status:', err);
// Retry after a longer delay on error
if (!ignore) {
timeout = setTimeout(refreshStatus, 10000);
}
}
}
// Start polling after a short delay
timeout = setTimeout(refreshStatus, 5000);
return () => {
ignore = true;
if (timeout) {
clearTimeout(timeout);
}
};
}, [dataSource, projectId, dataSourceId, onDataSourceUpdate]);
// Helper function to update data source and notify parent
const updateDataSourceAndNotify = useCallback(async () => {
try {
const updatedSource = await getDataSource(projectId, dataSourceId);
setDataSource(updatedSource);
onDataSourceUpdate?.();
} catch (err) {
console.error('Failed to reload data source:', err);
}
}, [projectId, dataSourceId, onDataSourceUpdate]);
// Load files function
const loadFiles = async (projectId: string, sourceId: string, page: number) => {
try {
setFilesLoading(true);
const { files, total } = await listDocsInDataSource({
projectId,
sourceId,
page,
limit: 10,
});
setFiles(files);
setFilesTotal(total);
setFilesPage(page);
} catch (err) {
console.error('Failed to load files:', err);
} finally {
setFilesLoading(false);
}
};
// URLs-related state
const [urls, setUrls] = useState<WithStringId<z.infer<typeof DataSourceDoc>>[]>([]);
const [urlsLoading, setUrlsLoading] = useState(false);
const [urlsPage, setUrlsPage] = useState(1);
const [urlsTotal, setUrlsTotal] = useState(0);
// Text-related state
const [textContent, setTextContent] = useState<string>('');
const [textLoading, setTextLoading] = useState(false);
const [savingText, setSavingText] = useState(false);
// URL form state
const [showAddUrlForm, setShowAddUrlForm] = useState(false);
const [addingUrls, setAddingUrls] = useState(false);
// File upload state
const [uploadingFiles, setUploadingFiles] = useState(false);
// Load URLs function
const loadUrls = async (projectId: string, sourceId: string, page: number) => {
try {
setUrlsLoading(true);
const { files, total } = await listDocsInDataSource({
projectId,
sourceId,
page,
limit: 10,
});
setUrls(files);
setUrlsTotal(total);
setUrlsPage(page);
} catch (err) {
console.error('Failed to load URLs:', err);
} finally {
setUrlsLoading(false);
}
};
// Load text content function
const loadTextContent = async (projectId: string, sourceId: string) => {
try {
setTextLoading(true);
const { files } = await listDocsInDataSource({
projectId,
sourceId,
limit: 1,
});
if (files.length > 0 && files[0].data.type === 'text') {
setTextContent(files[0].data.content);
} else {
setTextContent('');
}
} catch (err) {
console.error('Failed to load text content:', err);
setTextContent('');
} finally {
setTextLoading(false);
}
};
// Handle file deletion
const handleDeleteFile = async (fileId: string) => {
if (!window.confirm('Are you sure you want to delete this file?')) return;
try {
await deleteDocsFromDataSource({
projectId,
sourceId: dataSourceId,
docIds: [fileId],
});
// Reload files
await loadFiles(projectId, dataSourceId, filesPage);
// Reload data source to get updated status
await updateDataSourceAndNotify();
} catch (err) {
console.error('Failed to delete file:', err);
}
};
// Handle file download
const handleDownloadFile = async (fileId: string) => {
try {
const url = await getDownloadUrlForFile(projectId, dataSourceId, fileId);
window.open(url, '_blank');
} catch (err) {
console.error('Failed to download file:', err);
}
};
// Handle page change
const handlePageChange = (page: number) => {
loadFiles(projectId, dataSourceId, page);
};
// Handle URL deletion
const handleDeleteUrl = async (urlId: string) => {
if (!window.confirm('Are you sure you want to delete this URL?')) return;
try {
await deleteDocsFromDataSource({
projectId,
sourceId: dataSourceId,
docIds: [urlId],
});
// Reload URLs
await loadUrls(projectId, dataSourceId, urlsPage);
// Reload data source to get updated status
await updateDataSourceAndNotify();
} catch (err) {
console.error('Failed to delete URL:', err);
}
};
// Handle URL page change
const handleUrlPageChange = (page: number) => {
loadUrls(projectId, dataSourceId, page);
};
// Handle text content update
const handleUpdateTextContent = async (newContent: string) => {
setSavingText(true);
try {
// Delete existing text doc if it exists
const { files } = await listDocsInDataSource({
projectId,
sourceId: dataSourceId,
limit: 1,
});
if (files.length > 0) {
await deleteDocsFromDataSource({
projectId,
sourceId: dataSourceId,
docIds: [files[0]._id],
});
}
// Add new text doc
await addDocsToDataSource({
projectId,
sourceId: dataSourceId,
docData: [{
name: 'text',
data: {
type: 'text',
content: newContent,
},
}],
});
setTextContent(newContent);
// Reload data source to get updated status
await updateDataSourceAndNotify();
} catch (err) {
console.error('Failed to save text:', err);
} finally {
setSavingText(false);
}
};
// Handle URL addition
const handleAddUrls = async (formData: FormData) => {
setAddingUrls(true);
try {
const urls = formData.get('urls') as string;
const urlsArray = urls.split('\n')
.map(url => url.trim())
.filter(url => url.length > 0);
const first100Urls = urlsArray.slice(0, 100);
await addDocsToDataSource({
projectId,
sourceId: dataSourceId,
docData: first100Urls.map(url => ({
name: url,
data: {
type: 'url',
url,
},
})),
});
setShowAddUrlForm(false);
await loadUrls(projectId, dataSourceId, urlsPage);
// Reload data source to get updated status
await updateDataSourceAndNotify();
} catch (err) {
console.error('Failed to add URLs:', err);
} finally {
setAddingUrls(false);
}
};
// Handle file upload
const onFileDrop = useCallback(async (acceptedFiles: File[]) => {
if (!dataSource) return;
setUploadingFiles(true);
try {
const urls = await getUploadUrlsForFilesDataSource(projectId, dataSourceId, acceptedFiles.map(file => ({
name: file.name,
type: file.type,
size: file.size,
})));
// Upload files in parallel
await Promise.all(acceptedFiles.map(async (file, index) => {
await fetch(urls[index].uploadUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});
}));
// After successful uploads, update the database with file information
let docData: {
_id: string,
name: string,
data: z.infer<typeof DataSourceDoc>['data']
}[] = [];
const isS3 = dataSource.data.type === 'files_s3';
if (isS3) {
docData = acceptedFiles.map((file, index) => ({
_id: urls[index].fileId,
name: file.name,
data: {
type: 'file_s3' as const,
name: file.name,
size: file.size,
mimeType: file.type,
s3Key: urls[index].path,
},
}));
} else {
docData = acceptedFiles.map((file, index) => ({
_id: urls[index].fileId,
name: file.name,
data: {
type: 'file_local' as const,
name: file.name,
size: file.size,
mimeType: file.type,
},
}));
}
await addDocsToDataSource({
projectId,
sourceId: dataSourceId,
docData,
});
await loadFiles(projectId, dataSourceId, filesPage);
// Reload data source to get updated status
await updateDataSourceAndNotify();
} catch (error) {
console.error('Upload failed:', error);
} finally {
setUploadingFiles(false);
}
}, [projectId, dataSourceId, dataSource, filesPage, updateDataSourceAndNotify]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop: onFileDrop,
disabled: uploadingFiles,
accept: {
'application/pdf': ['.pdf'],
},
});
if (loading) {
return (
<Panel
title={
<div className="flex items-center justify-between w-full">
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">
Loading Data Source...
</div>
<Button
variant="secondary"
size="sm"
onClick={handleClose}
showHoverContent={true}
hoverContent="Close"
>
<XIcon className="w-4 h-4" />
</Button>
</div>
}
>
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
</div>
</Panel>
);
}
if (error || !dataSource) {
return (
<Panel
title={
<div className="flex items-center justify-between w-full">
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">
Error Loading Data Source
</div>
<Button
variant="secondary"
size="sm"
onClick={handleClose}
showHoverContent={true}
hoverContent="Close"
>
<XIcon className="w-4 h-4" />
</Button>
</div>
}
>
<div className="flex items-center justify-center h-64 text-red-500">
<div className="text-center">
<AlertTriangle className="w-12 h-12 mx-auto mb-4" />
<p>{error || 'Data source not found'}</p>
</div>
</div>
</Panel>
);
}
// Determine status
const isActive = dataSource.active && dataSource.status === 'ready';
const isPending = dataSource.status === 'pending';
const isError = dataSource.status === 'error';
// Status indicator
const statusIndicator = () => {
if (isPending) {
return (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-yellow-50 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400">
<Spinner size="sm" color="warning" />
<span className="text-sm font-medium">Processing</span>
</div>
);
} else if (isError) {
return (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400">
<AlertTriangle className="w-4 h-4" />
<span className="text-sm font-medium">Error</span>
</div>
);
} else if (isActive) {
return (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400">
<CheckCircle className="w-4 h-4" />
<span className="text-sm font-medium">Active</span>
</div>
);
} else {
return (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-gray-50 text-gray-700 dark:bg-gray-900/20 dark:text-gray-400">
<Circle className="w-4 h-4" />
<span className="text-sm font-medium">Inactive</span>
</div>
);
}
};
// Type display name
const getTypeDisplayName = (type: string) => {
switch (type) {
case 'urls': return 'Scraped URLs';
case 'files_local': return 'Local Files';
case 'files_s3': return 'S3 Files';
case 'text': return 'Text Content';
default: return type;
}
};
return (
<Panel
title={
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-3">
<DataSourceIcon
type={
dataSource.data.type === 'files_local' || dataSource.data.type === 'files_s3'
? 'files'
: dataSource.data.type
}
size="md"
/>
<div>
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">
{dataSource.name}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Data Source
</div>
</div>
</div>
<Button
variant="secondary"
size="sm"
onClick={handleClose}
showHoverContent={true}
hoverContent="Close"
>
<XIcon className="w-4 h-4" />
</Button>
</div>
}
>
<div className="h-full overflow-auto">
<div className="p-6 space-y-6">
{/* Status Section */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">Status</h3>
{statusIndicator()}
{isError && dataSource.error && (
<div className="mt-2 p-3 bg-red-50 dark:bg-red-900/20 rounded-md">
<p className="text-sm text-red-700 dark:text-red-400">{dataSource.error}</p>
</div>
)}
</div>
{/* Basic Information */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">Information</h3>
<div className="grid grid-cols-1 gap-4 text-sm">
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Type:</span>
<span className="text-gray-900 dark:text-gray-100">{getTypeDisplayName(dataSource.data.type)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-500 dark:text-gray-400">Status:</span>
<div className="flex items-center">
{statusIndicator()}
</div>
</div>
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Created:</span>
<span className="text-gray-900 dark:text-gray-100">
{new Date(dataSource.createdAt).toLocaleDateString()}
</span>
</div>
{dataSource.lastUpdatedAt && (
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Last Updated:</span>
<span className="text-gray-900 dark:text-gray-100">
{new Date(dataSource.lastUpdatedAt).toLocaleDateString()}
</span>
</div>
)}
</div>
</div>
{/* Description */}
{dataSource.description && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">Description</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 p-3 rounded-md">
{dataSource.description}
</p>
</div>
)}
{/* Files Section (for file-type data sources) */}
{(dataSource.data.type === 'files_local' || dataSource.data.type === 'files_s3') && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
Files ({filesTotal})
</h3>
</div>
{/* File Upload Area */}
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors
${isDragActive ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/10' : 'border-gray-300 dark:border-gray-700'}`}
>
<input {...getInputProps()} />
{uploadingFiles ? (
<div className="flex items-center justify-center gap-2">
<Spinner size="sm" />
<p className="text-sm">Uploading files...</p>
</div>
) : isDragActive ? (
<p className="text-sm">Drop the files here...</p>
) : (
<div className="space-y-2">
<div className="flex items-center justify-center gap-2">
<PlusIcon className="w-4 h-4" />
<p className="text-sm">Drag and drop files here, or click to select files</p>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Only PDF files are supported for now.
</p>
</div>
)}
</div>
{filesLoading ? (
<div className="flex items-center justify-center gap-2 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
<Spinner size="sm" />
<p className="text-gray-600 dark:text-gray-300">Loading files...</p>
</div>
) : files.length === 0 ? (
<div className="text-center p-8 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
<FileIcon className="w-12 h-12 mx-auto mb-4 text-gray-400" />
<p className="text-gray-500 dark:text-gray-400">No files uploaded yet</p>
</div>
) : (
<div className="space-y-2">
{files.map((file) => (
<div
key={file._id}
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg border"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<FileIcon className="w-4 h-4 text-gray-500 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{file.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<RelativeTime date={new Date(file.createdAt)} />
{file.data.type === 'file_local' && ' • Local'}
{file.data.type === 'file_s3' && ' • S3'}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{(file.data.type === 'file_local' || file.data.type === 'file_s3') && (
<Tooltip content="Download file">
<button
onClick={() => handleDownloadFile(file._id)}
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
>
<DownloadIcon className="w-4 h-4 text-gray-500" />
</button>
</Tooltip>
)}
<Tooltip content="Delete file">
<button
onClick={() => handleDeleteFile(file._id)}
className="p-1 hover:bg-red-100 dark:hover:bg-red-900/20 rounded transition-colors"
>
<Trash2 className="w-4 h-4 text-red-500" />
</button>
</Tooltip>
</div>
</div>
))}
{/* Pagination */}
{filesTotal > 10 && (
<div className="flex justify-center pt-4">
<Pagination
total={Math.ceil(filesTotal / 10)}
page={filesPage}
onChange={handlePageChange}
size="sm"
/>
</div>
)}
</div>
)}
</div>
)}
{/* URLs Section (for URL-type data sources) */}
{dataSource.data.type === 'urls' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
URLs ({urlsTotal})
</h3>
</div>
{/* Add URLs Button/Form */}
{!showAddUrlForm ? (
<HeroButton
onClick={() => setShowAddUrlForm(true)}
variant="bordered"
size="sm"
startContent={<PlusIcon className="w-4 h-4" />}
>
Add URLs
</HeroButton>
) : (
<form
action={handleAddUrls}
className="space-y-3 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border"
>
<div className="space-y-2">
<label className="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Add URLs (one per line)
</label>
<HeroTextarea
required
name="urls"
minRows={5}
placeholder="https://example.com"
className="w-full"
/>
</div>
<div className="flex gap-2">
<HeroButton
type="submit"
color="primary"
size="sm"
isDisabled={addingUrls}
isLoading={addingUrls}
startContent={!addingUrls ? <PlusIcon className="w-4 h-4" /> : undefined}
>
{addingUrls ? 'Adding...' : 'Add URLs'}
</HeroButton>
<HeroButton
type="button"
variant="bordered"
size="sm"
onClick={() => setShowAddUrlForm(false)}
isDisabled={addingUrls}
>
Cancel
</HeroButton>
</div>
</form>
)}
{urlsLoading ? (
<div className="flex items-center justify-center gap-2 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
<Spinner size="sm" />
<p className="text-gray-600 dark:text-gray-300">Loading URLs...</p>
</div>
) : urls.length === 0 ? (
<div className="text-center p-8 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
<GlobeIcon className="w-12 h-12 mx-auto mb-4 text-gray-400" />
<p className="text-gray-500 dark:text-gray-400">No URLs added yet</p>
</div>
) : (
<div className="space-y-2">
{urls.map((url) => (
<div
key={url._id}
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg border"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<GlobeIcon className="w-4 h-4 text-gray-500 flex-shrink-0" />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{url.name}
</p>
{url.data.type === 'url' && (
<a
href={url.data.url}
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
>
<ExternalLinkIcon className="w-3.5 h-3.5" />
</a>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
<RelativeTime date={new Date(url.createdAt)} />
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Tooltip content="Delete URL">
<button
onClick={() => handleDeleteUrl(url._id)}
className="p-1 hover:bg-red-100 dark:hover:bg-red-900/20 rounded transition-colors"
>
<Trash2 className="w-4 h-4 text-red-500" />
</button>
</Tooltip>
</div>
</div>
))}
{/* Pagination */}
{urlsTotal > 10 && (
<div className="flex justify-center pt-4">
<Pagination
total={Math.ceil(urlsTotal / 10)}
page={urlsPage}
onChange={handleUrlPageChange}
size="sm"
/>
</div>
)}
</div>
)}
</div>
)}
{/* Text Content Section (for text-type data sources) */}
{dataSource.data.type === 'text' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
Text Content
</h3>
</div>
{textLoading ? (
<div className="flex items-center justify-center gap-2 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
<Spinner size="sm" />
<p className="text-gray-600 dark:text-gray-300">Loading content...</p>
</div>
) : (
<div className="space-y-2">
<label className="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Text content
</label>
<InputField
type="text"
value={textContent}
onChange={handleUpdateTextContent}
multiline={true}
placeholder="Enter your text content here"
className="w-full"
disabled={savingText}
/>
{savingText && (
<div className="text-xs text-gray-500 dark:text-gray-400">
Saving...
</div>
)}
</div>
)}
</div>
)}
{/* Usage Information */}
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">Usage</h3>
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-md">
<div className="flex items-start gap-3">
<div className="w-5 h-5 text-blue-500 mt-0.5">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="text-sm text-blue-700 dark:text-blue-300">
<p className="font-medium mb-1">Using this data source</p>
<p>To use this data source in your agents, go to the RAG tab in individual agent settings and connect this data source.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</Panel>
);
}

View file

@ -6,7 +6,6 @@ import { createDataSource, addDocsToDataSource } from "../../../../actions/datas
import { FormStatusButton } from "../../../../lib/components/form-status-button"; import { FormStatusButton } from "../../../../lib/components/form-status-button";
import { DataSourceIcon } from "../../../../lib/components/datasource-icon"; import { DataSourceIcon } from "../../../../lib/components/datasource-icon";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { Dropdown } from "@/components/ui/dropdown"; import { Dropdown } from "@/components/ui/dropdown";
import { Panel } from "@/components/common/panel-common"; import { Panel } from "@/components/common/panel-common";
@ -15,14 +14,17 @@ export function Form({
useRagUploads, useRagUploads,
useRagS3Uploads, useRagS3Uploads,
useRagScraping, useRagScraping,
onSuccess,
hidePanel = false,
}: { }: {
projectId: string; projectId: string;
useRagUploads: boolean; useRagUploads: boolean;
useRagS3Uploads: boolean; useRagS3Uploads: boolean;
useRagScraping: boolean; useRagScraping: boolean;
onSuccess?: (sourceId: string) => void;
hidePanel?: boolean;
}) { }) {
const [sourceType, setSourceType] = useState(""); const [sourceType, setSourceType] = useState("");
const router = useRouter();
let dropdownOptions = [ let dropdownOptions = [
{ {
@ -79,7 +81,9 @@ export function Form({
}, },
})), })),
}); });
router.push(`/projects/${projectId}/sources/${source._id}`); if (onSuccess) {
onSuccess(source._id);
}
} }
async function createFilesDataSource(formData: FormData) { async function createFilesDataSource(formData: FormData) {
@ -92,7 +96,9 @@ export function Form({
}, },
}); });
router.push(`/projects/${projectId}/sources/${source._id}`); if (onSuccess) {
onSuccess(source._id);
}
} }
async function createTextDataSource(formData: FormData) { async function createTextDataSource(formData: FormData) {
@ -119,21 +125,14 @@ export function Form({
}], }],
}); });
router.push(`/projects/${projectId}/sources/${source._id}`); if (onSuccess) {
onSuccess(source._id);
}
} }
return ( const formContent = (
<Panel <div className={hidePanel ? "flex flex-col gap-4" : "h-full overflow-auto px-4 py-4"}>
title={ <div className={hidePanel ? "flex flex-col gap-4" : "max-w-[768px] mx-auto flex flex-col gap-4"}>
<div className="flex items-center gap-3">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
NEW DATA SOURCE
</div>
</div>
}
>
<div className="h-full overflow-auto px-4 py-4">
<div className="max-w-[768px] mx-auto flex flex-col gap-4">
<div className="p-4 bg-blue-50 dark:bg-blue-900/10 rounded-lg border border-blue-200 dark:border-blue-800"> <div className="p-4 bg-blue-50 dark:bg-blue-900/10 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<svg <svg
@ -345,8 +344,25 @@ export function Form({
}} }}
/> />
</form>} </form>}
</div>
</div> </div>
</div>
);
if (hidePanel) {
return formContent;
}
return (
<Panel
title={
<div className="flex items-center gap-3">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
NEW DATA SOURCE
</div>
</div>
}
>
{formContent}
</Panel> </Panel>
); );
} }

View file

@ -16,10 +16,16 @@ import { ModelsResponse } from "@/app/lib/types/billing_types";
export function App({ export function App({
projectId, projectId,
useRag, useRag,
useRagUploads,
useRagS3Uploads,
useRagScraping,
defaultModel, defaultModel,
}: { }: {
projectId: string; projectId: string;
useRag: boolean; useRag: boolean;
useRagUploads: boolean;
useRagS3Uploads: boolean;
useRagScraping: boolean;
defaultModel: string; defaultModel: string;
}) { }) {
const [mode, setMode] = useState<'draft' | 'live'>('draft'); const [mode, setMode] = useState<'draft' | 'live'>('draft');
@ -71,6 +77,33 @@ export function App({
setProjectMcpServers(projectConfig.mcpServers); setProjectMcpServers(projectConfig.mcpServers);
} }
}, [projectId]); }, [projectId]);
const handleDataSourcesUpdate = useCallback(async () => {
// Refresh data sources
const updatedDataSources = await listDataSources(projectId);
setDataSources(updatedDataSources);
}, [projectId]);
// Auto-update data sources when there are pending ones
useEffect(() => {
if (!dataSources) return;
const hasPendingSources = dataSources.some(ds => ds.status === 'pending');
if (!hasPendingSources) return;
const interval = setInterval(async () => {
const updatedDataSources = await listDataSources(projectId);
setDataSources(updatedDataSources);
// Stop polling if no more pending sources
const stillHasPending = updatedDataSources.some(ds => ds.status === 'pending');
if (!stillHasPending) {
clearInterval(interval);
}
}, 7000); // Poll every 7 seconds (reduced from 3)
return () => clearInterval(interval);
}, [dataSources, projectId]);
// Add this useEffect for initial load // Add this useEffect for initial load
useEffect(() => { useEffect(() => {
loadData(); loadData();
@ -101,12 +134,16 @@ export function App({
dataSources={dataSources} dataSources={dataSources}
projectConfig={projectConfig || project} projectConfig={projectConfig || project}
useRag={useRag} useRag={useRag}
useRagUploads={useRagUploads}
useRagS3Uploads={useRagS3Uploads}
useRagScraping={useRagScraping}
mcpServerUrls={projectMcpServers} mcpServerUrls={projectMcpServers}
defaultModel={defaultModel} defaultModel={defaultModel}
eligibleModels={eligibleModels} eligibleModels={eligibleModels}
onChangeMode={handleSetMode} onChangeMode={handleSetMode}
onRevertToLive={handleRevertToLive} onRevertToLive={handleRevertToLive}
onProjectToolsUpdated={handleProjectToolsUpdate} onProjectToolsUpdated={handleProjectToolsUpdate}
onDataSourcesUpdated={handleDataSourcesUpdate}
/>} />}
</> </>
} }

View file

@ -0,0 +1,117 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@heroui/react';
import { Button } from '@/components/ui/button';
import { Form } from '../../sources/new/form';
import { FilesSource } from '../../sources/components/files-source';
import { getDataSource } from '../../../../actions/datasource_actions';
import { WithStringId } from '../../../../lib/types/types';
import { DataSource } from '../../../../lib/types/datasource_types';
import { z } from 'zod';
interface DataSourcesModalProps {
isOpen: boolean;
onClose: () => void;
projectId: string;
onDataSourceAdded?: () => void;
useRagUploads: boolean;
useRagS3Uploads: boolean;
useRagScraping: boolean;
}
export function DataSourcesModal({
isOpen,
onClose,
projectId,
onDataSourceAdded,
useRagUploads,
useRagS3Uploads,
useRagScraping
}: DataSourcesModalProps) {
const [currentView, setCurrentView] = useState<'form' | 'upload'>('form');
const [createdSource, setCreatedSource] = useState<WithStringId<z.infer<typeof DataSource>> | null>(null);
const handleDataSourceCreated = async (sourceId: string) => {
// Get the created data source
const source = await getDataSource(projectId, sourceId);
// If it's a files data source, show the upload interface
if (source.data.type === 'files_local' || source.data.type === 'files_s3') {
setCreatedSource(source);
setCurrentView('upload');
} else {
// For other types (text, urls), close the modal
onDataSourceAdded?.();
onClose();
}
};
const handleFilesUploaded = () => {
// Just refresh the data sources list, don't close the modal
// User can continue uploading more files or close manually
onDataSourceAdded?.();
};
const handleModalClose = () => {
setCurrentView('form');
setCreatedSource(null);
onClose();
};
// Reset view when modal opens
useEffect(() => {
if (isOpen) {
setCurrentView('form');
setCreatedSource(null);
}
}, [isOpen]);
return (
<Modal
isOpen={isOpen}
onClose={handleModalClose}
size="5xl"
scrollBehavior="inside"
>
<ModalContent>
<ModalHeader>
<h3 className="text-lg font-semibold">
{currentView === 'form' ? 'Add data source' : 'Upload files'}
</h3>
</ModalHeader>
<ModalBody>
{currentView === 'form' ? (
<Form
projectId={projectId}
useRagUploads={useRagUploads}
useRagS3Uploads={useRagS3Uploads}
useRagScraping={useRagScraping}
onSuccess={handleDataSourceCreated}
hidePanel={true}
/>
) : (
createdSource && (
<FilesSource
projectId={projectId}
dataSource={createdSource}
handleReload={handleFilesUploaded}
type={createdSource.data.type as 'files_local' | 'files_s3'}
/>
)
)}
</ModalBody>
{currentView === 'upload' && (
<ModalFooter>
<Button
variant="primary"
onClick={handleModalClose}
>
Done
</Button>
</ModalFooter>
)}
</ModalContent>
</Modal>
);
}

View file

@ -1,10 +1,12 @@
import React from "react"; import React, { forwardRef, useImperativeHandle } from "react";
import { z } from "zod"; import { z } from "zod";
import { WorkflowPrompt, WorkflowAgent, WorkflowTool, Workflow } from "../../../lib/types/workflow_types"; import { WorkflowPrompt, WorkflowAgent, WorkflowTool, Workflow } from "../../../lib/types/workflow_types";
import { Project } from "../../../lib/types/project_types"; import { Project } from "../../../lib/types/project_types";
import { DataSource } from "../../../lib/types/datasource_types";
import { WithStringId } from "../../../lib/types/types";
import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react"; import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react";
import { useRef, useEffect, useState } from "react"; import { useRef, useEffect, useState } from "react";
import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, LinkIcon, UnlinkIcon, MoreVertical, Eye, Trash2, AlertTriangle, Circle } from "lucide-react"; import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, LinkIcon, UnlinkIcon, MoreVertical, Eye, Trash2, AlertTriangle, Circle, Database } from "lucide-react";
import { Tooltip } from "@heroui/react"; import { Tooltip } from "@heroui/react";
import { DndContext, DragEndEvent, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { DndContext, DragEndEvent, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
@ -17,6 +19,9 @@ import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/componen
import { ServerLogo } from '../tools/components/MCPServersCommon'; import { ServerLogo } from '../tools/components/MCPServersCommon';
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react";
import { ToolsModal } from './components/ToolsModal'; import { ToolsModal } from './components/ToolsModal';
import { DataSourcesModal } from './components/DataSourcesModal';
import { DataSourceIcon } from '../../../lib/components/datasource-icon';
import { deleteDataSource } from '../../../actions/datasource_actions';
import { ToolkitAuthModal } from '../tools/components/ToolkitAuthModal'; import { ToolkitAuthModal } from '../tools/components/ToolkitAuthModal';
import { deleteConnectedAccount } from '@/app/actions/composio_actions'; import { deleteConnectedAccount } from '@/app/actions/composio_actions';
import { ProjectWideChangeConfirmationModal } from '@/components/common/project-wide-change-confirmation-modal'; import { ProjectWideChangeConfirmationModal } from '@/components/common/project-wide-change-confirmation-modal';
@ -41,15 +46,17 @@ interface EntityListProps {
agents: z.infer<typeof WorkflowAgent>[]; agents: z.infer<typeof WorkflowAgent>[];
tools: z.infer<typeof WorkflowTool>[]; tools: z.infer<typeof WorkflowTool>[];
prompts: z.infer<typeof WorkflowPrompt>[]; prompts: z.infer<typeof WorkflowPrompt>[];
dataSources: WithStringId<z.infer<typeof DataSource>>[];
workflow: z.infer<typeof Workflow>; workflow: z.infer<typeof Workflow>;
selectedEntity: { selectedEntity: {
type: "agent" | "tool" | "prompt" | "visualise"; type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
name: string; name: string;
} | null; } | null;
startAgentName: string | null; startAgentName: string | null;
onSelectAgent: (name: string) => void; onSelectAgent: (name: string) => void;
onSelectTool: (name: string) => void; onSelectTool: (name: string) => void;
onSelectPrompt: (name: string) => void; onSelectPrompt: (name: string) => void;
onSelectDataSource?: (id: string) => void;
onAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void; onAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void;
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void; onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
onAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void; onAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
@ -60,7 +67,11 @@ interface EntityListProps {
onDeletePrompt: (name: string) => void; onDeletePrompt: (name: string) => void;
onShowVisualise: (name: string) => void; onShowVisualise: (name: string) => void;
onProjectToolsUpdated?: () => void; onProjectToolsUpdated?: () => void;
onDataSourcesUpdated?: () => void;
projectConfig?: z.infer<typeof Project>; projectConfig?: z.infer<typeof Project>;
useRagUploads: boolean;
useRagS3Uploads: boolean;
useRagScraping: boolean;
} }
interface EmptyStateProps { interface EmptyStateProps {
@ -163,7 +174,7 @@ interface ServerCardProps {
serverName: string; serverName: string;
tools: z.infer<typeof WorkflowTool>[]; tools: z.infer<typeof WorkflowTool>[];
selectedEntity: { selectedEntity: {
type: "agent" | "tool" | "prompt" | "visualise"; type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
name: string; name: string;
} | null; } | null;
onSelectTool: (name: string) => void; onSelectTool: (name: string) => void;
@ -247,16 +258,24 @@ type ComposioToolkit = {
tools: z.infer<typeof WorkflowTool>[]; tools: z.infer<typeof WorkflowTool>[];
} }
export function EntityList({ export const EntityList = forwardRef<
{ openDataSourcesModal: () => void },
EntityListProps & {
projectId: string,
onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void
}
>(function EntityList({
agents, agents,
tools, tools,
prompts, prompts,
dataSources,
workflow, workflow,
selectedEntity, selectedEntity,
startAgentName, startAgentName,
onSelectAgent, onSelectAgent,
onSelectTool, onSelectTool,
onSelectPrompt, onSelectPrompt,
onSelectDataSource,
onAddAgent, onAddAgent,
onAddTool, onAddTool,
onAddPrompt, onAddPrompt,
@ -266,16 +285,21 @@ export function EntityList({
onDeleteTool, onDeleteTool,
onDeletePrompt, onDeletePrompt,
onProjectToolsUpdated, onProjectToolsUpdated,
onDataSourcesUpdated,
projectId, projectId,
projectConfig, projectConfig,
onReorderAgents, onReorderAgents,
onShowVisualise, onShowVisualise,
useRagUploads,
useRagS3Uploads,
useRagScraping,
}: EntityListProps & { }: EntityListProps & {
projectId: string, projectId: string,
onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void
}) { }, ref) {
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false); const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
const [showToolsModal, setShowToolsModal] = useState(false); const [showToolsModal, setShowToolsModal] = useState(false);
const [showDataSourcesModal, setShowDataSourcesModal] = useState(false);
// State to track which toolkit's tools panel to open // State to track which toolkit's tools panel to open
const [selectedToolkitSlug, setSelectedToolkitSlug] = useState<string | null>(null); const [selectedToolkitSlug, setSelectedToolkitSlug] = useState<string | null>(null);
@ -308,18 +332,20 @@ export function EntityList({
const [expandedPanels, setExpandedPanels] = useState({ const [expandedPanels, setExpandedPanels] = useState({
agents: true, agents: true,
tools: true, tools: true,
data: true,
prompts: false prompts: false
}); });
// Default sizes when panels are expanded // Default sizes when panels are expanded
const DEFAULT_SIZES = { const DEFAULT_SIZES = {
agents: 40, agents: 30,
tools: 40, tools: 30,
data: 20,
prompts: 20 prompts: 20
}; };
// Calculate panel sizes based on expanded state // Calculate panel sizes based on expanded state
const getPanelSize = (panelName: 'agents' | 'tools' | 'prompts') => { const getPanelSize = (panelName: 'agents' | 'tools' | 'data' | 'prompts') => {
if (!expandedPanels[panelName]) { if (!expandedPanels[panelName]) {
return 8; // Collapsed height (53px equivalent) return 8; // Collapsed height (53px equivalent)
} }
@ -332,13 +358,23 @@ export function EntityList({
if (!expandedPanels.tools) { if (!expandedPanels.tools) {
size += DEFAULT_SIZES.tools; size += DEFAULT_SIZES.tools;
} }
if (!expandedPanels.data) {
size += DEFAULT_SIZES.data;
}
if (!expandedPanels.prompts) { if (!expandedPanels.prompts) {
size += DEFAULT_SIZES.prompts; size += DEFAULT_SIZES.prompts;
} }
} else if (panelName === 'tools') { } else if (panelName === 'tools') {
if (!expandedPanels.data && expandedPanels.agents) {
size += DEFAULT_SIZES.data;
}
if (!expandedPanels.prompts && expandedPanels.agents) { if (!expandedPanels.prompts && expandedPanels.agents) {
size += DEFAULT_SIZES.prompts; size += DEFAULT_SIZES.prompts;
} }
} else if (panelName === 'data') {
if (!expandedPanels.prompts && (expandedPanels.agents || expandedPanels.tools)) {
size += DEFAULT_SIZES.prompts;
}
} }
return size; return size;
@ -366,6 +402,10 @@ export function EntityList({
onSelectTool(name); onSelectTool(name);
} }
function handleSelectDataSource(id: string) {
onSelectDataSource?.(id);
}
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
@ -394,6 +434,12 @@ export function EntityList({
} }
}; };
useImperativeHandle(ref, () => ({
openDataSourcesModal: () => {
setShowDataSourcesModal(true);
}
}));
return ( return (
<div ref={containerRef} className="flex flex-col h-full min-h-0"> <div ref={containerRef} className="flex flex-col h-full min-h-0">
<ResizablePanelGroup <ResizablePanelGroup
@ -650,6 +696,155 @@ export function EntityList({
<ResizableHandle withHandle className="w-[3px] bg-transparent" /> <ResizableHandle withHandle className="w-[3px] bg-transparent" />
{/* Data Panel */}
<ResizablePanel
defaultSize={getPanelSize('data')}
minSize={expandedPanels.data ? 20 : 8}
maxSize={100}
className="flex flex-col min-h-0 h-full"
>
<Panel
variant="entity-list"
tourTarget="entity-data"
className={clsx(
"flex flex-col min-h-0 h-full overflow-hidden",
!expandedPanels.data && "h-[53px]!"
)}
title={
<div className={`${headerClasses} rounded-md transition-colors h-full`}>
<div className="flex items-center gap-2 h-full">
<button onClick={() => setExpandedPanels(prev => ({ ...prev, data: !prev.data }))}>
{expandedPanels.data ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
<Database className="w-4 h-4" />
<span>Data</span>
</div>
<div className="flex items-center gap-1">
<Button
variant="secondary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setExpandedPanels(prev => ({ ...prev, data: true }));
setShowDataSourcesModal(true);
}}
className={`group ${buttonClasses}`}
showHoverContent={true}
hoverContent="Add Data Source"
>
<PlusIcon className="w-4 h-4" />
</Button>
</div>
</div>
}
>
{expandedPanels.data && (
<div className="h-[calc(100%-53px)] overflow-y-auto">
<div className="p-2">
{dataSources.length > 0 ? (
<div className="space-y-1">
{dataSources.map((dataSource, index) => {
// Determine data source status
const isActive = dataSource.active && dataSource.status === 'ready';
const isPending = dataSource.status === 'pending';
const isError = dataSource.status === 'error';
let statusPill = null;
if (isPending) {
statusPill = (
<Tooltip content="Processing" size="sm" delay={500}>
<span className="flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-full border border-yellow-300 bg-yellow-50 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-200 dark:border-yellow-700">
<Circle className="w-2 h-2 animate-pulse" fill="currentColor" />
<span>Processing</span>
</span>
</Tooltip>
);
} else if (isError) {
statusPill = (
<Tooltip content={dataSource.error || "Error"} size="sm" delay={500}>
<span className="flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-full border border-red-300 bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-200 dark:border-red-700">
<Circle className="w-2 h-2" fill="currentColor" />
<span>Error</span>
</span>
</Tooltip>
);
} else if (isActive) {
statusPill = (
<Tooltip content="Active" size="sm" delay={500}>
<span className="flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-full border border-green-300 bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-200 dark:border-green-700">
<Circle className="w-2 h-2" fill="currentColor" />
<span>Active</span>
</span>
</Tooltip>
);
} else {
statusPill = (
<Tooltip content="Inactive" size="sm" delay={500}>
<span className="flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-full border border-gray-300 bg-gray-50 text-gray-700 dark:bg-gray-900 dark:text-gray-200 dark:border-gray-700">
<Circle className="w-2 h-2" fill="currentColor" />
<span>Inactive</span>
</span>
</Tooltip>
);
}
return (
<div key={`datasource-${index}`} className="group/datasource">
<div className={clsx(
"flex items-center gap-2 px-3 py-2 rounded-md min-h-[24px] cursor-pointer",
{
"bg-indigo-50 dark:bg-indigo-950/30": selectedEntity?.type === "datasource" && selectedEntity.name === dataSource._id,
"hover:bg-zinc-50 dark:hover:bg-zinc-800": !(selectedEntity?.type === "datasource" && selectedEntity.name === dataSource._id)
}
)}>
<button
ref={selectedEntity?.type === "datasource" && selectedEntity.name === dataSource._id ? selectedRef : undefined}
className="flex-1 flex items-center gap-2 text-sm text-left"
onClick={() => handleSelectDataSource(dataSource._id)}
>
<div className="shrink-0 flex items-center justify-center w-3 h-3">
<DataSourceIcon type={
dataSource.data.type === 'files_local' || dataSource.data.type === 'files_s3'
? 'files'
: dataSource.data.type
} />
</div>
<span className="text-xs flex-1">{dataSource.name}</span>
</button>
<div className="flex items-center gap-1">
{statusPill}
<div className="opacity-0 group-hover/datasource:opacity-100 transition-opacity">
<EntityDropdown
name={dataSource.name}
onDelete={async () => {
if (window.confirm(`Are you sure you want to delete the data source "${dataSource.name}"?`)) {
await deleteDataSource(projectId, dataSource._id);
onDataSourcesUpdated?.();
}
}}
/>
</div>
</div>
</div>
</div>
);
})}
</div>
) : (
<EmptyState entity="data sources" hasFilteredItems={false} />
)}
</div>
</div>
)}
</Panel>
</ResizablePanel>
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
{/* Prompts Panel */} {/* Prompts Panel */}
<ResizablePanel <ResizablePanel
defaultSize={getPanelSize('prompts')} defaultSize={getPanelSize('prompts')}
@ -742,9 +937,18 @@ export function EntityList({
onAddTool={onAddTool} onAddTool={onAddTool}
initialToolkitSlug={selectedToolkitSlug} initialToolkitSlug={selectedToolkitSlug}
/> />
<DataSourcesModal
isOpen={showDataSourcesModal}
onClose={() => setShowDataSourcesModal(false)}
projectId={projectId}
onDataSourceAdded={onDataSourcesUpdated}
useRagUploads={useRagUploads}
useRagS3Uploads={useRagS3Uploads}
useRagScraping={useRagScraping}
/>
</div> </div>
); );
} });
function AgentDropdown({ function AgentDropdown({
agent, agent,
@ -823,7 +1027,7 @@ function EntityDropdown({
interface ComposioCardProps { interface ComposioCardProps {
card: ComposioToolkit; card: ComposioToolkit;
selectedEntity: { selectedEntity: {
type: "agent" | "tool" | "prompt" | "visualise"; type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
name: string; name: string;
} | null; } | null;
onSelectTool: (name: string) => void; onSelectTool: (name: string) => void;

View file

@ -1,6 +1,6 @@
import { Metadata } from "next"; import { Metadata } from "next";
import { App } from "./app"; import { App } from "./app";
import { USE_RAG } from "@/app/lib/feature_flags"; import { USE_RAG, USE_RAG_UPLOADS, USE_RAG_S3_UPLOADS, USE_RAG_SCRAPING } from "@/app/lib/feature_flags";
import { projectsCollection } from "@/app/lib/mongodb"; import { projectsCollection } from "@/app/lib/mongodb";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { requireActiveBillingSubscription } from '@/app/lib/billing'; import { requireActiveBillingSubscription } from '@/app/lib/billing';
@ -38,6 +38,9 @@ export default async function Page(
<App <App
projectId={params.projectId} projectId={params.projectId}
useRag={USE_RAG} useRag={USE_RAG}
useRagUploads={USE_RAG_UPLOADS}
useRagS3Uploads={USE_RAG_S3_UPLOADS}
useRagScraping={USE_RAG_SCRAPING}
defaultModel={DEFAULT_MODEL} defaultModel={DEFAULT_MODEL}
/> />
); );

View file

@ -11,7 +11,7 @@ import { App as ChatApp } from "../playground/app";
import { z } from "zod"; import { z } from "zod";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"; import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
import { PromptConfig } from "../entities/prompt_config"; import { PromptConfig } from "../entities/prompt_config";
import { InputField } from "../../../lib/components/input-field"; import { DataSourceConfig } from "../entities/datasource_config";
import { RelativeTime } from "@primer/react"; import { RelativeTime } from "@primer/react";
import { USE_PRODUCT_TOUR } from "@/app/lib/feature_flags"; import { USE_PRODUCT_TOUR } from "@/app/lib/feature_flags";
@ -44,7 +44,7 @@ interface StateItem {
workflow: z.infer<typeof Workflow>; workflow: z.infer<typeof Workflow>;
publishing: boolean; publishing: boolean;
selection: { selection: {
type: "agent" | "tool" | "prompt" | "visualise"; type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
name: string; name: string;
} | null; } | null;
saving: boolean; saving: boolean;
@ -139,6 +139,11 @@ export type Action = {
} | { } | {
type: "reorder_agents"; type: "reorder_agents";
agents: z.infer<typeof WorkflowAgent>[]; agents: z.infer<typeof WorkflowAgent>[];
} | {
type: "select_datasource";
id: string;
} | {
type: "unselect_datasource";
} | { } | {
type: "show_visualise"; type: "show_visualise";
} | { } | {
@ -260,9 +265,16 @@ function reducer(state: State, action: Action): State {
name: action.name name: action.name
}; };
break; break;
case "select_datasource":
draft.selection = {
type: "datasource",
name: action.id
};
break;
case "unselect_agent": case "unselect_agent":
case "unselect_tool": case "unselect_tool":
case "unselect_prompt": case "unselect_prompt":
case "unselect_datasource":
draft.selection = null; draft.selection = null;
break; break;
case "add_agent": { case "add_agent": {
@ -575,6 +587,9 @@ export function WorkflowEditor({
dataSources, dataSources,
workflow, workflow,
useRag, useRag,
useRagUploads,
useRagS3Uploads,
useRagScraping,
mcpServerUrls, mcpServerUrls,
defaultModel, defaultModel,
projectConfig, projectConfig,
@ -583,11 +598,15 @@ export function WorkflowEditor({
onChangeMode, onChangeMode,
onRevertToLive, onRevertToLive,
onProjectToolsUpdated, onProjectToolsUpdated,
onDataSourcesUpdated,
}: { }: {
projectId: string; projectId: string;
dataSources: WithStringId<z.infer<typeof DataSource>>[]; dataSources: WithStringId<z.infer<typeof DataSource>>[];
workflow: z.infer<typeof Workflow>; workflow: z.infer<typeof Workflow>;
useRag: boolean; useRag: boolean;
useRagUploads: boolean;
useRagS3Uploads: boolean;
useRagScraping: boolean;
mcpServerUrls: Array<z.infer<typeof MCPServer>>; mcpServerUrls: Array<z.infer<typeof MCPServer>>;
defaultModel: string; defaultModel: string;
projectConfig: z.infer<typeof Project>; projectConfig: z.infer<typeof Project>;
@ -596,6 +615,7 @@ export function WorkflowEditor({
onChangeMode: (mode: 'draft' | 'live') => void; onChangeMode: (mode: 'draft' | 'live') => void;
onRevertToLive: () => void; onRevertToLive: () => void;
onProjectToolsUpdated?: () => void; onProjectToolsUpdated?: () => void;
onDataSourcesUpdated?: () => void;
}) { }) {
const [state, dispatch] = useReducer(reducer, { const [state, dispatch] = useReducer(reducer, {
@ -628,6 +648,7 @@ export function WorkflowEditor({
const [isInitialState, setIsInitialState] = useState(true); const [isInitialState, setIsInitialState] = useState(true);
const [showTour, setShowTour] = useState(true); const [showTour, setShowTour] = useState(true);
const copilotRef = useRef<{ handleUserMessage: (message: string) => void }>(null); const copilotRef = useRef<{ handleUserMessage: (message: string) => void }>(null);
const entityListRef = useRef<{ openDataSourcesModal: () => void } | null>(null);
// Modal state for revert confirmation // Modal state for revert confirmation
const { isOpen: isRevertModalOpen, onOpen: onRevertModalOpen, onClose: onRevertModalClose } = useDisclosure(); const { isOpen: isRevertModalOpen, onOpen: onRevertModalOpen, onClose: onRevertModalClose } = useDisclosure();
@ -662,6 +683,10 @@ export function WorkflowEditor({
}, 100); }, 100);
}, []); }, []);
const handleOpenDataSourcesModal = useCallback(() => {
entityListRef.current?.openDataSourcesModal();
}, []);
console.log(`workflow editor chat key: ${state.present.chatKey}`); console.log(`workflow editor chat key: ${state.present.chatKey}`);
// Auto-show copilot and increment key when prompt is present // Auto-show copilot and increment key when prompt is present
@ -698,6 +723,9 @@ export function WorkflowEditor({
function handleSelectPrompt(name: string) { function handleSelectPrompt(name: string) {
dispatch({ type: "select_prompt", name }); dispatch({ type: "select_prompt", name });
} }
function handleSelectDataSource(id: string) {
dispatch({ type: "select_datasource", id });
}
function handleUnselectAgent() { function handleUnselectAgent() {
dispatch({ type: "unselect_agent" }); dispatch({ type: "unselect_agent" });
@ -977,15 +1005,18 @@ export function WorkflowEditor({
<ResizablePanel minSize={10} defaultSize={PANEL_RATIOS.entityList}> <ResizablePanel minSize={10} defaultSize={PANEL_RATIOS.entityList}>
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<EntityList <EntityList
ref={entityListRef}
agents={state.present.workflow.agents} agents={state.present.workflow.agents}
tools={state.present.workflow.tools} tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts} prompts={state.present.workflow.prompts}
dataSources={dataSources}
workflow={state.present.workflow} workflow={state.present.workflow}
selectedEntity={ selectedEntity={
state.present.selection && state.present.selection &&
(state.present.selection.type === "agent" || (state.present.selection.type === "agent" ||
state.present.selection.type === "tool" || state.present.selection.type === "tool" ||
state.present.selection.type === "prompt") state.present.selection.type === "prompt" ||
state.present.selection.type === "datasource")
? state.present.selection ? state.present.selection
: null : null
} }
@ -993,6 +1024,7 @@ export function WorkflowEditor({
onSelectAgent={handleSelectAgent} onSelectAgent={handleSelectAgent}
onSelectTool={handleSelectTool} onSelectTool={handleSelectTool}
onSelectPrompt={handleSelectPrompt} onSelectPrompt={handleSelectPrompt}
onSelectDataSource={handleSelectDataSource}
onAddAgent={handleAddAgent} onAddAgent={handleAddAgent}
onAddTool={handleAddTool} onAddTool={handleAddTool}
onAddPrompt={handleAddPrompt} onAddPrompt={handleAddPrompt}
@ -1004,8 +1036,12 @@ export function WorkflowEditor({
onShowVisualise={handleShowVisualise} onShowVisualise={handleShowVisualise}
projectId={projectId} projectId={projectId}
onProjectToolsUpdated={onProjectToolsUpdated} onProjectToolsUpdated={onProjectToolsUpdated}
onDataSourcesUpdated={onDataSourcesUpdated}
projectConfig={projectConfig} projectConfig={projectConfig}
onReorderAgents={handleReorderAgents} onReorderAgents={handleReorderAgents}
useRagUploads={useRagUploads}
useRagS3Uploads={useRagS3Uploads}
useRagScraping={useRagScraping}
/> />
</div> </div>
</ResizablePanel> </ResizablePanel>
@ -1041,6 +1077,7 @@ export function WorkflowEditor({
useRag={useRag} useRag={useRag}
triggerCopilotChat={triggerCopilotChat} triggerCopilotChat={triggerCopilotChat}
eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels} eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels}
onOpenDataSourcesModal={handleOpenDataSourcesModal}
/>} />}
{state.present.selection?.type === "tool" && (() => { {state.present.selection?.type === "tool" && (() => {
const selectedTool = state.present.workflow.tools.find( const selectedTool = state.present.workflow.tools.find(
@ -1066,6 +1103,12 @@ export function WorkflowEditor({
handleUpdate={handleUpdatePrompt.bind(null, state.present.selection.name)} handleUpdate={handleUpdatePrompt.bind(null, state.present.selection.name)}
handleClose={handleUnselectPrompt} handleClose={handleUnselectPrompt}
/>} />}
{state.present.selection?.type === "datasource" && <DataSourceConfig
key={state.present.selection.name}
dataSourceId={state.present.selection.name}
handleClose={() => dispatch({ type: "unselect_datasource" })}
onDataSourceUpdate={onDataSourcesUpdated}
/>}
{state.present.selection?.type === "visualise" && ( {state.present.selection?.type === "visualise" && (
<Panel <Panel
title={ title={

View file

@ -1,4 +1,4 @@
import { USE_AUTH, USE_BILLING, USE_RAG } from "../lib/feature_flags"; import { USE_AUTH, USE_BILLING } from "../lib/feature_flags";
import AppLayout from './layout/components/app-layout'; import AppLayout from './layout/components/app-layout';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@ -9,7 +9,7 @@ export default function Layout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<AppLayout useRag={USE_RAG} useAuth={USE_AUTH} useBilling={USE_BILLING}> <AppLayout useAuth={USE_AUTH} useBilling={USE_BILLING}>
{children} {children}
</AppLayout> </AppLayout>
); );

View file

@ -8,12 +8,11 @@ import { useRouter } from 'next/navigation';
interface AppLayoutProps { interface AppLayoutProps {
children: ReactNode; children: ReactNode;
useRag?: boolean;
useAuth?: boolean; useAuth?: boolean;
useBilling?: boolean; useBilling?: boolean;
} }
export default function AppLayout({ children, useRag = false, useAuth = false, useBilling = false }: AppLayoutProps) { export default function AppLayout({ children, useAuth = false, useBilling = false }: AppLayoutProps) {
const router = useRouter(); const router = useRouter();
const [sidebarCollapsed, setSidebarCollapsed] = useState(true); const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const [billingPastDue, setBillingPastDue] = useState(false); const [billingPastDue, setBillingPastDue] = useState(false);
@ -46,7 +45,6 @@ export default function AppLayout({ children, useRag = false, useAuth = false, u
<div className="overflow-hidden rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm"> <div className="overflow-hidden rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm">
<Sidebar <Sidebar
projectId={projectId ?? undefined} projectId={projectId ?? undefined}
useRag={useRag}
useAuth={useAuth} useAuth={useAuth}
collapsed={sidebarCollapsed} collapsed={sidebarCollapsed}
onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)} onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}

View file

@ -5,7 +5,6 @@ import { usePathname } from "next/navigation";
import { Tooltip } from "@heroui/react"; import { Tooltip } from "@heroui/react";
import { UserButton } from "@/app/lib/components/user_button"; import { UserButton } from "@/app/lib/components/user_button";
import { import {
DatabaseIcon,
SettingsIcon, SettingsIcon,
WorkflowIcon, WorkflowIcon,
PlayIcon, PlayIcon,
@ -23,7 +22,6 @@ import { useHelpModal } from "@/app/providers/help-modal-provider";
interface SidebarProps { interface SidebarProps {
projectId?: string; projectId?: string;
useRag: boolean;
useAuth: boolean; useAuth: boolean;
collapsed?: boolean; collapsed?: boolean;
onToggleCollapse?: () => void; onToggleCollapse?: () => void;
@ -33,7 +31,7 @@ interface SidebarProps {
const EXPANDED_ICON_SIZE = 20; const EXPANDED_ICON_SIZE = 20;
const COLLAPSED_ICON_SIZE = 20; // DO NOT CHANGE THIS const COLLAPSED_ICON_SIZE = 20; // DO NOT CHANGE THIS
export default function Sidebar({ projectId, useRag, useAuth, collapsed = false, onToggleCollapse, useBilling }: SidebarProps) { export default function Sidebar({ projectId, useAuth, collapsed = false, onToggleCollapse, useBilling }: SidebarProps) {
const pathname = usePathname(); const pathname = usePathname();
const [projectName, setProjectName] = useState<string>("Select Project"); const [projectName, setProjectName] = useState<string>("Select Project");
const isProjectsRoute = pathname === '/projects'; const isProjectsRoute = pathname === '/projects';
@ -62,12 +60,6 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
icon: WorkflowIcon, icon: WorkflowIcon,
requiresProject: true requiresProject: true
}, },
...(useRag ? [{
href: 'sources',
label: 'RAG',
icon: DatabaseIcon,
requiresProject: true
}] : []),
{ {
href: 'config', href: 'config',
label: 'Settings', label: 'Settings',

View file

@ -1,7 +1,7 @@
'use client'; 'use client';
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { DatabaseIcon, SettingsIcon, WorkflowIcon, PlayIcon, LucideIcon } from "lucide-react"; import { SettingsIcon, WorkflowIcon, PlayIcon, LucideIcon } from "lucide-react";
import MenuItem from "./components/menu-item"; import MenuItem from "./components/menu-item";
interface NavLinkProps { interface NavLinkProps {
@ -29,11 +29,9 @@ function NavLink({ href, label, icon, collapsed, selected = false }: NavLinkProp
export default function Menu({ export default function Menu({
projectId, projectId,
collapsed, collapsed,
useRag,
}: { }: {
projectId: string; projectId: string;
collapsed: boolean; collapsed: boolean;
useRag: boolean;
}) { }) {
const pathname = usePathname(); const pathname = usePathname();
@ -53,15 +51,6 @@ export default function Menu({
icon={PlayIcon} icon={PlayIcon}
selected={pathname.startsWith(`/projects/${projectId}/test`)} selected={pathname.startsWith(`/projects/${projectId}/test`)}
/> />
{useRag && (
<NavLink
href={`/projects/${projectId}/sources`}
label="Knowledge"
collapsed={collapsed}
icon={DatabaseIcon}
selected={pathname.startsWith(`/projects/${projectId}/sources`)}
/>
)}
<NavLink <NavLink
href={`/projects/${projectId}/config`} href={`/projects/${projectId}/config`}
label="Settings" label="Settings"

View file

@ -9,10 +9,8 @@ import { FolderOpenIcon, PanelLeftCloseIcon, PanelLeftOpenIcon } from "lucide-re
export function Nav({ export function Nav({
projectId, projectId,
useRag,
}: { }: {
projectId: string; projectId: string;
useRag: boolean;
}) { }) {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [projectName, setProjectName] = useState<string | null>(null); const [projectName, setProjectName] = useState<string | null>(null);
@ -56,6 +54,6 @@ export function Nav({
<FolderOpenIcon size={16} className="ml-1" /> <FolderOpenIcon size={16} className="ml-1" />
</Link> </Link>
</Tooltip>} </Tooltip>}
<Menu projectId={projectId} collapsed={collapsed} useRag={useRag} /> <Menu projectId={projectId} collapsed={collapsed} />
</div>; </div>;
} }