From 61ce2ff11b3431fb2213393036b7e276e0753c6d Mon Sep 17 00:00:00 2001 From: Vukasin Date: Wed, 24 Sep 2025 19:40:04 +0200 Subject: [PATCH] feat: add direct entry --- .../knowledge-base/knowledge-base-page.tsx | 5 + .../knowledge-base/text-entry-modal.tsx | 303 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 frontend/src/components/knowledge-base/text-entry-modal.tsx diff --git a/frontend/src/components/knowledge-base/knowledge-base-page.tsx b/frontend/src/components/knowledge-base/knowledge-base-page.tsx index 10741389..b784a747 100644 --- a/frontend/src/components/knowledge-base/knowledge-base-page.tsx +++ b/frontend/src/components/knowledge-base/knowledge-base-page.tsx @@ -51,6 +51,7 @@ import { } from '@/components/ui/dropdown-menu'; import { SharedTreeItem, FileDragOverlay } from '@/components/knowledge-base/shared-kb-tree'; import { KBFilePreviewModal } from './kb-file-preview-modal'; +import { TextEntryModal } from './text-entry-modal'; // Get backend URL from environment variables const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || ''; @@ -914,6 +915,10 @@ export function KnowledgeBasePage() { New Folder + void; +} + +const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || ''; + +export function TextEntryModal({ folders, onUploadComplete }: TextEntryModalProps) { + const [isOpen, setIsOpen] = useState(false); + const [selectedFolder, setSelectedFolder] = useState(''); + const [filename, setFilename] = useState('untitled.txt'); + const [content, setContent] = useState(''); + const [isCreating, setIsCreating] = useState(false); + const [filenameError, setFilenameError] = useState(null); + + const validateFilename = (name: string) => { + if (!name.trim()) { + setFilenameError('Filename is required'); + return false; + } + + const trimmedName = name.trim(); + + // Add .txt extension if no extension is provided + const finalName = trimmedName.includes('.') ? trimmedName : `${trimmedName}.txt`; + + const validation = FileNameValidator.validateName(finalName, 'file'); + if (!validation.isValid) { + setFilenameError(FileNameValidator.getFriendlyErrorMessage(finalName, 'file')); + return false; + } + + setFilenameError(null); + return true; + }; + + const generateUniqueFilename = (baseName: string, extension: string = '.txt'): string => { + // Remove extension from baseName if present + const nameWithoutExt = baseName.replace(/\.[^/.]+$/, ''); + let counter = 1; + let filename = `${nameWithoutExt}${extension}`; + + // This is a simple client-side check - the backend will handle actual uniqueness + // But this gives better UX by suggesting incremental names + while (counter < 100) { // Prevent infinite loop + const exists = false; // We'll let backend handle actual collision detection + if (!exists) break; + filename = `${nameWithoutExt}${counter}${extension}`; + counter++; + } + + return filename; + }; + + const handleFilenameChange = (value: string) => { + setFilename(value); + if (value.trim()) { + validateFilename(value); + } else { + setFilenameError(null); + } + }; + + const handleCreate = async () => { + if (!selectedFolder) { + toast.error('Please select a folder'); + return; + } + + if (!validateFilename(filename)) { + return; + } + + if (!content.trim()) { + toast.error('Please enter some content'); + return; + } + + setIsCreating(true); + + try { + const supabase = createClient(); + const { data: { session } } = await supabase.auth.getSession(); + + if (!session?.access_token) { + throw new Error('No session found'); + } + + // Add .txt extension if not present + const finalFilename = filename.includes('.') ? filename.trim() : `${filename.trim()}.txt`; + + // Create a text file blob + const textBlob = new Blob([content], { type: 'text/plain' }); + const file = new File([textBlob], finalFilename, { type: 'text/plain' }); + + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch(`${API_URL}/knowledge-base/folders/${selectedFolder}/upload`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${session.access_token}`, + }, + body: formData + }); + + if (response.ok) { + const result = await response.json(); + toast.success('Text file created successfully'); + + if (result.filename_changed) { + toast.info(`File was renamed to "${result.final_filename}" to avoid conflicts`); + } + + onUploadComplete(); + setIsOpen(false); + setFilename(''); + setContent(''); + setSelectedFolder(''); + setFilenameError(null); + } else { + const errorData = await response.json().catch(() => null); + toast.error(errorData?.detail || 'Failed to create text file'); + } + } catch (error) { + console.error('Error creating text file:', error); + toast.error('Failed to create text file'); + } finally { + setIsCreating(false); + } + }; + + return ( + + + + + + +
+
+ +
+
+ Create Text Entry +

+ Write and save text content directly to your knowledge base +

+
+
+
+ +
+ {/* Information Section */} +
+
+
+ +
+
+

Create Text Entry

+
    +
  • • Write content directly without uploading files
  • +
  • • Automatically saved as .txt format
  • +
  • • Can be searched and referenced by AI agents
  • +
  • • Supports any file extension if specified
  • +
+
+
+
+ + {/* Folder Selection */} +
+ + +
+ + {/* Filename Input */} +
+ + handleFilenameChange(e.target.value)} + className={`h-11 ${filenameError ? 'border-red-500' : ''}`} + /> + {filenameError && ( +

{filenameError}

+ )} +

+ Will default to .txt extension if none specified +

+
+ + {/* Content Input */} +
+ +