import { useState, useEffect, useMemo } from 'react'
import { FileOperationsService } from '@/lib/services/file-operations'
import { FileSystemItem } from '@/lib/types/database'
import { supabase } from '@/lib/supabase'
import SearchInput, { SearchMode } from './search/SearchInput'
import SearchOptionsComponent, { SearchOptions } from './search/SearchOptions'
import IndexingStatus from './search/IndexingStatus'
import SearchResults from './search/SearchResults'
interface TextSearchResult {
id: string
name: string
file_extension: string
matches: {
line_number: number
content: string
context_before?: string
context_after?: string
}[]
total_matches: number
}
interface SemanticSearchResult {
chunk_id: string
file_id: string
file_name: string
content: string
similarity: number
line_start: number
line_end: number
}
interface UnifiedSearchTabProps {
bookId?: string
onFileSelect?: (file: FileSystemItem) => void
}
export default function UnifiedSearchTab({ bookId, onFileSelect }: UnifiedSearchTabProps) {
// Search state
const [searchQuery, setSearchQuery] = useState('')
const [searchMode, setSearchMode] = useState<SearchMode>('text')
const [isSearching, setIsSearching] = useState(false)
// Text search state
const [searchOptions, setSearchOptions] = useState<SearchOptions>({
matchCase: false,
matchWholeWord: false,
useRegex: false,
includeIgnored: false
})
const [includeFilter, setIncludeFilter] = useState('')
const [excludeFilter, setExcludeFilter] = useState('')
const [textResults, setTextResults] = useState<TextSearchResult[]>([])
// Semantic search state
const [semanticResults, setSemanticResults] = useState<SemanticSearchResult[]>([])
const [indexingStatus, setIndexingStatus] = useState<any>(null)
const [isIndexing, setIsIndexing] = useState(false)
const fileOperations = useMemo(() => new FileOperationsService(), [])
// Fetch indexing status on mount and mode change
useEffect(() => {
if (bookId && searchMode === 'semantic') {
fetchIndexingStatus()
}
}, [bookId, searchMode])
// Debounced search
useEffect(() => {
if (!searchQuery.trim() || !bookId) {
setTextResults([])
setSemanticResults([])
return
}
const timeoutId = setTimeout(async () => {
if (searchMode === 'text') {
await performTextSearch()
} else {
await performSemanticSearch()
}
}, 300)
return () => clearTimeout(timeoutId)
}, [searchQuery, searchMode, searchOptions, includeFilter, excludeFilter, bookId])
const fetchIndexingStatus = async () => {
if (!bookId) return
try {
const { data: { session } } = await supabase.auth.getSession()
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (session?.access_token) {
headers['Authorization'] = `Bearer ${session.access_token}`
}
const response = await fetch(`/api/books/${bookId}/semantic-search`, { headers })
if (response.ok) {
const data = await response.json()
setIndexingStatus(data.stats)
}
} catch (error) {
console.error('Error fetching indexing status:', error)
}
}
const performTextSearch = async () => {
if (!bookId || !searchQuery.trim()) return
setIsSearching(true)
try {
let searchPattern = searchQuery
if (searchOptions.useRegex) {
searchPattern = searchQuery
} else if (searchOptions.matchWholeWord) {
searchPattern = `\\b${searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`
} else {
searchPattern = searchQuery
}
const results = await fileOperations.grepFiles(
bookId,
searchPattern,
includeFilter || undefined,
searchOptions.matchCase,
searchOptions.useRegex
)
setTextResults(results)
} catch (error) {
console.error('Text search error:', error)
setTextResults([])
} finally {
setIsSearching(false)
}
}
const performSemanticSearch = async () => {
if (!bookId || !searchQuery.trim()) return
setIsSearching(true)
try {
const { data: { session } } = await supabase.auth.getSession()
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (session?.access_token) {
headers['Authorization'] = `Bearer ${session.access_token}`
}
const response = await fetch(`/api/books/${bookId}/semantic-search`, {
method: 'POST',
headers,
body: JSON.stringify({
query: searchQuery,
maxResults: 10,
similarityThreshold: 0.01
})
})
if (response.ok) {
const data = await response.json()
setSemanticResults(data.results || [])
} else {
console.error('Semantic search failed:', response.statusText)
setSemanticResults([])
}
} catch (error) {
console.error('Semantic search error:', error)
setSemanticResults([])
} finally {
setIsSearching(false)
}
}
const startIndexing = async () => {
if (!bookId || isIndexing) return
setIsIndexing(true)
try {
const { data: { session } } = await supabase.auth.getSession()
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (session?.access_token) {
headers['Authorization'] = `Bearer ${session.access_token}`
}
const response = await fetch(`/api/books/${bookId}/index`, {
method: 'POST',
headers,
})
if (response.ok) {
const interval = setInterval(() => {
fetchIndexingStatus()
}, 2000)
setTimeout(() => {
clearInterval(interval)
setIsIndexing(false)
}, 30000)
}
} catch (error) {
console.error('Error starting indexing:', error)
setIsIndexing(false)
}
}
const handleResultClick = async (result: any, lineNumber?: number) => {
if (!onFileSelect || !bookId) return
try {
let fileData, fileItem: FileSystemItem
if (searchMode === 'text') {
// Text search result
fileData = await fileOperations.readFile(bookId, result.id)
fileItem = {
id: result.id,
name: result.name,
type: 'file',
file_extension: result.file_extension,
content: fileData.content,
book_id: bookId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
parent_id: null,
mime_type: null,
file_size: null,
file_url: null,
expanded: false,
sort_order: 0,
metadata: {}
}
} else {
// Semantic search result
const { data: { session } } = await supabase.auth.getSession()
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (session?.access_token) {
headers['Authorization'] = `Bearer ${session.access_token}`
}
const response = await fetch(`/api/books/${bookId}/files/${result.file_id}`, { headers })
if (!response.ok) {
console.error('Failed to fetch file:', response.status, response.statusText)
return
}
fileData = await response.json()
fileItem = {
id: result.file_id,
name: result.file_name,
type: 'file',
file_extension: fileData.file_extension || 'md',
content: fileData.content,
book_id: bookId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
parent_id: null,
mime_type: null,
file_size: null,
file_url: null,
expanded: false,
sort_order: 0,
metadata: {}
}
}
onFileSelect(fileItem)
// Small delay to ensure file is loaded in editor before highlighting
setTimeout(() => {
if (searchMode === 'text') {
// Text search: navigate to specific line and highlight search terms
if (lineNumber) {
window.dispatchEvent(new CustomEvent('search-goto-line', {
detail: {
lineNumber: lineNumber,
searchQuery: searchQuery,
caseSensitive: searchOptions.matchCase,
useRegex: searchOptions.useRegex,
matchWholeWord: searchOptions.matchWholeWord,
isSemanticSearch: false
}
}))
} else {
// Highlight search terms throughout the file
window.dispatchEvent(new CustomEvent('search-highlight', {
detail: {
searchQuery: searchQuery,
caseSensitive: searchOptions.matchCase,
useRegex: searchOptions.useRegex,
matchWholeWord: searchOptions.matchWholeWord,
isSemanticSearch: false
}
}))
}
} else {
// Semantic search: navigate to the range and highlight the specific content
window.dispatchEvent(new CustomEvent('search-goto-line', {
detail: {
lineNumber: result.line_start,
endLineNumber: result.line_end,
searchQuery: searchQuery,
semanticContent: result.content,
isSemanticSearch: true,
highlightRange: {
startLine: result.line_start,
endLine: result.line_end,
content: result.content
}
}
}))
}
}, 100)
} catch (error) {
console.error('Error opening file from search:', error)
}
}
return (
<div className="h-full flex flex-col relative overflow-hidden">
{/* Search Input */}
<SearchInput
value={searchQuery}
onChange={setSearchQuery}
mode={searchMode}
onModeChange={setSearchMode}
isSearching={isSearching}
/>
{/* Search Options (Text Mode Only) */}
<SearchOptionsComponent
mode={searchMode}
options={searchOptions}
onOptionsChange={setSearchOptions}
includeFilter={includeFilter}
excludeFilter={excludeFilter}
onIncludeFilterChange={setIncludeFilter}
onExcludeFilterChange={setExcludeFilter}
/>
{/* Indexing Status (Semantic Mode Only) */}
<IndexingStatus
mode={searchMode}
indexingStatus={indexingStatus}
isIndexing={isIndexing}
onStartIndexing={startIndexing}
/>
{/* Search Results */}
<SearchResults
mode={searchMode}
searchQuery={searchQuery}
textResults={textResults}
semanticResults={semanticResults}
isSearching={isSearching}
searchOptions={searchOptions}
onResultClick={handleResultClick}
/>
</div>
)
}