Optimize the search view component, add model selection functionality, support multiple search modes (notes, insights, all), update internationalization support, improve user interaction prompts, enhance log output, and ensure better user experience and code readability.

This commit is contained in:
duanfuxiang 2025-07-07 16:56:12 +08:00
parent 3db334c6e8
commit c89186a40d
11 changed files with 1393 additions and 593 deletions

View File

@ -193,7 +193,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
} }
} }
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history' | 'workspace' | 'insights'>('chat') const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history' | 'workspace' | 'insights'>('search')
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([]) const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@ import { Mentionable } from '../../types/mentionable'
import { getFilesWithTag } from '../../utils/glob-utils' import { getFilesWithTag } from '../../utils/glob-utils'
import { openMarkdownFile } from '../../utils/obsidian' import { openMarkdownFile } from '../../utils/obsidian'
import { ModelSelect } from './chat-input/ModelSelect'
import SearchInputWithActions, { SearchInputRef } from './chat-input/SearchInputWithActions' import SearchInputWithActions, { SearchInputRef } from './chat-input/SearchInputWithActions'
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text' import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
@ -31,7 +32,22 @@ interface InsightFileGroup {
fileName: string fileName: string
maxSimilarity: number maxSimilarity: number
insights: Array<{ insights: Array<{
id: string id: number
insight: string
insight_type: string
similarity: number
source_path: string
}>
}
// 聚合文件分组结果接口
interface AllFileGroup {
path: string
fileName: string
maxSimilarity: number
blocks: (Omit<SelectVector, 'embedding'> & { similarity: number })[]
insights: Array<{
id: number
insight: string insight: string
insight_type: string insight_type: string
similarity: number similarity: number
@ -45,14 +61,14 @@ const SearchView = () => {
const app = useApp() const app = useApp()
const { settings } = useSettings() const { settings } = useSettings()
const searchInputRef = useRef<SearchInputRef>(null) const searchInputRef = useRef<SearchInputRef>(null)
// 工作区管理器 // 工作区管理器
const workspaceManager = useMemo(() => { const workspaceManager = useMemo(() => {
return new WorkspaceManager(app) return new WorkspaceManager(app)
}, [app]) }, [app])
const [searchResults, setSearchResults] = useState<(Omit<SelectVector, 'embedding'> & { similarity: number })[]>([]) const [searchResults, setSearchResults] = useState<(Omit<SelectVector, 'embedding'> & { similarity: number })[]>([])
const [insightResults, setInsightResults] = useState<Array<{ const [insightResults, setInsightResults] = useState<Array<{
id: string id: number
insight: string insight: string
insight_type: string insight_type: string
similarity: number similarity: number
@ -60,14 +76,12 @@ const SearchView = () => {
}>>([]) }>>([])
const [isSearching, setIsSearching] = useState(false) const [isSearching, setIsSearching] = useState(false)
const [hasSearched, setHasSearched] = useState(false) const [hasSearched, setHasSearched] = useState(false)
const [searchMode, setSearchMode] = useState<'notes' | 'insights'>('notes') // 搜索模式:笔记或洞察 const [searchMode, setSearchMode] = useState<'notes' | 'insights' | 'all'>('all') // 搜索模式:笔记、洞察或全部
// 展开状态管理 - 默认全部展开 // 展开状态管理 - 默认全部展开
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set()) const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set())
// 新增mentionables 状态管理 // 新增mentionables 状态管理
const [mentionables, setMentionables] = useState<Mentionable[]>([]) const [mentionables, setMentionables] = useState<Mentionable[]>([])
const [searchEditorState, setSearchEditorState] = useState<SerializedEditorState | null>(null) const [searchEditorState, setSearchEditorState] = useState<SerializedEditorState | null>(null)
// 当前搜索范围信息
const [currentSearchScope, setCurrentSearchScope] = useState<string>('')
// 统计信息状态 // 统计信息状态
const [statisticsInfo, setStatisticsInfo] = useState<{ const [statisticsInfo, setStatisticsInfo] = useState<{
@ -79,12 +93,15 @@ const SearchView = () => {
// 工作区 RAG 向量初始化状态 // 工作区 RAG 向量初始化状态
const [isInitializingRAG, setIsInitializingRAG] = useState(false) const [isInitializingRAG, setIsInitializingRAG] = useState(false)
const [ragInitProgress, setRAGInitProgress] = useState<{ const [ragInitProgress, setRAGInitProgress] = useState<{
type: 'indexing' | 'querying' | 'querying-done' type: 'indexing' | 'querying' | 'querying-done' | 'reading-mentionables' | 'reading-files'
indexProgress?: { indexProgress?: {
completedChunks: number completedChunks: number
totalChunks: number totalChunks: number
totalFiles: number totalFiles: number
} }
currentFile?: string
totalFiles?: number
completedFiles?: number
} | null>(null) } | null>(null)
const [ragInitSuccess, setRAGInitSuccess] = useState<{ const [ragInitSuccess, setRAGInitSuccess] = useState<{
show: boolean show: boolean
@ -92,7 +109,7 @@ const SearchView = () => {
totalChunks?: number totalChunks?: number
workspaceName?: string workspaceName?: string
}>({ show: false }) }>({ show: false })
// 删除和确认对话框状态 // 删除和确认对话框状态
const [isDeleting, setIsDeleting] = useState(false) const [isDeleting, setIsDeleting] = useState(false)
const [showRAGInitConfirm, setShowRAGInitConfirm] = useState(false) const [showRAGInitConfirm, setShowRAGInitConfirm] = useState(false)
@ -100,38 +117,37 @@ const SearchView = () => {
const handleSearch = useCallback(async (editorState?: SerializedEditorState) => { const handleSearch = useCallback(async (editorState?: SerializedEditorState) => {
let searchTerm = '' let searchTerm = ''
if (editorState) { if (editorState) {
// 使用成熟的函数从 Lexical 编辑器状态中提取文本内容 // 使用成熟的函数从 Lexical 编辑器状态中提取文本内容
searchTerm = editorStateToPlainText(editorState).trim() searchTerm = editorStateToPlainText(editorState).trim()
} }
if (!searchTerm.trim()) { if (!searchTerm.trim()) {
setSearchResults([]) setSearchResults([])
setInsightResults([]) setInsightResults([])
setHasSearched(false) setHasSearched(false)
setCurrentSearchScope('')
return return
} }
setIsSearching(true) setIsSearching(true)
setHasSearched(true) setHasSearched(true)
try { try {
// 获取当前工作区 // 获取当前工作区
let currentWorkspace: Workspace | null = null let currentWorkspace: Workspace | null = null
if (settings.workspace && settings.workspace !== 'vault') { if (settings.workspace && settings.workspace !== 'vault') {
currentWorkspace = await workspaceManager.findByName(String(settings.workspace)) currentWorkspace = await workspaceManager.findByName(String(settings.workspace))
} }
// 设置搜索范围信息 // 设置搜索范围信息(用于调试)
let scopeDescription = '' let scopeDescription = ''
if (currentWorkspace) { if (currentWorkspace) {
scopeDescription = `工作区: ${currentWorkspace.name}` scopeDescription = `工作区: ${currentWorkspace.name}`
} else { } else {
scopeDescription = '整个 Vault' scopeDescription = '整个 Vault'
} }
setCurrentSearchScope(scopeDescription) console.debug('搜索范围:', scopeDescription)
// 构建搜索范围 // 构建搜索范围
let scope: { files: string[], folders: string[] } | undefined let scope: { files: string[], folders: string[] } | undefined
@ -164,10 +180,10 @@ const SearchView = () => {
scope: scope, scope: scope,
limit: 50, limit: 50,
}) })
setSearchResults(results) setSearchResults(results)
setInsightResults([]) setInsightResults([])
} else { } else if (searchMode === 'insights') {
// 搜索洞察 // 搜索洞察
const transEngine = await getTransEngine() const transEngine = await getTransEngine()
const results = await transEngine.processQuery({ const results = await transEngine.processQuery({
@ -176,9 +192,31 @@ const SearchView = () => {
limit: 50, limit: 50,
minSimilarity: 0.3, minSimilarity: 0.3,
}) })
setInsightResults(results as any) setInsightResults(results)
setSearchResults([]) setSearchResults([])
} else {
// 搜索全部:同时搜索原始笔记和洞察
const ragEngine = await getRAGEngine()
const transEngine = await getTransEngine()
// 并行执行两个搜索
const [notesResults, insightsResults] = await Promise.all([
ragEngine.processQuery({
query: searchTerm,
scope: scope,
limit: 25, // 每个类型限制25个结果
}),
transEngine.processQuery({
query: searchTerm,
scope: scope,
limit: 25, // 每个类型限制25个结果
minSimilarity: 0.3,
})
])
setSearchResults(notesResults)
setInsightResults(insightsResults)
} }
} catch (error) { } catch (error) {
console.error('搜索失败:', error) console.error('搜索失败:', error)
@ -203,7 +241,7 @@ const SearchView = () => {
// 加载统计信息 // 加载统计信息
const loadStatistics = useCallback(async () => { const loadStatistics = useCallback(async () => {
setIsLoadingStats(true) setIsLoadingStats(true)
try { try {
// 获取当前工作区 // 获取当前工作区
let currentWorkspace: Workspace | null = null let currentWorkspace: Workspace | null = null
@ -241,7 +279,7 @@ const SearchView = () => {
} }
const ragEngine = await getRAGEngine() const ragEngine = await getRAGEngine()
// 使用新的 updateWorkspaceIndex 方法 // 使用新的 updateWorkspaceIndex 方法
await ragEngine.updateWorkspaceIndex( await ragEngine.updateWorkspaceIndex(
currentWorkspace, currentWorkspace,
@ -256,7 +294,7 @@ const SearchView = () => {
// 显示成功消息 // 显示成功消息
console.log(`✅ 工作区 RAG 向量初始化完成: ${currentWorkspace.name}`) console.log(`✅ 工作区 RAG 向量初始化完成: ${currentWorkspace.name}`)
// 显示成功状态 // 显示成功状态
setRAGInitSuccess({ setRAGInitSuccess({
show: true, show: true,
@ -264,7 +302,7 @@ const SearchView = () => {
totalChunks: ragInitProgress?.indexProgress?.totalChunks || 0, totalChunks: ragInitProgress?.indexProgress?.totalChunks || 0,
workspaceName: currentWorkspace.name workspaceName: currentWorkspace.name
}) })
// 3秒后自动隐藏成功消息 // 3秒后自动隐藏成功消息
setTimeout(() => { setTimeout(() => {
setRAGInitSuccess({ show: false }) setRAGInitSuccess({ show: false })
@ -315,10 +353,7 @@ const SearchView = () => {
setShowRAGInitConfirm(true) setShowRAGInitConfirm(true)
}, []) }, [])
// 确认删除工作区索引
const handleDeleteWorkspaceIndex = useCallback(() => {
setShowDeleteConfirm(true)
}, [])
// 确认初始化 RAG 向量 // 确认初始化 RAG 向量
const confirmInitWorkspaceRAG = useCallback(async () => { const confirmInitWorkspaceRAG = useCallback(async () => {
@ -426,7 +461,7 @@ const SearchView = () => {
// 移除图片显示,避免布局问题 // 移除图片显示,避免布局问题
img: () => <span className="obsidian-image-placeholder">[]</span>, img: () => <span className="obsidian-image-placeholder">[]</span>,
// 代码块样式 // 代码块样式
code: ({ children, inline }: { children: React.ReactNode; inline?: boolean; [key: string]: unknown }) => { code: ({ children, inline }: { children: React.ReactNode; inline?: boolean;[key: string]: unknown }) => {
if (inline) { if (inline) {
return <code className="obsidian-inline-code">{children}</code> return <code className="obsidian-inline-code">{children}</code>
} }
@ -449,11 +484,11 @@ const SearchView = () => {
// 按文件路径分组 // 按文件路径分组
const fileGroups = new Map<string, FileGroup>() const fileGroups = new Map<string, FileGroup>()
searchResults.forEach(result => { searchResults.forEach(result => {
const filePath = result.path const filePath = result.path
const fileName = filePath.split('/').pop() || filePath const fileName = filePath.split('/').pop() || filePath
if (!fileGroups.has(filePath)) { if (!fileGroups.has(filePath)) {
fileGroups.set(filePath, { fileGroups.set(filePath, {
path: filePath, path: filePath,
@ -462,7 +497,7 @@ const SearchView = () => {
blocks: [] blocks: []
}) })
} }
const group = fileGroups.get(filePath) const group = fileGroups.get(filePath)
if (group) { if (group) {
group.blocks.push(result) group.blocks.push(result)
@ -488,11 +523,11 @@ const SearchView = () => {
// 按文件路径分组 // 按文件路径分组
const fileGroups = new Map<string, InsightFileGroup>() const fileGroups = new Map<string, InsightFileGroup>()
insightResults.forEach(result => { insightResults.forEach(result => {
const filePath = result.source_path const filePath = result.source_path
const fileName = filePath.split('/').pop() || filePath const fileName = filePath.split('/').pop() || filePath
if (!fileGroups.has(filePath)) { if (!fileGroups.has(filePath)) {
fileGroups.set(filePath, { fileGroups.set(filePath, {
path: filePath, path: filePath,
@ -501,7 +536,7 @@ const SearchView = () => {
insights: [] insights: []
}) })
} }
const group = fileGroups.get(filePath) const group = fileGroups.get(filePath)
if (group) { if (group) {
group.insights.push(result) group.insights.push(result)
@ -521,8 +556,63 @@ const SearchView = () => {
return Array.from(fileGroups.values()).sort((a, b) => b.maxSimilarity - a.maxSimilarity) return Array.from(fileGroups.values()).sort((a, b) => b.maxSimilarity - a.maxSimilarity)
}, [insightResults]) }, [insightResults])
// 按文件分组并排序 - 全部聚合
const allGroupedResults = useMemo(() => {
if (!searchResults.length && !insightResults.length) return []
// 合并所有文件路径
const allFilePaths = new Set<string>()
// 从笔记结果中收集文件路径
searchResults.forEach(result => {
allFilePaths.add(result.path)
})
// 从洞察结果中收集文件路径
insightResults.forEach(result => {
allFilePaths.add(result.source_path)
})
// 按文件路径分组
const fileGroups = new Map<string, AllFileGroup>()
// 处理每个文件
Array.from(allFilePaths).forEach(filePath => {
const fileName = filePath.split('/').pop() || filePath
// 获取该文件的笔记块
const fileBlocks = searchResults.filter(result => result.path === filePath)
// 获取该文件的洞察
const fileInsights = insightResults.filter(result => result.source_path === filePath)
// 计算该文件的最高相似度
const blockMaxSimilarity = fileBlocks.length > 0 ? Math.max(...fileBlocks.map(b => b.similarity)) : 0
const insightMaxSimilarity = fileInsights.length > 0 ? Math.max(...fileInsights.map(i => i.similarity)) : 0
const maxSimilarity = Math.max(blockMaxSimilarity, insightMaxSimilarity)
if (fileBlocks.length > 0 || fileInsights.length > 0) {
// 对块和洞察分别按相似度排序
fileBlocks.sort((a, b) => b.similarity - a.similarity)
fileInsights.sort((a, b) => b.similarity - a.similarity)
fileGroups.set(filePath, {
path: filePath,
fileName,
maxSimilarity,
blocks: fileBlocks,
insights: fileInsights
})
}
})
// 将文件按最高相似度排序
return Array.from(fileGroups.values()).sort((a, b) => b.maxSimilarity - a.maxSimilarity)
}, [searchResults, insightResults])
const totalBlocks = searchResults.length const totalBlocks = searchResults.length
const totalFiles = groupedResults.length const totalFiles = groupedResults.length
const totalAllFiles = allGroupedResults.length
return ( return (
<div className="obsidian-search-container"> <div className="obsidian-search-container">
@ -530,29 +620,11 @@ const SearchView = () => {
<div className="obsidian-search-header-wrapper"> <div className="obsidian-search-header-wrapper">
<div className="obsidian-search-title"> <div className="obsidian-search-title">
<h3></h3> <h3></h3>
<div className="obsidian-search-actions">
<button
onClick={handleInitWorkspaceRAG}
disabled={isInitializingRAG || isDeleting || isSearching}
className="obsidian-search-init-btn"
title={statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '更新索引' : '初始化索引'}
>
{isInitializingRAG ? '🔄 正在初始化...' : (statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '🔄 更新索引' : '🚀 初始化索引')}
</button>
<button
onClick={handleDeleteWorkspaceIndex}
disabled={isDeleting || isInitializingRAG || isSearching}
className="obsidian-search-delete-btn"
title="清除索引"
>
{isDeleting ? '🗑️ 正在清除...' : '🗑️ 清除索引'}
</button>
</div>
</div> </div>
{/* 统计信息 */} {/* 统计信息 */}
{!isLoadingStats && statisticsInfo && ( <div className="obsidian-search-stats">
<div className="obsidian-search-stats"> {!isLoadingStats && statisticsInfo && (
<div className="obsidian-search-stats-overview"> <div className="obsidian-search-stats-overview">
<div className="obsidian-search-stats-main"> <div className="obsidian-search-stats-main">
<span className="obsidian-search-stats-number">{statisticsInfo.totalChunks}</span> <span className="obsidian-search-stats-number">{statisticsInfo.totalChunks}</span>
@ -566,6 +638,79 @@ const SearchView = () => {
</div> </div>
</div> </div>
</div> </div>
)}
<div className="infio-search-model-info">
<div className="infio-search-model-row">
<span className="infio-search-model-label">:</span>
<ModelSelect modelType="embedding" />
</div>
<div className="obsidian-search-actions">
<button
onClick={handleInitWorkspaceRAG}
disabled={isInitializingRAG || isDeleting || isSearching}
className="obsidian-search-init-btn"
title={statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '更新索引' : '初始化索引'}
>
{isInitializingRAG ? '正在初始化...' : (statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '更新索引' : '初始化索引')}
</button>
</div>
</div>
</div>
{/* 索引进度 */}
{isInitializingRAG && (
<div className="obsidian-rag-initializing">
<div className="obsidian-rag-init-header">
<h4> RAG </h4>
<p></p>
</div>
{ragInitProgress && ragInitProgress.type === 'indexing' && ragInitProgress.indexProgress && (
<div className="obsidian-rag-progress">
<div className="obsidian-rag-progress-info">
<span className="obsidian-rag-progress-stage"></span>
<span className="obsidian-rag-progress-counter">
{ragInitProgress.indexProgress.completedChunks} / {ragInitProgress.indexProgress.totalChunks}
</span>
</div>
<div className="obsidian-rag-progress-bar">
<div
className="obsidian-rag-progress-fill"
style={{
width: `${(ragInitProgress.indexProgress.completedChunks / Math.max(ragInitProgress.indexProgress.totalChunks, 1)) * 100}%`
}}
></div>
</div>
<div className="obsidian-rag-progress-details">
<div className="obsidian-rag-progress-files">
{ragInitProgress.indexProgress.totalFiles}
</div>
<div className="obsidian-rag-progress-percentage">
{Math.round((ragInitProgress.indexProgress.completedChunks / Math.max(ragInitProgress.indexProgress.totalChunks, 1)) * 100)}%
</div>
</div>
</div>
)}
</div>
)}
{/* RAG 初始化成功消息 */}
{ragInitSuccess.show && (
<div className="obsidian-rag-success">
<div className="obsidian-rag-success-content">
<span className="obsidian-rag-success-icon"></span>
<div className="obsidian-rag-success-text">
<span className="obsidian-rag-success-title">
RAG : {ragInitSuccess.workspaceName}
</span>
</div>
<button
className="obsidian-rag-success-close"
onClick={() => setRAGInitSuccess({ show: false })}
>
×
</button>
</div>
</div> </div>
)} )}
@ -581,107 +726,26 @@ const SearchView = () => {
placeholder="语义搜索(按回车键搜索)..." placeholder="语义搜索(按回车键搜索)..."
autoFocus={true} autoFocus={true}
disabled={isSearching} disabled={isSearching}
searchMode={searchMode}
onSearchModeChange={setSearchMode}
/> />
{/* 搜索模式切换 */}
<div className="obsidian-search-mode-toggle">
<button
className={`obsidian-search-mode-btn ${searchMode === 'notes' ? 'active' : ''}`}
onClick={() => setSearchMode('notes')}
title="搜索原始笔记内容"
>
📝
</button>
<button
className={`obsidian-search-mode-btn ${searchMode === 'insights' ? 'active' : ''}`}
onClick={() => setSearchMode('insights')}
title="搜索 AI 洞察内容"
>
🧠 AI
</button>
</div>
</div> </div>
</div> </div>
{/* 结果统计 */} {/* 索引统计 */}
{hasSearched && !isSearching && ( {hasSearched && !isSearching && (
<div className="obsidian-search-stats"> <div className="obsidian-search-stats">
<div className="obsidian-search-stats-line"> <div className="obsidian-search-stats-line">
{searchMode === 'notes' ? ( {searchMode === 'notes' ? (
`${totalFiles} 个文件,${totalBlocks} 个块` `${totalFiles} 个文件,${totalBlocks} 个块`
) : ( ) : searchMode === 'insights' ? (
`${insightGroupedResults.length} 个文件,${insightResults.length} 个洞察` `${insightGroupedResults.length} 个文件,${insightResults.length} 个洞察`
) : (
`${totalAllFiles} 个文件,${totalBlocks} 个块,${insightResults.length} 个洞察`
)} )}
</div> </div>
</div> </div>
)} )}
{/* 搜索进度 */}
{isSearching && (
<div className="obsidian-search-loading">
...
</div>
)}
{/* RAG 初始化进度 */}
{isInitializingRAG && (
<div className="obsidian-rag-initializing">
<div className="obsidian-rag-init-header">
<h4> RAG </h4>
<p></p>
</div>
{ragInitProgress && ragInitProgress.type === 'indexing' && ragInitProgress.indexProgress && (
<div className="obsidian-rag-progress">
<div className="obsidian-rag-progress-info">
<span className="obsidian-rag-progress-stage"></span>
<span className="obsidian-rag-progress-counter">
{ragInitProgress.indexProgress.completedChunks} / {ragInitProgress.indexProgress.totalChunks}
</span>
</div>
<div className="obsidian-rag-progress-bar">
<div
className="obsidian-rag-progress-fill"
style={{
width: `${(ragInitProgress.indexProgress.completedChunks / Math.max(ragInitProgress.indexProgress.totalChunks, 1)) * 100}%`
}}
></div>
</div>
<div className="obsidian-rag-progress-details">
<div className="obsidian-rag-progress-files">
{ragInitProgress.indexProgress.totalFiles}
</div>
<div className="obsidian-rag-progress-percentage">
{Math.round((ragInitProgress.indexProgress.completedChunks / Math.max(ragInitProgress.indexProgress.totalChunks, 1)) * 100)}%
</div>
</div>
</div>
)}
</div>
)}
{/* RAG 初始化成功消息 */}
{ragInitSuccess.show && (
<div className="obsidian-rag-success">
<div className="obsidian-rag-success-content">
<span className="obsidian-rag-success-icon"></span>
<div className="obsidian-rag-success-text">
<span className="obsidian-rag-success-title">
RAG : {ragInitSuccess.workspaceName}
</span>
<span className="obsidian-rag-success-summary">
{ragInitSuccess.totalFiles} {ragInitSuccess.totalChunks}
</span>
</div>
<button
className="obsidian-rag-success-close"
onClick={() => setRAGInitSuccess({ show: false })}
>
×
</button>
</div>
</div>
)}
{/* 确认删除对话框 */} {/* 确认删除对话框 */}
{showDeleteConfirm && ( {showDeleteConfirm && (
<div className="obsidian-confirm-dialog-overlay"> <div className="obsidian-confirm-dialog-overlay">
@ -727,20 +791,20 @@ const SearchView = () => {
</div> </div>
<div className="obsidian-confirm-dialog-body"> <div className="obsidian-confirm-dialog-body">
<p> <p>
{statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) {statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0)
? '将更新当前工作区的向量索引,重新处理所有文件以确保索引最新。' ? '将更新当前工作区的向量索引,重新处理所有文件以确保索引最新。'
: '将为当前工作区的所有文件建立向量索引,这将提高语义搜索的准确性。' : '将为当前工作区的所有文件建立向量索引,这将提高语义搜索的准确性。'
} }
</p> </p>
<div className="obsidian-confirm-dialog-info"> <div className="obsidian-confirm-dialog-info">
<div className="obsidian-confirm-dialog-info-item"> <div className="obsidian-confirm-dialog-info-item">
<strong>:</strong> <strong>:</strong>
<span className="obsidian-confirm-dialog-model"> <span className="obsidian-confirm-dialog-model">
{settings.embeddingModelProvider} / {settings.embeddingModelId || '默认模型'} {settings.embeddingModelId}
</span> </span>
</div> </div>
<div className="obsidian-confirm-dialog-info-item"> <div className="obsidian-confirm-dialog-info-item">
<strong>:</strong> <strong>:</strong>
<span className="obsidian-confirm-dialog-workspace"> <span className="obsidian-confirm-dialog-workspace">
{settings.workspace === 'vault' ? '整个 Vault' : settings.workspace} {settings.workspace === 'vault' ? '整个 Vault' : settings.workspace}
</span> </span>
@ -768,6 +832,13 @@ const SearchView = () => {
</div> </div>
)} )}
{/* 搜索进度 */}
{isSearching && (
<div className="obsidian-search-loading">
...
</div>
)}
{/* 搜索结果 */} {/* 搜索结果 */}
<div className="obsidian-search-results"> <div className="obsidian-search-results">
{searchMode === 'notes' ? ( {searchMode === 'notes' ? (
@ -777,7 +848,7 @@ const SearchView = () => {
{groupedResults.map((fileGroup) => ( {groupedResults.map((fileGroup) => (
<div key={fileGroup.path} className="obsidian-file-group"> <div key={fileGroup.path} className="obsidian-file-group">
{/* 文件头部 */} {/* 文件头部 */}
<div <div
className="obsidian-file-header" className="obsidian-file-header"
onClick={() => toggleFileExpansion(fileGroup.path)} onClick={() => toggleFileExpansion(fileGroup.path)}
> >
@ -827,14 +898,14 @@ const SearchView = () => {
))} ))}
</div> </div>
) )
) : ( ) : searchMode === 'insights' ? (
// AI 洞察搜索结果 // AI 洞察搜索结果
!isSearching && insightGroupedResults.length > 0 && ( !isSearching && insightGroupedResults.length > 0 && (
<div className="obsidian-results-list"> <div className="obsidian-results-list">
{insightGroupedResults.map((fileGroup) => ( {insightGroupedResults.map((fileGroup) => (
<div key={fileGroup.path} className="obsidian-file-group"> <div key={fileGroup.path} className="obsidian-file-group">
{/* 文件头部 */} {/* 文件头部 */}
<div <div
className="obsidian-file-header" className="obsidian-file-header"
onClick={() => toggleFileExpansion(fileGroup.path)} onClick={() => toggleFileExpansion(fileGroup.path)}
> >
@ -885,21 +956,131 @@ const SearchView = () => {
))} ))}
</div> </div>
) )
) : (
// 全部搜索结果:按文件聚合显示原始笔记和洞察
!isSearching && allGroupedResults.length > 0 && (
<div className="obsidian-results-list">
{allGroupedResults.map((fileGroup) => (
<div key={fileGroup.path} className="obsidian-file-group">
{/* 文件头部 */}
<div
className="obsidian-file-header"
onClick={() => toggleFileExpansion(fileGroup.path)}
>
<div className="obsidian-file-header-content">
<div className="obsidian-file-header-top">
<div className="obsidian-file-header-left">
{expandedFiles.has(fileGroup.path) ? (
<ChevronDown size={16} className="obsidian-expand-icon" />
) : (
<ChevronRight size={16} className="obsidian-expand-icon" />
)}
<span className="obsidian-file-name">{fileGroup.fileName}</span>
</div>
</div>
<div className="obsidian-file-path-row">
<span className="obsidian-file-path">{fileGroup.path}</span>
</div>
</div>
</div>
{/* 文件内容:混合显示笔记块和洞察 */}
{expandedFiles.has(fileGroup.path) && (
<div className="obsidian-file-blocks">
{/* AI 洞察 */}
{fileGroup.insights.map((insight, insightIndex) => (
<div
key={`insight-${insight.id}`}
className="obsidian-result-item obsidian-result-insight"
>
<div className="obsidian-result-header">
<span className="obsidian-result-index">{insightIndex + 1}</span>
<span className="obsidian-result-insight-type">
{insight.insight_type.toUpperCase()}
</span>
<span className="obsidian-result-similarity">
{insight.similarity.toFixed(3)}
</span>
</div>
<div className="obsidian-result-content">
<div className="obsidian-insight-content">
{insight.insight}
</div>
</div>
</div>
))}
{/* 原始笔记块 */}
{fileGroup.blocks.map((result, blockIndex) => (
<div
key={`block-${result.id}`}
className="obsidian-result-item obsidian-result-block"
onClick={() => handleResultClick(result)}
>
<div className="obsidian-result-header">
<span className="obsidian-result-index">{blockIndex + 1}</span>
<span className="obsidian-result-location">
L{result.metadata.startLine}-{result.metadata.endLine}
</span>
<span className="obsidian-result-similarity">
{result.similarity.toFixed(3)}
</span>
</div>
<div className="obsidian-result-content">
{renderMarkdownContent(result.content)}
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)
)} )}
{!isSearching && hasSearched && ( {!isSearching && hasSearched && (
(searchMode === 'notes' && groupedResults.length === 0) || (searchMode === 'notes' && groupedResults.length === 0) ||
(searchMode === 'insights' && insightGroupedResults.length === 0) (searchMode === 'insights' && insightGroupedResults.length === 0) ||
(searchMode === 'all' && allGroupedResults.length === 0)
) && ( ) && (
<div className="obsidian-no-results"> <div className="obsidian-no-results">
<p></p> <p></p>
</div> </div>
)} )}
</div> </div>
{/* 样式 */} {/* 样式 */}
<style> <style>
{` {`
.infio-search-model-info {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--size-4-3);
}
.infio-search-model-row {
display: flex;
align-items: center;
gap: var(--size-2-2);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
padding: var(--size-2-2);
}
.infio-search-model-label {
font-size: var(--font-ui-small);
color: var(--text-muted);
font-weight: var(--font-medium);
}
.infio-search-model-value {
font-size: var(--font-ui-small);
color: var(--text-accent);
font-weight: 600;
font-family: var(--font-monospace);
}
.obsidian-search-container { .obsidian-search-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1064,38 +1245,6 @@ const SearchView = () => {
/* padding 由父元素控制 */ /* padding 由父元素控制 */
} }
.obsidian-search-mode-toggle {
display: flex;
gap: 8px;
margin-top: 8px;
padding: 4px;
background-color: var(--background-modifier-border);
border-radius: var(--radius-m);
}
.obsidian-search-mode-btn {
flex: 1;
padding: 6px 12px;
background-color: transparent;
border: none;
border-radius: var(--radius-s);
color: var(--text-muted);
font-size: var(--font-ui-small);
cursor: pointer;
transition: all 0.2s ease;
}
.obsidian-search-mode-btn:hover {
background-color: var(--background-modifier-hover);
color: var(--text-normal);
}
.obsidian-search-mode-btn.active {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
font-weight: 500;
}
.obsidian-search-stats { .obsidian-search-stats {
@ -1395,13 +1544,70 @@ const SearchView = () => {
cursor: text; cursor: text;
} }
/* 全部搜索结果分组样式 */
.obsidian-result-section {
margin-bottom: 20px;
}
.obsidian-result-section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: var(--background-modifier-border);
border-radius: var(--radius-s);
margin-bottom: 8px;
}
.obsidian-result-section-title {
color: var(--text-normal);
font-size: var(--font-ui-medium);
font-weight: 600;
}
.obsidian-result-section-count {
color: var(--text-muted);
font-size: var(--font-ui-small);
font-family: var(--font-monospace);
}
/* 全部模式下的类型徽章样式 */
.obsidian-result-type-badge {
padding: 2px 6px;
border-radius: var(--radius-s);
font-size: var(--font-ui-smaller);
font-weight: 600;
font-family: var(--font-monospace);
margin-right: 8px;
flex-shrink: 0;
}
.obsidian-result-type-note {
background-color: var(--color-blue-light, #e3f2fd);
color: var(--color-blue-dark, #1976d2);
}
.obsidian-result-type-insight {
background-color: var(--color-amber-light, #fff3e0);
color: var(--color-amber-dark, #f57c00);
}
/* 全部模式下的结果项样式 */
.obsidian-result-block {
border-left: 3px solid var(--color-blue, #2196f3);
}
.obsidian-result-insight {
border-left: 3px solid var(--color-amber, #ff9800);
}
/* RAG 初始化进度样式 */ /* RAG 初始化进度样式 */
.obsidian-rag-initializing { .obsidian-rag-initializing {
padding: 20px; padding: 12px;
background-color: var(--background-secondary); background-color: var(--background-secondary);
border: 1px solid var(--background-modifier-border); border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-m); border-radius: var(--radius-m);
margin: 12px; margin-bottom: 12px;
} }
.obsidian-rag-init-header { .obsidian-rag-init-header {
@ -1488,7 +1694,7 @@ const SearchView = () => {
background-color: var(--background-secondary); background-color: var(--background-secondary);
border: 1px solid var(--color-green, #28a745); border: 1px solid var(--color-green, #28a745);
border-radius: var(--radius-m); border-radius: var(--radius-m);
margin: 12px; margin-bottom: 12px;
animation: slideInFromTop 0.3s ease-out; animation: slideInFromTop 0.3s ease-out;
} }
@ -1702,5 +1908,5 @@ const SearchView = () => {
) )
} }
export default SearchView export default SearchView

View File

@ -31,6 +31,7 @@ import OnMutationPlugin, {
} from './plugins/on-mutation/OnMutationPlugin' } from './plugins/on-mutation/OnMutationPlugin'
export type LexicalContentEditableProps = { export type LexicalContentEditableProps = {
rootTheme?: string
editorRef: RefObject<LexicalEditor> editorRef: RefObject<LexicalEditor>
contentEditableRef: RefObject<HTMLDivElement> contentEditableRef: RefObject<HTMLDivElement>
onChange?: (content: SerializedEditorState) => void onChange?: (content: SerializedEditorState) => void
@ -52,6 +53,7 @@ export type LexicalContentEditableProps = {
} }
export default function LexicalContentEditable({ export default function LexicalContentEditable({
rootTheme,
editorRef, editorRef,
contentEditableRef, contentEditableRef,
onChange, onChange,
@ -68,7 +70,7 @@ export default function LexicalContentEditable({
const initialConfig: InitialConfigType = { const initialConfig: InitialConfigType = {
namespace: 'LexicalContentEditable', namespace: 'LexicalContentEditable',
theme: { theme: {
root: 'infio-chat-lexical-content-editable-root', root: rootTheme || 'infio-chat-lexical-content-editable-root',
paragraph: 'infio-chat-lexical-content-editable-paragraph', paragraph: 'infio-chat-lexical-content-editable-paragraph',
}, },
nodes: [MentionNode], nodes: [MentionNode],

View File

@ -6,17 +6,17 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { useSettings } from '../../../contexts/SettingsContext' import { useSettings } from '../../../contexts/SettingsContext'
import { t } from '../../../lang/helpers' import { t } from '../../../lang/helpers'
import { ApiProvider } from '../../../types/llm/model' import { ApiProvider } from '../../../types/llm/model'
import { GetAllProviders, GetProviderModelIds } from "../../../utils/api" import { GetAllProviders, GetEmbeddingProviders, GetEmbeddingProviderModelIds, GetProviderModelsWithSettings } from "../../../utils/api"
// 优化模型名称显示的函数 // 优化模型名称显示的函数
const getOptimizedModelName = (modelId: string): string => { const getOptimizedModelName = (modelId: string): string => {
if (!modelId) return modelId; if (!modelId) return modelId;
// 限制长度,如果太长则截断并添加省略号 // 限制长度,如果太长则截断并添加省略号
if (modelId.length > 25) { if (modelId.length > 25) {
return modelId.substring(0, 22) + '...'; return modelId.substring(0, 22) + '...';
} }
return modelId; return modelId;
}; };
@ -146,25 +146,69 @@ const HighlightedText: React.FC<{ segments: TextSegment[] }> = ({ segments }) =>
); );
}; };
export function ModelSelect() { type ModelType = 'chat' | 'insight' | 'apply' | 'embedding'
interface ModelSelectProps {
modelType?: ModelType
}
export function ModelSelect({ modelType = 'chat' }: ModelSelectProps) {
const { settings, setSettings } = useSettings() const { settings, setSettings } = useSettings()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [modelProvider, setModelProvider] = useState(settings.chatModelProvider)
const [chatModelId, setChatModelId] = useState(settings.chatModelId) // 根据模型类型获取相应的设置
const currentModelProvider = useMemo(() => {
switch (modelType) {
case 'insight':
return settings.insightModelProvider
case 'apply':
return settings.applyModelProvider
case 'embedding':
return settings.embeddingModelProvider
default:
return settings.chatModelProvider
}
}, [modelType, settings.insightModelProvider, settings.applyModelProvider, settings.embeddingModelProvider, settings.chatModelProvider])
const currentModelId = useMemo(() => {
switch (modelType) {
case 'insight':
return settings.insightModelId
case 'apply':
return settings.applyModelId
case 'embedding':
return settings.embeddingModelId
default:
return settings.chatModelId
}
}, [modelType, settings.insightModelId, settings.applyModelId, settings.embeddingModelId, settings.chatModelId])
const [modelProvider, setModelProvider] = useState(currentModelProvider)
const [chatModelId, setChatModelId] = useState(currentModelId)
const [modelIds, setModelIds] = useState<string[]>([]) const [modelIds, setModelIds] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("") const [searchTerm, setSearchTerm] = useState("")
const [selectedIndex, setSelectedIndex] = useState(0) const [selectedIndex, setSelectedIndex] = useState(0)
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const providers = GetAllProviders() const providers = useMemo(() => {
if (modelType === 'embedding') {
return GetEmbeddingProviders()
}
return GetAllProviders()
}, [modelType])
useEffect(() => { useEffect(() => {
const fetchModels = async () => { const fetchModels = async () => {
setIsLoading(true) setIsLoading(true)
try { try {
const models = await GetProviderModelIds(modelProvider, settings) if (modelType === 'embedding') {
setModelIds(models) const models = GetEmbeddingProviderModelIds(modelProvider)
setModelIds(models)
} else {
const models = await GetProviderModelsWithSettings(modelProvider, settings)
setModelIds(Object.keys(models))
}
} catch (error) { } catch (error) {
console.error('Failed to fetch provider models:', error) console.error('Failed to fetch provider models:', error)
setModelIds([]) setModelIds([])
@ -176,16 +220,30 @@ export function ModelSelect() {
fetchModels() fetchModels()
}, [modelProvider, settings]) }, [modelProvider, settings])
// Sync chat model id & chat model provider // Sync model id & model provider based on modelType
useEffect(() => { useEffect(() => {
setModelProvider(settings.chatModelProvider) setModelProvider(currentModelProvider)
setChatModelId(settings.chatModelId) setChatModelId(currentModelId)
}, [settings.chatModelProvider, settings.chatModelId]) }, [currentModelProvider, currentModelId])
const searchableItems = useMemo(() => { const searchableItems = useMemo(() => {
// 根据模型类型获取相应的收藏列表
const getCollectedModels = () => {
switch (modelType) {
case 'insight':
return settings.collectedInsightModels || []
case 'apply':
return settings.collectedApplyModels || []
case 'embedding':
return settings.collectedEmbeddingModels || []
default:
return settings.collectedChatModels || []
}
}
// 检查是否在收藏列表中 // 检查是否在收藏列表中
const isInCollected = (id: string) => { const isInCollected = (id: string) => {
return settings.collectedChatModels?.some(item => item.provider === modelProvider && item.modelId === id) || false; return getCollectedModels().some(item => item.provider === modelProvider && item.modelId === id) || false;
}; };
return modelIds.map((id) => ({ return modelIds.map((id) => ({
@ -194,7 +252,7 @@ export function ModelSelect() {
provider: modelProvider, provider: modelProvider,
isCollected: isInCollected(id), isCollected: isInCollected(id),
})) }))
}, [modelIds, modelProvider, settings.collectedChatModels]) }, [modelIds, modelProvider, modelType, settings.collectedChatModels, settings.collectedInsightModels, settings.collectedApplyModels, settings.collectedEmbeddingModels])
const fuse = useMemo(() => { const fuse = useMemo(() => {
return new Fuse<SearchableItem>(searchableItems, { return new Fuse<SearchableItem>(searchableItems, {
@ -229,11 +287,26 @@ export function ModelSelect() {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
const isCurrentlyCollected = settings.collectedChatModels?.some( // 根据模型类型获取相应的收藏列表
const getCollectedModels = () => {
switch (modelType) {
case 'insight':
return settings.collectedInsightModels || []
case 'apply':
return settings.collectedApplyModels || []
case 'embedding':
return settings.collectedEmbeddingModels || []
default:
return settings.collectedChatModels || []
}
}
const currentCollectedModels = getCollectedModels();
const isCurrentlyCollected = currentCollectedModels.some(
item => item.provider === modelProvider && item.modelId === id item => item.provider === modelProvider && item.modelId === id
); );
let newCollectedModels = settings.collectedChatModels || []; let newCollectedModels = [...currentCollectedModels];
if (isCurrentlyCollected) { if (isCurrentlyCollected) {
// remove // remove
@ -245,10 +318,33 @@ export function ModelSelect() {
newCollectedModels = [...newCollectedModels, { provider: modelProvider, modelId: id }]; newCollectedModels = [...newCollectedModels, { provider: modelProvider, modelId: id }];
} }
setSettings({ // 根据模型类型更新相应的设置
...settings, switch (modelType) {
collectedChatModels: newCollectedModels, case 'insight':
}); setSettings({
...settings,
collectedInsightModels: newCollectedModels,
});
break;
case 'apply':
setSettings({
...settings,
collectedApplyModels: newCollectedModels,
});
break;
case 'embedding':
setSettings({
...settings,
collectedEmbeddingModels: newCollectedModels,
});
break;
default:
setSettings({
...settings,
collectedChatModels: newCollectedModels,
});
break;
}
}; };
return ( return (
@ -258,7 +354,7 @@ export function ModelSelect() {
{/* <div className="infio-chat-input-model-select__mode-icon"> {/* <div className="infio-chat-input-model-select__mode-icon">
<Brain size={16} /> <Brain size={16} />
</div> */} </div> */}
<div <div
className="infio-chat-input-model-select__model-name" className="infio-chat-input-model-select__model-name"
title={chatModelId} title={chatModelId}
> >
@ -272,62 +368,128 @@ export function ModelSelect() {
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content className="infio-popover infio-llm-setting-combobox-dropdown"> <DropdownMenu.Content className="infio-popover infio-llm-setting-combobox-dropdown">
{/* collected models */} {/* collected models */}
{settings.collectedChatModels?.length > 0 && ( {(() => {
<div className="infio-model-section"> const getCollectedModels = () => {
<div className="infio-model-section-title"> switch (modelType) {
<Star size={12} className="infio-star-active" /> {t('chat.input.collectedModels')} case 'insight':
</div> return settings.collectedInsightModels || []
<ul className="infio-collected-models-list"> case 'apply':
{settings.collectedChatModels.map((collectedModel, index) => ( return settings.collectedApplyModels || []
<DropdownMenu.Item case 'embedding':
key={`${collectedModel.provider}-${collectedModel.modelId}`} return settings.collectedEmbeddingModels || []
onSelect={() => { default:
setSettings({ return settings.collectedChatModels || []
...settings, }
chatModelProvider: collectedModel.provider, }
chatModelId: collectedModel.modelId,
})
setChatModelId(collectedModel.modelId)
setSearchTerm("")
setIsOpen(false)
}}
className={`infio-llm-setting-combobox-option ${index === selectedIndex ? 'is-selected' : ''}`}
onMouseEnter={() => setSelectedIndex(index)}
asChild
>
<li
className="infio-llm-setting-model-item infio-collected-model-item"
title={`${collectedModel.provider}/${collectedModel.modelId}`}
>
<div className="infio-model-item-text-wrapper">
<span className="infio-provider-badge">{collectedModel.provider}</span>
<span title={collectedModel.modelId}>{collectedModel.modelId}</span>
</div>
<div
className="infio-model-item-star"
title="remove from collected models"
>
<Star size={16} className="infio-star-active" onClick={(e) => {
e.stopPropagation();
e.preventDefault();
// delete
const newCollectedModels = settings.collectedChatModels.filter(
item => !(item.provider === collectedModel.provider && item.modelId === collectedModel.modelId)
);
setSettings({ const collectedModels = getCollectedModels()
...settings,
collectedChatModels: newCollectedModels, return collectedModels.length > 0 ? (
}); <div className="infio-model-section">
}} /> <div className="infio-model-section-title">
</div> <Star size={12} className="infio-star-active" /> {t('chat.input.collectedModels')}
</li> </div>
</DropdownMenu.Item> <ul className="infio-collected-models-list">
))} {collectedModels.map((collectedModel, index) => (
</ul> <DropdownMenu.Item
<div className="infio-model-separator"></div> key={`${collectedModel.provider}-${collectedModel.modelId}`}
</div> onSelect={() => {
)} // 根据模型类型更新相应的设置
switch (modelType) {
case 'insight':
setSettings({
...settings,
insightModelProvider: collectedModel.provider,
insightModelId: collectedModel.modelId,
})
break;
case 'apply':
setSettings({
...settings,
applyModelProvider: collectedModel.provider,
applyModelId: collectedModel.modelId,
})
break;
case 'embedding':
setSettings({
...settings,
embeddingModelProvider: collectedModel.provider,
embeddingModelId: collectedModel.modelId,
})
break;
default:
setSettings({
...settings,
chatModelProvider: collectedModel.provider,
chatModelId: collectedModel.modelId,
})
break;
}
setChatModelId(collectedModel.modelId)
setSearchTerm("")
setIsOpen(false)
}}
className={`infio-llm-setting-combobox-option ${index === selectedIndex ? 'is-selected' : ''}`}
onMouseEnter={() => setSelectedIndex(index)}
asChild
>
<li
className="infio-llm-setting-model-item infio-collected-model-item"
title={`${collectedModel.provider}/${collectedModel.modelId}`}
>
<div className="infio-model-item-text-wrapper">
<span className="infio-provider-badge">{collectedModel.provider}</span>
<span title={collectedModel.modelId}>{collectedModel.modelId}</span>
</div>
<div
className="infio-model-item-star"
title="remove from collected models"
>
<Star size={16} className="infio-star-active" onClick={(e) => {
e.stopPropagation();
e.preventDefault();
// delete
const newCollectedModels = collectedModels.filter(
item => !(item.provider === collectedModel.provider && item.modelId === collectedModel.modelId)
);
// 根据模型类型更新相应的设置
switch (modelType) {
case 'insight':
setSettings({
...settings,
collectedInsightModels: newCollectedModels,
});
break;
case 'apply':
setSettings({
...settings,
collectedApplyModels: newCollectedModels,
});
break;
case 'embedding':
setSettings({
...settings,
collectedEmbeddingModels: newCollectedModels,
});
break;
default:
setSettings({
...settings,
collectedChatModels: newCollectedModels,
});
break;
}
}} />
</div>
</li>
</DropdownMenu.Item>
))}
</ul>
<div className="infio-model-separator"></div>
</div>
) : null
})()}
<div className="infio-llm-setting-search-container"> <div className="infio-llm-setting-search-container">
<div className="infio-llm-setting-provider-container"> <div className="infio-llm-setting-provider-container">
@ -384,11 +546,37 @@ export function ModelSelect() {
e.preventDefault() e.preventDefault()
const selectedOption = filteredOptions[selectedIndex] const selectedOption = filteredOptions[selectedIndex]
if (selectedOption) { if (selectedOption) {
setSettings({ // 根据模型类型更新相应的设置
...settings, switch (modelType) {
chatModelProvider: modelProvider, case 'insight':
chatModelId: selectedOption.id, setSettings({
}) ...settings,
insightModelProvider: modelProvider,
insightModelId: selectedOption.id,
})
break;
case 'apply':
setSettings({
...settings,
applyModelProvider: modelProvider,
applyModelId: selectedOption.id,
})
break;
case 'embedding':
setSettings({
...settings,
embeddingModelProvider: modelProvider,
embeddingModelId: selectedOption.id,
})
break;
default:
setSettings({
...settings,
chatModelProvider: modelProvider,
chatModelId: selectedOption.id,
})
break;
}
setChatModelId(selectedOption.id) setChatModelId(selectedOption.id)
setSearchTerm("") setSearchTerm("")
setIsOpen(false) setIsOpen(false)
@ -421,11 +609,37 @@ export function ModelSelect() {
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault() e.preventDefault()
setSettings({ // 根据模型类型更新相应的设置
...settings, switch (modelType) {
chatModelProvider: modelProvider, case 'insight':
chatModelId: searchTerm, setSettings({
}) ...settings,
insightModelProvider: modelProvider,
insightModelId: searchTerm,
})
break;
case 'apply':
setSettings({
...settings,
applyModelProvider: modelProvider,
applyModelId: searchTerm,
})
break;
case 'embedding':
setSettings({
...settings,
embeddingModelProvider: modelProvider,
embeddingModelId: searchTerm,
})
break;
default:
setSettings({
...settings,
chatModelProvider: modelProvider,
chatModelId: searchTerm,
})
break;
}
setChatModelId(searchTerm) setChatModelId(searchTerm)
setIsOpen(false) setIsOpen(false)
} }
@ -448,11 +662,37 @@ export function ModelSelect() {
<DropdownMenu.Item <DropdownMenu.Item
key={option.id} key={option.id}
onSelect={() => { onSelect={() => {
setSettings({ // 根据模型类型更新相应的设置
...settings, switch (modelType) {
chatModelProvider: modelProvider, case 'insight':
chatModelId: option.id, setSettings({
}) ...settings,
insightModelProvider: modelProvider,
insightModelId: option.id,
})
break;
case 'apply':
setSettings({
...settings,
applyModelProvider: modelProvider,
applyModelId: option.id,
})
break;
case 'embedding':
setSettings({
...settings,
embeddingModelProvider: modelProvider,
embeddingModelId: option.id,
})
break;
default:
setSettings({
...settings,
chatModelProvider: modelProvider,
chatModelId: option.id,
})
break;
}
setChatModelId(option.id) setChatModelId(option.id)
setSearchTerm("") setSearchTerm("")
setIsOpen(false) setIsOpen(false)
@ -460,9 +700,21 @@ export function ModelSelect() {
className={`infio-llm-setting-combobox-option ${isSelected ? 'is-selected' : ''}`} className={`infio-llm-setting-combobox-option ${isSelected ? 'is-selected' : ''}`}
onMouseEnter={() => { onMouseEnter={() => {
// 计算正确的鼠标悬停索引 // 计算正确的鼠标悬停索引
const getCollectedModels = () => {
switch (modelType) {
case 'insight':
return settings.collectedInsightModels || []
case 'apply':
return settings.collectedApplyModels || []
case 'embedding':
return settings.collectedEmbeddingModels || []
default:
return settings.collectedChatModels || []
}
}
const hoverIndex = searchTerm const hoverIndex = searchTerm
? index ? index
: index + settings.collectedChatModels?.length; : index + getCollectedModels().length;
setSelectedIndex(hoverIndex); setSelectedIndex(hoverIndex);
}} }}
asChild asChild
@ -475,7 +727,7 @@ export function ModelSelect() {
{searchTerm ? ( {searchTerm ? (
<HighlightedText segments={option.html} /> <HighlightedText segments={option.html} />
) : ( ) : (
<span title={option.id}>{option.id}</span> <span title={option.id}>{option.id}</span>
)} )}
</div> </div>
<div <div
@ -617,9 +869,8 @@ export function ModelSelect() {
text-align: left; text-align: left;
appearance: none; appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border); border: 1px solid var(--background-modifier-border);
border-radius: 6px; border-radius: 4px;
font-weight: 500; font-weight: 500;
font-size: 0.9em; font-size: 0.9em;
transition: all 0.2s ease; transition: all 0.2s ease;
@ -634,7 +885,6 @@ export function ModelSelect() {
.infio-llm-setting-provider-switch:hover { .infio-llm-setting-provider-switch:hover {
border-color: var(--interactive-accent); border-color: var(--interactive-accent);
background-color: var(--background-primary-alt);
} }
.infio-llm-setting-provider-switch:focus { .infio-llm-setting-provider-switch:focus {
@ -728,12 +978,9 @@ export function ModelSelect() {
.infio-provider-badge { .infio-provider-badge {
font-size: 10px; font-size: 10px;
padding: 2px 6px; padding: 2px 6px;
background-color: var(--background-modifier-hover);
border-radius: 4px; border-radius: 4px;
margin-right: 6px; margin-right: 6px;
color: var(--text-muted);
font-weight: 500; font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
} }
`} `}
</style> </style>

View File

@ -6,10 +6,12 @@ import {
useState useState
} from 'react' } from 'react'
import { Mentionable } from '../../../types/mentionable' import { Mentionable } from '../../../types/mentionable'
import LexicalContentEditable from './LexicalContentEditable' import LexicalContentEditable from './LexicalContentEditable'
import { SearchButton } from './SearchButton' import { SearchButton } from './SearchButton'
import { SearchModeSelect } from './SearchModeSelect'
export type SearchInputRef = { export type SearchInputRef = {
focus: () => void focus: () => void
@ -25,26 +27,28 @@ export type SearchInputProps = {
placeholder?: string placeholder?: string
autoFocus?: boolean autoFocus?: boolean
disabled?: boolean disabled?: boolean
searchMode?: 'notes' | 'insights' | 'all'
onSearchModeChange?: (mode: 'notes' | 'insights' | 'all') => void
} }
// 检查编辑器状态是否为空的辅助函数 // 检查编辑器状态是否为空
const isEditorStateEmpty = (editorState: SerializedEditorState): boolean => { const isEditorStateEmpty = (editorState: SerializedEditorState): boolean => {
if (!editorState || !editorState.root || !editorState.root.children) { try {
const root = editorState.root
if (!root || !root.children) return true
// 检查是否有实际内容
const hasContent = root.children.some((child: any) => {
if (child.type === 'paragraph') {
return child.children && child.children.length > 0
}
return true
})
return !hasContent
} catch (error) {
return true return true
} }
const children = editorState.root.children
if (children.length === 0) {
return true
}
// 检查是否只有空的段落
if (children.length === 1 && children[0].type === 'paragraph') {
const paragraph = children[0] as any
return !paragraph.children || paragraph.children.length === 0
}
return false
} }
const SearchInputWithActions = forwardRef<SearchInputRef, SearchInputProps>( const SearchInputWithActions = forwardRef<SearchInputRef, SearchInputProps>(
@ -56,6 +60,8 @@ const SearchInputWithActions = forwardRef<SearchInputRef, SearchInputProps>(
placeholder = '', placeholder = '',
autoFocus = false, autoFocus = false,
disabled = false, disabled = false,
searchMode = 'all',
onSearchModeChange,
}, },
ref ref
) => { ) => {
@ -112,6 +118,7 @@ const SearchInputWithActions = forwardRef<SearchInputRef, SearchInputProps>(
</div> </div>
)} )}
<LexicalContentEditable <LexicalContentEditable
rootTheme="infio-search-lexical-content-editable-root"
initialEditorState={(editor) => { initialEditorState={(editor) => {
if (initialSerializedEditorState) { if (initialSerializedEditorState) {
editor.setEditorState( editor.setEditorState(
@ -139,7 +146,13 @@ const SearchInputWithActions = forwardRef<SearchInputRef, SearchInputProps>(
<div className="infio-chat-user-input-controls"> <div className="infio-chat-user-input-controls">
<div className="infio-chat-user-input-controls__model-select-container"> <div className="infio-chat-user-input-controls__model-select-container">
{/* TODO: add model select */} {onSearchModeChange && (
<SearchModeSelect
searchMode={searchMode}
onSearchModeChange={onSearchModeChange}
/>
)}
</div> </div>
<div className="infio-chat-user-input-controls__buttons"> <div className="infio-chat-user-input-controls__buttons">
<SearchButton onClick={() => handleSubmit()} /> <SearchButton onClick={() => handleSubmit()} />

View File

@ -0,0 +1,164 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { ChevronDown, ChevronUp, FileText, Lightbulb, Globe } from 'lucide-react'
import { useState } from 'react'
interface SearchModeSelectProps {
searchMode: 'notes' | 'insights' | 'all'
onSearchModeChange: (mode: 'notes' | 'insights' | 'all') => void
}
export function SearchModeSelect({ searchMode, onSearchModeChange }: SearchModeSelectProps) {
const [isOpen, setIsOpen] = useState(false)
const searchModes = [
{
value: 'all' as const,
name: '全部',
icon: <Globe size={14} />,
description: '聚合搜索原始笔记和 AI 洞察'
},
{
value: 'notes' as const,
name: '原始笔记',
icon: <FileText size={14} />,
description: '搜索原始笔记内容'
},
{
value: 'insights' as const,
name: 'AI 洞察',
icon: <Lightbulb size={14} />,
description: '搜索 AI 洞察内容'
}
]
const currentMode = searchModes.find((m) => m.value === searchMode)
return (
<>
<DropdownMenu.Root open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenu.Trigger className="infio-chat-input-search-mode-select">
<span className="infio-search-mode-icon">{currentMode?.icon}</span>
<div className="infio-chat-input-search-mode-select__mode-name">
{currentMode?.name}
</div>
<div className="infio-chat-input-search-mode-select__icon">
{isOpen ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="infio-popover infio-search-mode-select-content">
<ul>
{searchModes.map((mode) => (
<DropdownMenu.Item
key={mode.value}
onSelect={() => {
onSearchModeChange(mode.value)
}}
asChild
>
<li className="infio-search-mode-item">
<div className="infio-search-mode-left">
<span className="infio-search-mode-icon">{mode.icon}</span>
<div className="infio-search-mode-info">
<span className="infio-search-mode-name">{mode.name}</span>
<span className="infio-search-mode-description">{mode.description}</span>
</div>
</div>
</li>
</DropdownMenu.Item>
))}
</ul>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
<style>{`
button.infio-chat-input-search-mode-select {
background-color: transparent;
box-shadow: none;
border: 1px solid var(--background-modifier-border);
padding: var(--size-2-1) var(--size-2-2);
font-size: var(--font-smallest);
font-weight: var(--font-medium);
color: var(--text-muted);
display: flex;
justify-content: flex-start;
align-items: center;
cursor: pointer;
height: auto;
max-width: 100%;
gap: var(--size-2-2);
border-radius: var(--radius-l);
transition: all 0.15s ease-in-out;
&:hover {
color: var(--text-normal);
background-color: var(--background-modifier-hover);
}
.infio-chat-input-search-mode-select__mode-name {
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 1;
}
.infio-chat-input-search-mode-select__icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
.infio-search-mode-select-content {
min-width: auto !important;
width: fit-content !important;
max-width: 280px;
}
.infio-search-mode-item {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: var(--size-4-2) var(--size-4-2);
white-space: nowrap;
}
.infio-search-mode-left {
display: flex;
align-items: center;
gap: var(--size-2-3);
}
.infio-search-mode-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-accent);
flex-shrink: 0;
}
.infio-search-mode-info {
display: flex;
flex-direction: column;
gap: var(--size-2-1);
}
.infio-search-mode-name {
flex-shrink: 0;
font-weight: var(--font-medium);
}
.infio-search-mode-description {
font-size: var(--font-smallest);
color: var(--text-muted);
flex-shrink: 0;
}
`}</style>
</>
)
}

View File

@ -254,7 +254,7 @@ export class VectorManager {
await backOff( await backOff(
async () => { async () => {
// 在嵌入之前处理 markdown,只处理一次 // 在嵌入之前处理 markdown
const cleanedBatchData = batchChunks.map(chunk => { const cleanedBatchData = batchChunks.map(chunk => {
const cleanContent = removeMarkdown(chunk.content).replace(/\0/g, '') const cleanContent = removeMarkdown(chunk.content).replace(/\0/g, '')
return { chunk, cleanContent } return { chunk, cleanContent }

View File

@ -8,36 +8,153 @@ interface EmbedResult {
vec: number[]; vec: number[];
tokens: number; tokens: number;
embed_input?: string; embed_input?: string;
error?: string;
} }
// 定义工作器消息的参数类型
interface LoadParams {
model_key: string;
use_gpu?: boolean;
}
interface EmbedBatchParams {
inputs: EmbedInput[];
}
type WorkerParams = LoadParams | EmbedBatchParams | string | undefined;
interface WorkerMessage { interface WorkerMessage {
method: string; method: string;
params: any; params: WorkerParams;
id: number; id: number;
worker_id?: string; worker_id?: string;
} }
interface WorkerResponse { interface WorkerResponse {
id: number; id: number;
result?: any; result?: unknown;
error?: string; error?: string;
worker_id?: string; worker_id?: string;
} }
// 定义 Transformers.js 相关类型
interface TransformersEnv {
allowLocalModels: boolean;
allowRemoteModels: boolean;
backends: {
onnx: {
wasm: {
numThreads: number;
simd: boolean;
};
};
};
useFS: boolean;
useBrowserCache: boolean;
remoteHost?: string;
}
interface PipelineOptions {
quantized?: boolean;
progress_callback?: (progress: unknown) => void;
device?: string;
dtype?: string;
}
interface ModelInfo {
loaded: boolean;
model_key: string;
use_gpu: boolean;
}
interface TokenizerResult {
input_ids: {
data: number[];
};
}
interface GlobalTransformers {
pipelineFactory: (task: string, model: string, options?: PipelineOptions) => Promise<unknown>;
AutoTokenizer: {
from_pretrained: (model: string) => Promise<unknown>;
};
env: TransformersEnv;
}
// 全局变量 // 全局变量
let model: any = null; let model: ModelInfo | null = null;
let pipeline: any = null; let pipeline: unknown = null;
let tokenizer: any = null; let tokenizer: unknown = null;
let processing_message = false; let processing_message = false;
let transformersLoaded = false; let transformersLoaded = false;
/**
* 访
* @param {string} url URL
* @param {number} timeout ()
* @returns {Promise<boolean>} 访 true false
*/
async function testEndpoint(url: string, timeout = 3000): Promise<boolean> {
// AbortController 用于在超时后取消 fetch 请求
const controller = new AbortController();
const signal = controller.signal;
const timeoutId = setTimeout(() => {
console.log(`请求 ${url} 超时。`);
controller.abort();
}, timeout);
try {
console.log(`正在测试端点: ${url}`);
// 我们使用 'HEAD' 方法,因为它只请求头部信息,非常快速,适合做存活检测。
// 'no-cors' 模式允许我们在浏览器环境中进行跨域请求以进行简单的可达性测试,
// 即使我们不能读取响应内容,请求成功也意味着网络是通的。
await fetch(url, { method: 'HEAD', mode: 'no-cors', signal });
// 如果 fetch 成功,清除超时定时器并返回 true
clearTimeout(timeoutId);
console.log(`端点 ${url} 可访问。`);
return true;
} catch (error) {
// 如果发生网络错误或请求被中止 (超时),则进入 catch 块
clearTimeout(timeoutId); // 同样需要清除定时器
console.warn(`无法访问端点 ${url}:`, error instanceof Error && error.name === 'AbortError' ? '超时' : (error as Error).message);
return false;
}
}
/**
* Hugging Face
*/
async function initializeEndpoint(): Promise<void> {
const defaultEndpoint = 'https://huggingface.co';
const fallbackEndpoint = 'https://hf-mirror.com';
const isDefaultReachable = await testEndpoint(defaultEndpoint);
const globalTransformers = globalThis as unknown as { transformers?: GlobalTransformers };
if (!isDefaultReachable) {
console.log(`默认端点不可达,将切换到备用镜像: ${fallbackEndpoint}`);
// 这是关键步骤:在代码中设置 endpoint
if (globalTransformers.transformers?.env) {
globalTransformers.transformers.env.remoteHost = fallbackEndpoint;
}
} else {
console.log(`将使用默认端点: ${defaultEndpoint}`);
}
}
// 动态导入 Transformers.js // 动态导入 Transformers.js
async function loadTransformers() { async function loadTransformers(): Promise<void> {
if (transformersLoaded) return; if (transformersLoaded) return;
try { try {
console.log('Loading Transformers.js...'); console.log('Loading Transformers.js...');
// 首先初始化端点
await initializeEndpoint();
// 尝试使用旧版本的 Transformers.js它在 Worker 中更稳定 // 尝试使用旧版本的 Transformers.js它在 Worker 中更稳定
const { pipeline: pipelineFactory, env, AutoTokenizer } = await import('@xenova/transformers'); const { pipeline: pipelineFactory, env, AutoTokenizer } = await import('@xenova/transformers');
@ -53,9 +170,12 @@ async function loadTransformers() {
env.useFS = false; env.useFS = false;
env.useBrowserCache = true; env.useBrowserCache = true;
(globalThis as any).pipelineFactory = pipelineFactory; const globalTransformers = globalThis as unknown as { transformers?: GlobalTransformers };
(globalThis as any).AutoTokenizer = AutoTokenizer; globalTransformers.transformers = {
(globalThis as any).env = env; pipelineFactory,
AutoTokenizer,
env
};
transformersLoaded = true; transformersLoaded = true;
console.log('Transformers.js loaded successfully'); console.log('Transformers.js loaded successfully');
@ -65,22 +185,27 @@ async function loadTransformers() {
} }
} }
async function loadModel(modelKey: string, useGpu: boolean = false) { async function loadModel(modelKey: string, useGpu: boolean = false): Promise<{ model_loaded: boolean }> {
try { try {
console.log(`Loading model: ${modelKey}, GPU: ${useGpu}`); console.log(`Loading model: ${modelKey}, GPU: ${useGpu}`);
// 确保 Transformers.js 已加载 // 确保 Transformers.js 已加载
await loadTransformers(); await loadTransformers();
const pipelineFactory = (globalThis as any).pipelineFactory; const globalTransformers = globalThis as unknown as { transformers?: GlobalTransformers };
const AutoTokenizer = (globalThis as any).AutoTokenizer; const transformers = globalTransformers.transformers;
const env = (globalThis as any).env;
if (!transformers) {
throw new Error('Transformers.js not loaded');
}
const { pipelineFactory, AutoTokenizer } = transformers;
// 配置管道选项 // 配置管道选项
const pipelineOpts: any = { const pipelineOpts: PipelineOptions = {
quantized: true, quantized: true,
// 修复进度回调,添加错误处理 // 修复进度回调,添加错误处理
progress_callback: (progress: any) => { progress_callback: (progress: unknown) => {
try { try {
if (progress && typeof progress === 'object') { if (progress && typeof progress === 'object') {
// console.log('Model loading progress:', progress); // console.log('Model loading progress:', progress);
@ -96,9 +221,9 @@ async function loadModel(modelKey: string, useGpu: boolean = false) {
if (useGpu) { if (useGpu) {
try { try {
// 检查 WebGPU 支持 // 检查 WebGPU 支持
console.log("useGpu", useGpu) console.log("useGpu", useGpu);
if (typeof navigator !== 'undefined' && 'gpu' in navigator) { if (typeof navigator !== 'undefined' && 'gpu' in navigator) {
const gpu = (navigator as any).gpu; const gpu = (navigator as { gpu?: { requestAdapter?: () => unknown } }).gpu;
if (gpu && typeof gpu.requestAdapter === 'function') { if (gpu && typeof gpu.requestAdapter === 'function') {
console.log('[Transformers] Attempting to use GPU'); console.log('[Transformers] Attempting to use GPU');
pipelineOpts.device = 'webgpu'; pipelineOpts.device = 'webgpu';
@ -137,21 +262,17 @@ async function loadModel(modelKey: string, useGpu: boolean = false) {
} }
} }
async function unloadModel() { async function unloadModel(): Promise<{ model_unloaded: boolean }> {
try { try {
console.log('Unloading model...'); console.log('Unloading model...');
if (pipeline) { if (pipeline && typeof pipeline === 'object' && 'destroy' in pipeline) {
if (pipeline.destroy) { const pipelineWithDestroy = pipeline as { destroy: () => void };
pipeline.destroy(); pipelineWithDestroy.destroy();
}
pipeline = null;
}
if (tokenizer) {
tokenizer = null;
} }
pipeline = null;
tokenizer = null;
model = null; model = null;
console.log('Model unloaded successfully'); console.log('Model unloaded successfully');
@ -163,13 +284,14 @@ async function unloadModel() {
} }
} }
async function countTokens(input: string) { async function countTokens(input: string): Promise<{ tokens: number }> {
try { try {
if (!tokenizer) { if (!tokenizer) {
throw new Error('Tokenizer not loaded'); throw new Error('Tokenizer not loaded');
} }
const { input_ids } = await tokenizer(input); const tokenizerWithCall = tokenizer as (input: string) => Promise<TokenizerResult>;
const { input_ids } = await tokenizerWithCall(input);
return { tokens: input_ids.data.length }; return { tokens: input_ids.data.length };
} catch (error) { } catch (error) {
@ -249,7 +371,8 @@ async function processBatch(batchInputs: EmbedInput[]): Promise<EmbedResult[]> {
); );
// 生成嵌入向量 // 生成嵌入向量
const resp = await pipeline(embedInputs, { pooling: 'mean', normalize: true }); const pipelineCall = pipeline as (inputs: string[], options: { pooling: string; normalize: boolean }) => Promise<{ data: number[] }[]>;
const resp = await pipelineCall(embedInputs, { pooling: 'mean', normalize: true });
// 处理结果 // 处理结果
return batchInputs.map((item, i) => ({ return batchInputs.map((item, i) => ({
@ -262,10 +385,11 @@ async function processBatch(batchInputs: EmbedInput[]): Promise<EmbedResult[]> {
console.error('Error processing batch:', error); console.error('Error processing batch:', error);
// 如果批处理失败,尝试逐个处理 // 如果批处理失败,尝试逐个处理
return Promise.all( const results = await Promise.all(
batchInputs.map(async (item) => { batchInputs.map(async (item): Promise<EmbedResult> => {
try { try {
const result = await pipeline(item.embed_input, { pooling: 'mean', normalize: true }); const pipelineCall = pipeline as (input: string, options: { pooling: string; normalize: boolean }) => Promise<{ data: number[] }[]>;
const result = await pipelineCall(item.embed_input, { pooling: 'mean', normalize: true });
const tokenCount = await countTokens(item.embed_input); const tokenCount = await countTokens(item.embed_input);
return { return {
@ -279,11 +403,13 @@ async function processBatch(batchInputs: EmbedInput[]): Promise<EmbedResult[]> {
vec: [], vec: [],
tokens: 0, tokens: 0,
embed_input: item.embed_input, embed_input: item.embed_input,
error: (singleError as Error).message error: singleError instanceof Error ? singleError.message : 'Unknown error'
} as any; };
} }
}) })
); );
return results;
} }
} }
@ -291,12 +417,13 @@ async function processMessage(data: WorkerMessage): Promise<WorkerResponse> {
const { method, params, id, worker_id } = data; const { method, params, id, worker_id } = data;
try { try {
let result: any; let result: unknown;
switch (method) { switch (method) {
case 'load': case 'load':
console.log('Load method called with params:', params); console.log('Load method called with params:', params);
result = await loadModel(params.model_key, params.use_gpu || false); const loadParams = params as LoadParams;
result = await loadModel(loadParams.model_key, loadParams.use_gpu || false);
break; break;
case 'unload': case 'unload':
@ -318,7 +445,8 @@ async function processMessage(data: WorkerMessage): Promise<WorkerResponse> {
} }
processing_message = true; processing_message = true;
result = await embedBatch(params.inputs); const embedParams = params as EmbedBatchParams;
result = await embedBatch(embedParams.inputs);
processing_message = false; processing_message = false;
break; break;
@ -336,7 +464,8 @@ async function processMessage(data: WorkerMessage): Promise<WorkerResponse> {
} }
processing_message = true; processing_message = true;
result = await countTokens(params); const tokenParams = params as string;
result = await countTokens(tokenParams);
processing_message = false; processing_message = false;
break; break;
@ -349,7 +478,7 @@ async function processMessage(data: WorkerMessage): Promise<WorkerResponse> {
} catch (error) { } catch (error) {
console.error('Error processing message:', error); console.error('Error processing message:', error);
processing_message = false; processing_message = false;
return { id, error: (error as Error).message, worker_id }; return { id, error: error instanceof Error ? error.message : 'Unknown error', worker_id };
} }
} }
@ -367,14 +496,14 @@ self.addEventListener('message', async (event) => {
return; return;
} }
const response = await processMessage(event.data); const response = await processMessage(event.data as WorkerMessage);
console.log('Worker sending response:', response); console.log('Worker sending response:', response);
self.postMessage(response); self.postMessage(response);
} catch (error) { } catch (error) {
console.error('Unhandled error in worker message handler:', error); console.error('Unhandled error in worker message handler:', error);
self.postMessage({ self.postMessage({
id: event.data?.id || -1, id: (event.data as { id?: number })?.id || -1,
error: `Worker error: ${error.message || 'Unknown error'}` error: `Worker error: ${error instanceof Error ? error.message : 'Unknown error'}`
}); });
} }
}); });

View File

@ -282,6 +282,24 @@ export const InfioSettingsSchema = z.object({
modelId: z.string(), modelId: z.string(),
})).catch([]), })).catch([]),
// Insight Model start list
collectedInsightModels: z.array(z.object({
provider: z.nativeEnum(ApiProvider),
modelId: z.string(),
})).catch([]),
// Apply Model start list
collectedApplyModels: z.array(z.object({
provider: z.nativeEnum(ApiProvider),
modelId: z.string(),
})).catch([]),
// Embedding Model start list
collectedEmbeddingModels: z.array(z.object({
provider: z.nativeEnum(ApiProvider),
modelId: z.string(),
})).catch([]),
// Active Provider Tab (for UI state) // Active Provider Tab (for UI state)
activeProviderTab: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio), activeProviderTab: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),

View File

@ -828,6 +828,24 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
word-break: break-all; word-break: break-all;
} }
.infio-search-lexical-content-editable-root {
min-height: 36px;
max-height: 500px;
overflow-y: auto;
}
.infio-search-lexical-content-editable-root .mention {
background-color: var(--tag-background);
color: var(--tag-color);
padding: var(--size-2-1) calc(var(--size-2-1));
border-radius: var(--radius-s);
background-color: var(--tag-background);
color: var(--tag-color);
padding: 0 calc(var(--size-2-1));
border-radius: var(--radius-s);
word-break: break-all;
}
.infio-chat-lexical-content-editable-paragraph { .infio-chat-lexical-content-editable-paragraph {
margin: 0; margin: 0;
line-height: 1.6; line-height: 1.6;