mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-09 07:12:39 +02:00
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:
parent
6fd897e569
commit
aa95b590e2
16 changed files with 1387 additions and 71 deletions
|
|
@ -1,5 +1,4 @@
|
|||
'use server';
|
||||
import { redirect } from "next/navigation";
|
||||
import { ObjectId, WithId } from "mongodb";
|
||||
import { dataSourcesCollection, dataSourceDocsCollection } from "../lib/mongodb";
|
||||
import { z } from 'zod';
|
||||
|
|
@ -132,8 +131,6 @@ export async function deleteDataSource(projectId: string, sourceId: string) {
|
|||
version: 1,
|
||||
},
|
||||
});
|
||||
|
||||
redirect(`/projects/${projectId}/sources`);
|
||||
}
|
||||
|
||||
export async function toggleDataSource(projectId: string, sourceId: string, active: boolean) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default function Layout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<AppLayout useRag={false} useAuth={true} useBilling={true}>
|
||||
<AppLayout useAuth={true} useBilling={true}>
|
||||
{children}
|
||||
</AppLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default function Layout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<AppLayout useRag={false} useAuth={true} useBilling={true}>
|
||||
<AppLayout useAuth={true} useBilling={true}>
|
||||
{children}
|
||||
</AppLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { Info } from "lucide-react";
|
|||
import { useCopilot } from "../copilot/use-copilot";
|
||||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||||
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SectionCard } from "@/components/common/section-card";
|
||||
|
||||
// Common section header styles
|
||||
|
|
@ -49,6 +48,7 @@ export function AgentConfig({
|
|||
useRag,
|
||||
triggerCopilotChat,
|
||||
eligibleModels,
|
||||
onOpenDataSourcesModal,
|
||||
}: {
|
||||
projectId: string,
|
||||
workflow: z.infer<typeof Workflow>,
|
||||
|
|
@ -63,6 +63,7 @@ export function AgentConfig({
|
|||
useRag: boolean,
|
||||
triggerCopilotChat: (message: string) => void,
|
||||
eligibleModels: z.infer<typeof ModelsResponse.shape.agentModels> | "*",
|
||||
onOpenDataSourcesModal?: () => void,
|
||||
}) {
|
||||
const [isAdvancedConfigOpen, setIsAdvancedConfigOpen] = useState(false);
|
||||
const [showGenerateModal, setShowGenerateModal] = useState(false);
|
||||
|
|
@ -75,7 +76,6 @@ export function AgentConfig({
|
|||
const [showRagCta, setShowRagCta] = useState(false);
|
||||
const [previousRagSources, setPreviousRagSources] = useState<string[]>([]);
|
||||
const [billingError, setBillingError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [showSavedBanner, setShowSavedBanner] = useState(false);
|
||||
|
||||
const {
|
||||
|
|
@ -706,11 +706,11 @@ export function AgentConfig({
|
|||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
router.push(`/projects/${projectId}/sources`);
|
||||
onOpenDataSourcesModal?.();
|
||||
}}
|
||||
startContent={<DatabaseIcon className="w-3 h-3" />}
|
||||
>
|
||||
Go to RAG Sources
|
||||
Add Data Source
|
||||
</CustomButton>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import { createDataSource, addDocsToDataSource } from "../../../../actions/datas
|
|||
import { FormStatusButton } from "../../../../lib/components/form-status-button";
|
||||
import { DataSourceIcon } from "../../../../lib/components/datasource-icon";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Dropdown } from "@/components/ui/dropdown";
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
|
||||
|
|
@ -15,14 +14,17 @@ export function Form({
|
|||
useRagUploads,
|
||||
useRagS3Uploads,
|
||||
useRagScraping,
|
||||
onSuccess,
|
||||
hidePanel = false,
|
||||
}: {
|
||||
projectId: string;
|
||||
useRagUploads: boolean;
|
||||
useRagS3Uploads: boolean;
|
||||
useRagScraping: boolean;
|
||||
onSuccess?: (sourceId: string) => void;
|
||||
hidePanel?: boolean;
|
||||
}) {
|
||||
const [sourceType, setSourceType] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
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) {
|
||||
|
|
@ -92,7 +96,9 @@ export function Form({
|
|||
},
|
||||
});
|
||||
|
||||
router.push(`/projects/${projectId}/sources/${source._id}`);
|
||||
if (onSuccess) {
|
||||
onSuccess(source._id);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<div className="h-full overflow-auto px-4 py-4">
|
||||
<div className="max-w-[768px] mx-auto flex flex-col gap-4">
|
||||
const formContent = (
|
||||
<div className={hidePanel ? "flex flex-col gap-4" : "h-full overflow-auto px-4 py-4"}>
|
||||
<div className={hidePanel ? "flex flex-col gap-4" : "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="flex items-start gap-3">
|
||||
<svg
|
||||
|
|
@ -347,6 +346,23 @@ export function Form({
|
|||
</form>}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,10 +16,16 @@ import { ModelsResponse } from "@/app/lib/types/billing_types";
|
|||
export function App({
|
||||
projectId,
|
||||
useRag,
|
||||
useRagUploads,
|
||||
useRagS3Uploads,
|
||||
useRagScraping,
|
||||
defaultModel,
|
||||
}: {
|
||||
projectId: string;
|
||||
useRag: boolean;
|
||||
useRagUploads: boolean;
|
||||
useRagS3Uploads: boolean;
|
||||
useRagScraping: boolean;
|
||||
defaultModel: string;
|
||||
}) {
|
||||
const [mode, setMode] = useState<'draft' | 'live'>('draft');
|
||||
|
|
@ -71,6 +77,33 @@ export function App({
|
|||
setProjectMcpServers(projectConfig.mcpServers);
|
||||
}
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
|
|
@ -101,12 +134,16 @@ export function App({
|
|||
dataSources={dataSources}
|
||||
projectConfig={projectConfig || project}
|
||||
useRag={useRag}
|
||||
useRagUploads={useRagUploads}
|
||||
useRagS3Uploads={useRagS3Uploads}
|
||||
useRagScraping={useRagScraping}
|
||||
mcpServerUrls={projectMcpServers}
|
||||
defaultModel={defaultModel}
|
||||
eligibleModels={eligibleModels}
|
||||
onChangeMode={handleSetMode}
|
||||
onRevertToLive={handleRevertToLive}
|
||||
onProjectToolsUpdated={handleProjectToolsUpdate}
|
||||
onDataSourcesUpdated={handleDataSourcesUpdate}
|
||||
/>}
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import React from "react";
|
||||
import React, { forwardRef, useImperativeHandle } from "react";
|
||||
import { z } from "zod";
|
||||
import { WorkflowPrompt, WorkflowAgent, WorkflowTool, Workflow } from "../../../lib/types/workflow_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 { 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 { DndContext, DragEndEvent, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
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 { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react";
|
||||
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 { deleteConnectedAccount } from '@/app/actions/composio_actions';
|
||||
import { ProjectWideChangeConfirmationModal } from '@/components/common/project-wide-change-confirmation-modal';
|
||||
|
|
@ -41,15 +46,17 @@ interface EntityListProps {
|
|||
agents: z.infer<typeof WorkflowAgent>[];
|
||||
tools: z.infer<typeof WorkflowTool>[];
|
||||
prompts: z.infer<typeof WorkflowPrompt>[];
|
||||
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
selectedEntity: {
|
||||
type: "agent" | "tool" | "prompt" | "visualise";
|
||||
type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
|
||||
name: string;
|
||||
} | null;
|
||||
startAgentName: string | null;
|
||||
onSelectAgent: (name: string) => void;
|
||||
onSelectTool: (name: string) => void;
|
||||
onSelectPrompt: (name: string) => void;
|
||||
onSelectDataSource?: (id: string) => void;
|
||||
onAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void;
|
||||
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
|
||||
onAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||
|
|
@ -60,7 +67,11 @@ interface EntityListProps {
|
|||
onDeletePrompt: (name: string) => void;
|
||||
onShowVisualise: (name: string) => void;
|
||||
onProjectToolsUpdated?: () => void;
|
||||
onDataSourcesUpdated?: () => void;
|
||||
projectConfig?: z.infer<typeof Project>;
|
||||
useRagUploads: boolean;
|
||||
useRagS3Uploads: boolean;
|
||||
useRagScraping: boolean;
|
||||
}
|
||||
|
||||
interface EmptyStateProps {
|
||||
|
|
@ -163,7 +174,7 @@ interface ServerCardProps {
|
|||
serverName: string;
|
||||
tools: z.infer<typeof WorkflowTool>[];
|
||||
selectedEntity: {
|
||||
type: "agent" | "tool" | "prompt" | "visualise";
|
||||
type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
|
||||
name: string;
|
||||
} | null;
|
||||
onSelectTool: (name: string) => void;
|
||||
|
|
@ -247,16 +258,24 @@ type ComposioToolkit = {
|
|||
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,
|
||||
tools,
|
||||
prompts,
|
||||
dataSources,
|
||||
workflow,
|
||||
selectedEntity,
|
||||
startAgentName,
|
||||
onSelectAgent,
|
||||
onSelectTool,
|
||||
onSelectPrompt,
|
||||
onSelectDataSource,
|
||||
onAddAgent,
|
||||
onAddTool,
|
||||
onAddPrompt,
|
||||
|
|
@ -266,16 +285,21 @@ export function EntityList({
|
|||
onDeleteTool,
|
||||
onDeletePrompt,
|
||||
onProjectToolsUpdated,
|
||||
onDataSourcesUpdated,
|
||||
projectId,
|
||||
projectConfig,
|
||||
onReorderAgents,
|
||||
onShowVisualise,
|
||||
useRagUploads,
|
||||
useRagS3Uploads,
|
||||
useRagScraping,
|
||||
}: EntityListProps & {
|
||||
projectId: string,
|
||||
onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void
|
||||
}) {
|
||||
}, ref) {
|
||||
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
|
||||
const [showToolsModal, setShowToolsModal] = useState(false);
|
||||
const [showDataSourcesModal, setShowDataSourcesModal] = useState(false);
|
||||
// State to track which toolkit's tools panel to open
|
||||
const [selectedToolkitSlug, setSelectedToolkitSlug] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -308,18 +332,20 @@ export function EntityList({
|
|||
const [expandedPanels, setExpandedPanels] = useState({
|
||||
agents: true,
|
||||
tools: true,
|
||||
data: true,
|
||||
prompts: false
|
||||
});
|
||||
|
||||
// Default sizes when panels are expanded
|
||||
const DEFAULT_SIZES = {
|
||||
agents: 40,
|
||||
tools: 40,
|
||||
agents: 30,
|
||||
tools: 30,
|
||||
data: 20,
|
||||
prompts: 20
|
||||
};
|
||||
|
||||
// Calculate panel sizes based on expanded state
|
||||
const getPanelSize = (panelName: 'agents' | 'tools' | 'prompts') => {
|
||||
const getPanelSize = (panelName: 'agents' | 'tools' | 'data' | 'prompts') => {
|
||||
if (!expandedPanels[panelName]) {
|
||||
return 8; // Collapsed height (53px equivalent)
|
||||
}
|
||||
|
|
@ -332,13 +358,23 @@ export function EntityList({
|
|||
if (!expandedPanels.tools) {
|
||||
size += DEFAULT_SIZES.tools;
|
||||
}
|
||||
if (!expandedPanels.data) {
|
||||
size += DEFAULT_SIZES.data;
|
||||
}
|
||||
if (!expandedPanels.prompts) {
|
||||
size += DEFAULT_SIZES.prompts;
|
||||
}
|
||||
} else if (panelName === 'tools') {
|
||||
if (!expandedPanels.data && expandedPanels.agents) {
|
||||
size += DEFAULT_SIZES.data;
|
||||
}
|
||||
if (!expandedPanels.prompts && expandedPanels.agents) {
|
||||
size += DEFAULT_SIZES.prompts;
|
||||
}
|
||||
} else if (panelName === 'data') {
|
||||
if (!expandedPanels.prompts && (expandedPanels.agents || expandedPanels.tools)) {
|
||||
size += DEFAULT_SIZES.prompts;
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
|
|
@ -366,6 +402,10 @@ export function EntityList({
|
|||
onSelectTool(name);
|
||||
}
|
||||
|
||||
function handleSelectDataSource(id: string) {
|
||||
onSelectDataSource?.(id);
|
||||
}
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
|
|
@ -394,6 +434,12 @@ export function EntityList({
|
|||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openDataSourcesModal: () => {
|
||||
setShowDataSourcesModal(true);
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex flex-col h-full min-h-0">
|
||||
<ResizablePanelGroup
|
||||
|
|
@ -650,6 +696,155 @@ export function EntityList({
|
|||
|
||||
<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 */}
|
||||
<ResizablePanel
|
||||
defaultSize={getPanelSize('prompts')}
|
||||
|
|
@ -742,9 +937,18 @@ export function EntityList({
|
|||
onAddTool={onAddTool}
|
||||
initialToolkitSlug={selectedToolkitSlug}
|
||||
/>
|
||||
<DataSourcesModal
|
||||
isOpen={showDataSourcesModal}
|
||||
onClose={() => setShowDataSourcesModal(false)}
|
||||
projectId={projectId}
|
||||
onDataSourceAdded={onDataSourcesUpdated}
|
||||
useRagUploads={useRagUploads}
|
||||
useRagS3Uploads={useRagS3Uploads}
|
||||
useRagScraping={useRagScraping}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function AgentDropdown({
|
||||
agent,
|
||||
|
|
@ -823,7 +1027,7 @@ function EntityDropdown({
|
|||
interface ComposioCardProps {
|
||||
card: ComposioToolkit;
|
||||
selectedEntity: {
|
||||
type: "agent" | "tool" | "prompt" | "visualise";
|
||||
type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
|
||||
name: string;
|
||||
} | null;
|
||||
onSelectTool: (name: string) => void;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Metadata } from "next";
|
||||
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 { notFound } from "next/navigation";
|
||||
import { requireActiveBillingSubscription } from '@/app/lib/billing';
|
||||
|
|
@ -38,6 +38,9 @@ export default async function Page(
|
|||
<App
|
||||
projectId={params.projectId}
|
||||
useRag={USE_RAG}
|
||||
useRagUploads={USE_RAG_UPLOADS}
|
||||
useRagS3Uploads={USE_RAG_S3_UPLOADS}
|
||||
useRagScraping={USE_RAG_SCRAPING}
|
||||
defaultModel={DEFAULT_MODEL}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { App as ChatApp } from "../playground/app";
|
|||
import { z } from "zod";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
|
||||
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 { USE_PRODUCT_TOUR } from "@/app/lib/feature_flags";
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ interface StateItem {
|
|||
workflow: z.infer<typeof Workflow>;
|
||||
publishing: boolean;
|
||||
selection: {
|
||||
type: "agent" | "tool" | "prompt" | "visualise";
|
||||
type: "agent" | "tool" | "prompt" | "datasource" | "visualise";
|
||||
name: string;
|
||||
} | null;
|
||||
saving: boolean;
|
||||
|
|
@ -139,6 +139,11 @@ export type Action = {
|
|||
} | {
|
||||
type: "reorder_agents";
|
||||
agents: z.infer<typeof WorkflowAgent>[];
|
||||
} | {
|
||||
type: "select_datasource";
|
||||
id: string;
|
||||
} | {
|
||||
type: "unselect_datasource";
|
||||
} | {
|
||||
type: "show_visualise";
|
||||
} | {
|
||||
|
|
@ -260,9 +265,16 @@ function reducer(state: State, action: Action): State {
|
|||
name: action.name
|
||||
};
|
||||
break;
|
||||
case "select_datasource":
|
||||
draft.selection = {
|
||||
type: "datasource",
|
||||
name: action.id
|
||||
};
|
||||
break;
|
||||
case "unselect_agent":
|
||||
case "unselect_tool":
|
||||
case "unselect_prompt":
|
||||
case "unselect_datasource":
|
||||
draft.selection = null;
|
||||
break;
|
||||
case "add_agent": {
|
||||
|
|
@ -575,6 +587,9 @@ export function WorkflowEditor({
|
|||
dataSources,
|
||||
workflow,
|
||||
useRag,
|
||||
useRagUploads,
|
||||
useRagS3Uploads,
|
||||
useRagScraping,
|
||||
mcpServerUrls,
|
||||
defaultModel,
|
||||
projectConfig,
|
||||
|
|
@ -583,11 +598,15 @@ export function WorkflowEditor({
|
|||
onChangeMode,
|
||||
onRevertToLive,
|
||||
onProjectToolsUpdated,
|
||||
onDataSourcesUpdated,
|
||||
}: {
|
||||
projectId: string;
|
||||
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
useRag: boolean;
|
||||
useRagUploads: boolean;
|
||||
useRagS3Uploads: boolean;
|
||||
useRagScraping: boolean;
|
||||
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
||||
defaultModel: string;
|
||||
projectConfig: z.infer<typeof Project>;
|
||||
|
|
@ -596,6 +615,7 @@ export function WorkflowEditor({
|
|||
onChangeMode: (mode: 'draft' | 'live') => void;
|
||||
onRevertToLive: () => void;
|
||||
onProjectToolsUpdated?: () => void;
|
||||
onDataSourcesUpdated?: () => void;
|
||||
}) {
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
|
|
@ -628,6 +648,7 @@ export function WorkflowEditor({
|
|||
const [isInitialState, setIsInitialState] = useState(true);
|
||||
const [showTour, setShowTour] = useState(true);
|
||||
const copilotRef = useRef<{ handleUserMessage: (message: string) => void }>(null);
|
||||
const entityListRef = useRef<{ openDataSourcesModal: () => void } | null>(null);
|
||||
|
||||
// Modal state for revert confirmation
|
||||
const { isOpen: isRevertModalOpen, onOpen: onRevertModalOpen, onClose: onRevertModalClose } = useDisclosure();
|
||||
|
|
@ -662,6 +683,10 @@ export function WorkflowEditor({
|
|||
}, 100);
|
||||
}, []);
|
||||
|
||||
const handleOpenDataSourcesModal = useCallback(() => {
|
||||
entityListRef.current?.openDataSourcesModal();
|
||||
}, []);
|
||||
|
||||
console.log(`workflow editor chat key: ${state.present.chatKey}`);
|
||||
|
||||
// Auto-show copilot and increment key when prompt is present
|
||||
|
|
@ -698,6 +723,9 @@ export function WorkflowEditor({
|
|||
function handleSelectPrompt(name: string) {
|
||||
dispatch({ type: "select_prompt", name });
|
||||
}
|
||||
function handleSelectDataSource(id: string) {
|
||||
dispatch({ type: "select_datasource", id });
|
||||
}
|
||||
|
||||
function handleUnselectAgent() {
|
||||
dispatch({ type: "unselect_agent" });
|
||||
|
|
@ -977,15 +1005,18 @@ export function WorkflowEditor({
|
|||
<ResizablePanel minSize={10} defaultSize={PANEL_RATIOS.entityList}>
|
||||
<div className="flex flex-col h-full">
|
||||
<EntityList
|
||||
ref={entityListRef}
|
||||
agents={state.present.workflow.agents}
|
||||
tools={state.present.workflow.tools}
|
||||
prompts={state.present.workflow.prompts}
|
||||
dataSources={dataSources}
|
||||
workflow={state.present.workflow}
|
||||
selectedEntity={
|
||||
state.present.selection &&
|
||||
(state.present.selection.type === "agent" ||
|
||||
state.present.selection.type === "tool" ||
|
||||
state.present.selection.type === "prompt")
|
||||
state.present.selection.type === "prompt" ||
|
||||
state.present.selection.type === "datasource")
|
||||
? state.present.selection
|
||||
: null
|
||||
}
|
||||
|
|
@ -993,6 +1024,7 @@ export function WorkflowEditor({
|
|||
onSelectAgent={handleSelectAgent}
|
||||
onSelectTool={handleSelectTool}
|
||||
onSelectPrompt={handleSelectPrompt}
|
||||
onSelectDataSource={handleSelectDataSource}
|
||||
onAddAgent={handleAddAgent}
|
||||
onAddTool={handleAddTool}
|
||||
onAddPrompt={handleAddPrompt}
|
||||
|
|
@ -1004,8 +1036,12 @@ export function WorkflowEditor({
|
|||
onShowVisualise={handleShowVisualise}
|
||||
projectId={projectId}
|
||||
onProjectToolsUpdated={onProjectToolsUpdated}
|
||||
onDataSourcesUpdated={onDataSourcesUpdated}
|
||||
projectConfig={projectConfig}
|
||||
onReorderAgents={handleReorderAgents}
|
||||
useRagUploads={useRagUploads}
|
||||
useRagS3Uploads={useRagS3Uploads}
|
||||
useRagScraping={useRagScraping}
|
||||
/>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
|
@ -1041,6 +1077,7 @@ export function WorkflowEditor({
|
|||
useRag={useRag}
|
||||
triggerCopilotChat={triggerCopilotChat}
|
||||
eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels}
|
||||
onOpenDataSourcesModal={handleOpenDataSourcesModal}
|
||||
/>}
|
||||
{state.present.selection?.type === "tool" && (() => {
|
||||
const selectedTool = state.present.workflow.tools.find(
|
||||
|
|
@ -1066,6 +1103,12 @@ export function WorkflowEditor({
|
|||
handleUpdate={handleUpdatePrompt.bind(null, state.present.selection.name)}
|
||||
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" && (
|
||||
<Panel
|
||||
title={
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
|
@ -9,7 +9,7 @@ export default function Layout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<AppLayout useRag={USE_RAG} useAuth={USE_AUTH} useBilling={USE_BILLING}>
|
||||
<AppLayout useAuth={USE_AUTH} useBilling={USE_BILLING}>
|
||||
{children}
|
||||
</AppLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,12 +8,11 @@ import { useRouter } from 'next/navigation';
|
|||
|
||||
interface AppLayoutProps {
|
||||
children: ReactNode;
|
||||
useRag?: boolean;
|
||||
useAuth?: 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 [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
||||
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">
|
||||
<Sidebar
|
||||
projectId={projectId ?? undefined}
|
||||
useRag={useRag}
|
||||
useAuth={useAuth}
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { usePathname } from "next/navigation";
|
|||
import { Tooltip } from "@heroui/react";
|
||||
import { UserButton } from "@/app/lib/components/user_button";
|
||||
import {
|
||||
DatabaseIcon,
|
||||
SettingsIcon,
|
||||
WorkflowIcon,
|
||||
PlayIcon,
|
||||
|
|
@ -23,7 +22,6 @@ import { useHelpModal } from "@/app/providers/help-modal-provider";
|
|||
|
||||
interface SidebarProps {
|
||||
projectId?: string;
|
||||
useRag: boolean;
|
||||
useAuth: boolean;
|
||||
collapsed?: boolean;
|
||||
onToggleCollapse?: () => void;
|
||||
|
|
@ -33,7 +31,7 @@ interface SidebarProps {
|
|||
const EXPANDED_ICON_SIZE = 20;
|
||||
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 [projectName, setProjectName] = useState<string>("Select Project");
|
||||
const isProjectsRoute = pathname === '/projects';
|
||||
|
|
@ -62,12 +60,6 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
|
|||
icon: WorkflowIcon,
|
||||
requiresProject: true
|
||||
},
|
||||
...(useRag ? [{
|
||||
href: 'sources',
|
||||
label: 'RAG',
|
||||
icon: DatabaseIcon,
|
||||
requiresProject: true
|
||||
}] : []),
|
||||
{
|
||||
href: 'config',
|
||||
label: 'Settings',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
import { usePathname } from "next/navigation";
|
||||
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";
|
||||
|
||||
interface NavLinkProps {
|
||||
|
|
@ -29,11 +29,9 @@ function NavLink({ href, label, icon, collapsed, selected = false }: NavLinkProp
|
|||
export default function Menu({
|
||||
projectId,
|
||||
collapsed,
|
||||
useRag,
|
||||
}: {
|
||||
projectId: string;
|
||||
collapsed: boolean;
|
||||
useRag: boolean;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
|
|
@ -53,15 +51,6 @@ export default function Menu({
|
|||
icon={PlayIcon}
|
||||
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
|
||||
href={`/projects/${projectId}/config`}
|
||||
label="Settings"
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@ import { FolderOpenIcon, PanelLeftCloseIcon, PanelLeftOpenIcon } from "lucide-re
|
|||
|
||||
export function Nav({
|
||||
projectId,
|
||||
useRag,
|
||||
}: {
|
||||
projectId: string;
|
||||
useRag: boolean;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [projectName, setProjectName] = useState<string | null>(null);
|
||||
|
|
@ -56,6 +54,6 @@ export function Nav({
|
|||
<FolderOpenIcon size={16} className="ml-1" />
|
||||
</Link>
|
||||
</Tooltip>}
|
||||
<Menu projectId={projectId} collapsed={collapsed} useRag={useRag} />
|
||||
<Menu projectId={projectId} collapsed={collapsed} />
|
||||
</div>;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue