mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-08 14:52:38 +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';
|
'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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 { 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
|
||||||
|
|
@ -347,6 +346,23 @@ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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}
|
||||||
/>}
|
/>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { 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;
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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={
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue