import { SerializedEditorState } from 'lexical' import { ChevronDown, ChevronRight } from 'lucide-react' import { useCallback, useMemo, useRef, useState } from 'react' import ReactMarkdown from 'react-markdown' import { useApp } from '../../contexts/AppContext' import { useRAG } from '../../contexts/RAGContext' import { SelectVector } from '../../database/schema' import { Mentionable } from '../../types/mentionable' import { openMarkdownFile } from '../../utils/obsidian' import SearchInputWithActions, { SearchInputRef } from './chat-input/SearchInputWithActions' import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text' // 文件分组结果接口 interface FileGroup { path: string fileName: string maxSimilarity: number blocks: (Omit & { similarity: number })[] } const SearchView = () => { const { getRAGEngine } = useRAG() const app = useApp() const searchInputRef = useRef(null) const [searchResults, setSearchResults] = useState<(Omit & { similarity: number })[]>([]) const [isSearching, setIsSearching] = useState(false) const [hasSearched, setHasSearched] = useState(false) // 展开状态管理 - 默认全部展开 const [expandedFiles, setExpandedFiles] = useState>(new Set()) // 新增:mentionables 状态管理 const [mentionables, setMentionables] = useState([]) const [searchEditorState, setSearchEditorState] = useState(null) const handleSearch = useCallback(async (editorState?: SerializedEditorState) => { let searchTerm = '' if (editorState) { // 使用成熟的函数从 Lexical 编辑器状态中提取文本内容 searchTerm = editorStateToPlainText(editorState).trim() } if (!searchTerm.trim()) { setSearchResults([]) setHasSearched(false) return } setIsSearching(true) setHasSearched(true) try { const ragEngine = await getRAGEngine() const results = await ragEngine.processQuery({ query: searchTerm, limit: 50, // 使用用户选择的限制数量 }) setSearchResults(results) // 默认展开所有文件 // const uniquePaths = new Set(results.map(r => r.path)) // setExpandedFiles(new Set(uniquePaths)) } catch (error) { console.error('搜索失败:', error) setSearchResults([]) } finally { setIsSearching(false) } }, [getRAGEngine]) const handleResultClick = (result: Omit & { similarity: number }) => { openMarkdownFile(app, result.path, result.metadata.startLine) } const toggleFileExpansion = (filePath: string) => { const newExpandedFiles = new Set(expandedFiles) if (newExpandedFiles.has(filePath)) { newExpandedFiles.delete(filePath) } else { newExpandedFiles.add(filePath) } setExpandedFiles(newExpandedFiles) } // 限制文本显示行数 const truncateContent = (content: string, maxLines: number = 3) => { const lines = content.split('\n') if (lines.length <= maxLines) { return content } return lines.slice(0, maxLines).join('\n') + '...' } // 渲染markdown内容 const renderMarkdownContent = (content: string, maxLines: number = 3) => { const truncatedContent = truncateContent(content, maxLines) return (

{children}

, h2: ({ children }) =>

{children}

, h3: ({ children }) =>

{children}

, h4: ({ children }) =>

{children}

, h5: ({ children }) =>
{children}
, h6: ({ children }) =>
{children}
, // 移除图片显示,避免布局问题 img: () => [图片], // 代码块样式 code: ({ children, inline, ...props }: { children: React.ReactNode; inline?: boolean; [key: string]: unknown }) => { if (inline) { return {children} } return
{children}
}, // 链接样式 a: ({ href, children }) => ( {children} ), }} > {truncatedContent}
) } // 按文件分组并排序 const groupedResults = useMemo(() => { if (!searchResults.length) return [] // 按文件路径分组 const fileGroups = new Map() searchResults.forEach(result => { const filePath = result.path const fileName = filePath.split('/').pop() || filePath if (!fileGroups.has(filePath)) { fileGroups.set(filePath, { path: filePath, fileName, maxSimilarity: result.similarity, blocks: [] }) } const group = fileGroups.get(filePath) if (group) { group.blocks.push(result) // 更新最高相似度 if (result.similarity > group.maxSimilarity) { group.maxSimilarity = result.similarity } } }) // 对每个文件内的块按相似度排序 fileGroups.forEach(group => { group.blocks.sort((a, b) => b.similarity - a.similarity) }) // 将文件按最高相似度排序 return Array.from(fileGroups.values()).sort((a, b) => b.maxSimilarity - a.maxSimilarity) }, [searchResults]) const totalBlocks = searchResults.length const totalFiles = groupedResults.length return (
{/* 搜索输入框 */}
{/* 结果统计 */} {hasSearched && !isSearching && (
{totalFiles} 个文件,{totalBlocks} 个块
)} {/* 搜索进度 */} {isSearching && (
正在搜索...
)} {/* 搜索结果 */}
{!isSearching && groupedResults.length > 0 && (
{groupedResults.map((fileGroup, fileIndex) => (
{/* 文件头部 */}
toggleFileExpansion(fileGroup.path)} >
{expandedFiles.has(fileGroup.path) ? ( ) : ( )} {/* {fileIndex + 1} */} {fileGroup.fileName} {/* ({fileGroup.path}) */}
{/* {fileGroup.blocks.length} 块 */} {/* {fileGroup.maxSimilarity.toFixed(3)} */}
{/* 文件块列表 */} {expandedFiles.has(fileGroup.path) && (
{fileGroup.blocks.map((result, blockIndex) => (
handleResultClick(result)} >
{blockIndex + 1} L{result.metadata.startLine}-{result.metadata.endLine} {result.similarity.toFixed(3)}
{renderMarkdownContent(result.content)}
))}
)}
))}
)} {!isSearching && hasSearched && groupedResults.length === 0 && (

未找到相关结果

)}
{/* 样式 */}
) } export default SearchView