refactor rag; add files support; use qdrant

This commit is contained in:
ramnique 2025-02-08 00:41:01 +05:30
parent b438e8f307
commit 7847c96977
43 changed files with 4556 additions and 2372 deletions

View file

@ -3,7 +3,7 @@
import { Metadata } from "next";
import { Spinner, Textarea, Button, Dropdown, DropdownMenu, DropdownItem, DropdownTrigger, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure, Divider } from "@nextui-org/react";
import { ReactNode, useEffect, useState, useCallback } from "react";
import { getProjectConfig, updateProjectName, updateWebhookUrl, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "@/app/actions";
import { getProjectConfig, updateProjectName, updateWebhookUrl, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "@/app/actions/project_actions";
import { CopyButton } from "@/app/lib/components/copy-button";
import { EditableField } from "@/app/lib/components/editable-field";
import { EyeIcon, EyeOffIcon, CopyIcon, MoreVerticalIcon, PlusIcon, EllipsisVerticalIcon } from "lucide-react";

View file

@ -4,7 +4,7 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import clsx from "clsx";
import Menu from "./menu";
import { getProjectConfig } from "@/app/actions";
import { getProjectConfig } from "@/app/actions/project_actions";
import { ChevronsLeftIcon, ChevronsRightIcon, FolderOpenIcon, PanelLeftCloseIcon, PanelLeftOpenIcon } from "lucide-react";
export function Nav({

View file

@ -1,5 +1,5 @@
'use client';
import { getAssistantResponse, simulateUserResponse } from "@/app/actions";
import { getAssistantResponse, simulateUserResponse } from "@/app/actions/actions";
import { useEffect, useState } from "react";
import { Messages } from "./messages";
import z from "zod";

View file

@ -3,7 +3,7 @@ import { Button, Spinner, Textarea } from "@nextui-org/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import z from "zod";
import { GetInformationToolResult, WebpageCrawlResponse, Workflow, WorkflowTool } from "@/app/lib/types";
import { executeClientTool, getInformationTool, scrapeWebpage, suggestToolResponse } from "@/app/actions";
import { executeClientTool, getInformationTool, scrapeWebpage, suggestToolResponse } from "@/app/actions/actions";
import MarkdownContent from "@/app/lib/components/markdown-content";
import Link from "next/link";
import { apiV1 } from "rowboat-shared";
@ -293,14 +293,15 @@ function GetInformationToolCall({
{typedResult && typedResult.results.length === 0 && <div>No matches found.</div>}
{typedResult && typedResult.results.length > 0 && <ul className="list-disc ml-6">
{typedResult.results.map((result, index) => {
return <li key={'' + index}>
<Link target="_blank" className="underline" href={result.url}>
{result.url}
</Link>
return <li key={'' + index} className="mb-2">
<ExpandableContent
label={result.title || result.name}
content={result.content}
expanded={false}
/>
</li>
})}
</ul>
}
</ul>}
</div>}
</div>
</div>

View file

@ -2,7 +2,7 @@
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Spinner, Textarea } from "@nextui-org/react";
import { useState, useEffect } from "react";
import { getScenarios, createScenario, updateScenario, deleteScenario } from "@/app/actions";
import { getScenarios, createScenario, updateScenario, deleteScenario } from "@/app/actions/scenario_actions";
import { Scenario, WithStringId } from "@/app/lib/types";
import { z } from "zod";
import { EditableField } from "@/app/lib/components/editable-field";

View file

@ -3,7 +3,7 @@ import { Input, Textarea } from "@nextui-org/react";
import { FormStatusButton } from "@/app/lib/components/FormStatusButton";
import { SimulationData } from "@/app/lib/types";
import { z } from "zod";
import { scrapeWebpage } from "@/app/actions";
import { scrapeWebpage } from "@/app/actions/actions";
import { ScenarioList } from "./scenario-list";
export function SimulateURLOption({

View file

@ -1,6 +1,6 @@
'use client';
import { deleteDataSource } from "@/app/actions";
import { deleteDataSource } from "@/app/actions/datasource_actions";
import { FormStatusButton } from "@/app/lib/components/FormStatusButton";
export function DeleteSource({

View file

@ -0,0 +1,290 @@
"use client";
import { PageSection } from "@/app/lib/components/PageSection";
import { DataSource, DataSourceDoc, WithStringId } from "@/app/lib/types";
import { z } from "zod";
import { useCallback, useEffect, useState } from "react";
import { useDropzone } from "react-dropzone";
import { deleteDocsFromDataSource, getUploadUrlsForFilesDataSource, addDocsToDataSource, getDownloadUrlForFile, listDocsInDataSource } from "@/app/actions/datasource_actions";
import { RelativeTime } from "@primer/react";
import { Pagination, Spinner } from "@nextui-org/react";
import { DownloadIcon } from "lucide-react";
function FileListItem({
projectId,
sourceId,
file,
onDelete,
}: {
projectId: string,
sourceId: string,
file: WithStringId<z.infer<typeof DataSourceDoc>>,
onDelete: (fileId: string) => Promise<void>;
}) {
const [isDeleting, setIsDeleting] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const handleDeleteClick = async () => {
setIsDeleting(true);
try {
await onDelete(file._id);
} finally {
setIsDeleting(false);
}
};
const handleDownloadClick = async () => {
setIsDownloading(true);
try {
const url = await getDownloadUrlForFile(projectId, sourceId, file._id);
window.open(url, '_blank');
} catch (error) {
console.error('Download failed:', error);
// TODO: Add error handling
} finally {
setIsDownloading(false);
}
};
if (file.data.type !== 'file') {
return null;
}
return (
<div className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div>
<div className="flex items-center gap-2">
<p className="font-medium">{file.name}</p>
<div className="shrink-0">
{isDownloading ? (
<Spinner size="sm" />
) : (
<button
onClick={handleDownloadClick}
className={`shrink-0 text-gray-500 hover:text-gray-700`}
>
<DownloadIcon className="w-4 h-4" />
</button>
)}
</div>
</div>
<p className="text-sm text-gray-500">
uploaded <RelativeTime date={new Date(file.createdAt)} /> - {formatFileSize(file.data.size)}
</p>
</div>
<div className="flex gap-2 items-center">
<button
onClick={handleDeleteClick}
disabled={isDeleting}
className={`${isDeleting ? 'text-gray-400' : 'text-red-600 hover:text-red-800'}`}
>
{isDeleting ? (
<Spinner size="sm" />
) : (
'Delete'
)}
</button>
</div>
</div>
);
}
function PaginatedFileList({
projectId,
sourceId,
handleReload,
onDelete,
}: {
projectId: string,
sourceId: string,
handleReload: () => void;
onDelete: (fileId: string) => Promise<void>;
}) {
const [files, setFiles] = useState<WithStringId<z.infer<typeof DataSourceDoc>>[]>([]);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const totalPages = Math.ceil(total / 10);
useEffect(() => {
let ignore = false;
async function fetchFiles() {
setLoading(true);
try {
const { files, total } = await listDocsInDataSource({
projectId,
sourceId,
page,
limit: 10,
});
if (!ignore) {
setFiles(files);
setTotal(total);
}
} catch (error) {
console.error('Error fetching files:', error);
} finally {
setLoading(false);
}
}
fetchFiles();
return () => {
ignore = true;
}
}, [projectId, sourceId, page]);
return (
<div className="mt-6">
<h3 className="text-lg font-semibold mb-3">Uploaded Files</h3>
{loading && <div className="flex items-center justify-center gap-2">
<Spinner size="sm" />
<p>Loading list...</p>
</div>}
{!loading && files.length === 0 && <div className="flex items-center justify-center gap-2">
<p>No files uploaded yet</p>
</div>}
{!loading && files.length > 0 && <div className="space-y-2">
{files.map(file => (
<FileListItem
key={file._id}
file={file}
projectId={projectId}
sourceId={sourceId}
onDelete={onDelete}
/>
))}
{totalPages > 1 && <Pagination
total={totalPages}
page={page}
onChange={setPage}
/>}
</div>}
</div>
)
}
export function FilesSource({
projectId,
dataSource,
handleReload,
}: {
projectId: string,
dataSource: WithStringId<z.infer<typeof DataSource>>,
handleReload: () => void;
}) {
const [uploading, setUploading] = useState(false);
const [fileListKey, setFileListKey] = useState(0);
const onDrop = useCallback(async (acceptedFiles: File[]) => {
setUploading(true);
try {
const urls = await getUploadUrlsForFilesDataSource(projectId, dataSource._id, 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].presignedUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});
}));
// After successful uploads, update the database with file information
await addDocsToDataSource({
projectId,
sourceId: dataSource._id,
docData: acceptedFiles.map((file, index) => ({
_id: urls[index].fileId,
name: file.name,
data: {
type: 'file',
name: file.name,
size: file.size,
mimeType: file.type,
s3Key: urls[index].s3Key,
},
})),
});
handleReload();
setFileListKey(prev => prev + 1);
} catch (error) {
console.error('Upload failed:', error);
// TODO: Add error handling
} finally {
setUploading(false);
}
}, [projectId, dataSource._id, handleReload]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
disabled: uploading,
accept: {
'application/pdf': ['.pdf'],
'text/plain': ['.txt'],
'application/msword': ['.doc'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
},
});
const handleDelete = async (docId: string) => {
await deleteDocsFromDataSource({
projectId,
sourceId: dataSource._id,
docIds: [docId],
});
handleReload();
setFileListKey(fileListKey + 1);
};
return (
<PageSection title="Upload files">
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
${isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}`}
>
<input {...getInputProps()} />
{uploading ? (
<div className="flex items-center justify-center gap-2">
<Spinner size="sm" />
<p>Uploading files...</p>
</div>
) : isDragActive ? (
<p>Drop the files here...</p>
) : (
<div>
<p>Drag and drop files here, or click to select files</p>
<p className="text-sm text-gray-500">
Supported file types: PDF, TXT, DOC, DOCX
</p>
</div>
)}
</div>
<PaginatedFileList
key={fileListKey}
projectId={projectId}
sourceId={dataSource._id}
handleReload={handleReload}
onDelete={handleDelete}
/>
</PageSection>
);
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

View file

@ -1,9 +1,4 @@
import { notFound } from "next/navigation";
import { dataSourcesCollection } from "@/app/lib/mongodb";
import { ObjectId } from "mongodb";
import { Metadata } from "next";
import { SourcePage } from "./source-page";
import { getDataSource } from "@/app/actions";
export default async function Page({
params,

View file

@ -0,0 +1,273 @@
"use client";
import { PageSection } from "@/app/lib/components/PageSection";
import { DataSource, DataSourceDoc, WithStringId } from "@/app/lib/types";
import { z } from "zod";
import { Recrawl } from "./web-recrawl";
import { deleteDocsFromDataSource, listDocsInDataSource, recrawlWebDataSource, addDocsToDataSource } from "@/app/actions/datasource_actions";
import { useState, useEffect } from "react";
import { Spinner } from "@nextui-org/react";
import { Pagination } from "@nextui-org/react";
import { ExternalLinkIcon } from "lucide-react";
import { Textarea } from "@nextui-org/react";
import { FormStatusButton } from "@/app/lib/components/FormStatusButton";
import { PlusIcon } from "lucide-react";
function UrlListItem({
file,
onDelete,
}: {
file: WithStringId<z.infer<typeof DataSourceDoc>>,
onDelete: (fileId: string) => Promise<void>;
}) {
const [isDeleting, setIsDeleting] = useState(false);
const handleDeleteClick = async () => {
setIsDeleting(true);
try {
await onDelete(file._id);
} finally {
setIsDeleting(false);
}
};
if (file.data.type !== 'url') {
return null;
}
return (
<div className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div>
<div className="flex items-center gap-2">
<p className="font-medium">{file.name}</p>
<div className="shrink-0">
<a href={file.data.url} target="_blank" rel="noopener noreferrer">
<ExternalLinkIcon className="w-4 h-4" />
</a>
</div>
</div>
</div>
<div className="flex gap-2 items-center">
<button
onClick={handleDeleteClick}
disabled={isDeleting}
className={`${isDeleting ? 'text-gray-400' : 'text-red-600 hover:text-red-800'}`}
>
{isDeleting ? (
<Spinner size="sm" />
) : (
'Delete'
)}
</button>
</div>
</div>
);
}
function UrlList({
projectId,
sourceId,
onDelete,
}: {
projectId: string,
sourceId: string,
onDelete: (fileId: string) => Promise<void>,
}) {
const [files, setFiles] = useState<WithStringId<z.infer<typeof DataSourceDoc>>[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const totalPages = Math.ceil(total / 10);
useEffect(() => {
let ignore = false;
async function fetchFiles() {
setLoading(true);
try {
const { files, total } = await listDocsInDataSource({ projectId, sourceId, page, limit: 10 });
if (!ignore) {
setFiles(files);
setTotal(total);
}
} catch (error) {
console.error('Error fetching files:', error);
} finally {
setLoading(false);
}
}
fetchFiles();
return () => {
ignore = true;
};
}, [projectId, sourceId, page]);
return (
<div className="mt-6">
<h3 className="text-lg font-semibold mb-3">URLs</h3>
{loading && <div className="flex items-center justify-center gap-2">
<Spinner size="sm" />
<p>Loading list...</p>
</div>}
{!loading && files.length === 0 && <div className="flex items-center justify-center gap-2">
<p>No files uploaded yet</p>
</div>}
{!loading && files.length > 0 && <div className="space-y-2">
{files.map(file => (
<UrlListItem
key={file._id}
file={file}
onDelete={onDelete}
/>
))}
{totalPages > 1 && <Pagination
total={totalPages}
page={page}
onChange={setPage}
/>}
</div>}
</div>
)
}
function AddUrls({
projectId,
sourceId,
onAdd,
}: {
projectId: string,
sourceId: string,
onAdd: () => void,
}) {
const [isAdding, setIsAdding] = useState(false);
const [showForm, setShowForm] = useState(false);
async function handleSubmit(formData: FormData) {
setIsAdding(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,
docData: first100Urls.map(url => ({
name: url,
data: {
type: 'url',
url,
},
})),
});
onAdd();
setShowForm(false); // Hide form after successful submission
} finally {
setIsAdding(false);
}
}
return (
<div>
{!showForm ? (
<FormStatusButton
props={{
onClick: () => setShowForm(true),
children: "Add more URLs",
className: "self-start",
startContent: <PlusIcon className="w-[24px] h-[24px]" />,
}}
/>
) : (
<div className="space-y-4">
<form action={handleSubmit} className="flex flex-col gap-4">
<Textarea
required
type="text"
name="urls"
label="Add more URLs (one per line)"
minRows={5}
maxRows={10}
labelPlacement="outside"
placeholder="https://example.com"
variant="bordered"
/>
<div className="flex gap-2">
<FormStatusButton
props={{
type: "submit",
children: "Add URLs",
className: "self-start",
startContent: <PlusIcon className="w-[24px] h-[24px]" />,
isLoading: isAdding,
}}
/>
<button
type="button"
onClick={() => setShowForm(false)}
className="text-gray-500 hover:text-gray-700"
>
Cancel
</button>
</div>
</form>
</div>
)}
</div>
);
}
export function ScrapeSource({
projectId,
dataSource,
handleReload,
}: {
projectId: string,
dataSource: WithStringId<z.infer<typeof DataSource>>,
handleReload: () => void;
}) {
const [fileListKey, setFileListKey] = useState(0);
async function handleRefresh() {
await recrawlWebDataSource(projectId, dataSource._id);
handleReload();
setFileListKey(prev => prev + 1);
}
async function handleDelete(docId: string) {
await deleteDocsFromDataSource({
projectId,
sourceId: dataSource._id,
docIds: [docId],
});
handleReload();
setFileListKey(prev => prev + 1);
}
return <>
<PageSection title="Add URLs">
<AddUrls
projectId={projectId}
sourceId={dataSource._id}
onAdd={() => handleReload()}
/>
</PageSection>
<PageSection title="Index details">
<UrlList
projectId={projectId}
sourceId={dataSource._id}
onDelete={handleDelete}
/>
</PageSection>
{(dataSource.status === 'ready' || dataSource.status === 'error') && <PageSection title="Refresh">
<div className="flex flex-col gap-2 items-start">
<p>Scrape the URLs again to fetch updated content:</p>
<Recrawl projectId={projectId} sourceId={dataSource._id} handleRefresh={handleRefresh} />
</div>
</PageSection>}
</>;
}

View file

@ -0,0 +1,13 @@
export function UrlList({ urls }: { urls: string }) {
return <pre className="max-w-[450px] border p-1 border-gray-300 rounded overflow-auto min-h-7 max-h-52 text-nowrap">
{urls}
</pre>;
}
export function TableLabel({ children, className }: { children: React.ReactNode, className?: string }) {
return <th className={`font-medium text-gray-800 text-left align-top pr-4 py-4 ${className}`}>{children}</th>;
}
export function TableValue({ children, className }: { children: React.ReactNode, className?: string }) {
return <td className={`align-top py-4 ${className}`}>{children}</td>;
}

View file

@ -1,29 +1,17 @@
'use client';
import { DataSource } from "@/app/lib/types";
import { DataSource, WithStringId } from "@/app/lib/types";
import { PageSection } from "@/app/lib/components/PageSection";
import { ToggleSource } from "../toggle-source";
import { Link, Spinner } from "@nextui-org/react";
import { Spinner } from "@nextui-org/react";
import { SourceStatus } from "../source-status";
import { DeleteSource } from "./delete";
import { Recrawl } from "./web-recrawl";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { getDataSource, recrawlWebDataSource } from "@/app/actions";
import { DataSourceIcon } from "@/app/lib/components/datasource-icon";
import { z } from "zod";
function UrlList({ urls }: { urls: string }) {
return <pre className="max-w-[450px] border p-1 border-gray-300 rounded overflow-auto min-h-7 max-h-52 text-nowrap">
{urls}
</pre>;
}
function TableLabel({ children, className }: { children: React.ReactNode, className?: string }) {
return <th className={`font-medium text-gray-800 text-left align-top pr-4 py-4 ${className}`}>{children}</th>;
}
function TableValue({ children, className }: { children: React.ReactNode, className?: string }) {
return <td className={`align-top py-4 ${className}`}>{children}</td>;
}
import { TableLabel, TableValue } from "./shared";
import { ScrapeSource } from "./scrape-source";
import { FilesSource } from "./files-source";
import { getDataSource } from "@/app/actions/datasource_actions";
export function SourcePage({
sourceId,
@ -32,16 +20,25 @@ export function SourcePage({
sourceId: string;
projectId: string;
}) {
const searchParams = useSearchParams();
const [source, setSource] = useState<z.infer<typeof DataSource> | null>(null);
const [source, setSource] = useState<WithStringId<z.infer<typeof DataSource>> | null>(null);
const [isLoading, setIsLoading] = useState(true);
// fetch source daat first time
async function handleReload() {
setIsLoading(true);
const updatedSource = await getDataSource(projectId, sourceId);
setSource(updatedSource);
setIsLoading(false);
}
// fetch source data first time
useEffect(() => {
let ignore = false;
async function fetchSource() {
setIsLoading(true);
const source = await getDataSource(projectId, sourceId);
if (!ignore) {
setSource(source);
setIsLoading(false);
}
}
fetchSource();
@ -59,7 +56,7 @@ export function SourcePage({
if (!source) {
return;
}
if (source.status !== 'processing' && source.status !== 'new') {
if (source.status !== 'pending') {
return;
}
@ -83,13 +80,9 @@ export function SourcePage({
};
}, [source, projectId, sourceId]);
async function handleRefresh() {
await recrawlWebDataSource(projectId, sourceId);
const updatedSource = await getDataSource(projectId, sourceId);
setSource(updatedSource);
}
if (!source) {
if (!source || isLoading) {
return <div className="flex items-center gap-2">
<Spinner size="sm" />
<div>Loading...</div>
@ -116,14 +109,14 @@ export function SourcePage({
<tr>
<TableLabel>Type:</TableLabel>
<TableValue>
{source.data.type === 'crawl' && <div className="flex gap-1 items-center">
<DataSourceIcon type="crawl" />
<div>Crawl URLs</div>
</div>}
{source.data.type === 'urls' && <div className="flex gap-1 items-center">
<DataSourceIcon type="urls" />
<div>Specify URLs</div>
</div>}
{source.data.type === 'files' && <div className="flex gap-1 items-center">
<DataSourceIcon type="files" />
<div>File upload</div>
</div>}
</TableValue>
</tr>
<tr>
@ -132,77 +125,12 @@ export function SourcePage({
<SourceStatus status={source.status} projectId={projectId} />
</TableValue>
</tr>
{source.data.type === 'urls' && source.data.missingUrls && <tr>
<TableLabel className="text-red-500">Errors:</TableLabel>
<TableValue>
<div>Some URLs could not be scraped. See the list below.</div>
</TableValue>
</tr>}
</tbody>
</table>
</PageSection>
{source.data.type === 'crawl' && <PageSection title="Crawl details">
<table className="table-auto">
<tbody>
<tr>
<TableLabel>Starting URL:</TableLabel>
<TableValue>
<Link
href={source.data.startUrl}
target="_blank"
showAnchorIcon
color="foreground"
underline="always"
>
{source.data.startUrl}
</Link>
</TableValue>
</tr>
<tr>
<TableLabel>Limit:</TableLabel>
<TableValue>
{source.data.limit} pages
</TableValue>
</tr>
{source.data.crawledUrls && <tr>
<TableLabel>Crawled URLs:</TableLabel>
<TableValue>
<UrlList urls={source.data.crawledUrls} />
</TableValue>
</tr>}
</tbody>
</table>
</PageSection>}
{source.data.type === 'urls' && <PageSection title="Index details">
<table className="table-auto">
<tbody>
<tr>
<TableLabel>Input URLs:</TableLabel>
<TableValue>
<UrlList urls={source.data.urls.join('\n')} />
</TableValue>
</tr>
{source.data.scrapedUrls && <tr>
<TableLabel>Scraped URLs:</TableLabel>
<TableValue>
<UrlList urls={source.data.scrapedUrls} />
</TableValue>
</tr>}
{source.data.missingUrls && <tr>
<TableLabel className="text-red-500">The following URLs could not be scraped:</TableLabel>
<TableValue>
<UrlList urls={source.data.missingUrls} />
</TableValue>
</tr>}
</tbody>
</table>
</PageSection>}
{(source.status === 'completed' || source.status === 'error') && (source.data.type === 'crawl' || source.data.type === 'urls') && <PageSection title="Refresh">
<div className="flex flex-col gap-2 items-start">
<p>{source.data.type === 'crawl' ? 'Crawl' : 'Scrape'} the URLs again to fetch updated content:</p>
<Recrawl projectId={projectId} sourceId={sourceId} handleRefresh={handleRefresh} />
</div>
</PageSection>}
{source.data.type === 'urls' && <ScrapeSource projectId={projectId} dataSource={source} handleReload={handleReload} />}
{source.data.type === 'files' && <FilesSource projectId={projectId} dataSource={source} handleReload={handleReload} />}
<PageSection title="Danger zone">
<div className="flex flex-col gap-2 items-start">
<p>Delete this data source:</p>

View file

@ -1,7 +1,6 @@
'use client';
import { recrawlWebDataSource } from "@/app/actions";
import { FormStatusButton } from "@/app/lib/components/FormStatusButton";
import { RefreshCwIcon } from "lucide-react";
export function Recrawl({
projectId,
@ -16,9 +15,7 @@ export function Recrawl({
<FormStatusButton
props={{
type: "submit",
startContent: <svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M17.651 7.65a7.131 7.131 0 0 0-12.68 3.15M18.001 4v4h-4m-7.652 8.35a7.13 7.13 0 0 0 12.68-3.15M6 20v-4h4" />
</svg>,
startContent: <RefreshCwIcon />,
children: "Refresh",
}}
/>

View file

@ -1,9 +1,11 @@
'use client';
import { Input, Select, SelectItem, Textarea } from "@nextui-org/react"
import { useState } from "react";
import { createCrawlDataSource, createUrlsDataSource } from "@/app/actions";
import { createDataSource, addDocsToDataSource } from "@/app/actions/datasource_actions";
import { FormStatusButton } from "@/app/lib/components/FormStatusButton";
import { DataSourceIcon } from "@/app/lib/components/datasource-icon";
import { PlusIcon } from "lucide-react";
import { useRouter } from "next/navigation";
export function Form({
projectId
@ -11,9 +13,62 @@ export function Form({
projectId: string;
}) {
const [sourceType, setSourceType] = useState("");
const router = useRouter();
// const createCrawlDataSourceWithProjectId = createCrawlDataSource.bind(null, projectId);
const createUrlsDataSourceWithProjectId = createUrlsDataSource.bind(null, projectId);
// async function createCrawlDataSource(formData: FormData) {
// const source = await createDataSource({
// projectId,
// name: formData.get('name') as string,
// data: {
// type: 'crawl',
// startUrl: formData.get('startUrl') as string,
// limit: parseInt(formData.get('limit') as string),
// },
// status: 'queued',
// });
// router.push(`/projects/${projectId}/sources/${source._id}`);
// }
async function createUrlsDataSource(formData: FormData) {
const source = await createDataSource({
projectId,
name: formData.get('name') as string,
data: {
type: 'urls',
},
status: 'pending',
});
const urls = formData.get('urls') as string;
const urlsArray = urls.split('\n').map(url => url.trim()).filter(url => url.length > 0);
// pick first 100
const first100Urls = urlsArray.slice(0, 100);
await addDocsToDataSource({
projectId,
sourceId: source._id,
docData: first100Urls.map(url => ({
name: url,
data: {
type: 'url',
url,
},
})),
});
router.push(`/projects/${projectId}/sources/${source._id}`);
}
async function createFilesDataSource(formData: FormData) {
const source = await createDataSource({
projectId,
name: formData.get('name') as string,
data: {
type: 'files',
},
status: 'ready',
});
router.push(`/projects/${projectId}/sources/${source._id}`);
}
function handleSourceTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
setSourceType(event.target.value);
@ -40,7 +95,14 @@ export function Form({
>
Scrape URLs
</SelectItem>
</Select>
<SelectItem
key="files"
value="files"
startContent={<DataSourceIcon type="files" />}
>
Upload files
</SelectItem>
</Select>
{/* {sourceType === "crawl" && <form
action={createCrawlDataSourceWithProjectId}
@ -99,7 +161,7 @@ export function Form({
</form>} */}
{sourceType === "urls" && <form
action={createUrlsDataSourceWithProjectId}
action={createUrlsDataSource}
className="flex flex-col gap-4"
>
<Textarea
@ -136,9 +198,35 @@ export function Form({
type: "submit",
children: "Add data source",
className: "self-start",
startContent: <svg className="w-[24px] h-[24px]" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M5 12h14m-7 7V5" />
</svg>,
startContent: <PlusIcon className="w-[24px] h-[24px]" />
}}
/>
</form>}
{sourceType === "files" && <form
action={createFilesDataSource}
className="flex flex-col gap-4"
>
<div className="self-start">
<Input
required
type="text"
name="name"
label="Name this data source"
labelPlacement="outside"
placeholder="e.g. Documentation files"
variant="bordered"
/>
</div>
<div className="text-sm">
<p>You will be able to upload files in the next step</p>
</div>
<FormStatusButton
props={{
type: "submit",
children: "Add data source",
className: "self-start",
startContent: <PlusIcon className="w-[24px] h-[24px]" />
}}
/>
</form>}

View file

@ -1,5 +1,5 @@
'use client';
import { getUpdatedSourceStatus } from "@/app/actions";
import { getDataSource } from "@/app/actions/datasource_actions";
import { DataSource } from "@/app/lib/types";
import { useEffect, useState } from "react";
import { z } from 'zod';
@ -19,33 +19,29 @@ export function SelfUpdatingSourceStatus({
const [status, setStatus] = useState(initialStatus);
useEffect(() => {
console.log("in effect i'm here")
let unmounted = false;
if (status !== 'processing' && status !== 'new') {
return;
let ignore = false;
let timeoutId: NodeJS.Timeout | null = null;
async function check() {
if (ignore) {
return;
}
const source = await getDataSource(projectId, sourceId);
setStatus(source.status);
timeoutId = setTimeout(check, 15 * 1000);
}
function check() {
if (unmounted) {
return;
}
if (status !== 'processing' && status !== 'new') {
return;
}
console.log("i'm here")
getUpdatedSourceStatus(projectId, sourceId)
.then((updatedStatus) => {
console.log("updatedStatus", updatedStatus)
setStatus(updatedStatus);
setTimeout(check, 15 * 1000);
});
if (status == 'pending') {
timeoutId = setTimeout(check, 15 * 1000);
}
setTimeout(check, 15 * 1000);
return () => {
unmounted = true;
ignore = true;
if (timeoutId) {
clearTimeout(timeoutId);
}
};
});
}, [status, projectId, sourceId]);
return <SourceStatus status={status} compact={compact} projectId={projectId} />;
}

View file

@ -24,7 +24,7 @@ export function SourceStatus({
There was an unexpected error while processing this resource.
</div>}
</div>}
{status == 'processing' && <div className="flex flex-col gap-1 items-start">
{status == 'pending' && <div className="flex flex-col gap-1 items-start">
<div className="flex gap-1 items-center">
<Spinner size="sm" />
<div className="text-gray-400">
@ -35,20 +35,7 @@ export function SourceStatus({
This source is being processed. This may take a few minutes.
</div>}
</div>}
{status == 'new' && <div className="flex flex-col gap-1 items-start">
<div className="flex gap-1 items-center">
<svg className="w-[24px] h-[24px]" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M12 8v4l3 3m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<div>
Queued
</div>
</div>
{!compact && <div className="text-sm text-gray-400">
This source is waiting to be processed.
</div>}
</div>}
{status === 'completed' && <div className="flex flex-col gap-1 items-start">
{status === 'ready' && <div className="flex flex-col gap-1 items-start">
<div className="flex gap-1 items-center">
<svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fillRule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm13.707-1.293a1 1 0 0 0-1.414-1.414L11 12.586l-1.793-1.793a1 1 0 0 0-1.414 1.414l2.5 2.5a1 1 0 0 0 1.414 0l4-4Z" clipRule="evenodd" />

View file

@ -7,7 +7,7 @@ import { DataSourceIcon } from "@/app/lib/components/datasource-icon";
import { useEffect, useState } from "react";
import { DataSource, WithStringId } from "@/app/lib/types";
import { z } from "zod";
import { listSources } from "@/app/actions";
import { listDataSources } from "@/app/actions/datasource_actions";
export function SourcesList({
projectId,
@ -22,7 +22,7 @@ export function SourcesList({
async function fetchSources() {
setLoading(true);
const sources = await listSources(projectId);
const sources = await listDataSources(projectId);
if (!ignore) {
setSources(sources);
setLoading(false);
@ -81,13 +81,9 @@ export function SourcesList({
</Link>
</td>
<td className="py-4">
{source.data.type == 'crawl' && <div className="flex gap-1 items-center">
<DataSourceIcon type="crawl" />
<div>Crawl URLs</div>
</div>}
{source.data.type == 'urls' && <div className="flex gap-1 items-center">
<DataSourceIcon type="urls" />
<div>Specify URLs</div>
<div>List URLs</div>
</div>}
</td>
<td className="py-4">

View file

@ -1,5 +1,5 @@
'use client';
import { toggleDataSource } from "@/app/actions";
import { toggleDataSource } from "@/app/actions/datasource_actions";
import { Spinner } from "@nextui-org/react";
import { Switch } from "@nextui-org/react";
import { useState } from "react";
@ -39,6 +39,6 @@ export function ToggleSource({
</Switch>
{loading && <Spinner size="sm" />}
</div>
{!compact && !isActive && <p className="text-sm text-red-800">This data source will not be used in chats.</p>}
{!compact && !isActive && <p className="text-sm text-red-800">This data source will not be used for RAG.</p>}
</div>;
}

View file

@ -5,7 +5,8 @@ import { useCallback, useEffect, useState } from "react";
import { WorkflowEditor } from "./workflow_editor";
import { WorkflowSelector } from "./workflow_selector";
import { Spinner } from "@nextui-org/react";
import { cloneWorkflow, createWorkflow, fetchPublishedWorkflowId, fetchWorkflow, listSources } from "@/app/actions";
import { cloneWorkflow, createWorkflow, fetchPublishedWorkflowId, fetchWorkflow } from "@/app/actions/workflow_actions";
import { listDataSources } from "@/app/actions/datasource_actions";
export function App({
projectId,
@ -23,7 +24,7 @@ export function App({
setLoading(true);
const workflow = await fetchWorkflow(projectId, workflowId);
const publishedWorkflowId = await fetchPublishedWorkflowId(projectId);
const dataSources = await listSources(projectId);
const dataSources = await listDataSources(projectId);
// Store the selected workflow ID in local storage
localStorage.setItem(`lastWorkflowId_${projectId}`, workflowId);
setWorkflow(workflow);
@ -43,7 +44,7 @@ export function App({
setLoading(true);
const workflow = await createWorkflow(projectId);
const publishedWorkflowId = await fetchPublishedWorkflowId(projectId);
const dataSources = await listSources(projectId);
const dataSources = await listDataSources(projectId);
// Store the selected workflow ID in local storage
localStorage.setItem(`lastWorkflowId_${projectId}`, workflow._id);
setWorkflow(workflow);
@ -56,7 +57,7 @@ export function App({
setLoading(true);
const workflow = await cloneWorkflow(projectId, workflowId);
const publishedWorkflowId = await fetchPublishedWorkflowId(projectId);
const dataSources = await listSources(projectId);
const dataSources = await listDataSources(projectId);
// Store the selected workflow ID in local storage
localStorage.setItem(`lastWorkflowId_${projectId}`, workflow._id);
setWorkflow(workflow);

View file

@ -4,7 +4,7 @@ import { ActionButton, Pane } from "./pane";
import { useEffect, useRef, useState, createContext, useContext, useCallback } from "react";
import { CopilotAssistantMessage, CopilotMessage, CopilotUserMessage, Workflow, CopilotChatContext, CopilotAssistantMessageActionPart } from "@/app/lib/types";
import { z } from "zod";
import { getCopilotResponse } from "@/app/actions";
import { getCopilotResponse } from "@/app/actions/actions";
import { Action } from "./copilot_actions";
import clsx from "clsx";
import { Action as WorkflowDispatch } from "./workflow_editor";

View file

@ -18,7 +18,7 @@ import {
} from "@/components/ui/resizable"
import { Copilot } from "./copilot";
import { apiV1 } from "rowboat-shared";
import { publishWorkflow, renameWorkflow, saveWorkflow } from "@/app/actions";
import { publishWorkflow, renameWorkflow, saveWorkflow } from "@/app/actions/workflow_actions";
import { PublishedBadge } from "./published_badge";
import { BackIcon, HamburgerIcon, WorkflowIcon } from "@/app/lib/components/icons";
import { CopyIcon, Layers2Icon, RadioIcon, RedoIcon, UndoIcon } from "lucide-react";

View file

@ -4,7 +4,7 @@ import { z } from "zod";
import { useEffect, useState, useCallback } from "react";
import { PublishedBadge } from "./published_badge";
import { RelativeTime } from "@primer/react";
import { listWorkflows } from "@/app/actions";
import { listWorkflows } from "@/app/actions/workflow_actions";
import { Button, Divider, Pagination } from "@nextui-org/react";
import { WorkflowIcon } from "@/app/lib/components/icons";
import { PlusIcon } from "lucide-react";