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<string> {
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<FileSystemItem[]> {
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<FileSystemItem | null> {
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<any[]> {
// 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
}
}