From 50cca993c83d367e3a2840608a4f6060cf357711 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Mon, 16 Jun 2025 18:39:40 -0700 Subject: [PATCH 01/19] refactor: consolidate create modal file --- .../components/create-form/create-form.tsx | 625 ----------------- .../components/create-modal/create-modal.tsx | 628 +++++++++++++++++- 2 files changed, 621 insertions(+), 632 deletions(-) delete mode 100644 apps/sim/app/w/knowledge/components/create-modal/components/create-form/create-form.tsx diff --git a/apps/sim/app/w/knowledge/components/create-modal/components/create-form/create-form.tsx b/apps/sim/app/w/knowledge/components/create-modal/components/create-form/create-form.tsx deleted file mode 100644 index 7f53d905ea3..00000000000 --- a/apps/sim/app/w/knowledge/components/create-modal/components/create-form/create-form.tsx +++ /dev/null @@ -1,625 +0,0 @@ -'use client' - -import { useEffect, useRef, useState } from 'react' -import { zodResolver } from '@hookform/resolvers/zod' -import { AlertCircle, CheckCircle2, X } from 'lucide-react' -import { useForm } from 'react-hook-form' -import { z } from 'zod' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Textarea } from '@/components/ui/textarea' -import { createLogger } from '@/lib/logs/console-logger' -import { getDocumentIcon } from '@/app/w/knowledge/components/icons/document-icons' -import type { DocumentData, KnowledgeBaseData } from '@/stores/knowledge/store' -import { useKnowledgeStore } from '@/stores/knowledge/store' - -const logger = createLogger('CreateForm') - -const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB -const ACCEPTED_FILE_TYPES = [ - 'application/pdf', - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'text/plain', - 'text/csv', - 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', -] - -interface ProcessedDocumentResponse { - documentId: string - filename: string - status: string -} - -interface FileWithPreview extends File { - preview: string -} - -interface CreateFormProps { - onClose: () => void - onKnowledgeBaseCreated?: (knowledgeBase: KnowledgeBaseData) => void -} - -const FormSchema = z.object({ - name: z - .string() - .min(1, 'Name is required') - .max(100, 'Name must be less than 100 characters') - .refine((value) => value.trim().length > 0, 'Name cannot be empty'), - description: z.string().max(500, 'Description must be less than 500 characters').optional(), -}) - -type FormValues = z.infer - -interface SubmitStatus { - type: 'success' | 'error' - message: string -} - -export function CreateForm({ onClose, onKnowledgeBaseCreated }: CreateFormProps) { - const fileInputRef = useRef(null) - const [isSubmitting, setIsSubmitting] = useState(false) - const [submitStatus, setSubmitStatus] = useState(null) - const [files, setFiles] = useState([]) - const [fileError, setFileError] = useState(null) - const [isDragging, setIsDragging] = useState(false) - const [dragCounter, setDragCounter] = useState(0) // Track drag events to handle nested elements - const scrollContainerRef = useRef(null) - const dropZoneRef = useRef(null) - - // Cleanup file preview URLs when component unmounts to prevent memory leaks - useEffect(() => { - return () => { - files.forEach((file) => { - if (file.preview) { - URL.revokeObjectURL(file.preview) - } - }) - } - }, [files]) - - const { - register, - handleSubmit, - reset, - formState: { errors }, - } = useForm({ - resolver: zodResolver(FormSchema), - defaultValues: { - name: '', - description: '', - }, - mode: 'onChange', - }) - - const processFiles = async (fileList: FileList | File[]) => { - setFileError(null) - - if (!fileList || fileList.length === 0) return - - try { - const newFiles: FileWithPreview[] = [] - let hasError = false - - for (const file of Array.from(fileList)) { - // Check file size - if (file.size > MAX_FILE_SIZE) { - setFileError(`File ${file.name} is too large. Maximum size is 100MB per file.`) - hasError = true - continue - } - - // Check file type - if (!ACCEPTED_FILE_TYPES.includes(file.type)) { - setFileError( - `File ${file.name} has an unsupported format. Please use PDF, DOC, DOCX, TXT, CSV, XLS, or XLSX.` - ) - hasError = true - continue - } - - // Create file with preview (using file icon since these aren't images) - const fileWithPreview = Object.assign(file, { - preview: URL.createObjectURL(file), - }) as FileWithPreview - - newFiles.push(fileWithPreview) - } - - if (!hasError && newFiles.length > 0) { - setFiles((prev) => [...prev, ...newFiles]) - } - } catch (error) { - logger.error('Error processing files:', error) - setFileError('An error occurred while processing files. Please try again.') - } finally { - // Reset the input - if (fileInputRef.current) { - fileInputRef.current.value = '' - } - } - } - - const handleFileChange = async (e: React.ChangeEvent) => { - if (e.target.files) { - await processFiles(e.target.files) - } - } - - // Handle drag events - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter((prev) => { - const newCount = prev + 1 - if (newCount === 1) { - setIsDragging(true) - } - return newCount - }) - } - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragCounter((prev) => { - const newCount = prev - 1 - if (newCount === 0) { - setIsDragging(false) - } - return newCount - }) - } - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - // Add visual feedback for valid drop zone - e.dataTransfer.dropEffect = 'copy' - } - - const handleDrop = async (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragging(false) - setDragCounter(0) - - if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - await processFiles(e.dataTransfer.files) - } - } - - const removeFile = (index: number) => { - setFiles((prev) => { - // Revoke the URL to avoid memory leaks - URL.revokeObjectURL(prev[index].preview) - return prev.filter((_, i) => i !== index) - }) - } - - const getFileIcon = (mimeType: string, filename: string) => { - const IconComponent = getDocumentIcon(mimeType, filename) - return - } - - 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 `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}` - } - - const onSubmit = async (data: FormValues) => { - setIsSubmitting(true) - setSubmitStatus(null) - - try { - // First create the knowledge base - const knowledgeBasePayload = { - name: data.name, - description: data.description || undefined, - } - - const response = await fetch('/api/knowledge', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(knowledgeBasePayload), - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Failed to create knowledge base') - } - - const result = await response.json() - - if (!result.success) { - throw new Error(result.error || 'Failed to create knowledge base') - } - - const newKnowledgeBase = result.data - - // If files are uploaded, upload them and start processing - if (files.length > 0) { - // First, upload all files to get their URLs - interface UploadedFile { - filename: string - fileUrl: string - fileSize: number - mimeType: string - fileHash: string | undefined - } - - const uploadedFiles: UploadedFile[] = [] - - for (const file of files) { - try { - const presignedResponse = await fetch('/api/files/presigned', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - fileName: file.name, - contentType: file.type, - fileSize: file.size, - }), - }) - - const presignedData = await presignedResponse.json() - - if (presignedResponse.ok && presignedData.directUploadSupported) { - const uploadHeaders: Record = { - 'Content-Type': file.type, - } - - // Add Azure-specific headers if provided - if (presignedData.uploadHeaders) { - Object.assign(uploadHeaders, presignedData.uploadHeaders) - } - - const uploadResponse = await fetch(presignedData.presignedUrl, { - method: 'PUT', - headers: uploadHeaders, // Use the merged headers - body: file, - }) - - if (!uploadResponse.ok) { - throw new Error( - `Direct upload failed: ${uploadResponse.status} ${uploadResponse.statusText}` - ) - } - - uploadedFiles.push({ - filename: file.name, - fileUrl: presignedData.fileInfo.path.startsWith('http') - ? presignedData.fileInfo.path - : `${window.location.origin}${presignedData.fileInfo.path}`, - fileSize: file.size, - mimeType: file.type, - fileHash: undefined, - }) - } else { - const formData = new FormData() - formData.append('file', file) - - const uploadResponse = await fetch('/api/files/upload', { - method: 'POST', - body: formData, - }) - - if (!uploadResponse.ok) { - const errorData = await uploadResponse.json() - throw new Error( - `Failed to upload ${file.name}: ${errorData.error || 'Unknown error'}` - ) - } - - const uploadResult = await uploadResponse.json() - uploadedFiles.push({ - filename: file.name, - fileUrl: uploadResult.path.startsWith('http') - ? uploadResult.path - : `${window.location.origin}${uploadResult.path}`, - fileSize: file.size, - mimeType: file.type, - fileHash: undefined, - }) - } - } catch (error) { - throw new Error( - `Failed to upload ${file.name}: ${error instanceof Error ? error.message : 'Unknown error'}` - ) - } - } - - // Start async document processing - const processResponse = await fetch( - `/api/knowledge/${newKnowledgeBase.id}/process-documents`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - documents: uploadedFiles, - processingOptions: { - chunkSize: 1024, - minCharactersPerChunk: 24, - recipe: 'default', - lang: 'en', - }, - }), - } - ) - - if (!processResponse.ok) { - throw new Error('Failed to start document processing') - } - - const processResult = await processResponse.json() - - // Create pending document objects and add them to the store immediately - if (processResult.success && processResult.data.documentsCreated) { - const pendingDocuments: DocumentData[] = processResult.data.documentsCreated.map( - (doc: ProcessedDocumentResponse, index: number) => ({ - id: doc.documentId, - knowledgeBaseId: newKnowledgeBase.id, - filename: doc.filename, - fileUrl: uploadedFiles[index].fileUrl, - fileSize: uploadedFiles[index].fileSize, - mimeType: uploadedFiles[index].mimeType, - fileHash: uploadedFiles[index].fileHash || null, - chunkCount: 0, - tokenCount: 0, - characterCount: 0, - processingStatus: 'pending' as const, - processingStartedAt: null, - processingCompletedAt: null, - processingError: null, - enabled: true, - uploadedAt: new Date().toISOString(), - }) - ) - - // Add pending documents to store for immediate UI update - useKnowledgeStore.getState().addPendingDocuments(newKnowledgeBase.id, pendingDocuments) - } - - // Update the knowledge base object with the correct document count - newKnowledgeBase.docCount = uploadedFiles.length - - logger.info(`Started processing ${uploadedFiles.length} documents in the background`) - } - - setSubmitStatus({ - type: 'success', - message: 'Your knowledge base has been created successfully!', - }) - reset() - - // Clean up file previews - files.forEach((file) => URL.revokeObjectURL(file.preview)) - setFiles([]) - - // Call the callback if provided - if (onKnowledgeBaseCreated) { - onKnowledgeBaseCreated(newKnowledgeBase) - } - - // Close modal after a short delay to show success message - setTimeout(() => { - onClose() - }, 1500) - } catch (error) { - logger.error('Error creating knowledge base:', error) - setSubmitStatus({ - type: 'error', - message: error instanceof Error ? error.message : 'An unknown error occurred', - }) - } finally { - setIsSubmitting(false) - } - } - - return ( -
- {/* Scrollable Content */} -
-
- {submitStatus && submitStatus.type === 'success' ? ( - -
-
- -
-
- - Success - - - {submitStatus.message} - -
-
-
- ) : submitStatus && submitStatus.type === 'error' ? ( - - - Error - {submitStatus.message} - - ) : null} - -
-
- - - {errors.name &&

{errors.name.message}

} -
- -
- -