mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
refactor rag; add files support; use qdrant
This commit is contained in:
parent
b438e8f307
commit
7847c96977
43 changed files with 4556 additions and 2372 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
</>;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue