import { supabase } from '@/lib/supabase' import type { FileSystemItem } from '@/lib/types/database' export class FileOperationsService { private supabase: any constructor(customSupabaseClient?: any) { // Use custom Supabase client if provided (for server-side), otherwise use client-side this.supabase = customSupabaseClient || supabase } async searchFiles(bookId: string, query: string) { const { data: files } = await this.supabase .from('file_system_items') .select('*') .eq('book_id', bookId) .eq('type', 'file') .or(`name.ilike.%${query}%,content.ilike.%${query}%`) .limit(10) const result = files?.map((file: any) => ({ id: file.id, name: file.name, file_extension: file.file_extension, content_preview: file.content ? file.content.substring(0, 200) + (file.content.length > 200 ? '...' : '') : '' })) || [] console.log(`Search for "${query}" found ${result.length} files:`, result.map((f: any) => ({ id: f.id, name: f.name }))) return result } async readFile(bookId: string, fileId: string) { const { data: file } = await this.supabase .from('file_system_items') .select('*') .eq('book_id', bookId) .eq('id', fileId) .eq('type', 'file') .single() if (!file) { throw new Error('File not found') } return { id: file.id, name: file.name, content: file.content || '', file_extension: file.file_extension } } async updateFile(bookId: string, fileId: string, newContent: string, changeSummary: string) { // First verify the file exists and is unique const { data: existingFile, error: checkError } = await this.supabase .from('file_system_items') .select('id, name') .eq('book_id', bookId) .eq('id', fileId) .eq('type', 'file') if (checkError) { throw new Error(`Failed to verify file: ${checkError.message}`) } if (!existingFile || existingFile.length === 0) { throw new Error('File not found') } if (existingFile.length > 1) { throw new Error(`Multiple files found with ID ${fileId}`) } // Update the file const { data: file, error } = await this.supabase .from('file_system_items') .update({ content: newContent }) .eq('book_id', bookId) .eq('id', fileId) .eq('type', 'file') .select('id, name') .single() if (error) { throw new Error(`Failed to update file: ${error.message}`) } return { id: file.id, name: file.name, change_summary: changeSummary, success: true } } async listFiles(bookId: string, folderId?: string) { const query = this.supabase .from('file_system_items') .select('*') .eq('book_id', bookId) .order('type', { ascending: false }) // folders first .order('name', { ascending: true }) if (folderId) { query.eq('parent_id', folderId) } else { query.is('parent_id', null) // root level } const { data: items } = await query return items?.map((item: any) => ({ id: item.id, name: item.name, type: item.type, file_extension: item.file_extension, created_at: item.created_at, updated_at: item.updated_at, content_preview: item.type === 'file' && item.content ? item.content.substring(0, 100) + (item.content.length > 100 ? '...' : '') : null })) || [] } async createFile(bookId: string, name: string, content: string, parentFolderId?: string) { const { data: file, error } = await this.supabase .from('file_system_items') .insert({ book_id: bookId, name, type: 'file', file_extension: 'md', content: content, parent_id: parentFolderId || null }) .select() .single() if (error) { throw new Error(`Failed to create file: ${error.message}`) } return { id: file.id, name: file.name, type: file.type, file_extension: file.file_extension, success: true } } async createFolder(bookId: string, name: string, parentFolderId?: string) { const { data: folder, error } = await this.supabase .from('file_system_items') .insert({ book_id: bookId, name, type: 'folder', expanded: false, parent_id: parentFolderId || null }) .select() .single() if (error) { throw new Error(`Failed to create folder: ${error.message}`) } return { id: folder.id, name: folder.name, type: folder.type, success: true } } async deleteFile(bookId: string, fileId: string) { // First check if the item exists and get its info const { data: item } = await this.supabase .from('file_system_items') .select('name, type') .eq('book_id', bookId) .eq('id', fileId) .single() if (!item) { throw new Error('File or folder not found') } // Delete the item - CASCADE DELETE will handle children automatically const { error } = await this.supabase .from('file_system_items') .delete() .eq('book_id', bookId) .eq('id', fileId) if (error) { throw new Error(`Failed to delete ${item.type}: ${error.message}`) } return { name: item.name, type: item.type, success: true } } async getBookTitle(bookId: string): Promise { const { data: book } = await this.supabase .from('books') .select('title') .eq('id', bookId) .single() return book?.title || 'your book' } async searchRelevantFiles(bookId: string, query: string, mentionedFiles: string[] = []): Promise { const relevantFiles: FileSystemItem[] = [] // First, get specifically mentioned files if (mentionedFiles.length > 0) { for (const mention of mentionedFiles) { const { data: files } = await this.supabase .from('file_system_items') .select('*') .eq('book_id', bookId) .eq('type', 'file') .or(`name.ilike.%${mention}%,content.ilike.%${mention}%`) .limit(5) if (files) { relevantFiles.push(...files) } } } // Try semantic search first (if available) try { const { semanticIndexingService } = await import('./semantic-indexing-service') const semanticResults = await semanticIndexingService.semanticSearch( bookId, query, { maxResults: 6, similarityThreshold: 0.01 } ) if (semanticResults.length > 0) { // Get full file data for semantic results const fileIds = Array.from(new Set(semanticResults.map(r => r.file_id))) const { data: semanticFiles } = await this.supabase .from('file_system_items') .select('*') .in('id', fileIds) if (semanticFiles) { relevantFiles.push(...semanticFiles) } } } catch (error) { console.log('Semantic search not available, falling back to keyword search') // Fallback to keyword search const searchTerms = this.extractSearchTerms(query) if (searchTerms.length > 0) { for (const term of searchTerms.slice(0, 3)) { // Limit to top 3 terms const { data: files } = await this.supabase .from('file_system_items') .select('*') .eq('book_id', bookId) .eq('type', 'file') .ilike('content', `%${term}%`) .limit(3) if (files) { relevantFiles.push(...files) } } } } // Remove duplicates and limit total files const uniqueFiles = relevantFiles.filter((file, index, self) => index === self.findIndex(f => f.id === file.id) ) return uniqueFiles.slice(0, 8) // Limit to 8 files max } async getFileByName(bookId: string, fileName: string): Promise { const { data: file } = await this.supabase .from('file_system_items') .select('*') .eq('book_id', bookId) .eq('name', fileName) .eq('type', 'file') .single() return file || null } async grepFiles(bookId: string, pattern: string, fileFilter?: string, caseSensitive: boolean = false, useRegex: boolean = false): Promise { // First get all files, potentially filtered let query = this.supabase .from('file_system_items') .select('*') .eq('book_id', bookId) .eq('type', 'file') .not('content', 'is', null) // Apply file filter if provided if (fileFilter) { if (fileFilter.startsWith('*.')) { // Handle file extension filter const extension = fileFilter.substring(2) query = query.eq('file_extension', extension) } else { // Handle name pattern filter query = query.ilike('name', `%${fileFilter}%`) } } const { data: files } = await query.limit(50) if (!files || files.length === 0) { return [] } const results: any[] = [] let searchRegex: RegExp try { if (useRegex) { // Use the pattern as a regex const flags = caseSensitive ? 'g' : 'gi' searchRegex = new RegExp(pattern, flags) } else { // Simple string search - escape regex special characters const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const flags = caseSensitive ? 'g' : 'gi' searchRegex = new RegExp(escapedPattern, flags) } } catch (error) { // If regex is invalid, fall back to simple string search const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const flags = caseSensitive ? 'g' : 'gi' searchRegex = new RegExp(escapedPattern, flags) } for (const file of files) { if (!file.content) continue const lines = file.content.split('\n') const matches: any[] = [] lines.forEach((line: string, index: number) => { // Reset regex lastIndex for global searches searchRegex.lastIndex = 0 if (searchRegex.test(line)) { matches.push({ line_number: index + 1, content: line.trim(), context_before: index > 0 ? lines[index - 1]?.trim() : null, context_after: index < lines.length - 1 ? lines[index + 1]?.trim() : null }) } }) if (matches.length > 0) { results.push({ id: file.id, name: file.name, file_extension: file.file_extension, matches: matches.slice(0, 10), // Limit to 10 matches per file total_matches: matches.length }) } } console.log(`Grep search for "${pattern}" found ${results.length} files with matches`) return results } private extractSearchTerms(query: string): string[] { const commonWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'about', 'how', 'what', 'when', 'where', 'why', 'who', 'can', 'could', 'would', 'should', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'shall', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'her', 'its', 'our', 'their']) return query .toLowerCase() .replace(/[^\w\s]/g, ' ') .split(/\s+/) .filter(word => word.length > 2 && !commonWords.has(word)) .slice(0, 5) // Limit to 5 key terms } }