mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-16 08:25:18 +02:00
feat: knowledge base functionality for the voice agent (#120)
* feat: upload file and store embedding * feat: add documents in nodes * feat: add openai embedding service
This commit is contained in:
parent
e2fa4bbb98
commit
ef5b9e40a9
52 changed files with 4551 additions and 114 deletions
249
ui/src/app/files/DocumentList.tsx
Normal file
249
ui/src/app/files/DocumentList.tsx
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
'use client';
|
||||
|
||||
import { FileText, RefreshCw, Search, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
deleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDelete,
|
||||
listDocumentsApiV1KnowledgeBaseDocumentsGet,
|
||||
} from '@/client/sdk.gen';
|
||||
import type { DocumentResponseSchema } from '@/client/types.gen';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import logger from '@/lib/logger';
|
||||
|
||||
interface DocumentListProps {
|
||||
accessToken: string;
|
||||
refreshTrigger: number;
|
||||
}
|
||||
|
||||
export default function DocumentList({ accessToken, refreshTrigger }: DocumentListProps) {
|
||||
const [documents, setDocuments] = useState<DocumentResponseSchema[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchDocuments = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await listDocumentsApiV1KnowledgeBaseDocumentsGet({
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
query: {
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error || !response.data) {
|
||||
throw new Error('Failed to fetch documents');
|
||||
}
|
||||
|
||||
setDocuments(response.data.documents);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch documents');
|
||||
logger.error('Error fetching documents:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
// Fetch documents on mount and when refreshTrigger changes
|
||||
useEffect(() => {
|
||||
fetchDocuments();
|
||||
}, [fetchDocuments, refreshTrigger]);
|
||||
|
||||
// Poll for documents that are processing
|
||||
useEffect(() => {
|
||||
const processingDocs = documents.filter(
|
||||
(doc) => doc.processing_status === 'processing' || doc.processing_status === 'pending'
|
||||
);
|
||||
|
||||
if (processingDocs.length === 0) return;
|
||||
|
||||
const pollInterval = setInterval(() => {
|
||||
logger.info(`Polling for ${processingDocs.length} processing documents...`);
|
||||
fetchDocuments();
|
||||
}, 5000); // Poll every 5 seconds
|
||||
|
||||
return () => clearInterval(pollInterval);
|
||||
}, [documents, fetchDocuments]);
|
||||
|
||||
const handleDelete = async (documentUuid: string, filename: string) => {
|
||||
if (!confirm(`Are you sure you want to delete "${filename}"?`)) return;
|
||||
|
||||
try {
|
||||
const response = await deleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDelete({
|
||||
path: {
|
||||
document_uuid: documentUuid,
|
||||
},
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error('Failed to delete document');
|
||||
}
|
||||
|
||||
toast.success(`Deleted "${filename}"`);
|
||||
fetchDocuments();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to delete document');
|
||||
logger.error('Error deleting document:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <Badge className="bg-green-500">Completed</Badge>;
|
||||
case 'processing':
|
||||
return (
|
||||
<Badge variant="secondary" className="animate-pulse">
|
||||
Processing
|
||||
</Badge>
|
||||
);
|
||||
case 'pending':
|
||||
return <Badge variant="outline">Pending</Badge>;
|
||||
case 'failed':
|
||||
return <Badge variant="destructive">Failed</Badge>;
|
||||
default:
|
||||
return <Badge variant="outline">{status}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
||||
};
|
||||
|
||||
const filteredDocuments = documents.filter((doc) =>
|
||||
doc.filename.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
if (isLoading && documents.length === 0) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="space-y-2 flex-1">
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<Skeleton className="h-3 w-64" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-24" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Search and Refresh */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search documents..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={fetchDocuments}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Document List */}
|
||||
{filteredDocuments.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<FileText className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">
|
||||
{searchQuery
|
||||
? 'No documents match your search'
|
||||
: 'No documents uploaded yet'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{filteredDocuments.map((doc) => (
|
||||
<div
|
||||
key={doc.document_uuid}
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium truncate">{doc.filename}</span>
|
||||
{getStatusBadge(doc.processing_status)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>{formatFileSize(doc.file_size_bytes)}</span>
|
||||
{doc.processing_status === 'completed' && (
|
||||
<span>{doc.total_chunks} chunks</span>
|
||||
)}
|
||||
<span>{formatDate(doc.created_at)}</span>
|
||||
</div>
|
||||
{doc.processing_error && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
Error: {doc.processing_error}
|
||||
</p>
|
||||
)}
|
||||
{doc.docling_metadata &&
|
||||
typeof doc.docling_metadata === 'object' &&
|
||||
'duplicate_of' in doc.docling_metadata && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Duplicate of another document
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(doc.document_uuid, doc.filename)}
|
||||
className="text-destructive hover:text-destructive/90"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
220
ui/src/app/files/DocumentUpload.tsx
Normal file
220
ui/src/app/files/DocumentUpload.tsx
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
'use client';
|
||||
|
||||
import { Upload } from 'lucide-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
getUploadUrlApiV1KnowledgeBaseUploadUrlPost,
|
||||
processDocumentApiV1KnowledgeBaseProcessDocumentPost,
|
||||
} from '@/client/sdk.gen';
|
||||
import type { DocumentUploadResponseSchema } from '@/client/types.gen';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import logger from '@/lib/logger';
|
||||
|
||||
interface DocumentUploadProps {
|
||||
accessToken: string;
|
||||
onUploadSuccess: () => void;
|
||||
}
|
||||
|
||||
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
|
||||
const ACCEPTED_FILE_TYPES = ['.pdf', '.docx', '.doc', '.txt'];
|
||||
|
||||
export default function DocumentUpload({ accessToken, onUploadSuccess }: DocumentUploadProps) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const validateFile = (file: File): boolean => {
|
||||
// Validate file type
|
||||
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
if (!ACCEPTED_FILE_TYPES.includes(fileExtension)) {
|
||||
toast.error(`Please select a supported file type: ${ACCEPTED_FILE_TYPES.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
toast.error('File size must be less than 100MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
if (!validateFile(file)) return;
|
||||
|
||||
setUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
try {
|
||||
// Step 1: Request presigned upload URL
|
||||
logger.info('Requesting presigned upload URL for:', file.name);
|
||||
const uploadUrlResponse = await getUploadUrlApiV1KnowledgeBaseUploadUrlPost({
|
||||
body: {
|
||||
filename: file.name,
|
||||
mime_type: file.type || 'application/octet-stream',
|
||||
custom_metadata: {
|
||||
original_filename: file.name,
|
||||
uploaded_at: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (uploadUrlResponse.error || !uploadUrlResponse.data) {
|
||||
throw new Error('Failed to get upload URL');
|
||||
}
|
||||
|
||||
const uploadData: DocumentUploadResponseSchema = uploadUrlResponse.data;
|
||||
logger.info('Received presigned URL, uploading file...');
|
||||
|
||||
setUploadProgress(25);
|
||||
|
||||
// Step 2: Upload file directly to S3/MinIO using PUT
|
||||
const uploadResponse = await fetch(uploadData.upload_url, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers: {
|
||||
'Content-Type': file.type || 'application/octet-stream',
|
||||
},
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error('Failed to upload file to storage');
|
||||
}
|
||||
|
||||
setUploadProgress(75);
|
||||
logger.info('File uploaded successfully, triggering processing...');
|
||||
|
||||
// Step 3: Trigger document processing
|
||||
const processResponse = await processDocumentApiV1KnowledgeBaseProcessDocumentPost({
|
||||
body: {
|
||||
document_uuid: uploadData.document_uuid,
|
||||
s3_key: uploadData.s3_key,
|
||||
},
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (processResponse.error) {
|
||||
throw new Error('Failed to trigger processing');
|
||||
}
|
||||
|
||||
setUploadProgress(100);
|
||||
logger.info('Document processing triggered successfully');
|
||||
|
||||
toast.success(`File uploaded: ${file.name}. Processing started.`);
|
||||
onUploadSuccess();
|
||||
} catch (error) {
|
||||
logger.error('Error uploading document:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to upload document');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
setUploadProgress(0);
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
await uploadFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrag = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
setDragActive(true);
|
||||
} else if (e.type === 'dragleave') {
|
||||
setDragActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = async (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) {
|
||||
await uploadFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={ACCEPTED_FILE_TYPES.join(',')}
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
disabled={uploading}
|
||||
/>
|
||||
|
||||
{/* Drag and Drop Area */}
|
||||
<div
|
||||
className={`
|
||||
border-2 border-dashed rounded-lg p-8 text-center transition-colors
|
||||
${dragActive ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
|
||||
${uploading ? 'opacity-50 pointer-events-none' : 'cursor-pointer hover:border-primary hover:bg-muted/50'}
|
||||
`}
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<Upload className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
|
||||
<p className="text-lg font-medium mb-2">
|
||||
{uploading ? 'Uploading...' : 'Drop your document here'}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
or click to browse
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Supported formats: {ACCEPTED_FILE_TYPES.join(', ')} (Max 100MB)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Upload Progress */}
|
||||
{uploading && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Uploading...</span>
|
||||
<span>{uploadProgress}%</span>
|
||||
</div>
|
||||
<Progress value={uploadProgress} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manual Upload Button */}
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleButtonClick}
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading ? 'Uploading...' : 'Choose File'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
104
ui/src/app/files/page.tsx
Normal file
104
ui/src/app/files/page.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
import DocumentList from "./DocumentList";
|
||||
import DocumentUpload from "./DocumentUpload";
|
||||
|
||||
export default function FilesPage() {
|
||||
const { user, getAccessToken, redirectToLogin, loading } = useAuth();
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [accessToken, setAccessToken] = useState<string>('');
|
||||
|
||||
// Redirect if not authenticated
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
redirectToLogin();
|
||||
}
|
||||
}, [loading, user, redirectToLogin]);
|
||||
|
||||
// Get access token
|
||||
const fetchAccessToken = useCallback(async () => {
|
||||
if (user) {
|
||||
const token = await getAccessToken();
|
||||
setAccessToken(token);
|
||||
}
|
||||
}, [user, getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccessToken();
|
||||
}, [fetchAccessToken]);
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
// Trigger refresh of document list
|
||||
setRefreshKey(prev => prev + 1);
|
||||
};
|
||||
|
||||
if (loading || !user || !accessToken) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-12 w-64" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Knowledge Base Files</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Upload and manage documents for your voice agents to reference.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="all" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">All Files</TabsTrigger>
|
||||
<TabsTrigger value="upload">Upload New</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="all" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Your Documents</CardTitle>
|
||||
<CardDescription>
|
||||
View and manage your uploaded documents
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DocumentList
|
||||
accessToken={accessToken}
|
||||
refreshTrigger={refreshKey}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="upload" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Upload Document</CardTitle>
|
||||
<CardDescription>
|
||||
Upload a PDF or document file to add to your knowledge base
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DocumentUpload
|
||||
accessToken={accessToken}
|
||||
onUploadSuccess={handleUploadSuccess}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,8 +7,10 @@ import {
|
|||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { BrushCleaning, Maximize2, Minus, Plus, Rocket, Settings, Variable } from 'lucide-react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { listDocumentsApiV1KnowledgeBaseDocumentsGet, listToolsApiV1ToolsGet } from '@/client';
|
||||
import type { DocumentResponseSchema, ToolResponse } from '@/client/types.gen';
|
||||
import { FlowEdge, FlowNode, NodeType } from "@/components/flow/types";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
|
@ -63,6 +65,8 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
|
|||
const [isConfigurationsDialogOpen, setIsConfigurationsDialogOpen] = useState(false);
|
||||
const [isEmbedDialogOpen, setIsEmbedDialogOpen] = useState(false);
|
||||
const [isPhoneCallDialogOpen, setIsPhoneCallDialogOpen] = useState(false);
|
||||
const [documents, setDocuments] = useState<DocumentResponseSchema[] | undefined>(undefined);
|
||||
const [tools, setTools] = useState<ToolResponse[] | undefined>(undefined);
|
||||
|
||||
const {
|
||||
rfInstance,
|
||||
|
|
@ -95,6 +99,36 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
|
|||
getAccessToken
|
||||
});
|
||||
|
||||
// Fetch documents and tools once for the entire workflow
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
|
||||
// Fetch documents
|
||||
const documentsResponse = await listDocumentsApiV1KnowledgeBaseDocumentsGet({
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
query: { limit: 100 },
|
||||
});
|
||||
if (documentsResponse.data) {
|
||||
setDocuments(documentsResponse.data.documents);
|
||||
}
|
||||
|
||||
// Fetch tools
|
||||
const toolsResponse = await listToolsApiV1ToolsGet({
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (toolsResponse.data) {
|
||||
setTools(toolsResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch documents and tools:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [getAccessToken]);
|
||||
|
||||
// Memoize defaultEdgeOptions to prevent unnecessary re-renders
|
||||
const defaultEdgeOptions = useMemo(() => ({
|
||||
animated: true,
|
||||
|
|
@ -102,7 +136,11 @@ function RenderWorkflow({ initialWorkflowName, workflowId, initialFlow, initialT
|
|||
}), []);
|
||||
|
||||
// Memoize the context value to prevent unnecessary re-renders
|
||||
const workflowContextValue = useMemo(() => ({ saveWorkflow }), [saveWorkflow]);
|
||||
const workflowContextValue = useMemo(() => ({
|
||||
saveWorkflow,
|
||||
documents,
|
||||
tools
|
||||
}), [saveWorkflow, documents, tools]);
|
||||
|
||||
return (
|
||||
<WorkflowProvider value={workflowContextValue}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
|
||||
import type { DocumentResponseSchema, ToolResponse } from '@/client/types.gen';
|
||||
|
||||
interface WorkflowContextType {
|
||||
saveWorkflow: (updateWorkflowDefinition?: boolean) => Promise<void>;
|
||||
documents?: DocumentResponseSchema[];
|
||||
tools?: ToolResponse[];
|
||||
}
|
||||
|
||||
const WorkflowContext = createContext<WorkflowContextType | undefined>(undefined);
|
||||
|
|
@ -15,3 +19,8 @@ export const useWorkflow = () => {
|
|||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Optional hook that doesn't throw if context is not available
|
||||
export const useWorkflowOptional = () => {
|
||||
return useContext(WorkflowContext);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue