From 3db334c6e85b328308a17c8a7dd77bc9d1f95522 Mon Sep 17 00:00:00 2001 From: duanfuxiang Date: Mon, 7 Jul 2025 09:47:37 +0800 Subject: [PATCH] Optimize the search view component, add workspace statistics and RAG vector initialization features, update internationalization support, improve user interaction prompts, enhance log output, and ensure better user experience and code readability. --- src/components/chat-view/InsightView.tsx | 2 +- src/components/chat-view/SearchView.tsx | 884 +++++++++++++++++- src/core/rag/rag-engine.ts | 93 ++ src/database/modules/vector/vector-manager.ts | 414 ++++++++ .../modules/vector/vector-repository.ts | 90 ++ src/lang/locale/en.ts | 3 + src/lang/locale/zh-cn.ts | 3 + .../components/ModelProviderSettings.tsx | 39 +- src/types/settings.ts | 4 + src/utils/api.ts | 39 +- 10 files changed, 1532 insertions(+), 39 deletions(-) diff --git a/src/components/chat-view/InsightView.tsx b/src/components/chat-view/InsightView.tsx index 5c92026..54e0816 100644 --- a/src/components/chat-view/InsightView.tsx +++ b/src/components/chat-view/InsightView.tsx @@ -723,7 +723,7 @@ const InsightView = () => {
{t('insights.initConfirm.modelLabel')} - {settings.chatModelProvider} / {settings.chatModelId || t('insights.initConfirm.defaultModel')} + {settings.chatModelProvider}/{settings.chatModelId || t('insights.initConfirm.defaultModel')}
diff --git a/src/components/chat-view/SearchView.tsx b/src/components/chat-view/SearchView.tsx index 4a5e442..7c1b3b7 100644 --- a/src/components/chat-view/SearchView.tsx +++ b/src/components/chat-view/SearchView.tsx @@ -69,6 +69,35 @@ const SearchView = () => { // 当前搜索范围信息 const [currentSearchScope, setCurrentSearchScope] = useState('') + // 统计信息状态 + const [statisticsInfo, setStatisticsInfo] = useState<{ + totalFiles: number + totalChunks: number + } | null>(null) + const [isLoadingStats, setIsLoadingStats] = useState(false) + + // 工作区 RAG 向量初始化状态 + const [isInitializingRAG, setIsInitializingRAG] = useState(false) + const [ragInitProgress, setRAGInitProgress] = useState<{ + type: 'indexing' | 'querying' | 'querying-done' + indexProgress?: { + completedChunks: number + totalChunks: number + totalFiles: number + } + } | null>(null) + const [ragInitSuccess, setRAGInitSuccess] = useState<{ + show: boolean + totalFiles?: number + totalChunks?: number + workspaceName?: string + }>({ show: false }) + + // 删除和确认对话框状态 + const [isDeleting, setIsDeleting] = useState(false) + const [showRAGInitConfirm, setShowRAGInitConfirm] = useState(false) + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const handleSearch = useCallback(async (editorState?: SerializedEditorState) => { let searchTerm = '' @@ -171,6 +200,148 @@ const SearchView = () => { } }, [searchMode, handleSearch]) // 监听搜索模式变化 + // 加载统计信息 + const loadStatistics = useCallback(async () => { + setIsLoadingStats(true) + + try { + // 获取当前工作区 + let currentWorkspace: Workspace | null = null + if (settings.workspace && settings.workspace !== 'vault') { + currentWorkspace = await workspaceManager.findByName(String(settings.workspace)) + } + + const ragEngine = await getRAGEngine() + const stats = await ragEngine.getWorkspaceStatistics(currentWorkspace) + setStatisticsInfo(stats) + + } catch (error) { + console.error('加载统计信息失败:', error) + setStatisticsInfo({ totalFiles: 0, totalChunks: 0 }) + } finally { + setIsLoadingStats(false) + } + }, [getRAGEngine, settings, workspaceManager]) + + // 初始化工作区 RAG 向量 + const initializeWorkspaceRAG = useCallback(async () => { + setIsInitializingRAG(true) + setRAGInitProgress(null) + + try { + // 获取当前工作区 + let currentWorkspace: Workspace | null = null + if (settings.workspace && settings.workspace !== 'vault') { + currentWorkspace = await workspaceManager.findByName(String(settings.workspace)) + } + + if (!currentWorkspace) { + // 如果没有当前工作区,使用默认的 vault 工作区 + currentWorkspace = await workspaceManager.ensureDefaultVaultWorkspace() + } + + const ragEngine = await getRAGEngine() + + // 使用新的 updateWorkspaceIndex 方法 + await ragEngine.updateWorkspaceIndex( + currentWorkspace, + { reindexAll: true }, + (progress) => { + setRAGInitProgress(progress) + } + ) + + // 刷新统计信息 + await loadStatistics() + + // 显示成功消息 + console.log(`✅ 工作区 RAG 向量初始化完成: ${currentWorkspace.name}`) + + // 显示成功状态 + setRAGInitSuccess({ + show: true, + totalFiles: ragInitProgress?.indexProgress?.totalFiles || 0, + totalChunks: ragInitProgress?.indexProgress?.totalChunks || 0, + workspaceName: currentWorkspace.name + }) + + // 3秒后自动隐藏成功消息 + setTimeout(() => { + setRAGInitSuccess({ show: false }) + }, 5000) + + } catch (error) { + console.error('工作区 RAG 向量初始化失败:', error) + setRAGInitSuccess({ show: false }) + } finally { + setIsInitializingRAG(false) + setRAGInitProgress(null) + } + }, [getRAGEngine, settings, workspaceManager, loadStatistics]) + + // 清除工作区索引 + const clearWorkspaceIndex = useCallback(async () => { + setIsDeleting(true) + + try { + // 获取当前工作区 + let currentWorkspace: Workspace | null = null + if (settings.workspace && settings.workspace !== 'vault') { + currentWorkspace = await workspaceManager.findByName(String(settings.workspace)) + } + + const ragEngine = await getRAGEngine() + await ragEngine.clearWorkspaceIndex(currentWorkspace) + + // 刷新统计信息 + await loadStatistics() + + console.log('✅ 工作区索引清除完成') + + } catch (error) { + console.error('清除工作区索引失败:', error) + } finally { + setIsDeleting(false) + } + }, [getRAGEngine, settings, workspaceManager, loadStatistics]) + + // 组件加载时自动获取统计信息 + useEffect(() => { + loadStatistics() + }, [loadStatistics]) + + // 确认初始化/更新 RAG 向量 + const handleInitWorkspaceRAG = useCallback(() => { + setShowRAGInitConfirm(true) + }, []) + + // 确认删除工作区索引 + const handleDeleteWorkspaceIndex = useCallback(() => { + setShowDeleteConfirm(true) + }, []) + + // 确认初始化 RAG 向量 + const confirmInitWorkspaceRAG = useCallback(async () => { + setShowRAGInitConfirm(false) + await initializeWorkspaceRAG() + }, [initializeWorkspaceRAG]) + + // 确认删除工作区索引 + const confirmDeleteWorkspaceIndex = useCallback(async () => { + setShowDeleteConfirm(false) + await clearWorkspaceIndex() + }, [clearWorkspaceIndex]) + + // 取消初始化确认 + const cancelRAGInitConfirm = useCallback(() => { + setShowRAGInitConfirm(false) + }, []) + + // 取消删除确认 + const cancelDeleteConfirm = useCallback(() => { + setShowDeleteConfirm(false) + }, []) + const handleResultClick = (result: Omit & { similarity: number }) => { // 如果用户正在选择文本,不触发点击事件 const selection = window.getSelection() @@ -355,36 +526,80 @@ const SearchView = () => { return (
- {/* 搜索输入框 */} -
- - - {/* 搜索模式切换 */} -
- - + {/* 头部信息 */} +
+
+

语义索引

+
+ + +
+
+ + {/* 统计信息 */} + {!isLoadingStats && statisticsInfo && ( +
+
+
+ {statisticsInfo.totalChunks} + 个向量块 +
+
+
+ 📄 + {statisticsInfo.totalFiles} + 文件 +
+
+
+
+ )} + + {/* 搜索输入框 */} +
+ + + {/* 搜索模式切换 */} +
+ + +
@@ -398,11 +613,6 @@ const SearchView = () => { `${insightGroupedResults.length} 个文件,${insightResults.length} 个洞察` )}
- {currentSearchScope && ( -
- 搜索范围: {currentSearchScope} -
- )}
)} @@ -413,6 +623,151 @@ const SearchView = () => {
)} + {/* RAG 初始化进度 */} + {isInitializingRAG && ( +
+
+

正在初始化工作区 RAG 向量索引

+

为当前工作区的文件建立向量索引,提高搜索精度

+
+ {ragInitProgress && ragInitProgress.type === 'indexing' && ragInitProgress.indexProgress && ( +
+
+ 建立向量索引 + + {ragInitProgress.indexProgress.completedChunks} / {ragInitProgress.indexProgress.totalChunks} 块 + +
+
+
+
+
+
+ 共 {ragInitProgress.indexProgress.totalFiles} 个文件 +
+
+ {Math.round((ragInitProgress.indexProgress.completedChunks / Math.max(ragInitProgress.indexProgress.totalChunks, 1)) * 100)}% +
+
+
+ )} +
+ )} + + {/* RAG 初始化成功消息 */} + {ragInitSuccess.show && ( +
+
+ +
+ + 工作区 RAG 向量索引初始化完成: {ragInitSuccess.workspaceName} + + + 处理了 {ragInitSuccess.totalFiles} 个文件,生成 {ragInitSuccess.totalChunks} 个向量块 + +
+ +
+
+ )} + + {/* 确认删除对话框 */} + {showDeleteConfirm && ( +
+
+
+

清除工作区索引

+
+
+

+ 将清除当前工作区的所有向量索引数据。 +

+

+ 此操作无法撤销,清除后需要重新初始化索引才能进行语义搜索。 +

+
+ 工作区: {settings.workspace === 'vault' ? '整个 Vault' : settings.workspace} +
+
+
+ + +
+
+
+ )} + + {/* 确认初始化对话框 */} + {showRAGInitConfirm && ( +
+
+
+

{statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '更新工作区索引' : '初始化工作区索引'}

+
+
+

+ {statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) + ? '将更新当前工作区的向量索引,重新处理所有文件以确保索引最新。' + : '将为当前工作区的所有文件建立向量索引,这将提高语义搜索的准确性。' + } +

+
+
+ 嵌入模型: + + {settings.embeddingModelProvider} / {settings.embeddingModelId || '默认模型'} + +
+
+ 工作区: + + {settings.workspace === 'vault' ? '整个 Vault' : settings.workspace} + +
+
+

+ 此操作可能需要几分钟时间,具体取决于文件数量和大小。 +

+
+
+ + +
+
+
+ )} + {/* 搜索结果 */}
{searchMode === 'notes' ? ( @@ -552,8 +907,161 @@ const SearchView = () => { font-family: var(--font-interface); } - .obsidian-search-header { + .obsidian-search-header-wrapper { padding: 12px; + border-bottom: 1px solid var(--background-modifier-border); + } + + .obsidian-search-title { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + } + + .obsidian-search-title h3 { + margin: 0; + color: var(--text-normal); + font-size: var(--font-ui-large); + font-weight: 600; + } + + .obsidian-search-actions { + display: flex; + gap: 8px; + } + + .obsidian-search-init-btn { + padding: 6px 12px; + background-color: var(--interactive-accent); + border: none; + border-radius: var(--radius-s); + color: var(--text-on-accent); + font-size: var(--font-ui-small); + cursor: pointer; + transition: background-color 0.2s ease; + font-weight: 500; + } + + .obsidian-search-init-btn:hover:not(:disabled) { + background-color: var(--interactive-accent-hover); + } + + .obsidian-search-init-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .obsidian-search-delete-btn { + padding: 6px 12px; + background-color: #dc3545; + border: none; + border-radius: var(--radius-s); + color: white; + font-size: var(--font-ui-small); + cursor: pointer; + transition: background-color 0.2s ease; + font-weight: 500; + } + + .obsidian-search-delete-btn:hover:not(:disabled) { + background-color: #c82333; + } + + .obsidian-search-delete-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .obsidian-search-stats { + background-color: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + padding: 12px; + margin-bottom: 12px; + } + + .obsidian-search-stats-overview { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + } + + .obsidian-search-stats-main { + display: flex; + align-items: baseline; + gap: 6px; + } + + .obsidian-search-stats-number { + font-size: var(--font-ui-large); + font-weight: 700; + color: var(--text-accent); + font-family: var(--font-monospace); + } + + .obsidian-search-stats-label { + font-size: var(--font-ui-medium); + color: var(--text-normal); + font-weight: 500; + } + + .obsidian-search-stats-breakdown { + flex: 1; + display: flex; + justify-content: flex-end; + } + + .obsidian-search-stats-item { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + background-color: var(--background-modifier-border); + border-radius: var(--radius-s); + } + + .obsidian-search-stats-item-icon { + font-size: 12px; + line-height: 1; + } + + .obsidian-search-stats-item-value { + font-size: var(--font-ui-small); + font-weight: 600; + color: var(--text-normal); + font-family: var(--font-monospace); + } + + .obsidian-search-stats-item-label { + font-size: var(--font-ui-smaller); + color: var(--text-muted); + } + + .obsidian-search-scope { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + background-color: var(--background-modifier-border-hover); + border-radius: var(--radius-s); + } + + .obsidian-search-scope-label { + font-size: var(--font-ui-smaller); + color: var(--text-muted); + font-weight: 500; + } + + .obsidian-search-scope-value { + font-size: var(--font-ui-smaller); + color: var(--text-accent); + font-weight: 600; + } + + .obsidian-search-input-section { + /* padding 由父元素控制 */ } .obsidian-search-mode-toggle { @@ -588,6 +1096,8 @@ const SearchView = () => { font-weight: 500; } + + .obsidian-search-stats { padding: 8px 12px; font-size: var(--font-ui-small); @@ -884,6 +1394,308 @@ const SearchView = () => { user-select: text; cursor: text; } + + /* RAG 初始化进度样式 */ + .obsidian-rag-initializing { + padding: 20px; + background-color: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + margin: 12px; + } + + .obsidian-rag-init-header { + text-align: center; + margin-bottom: 16px; + } + + .obsidian-rag-init-header h4 { + margin: 0 0 8px 0; + color: var(--text-normal); + font-size: var(--font-ui-medium); + font-weight: 600; + } + + .obsidian-rag-init-header p { + margin: 0; + color: var(--text-muted); + font-size: var(--font-ui-small); + } + + .obsidian-rag-progress { + background-color: var(--background-primary); + padding: 12px; + border-radius: var(--radius-s); + border: 1px solid var(--background-modifier-border); + } + + .obsidian-rag-progress-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + .obsidian-rag-progress-stage { + color: var(--text-normal); + font-size: var(--font-ui-small); + font-weight: 500; + } + + .obsidian-rag-progress-counter { + color: var(--text-muted); + font-size: var(--font-ui-small); + font-family: var(--font-monospace); + } + + .obsidian-rag-progress-bar { + width: 100%; + height: 6px; + background-color: var(--background-modifier-border); + border-radius: 3px; + overflow: hidden; + margin-bottom: 8px; + } + + .obsidian-rag-progress-fill { + height: 100%; + background-color: var(--interactive-accent); + border-radius: 3px; + transition: width 0.3s ease; + } + + .obsidian-rag-progress-details { + display: flex; + justify-content: space-between; + align-items: center; + } + + .obsidian-rag-progress-files { + color: var(--text-normal); + font-size: var(--font-ui-small); + font-weight: 500; + } + + .obsidian-rag-progress-percentage { + color: var(--text-accent); + font-size: var(--font-ui-small); + font-weight: 600; + font-family: var(--font-monospace); + } + + /* RAG 初始化成功样式 */ + .obsidian-rag-success { + background-color: var(--background-secondary); + border: 1px solid var(--color-green, #28a745); + border-radius: var(--radius-m); + margin: 12px; + animation: slideInFromTop 0.3s ease-out; + } + + .obsidian-rag-success-content { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + } + + .obsidian-rag-success-icon { + font-size: 16px; + line-height: 1; + color: var(--color-green, #28a745); + flex-shrink: 0; + } + + .obsidian-rag-success-text { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + min-width: 0; + } + + .obsidian-rag-success-title { + font-size: var(--font-ui-medium); + font-weight: 600; + color: var(--text-normal); + line-height: 1.3; + } + + .obsidian-rag-success-summary { + font-size: var(--font-ui-small); + color: var(--text-muted); + line-height: 1.3; + } + + .obsidian-rag-success-close { + background: none; + border: none; + color: var(--text-muted); + font-size: 16px; + font-weight: bold; + cursor: pointer; + padding: 4px; + border-radius: var(--radius-s); + transition: all 0.2s ease; + flex-shrink: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + } + + .obsidian-rag-success-close:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); + } + + /* 确认对话框样式 */ + .obsidian-confirm-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + .obsidian-confirm-dialog { + background-color: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-l); + box-shadow: var(--shadow-l); + max-width: 400px; + width: 90%; + max-height: 80vh; + overflow: hidden; + } + + .obsidian-confirm-dialog-header { + padding: 16px 20px; + border-bottom: 1px solid var(--background-modifier-border); + background-color: var(--background-secondary); + } + + .obsidian-confirm-dialog-header h3 { + margin: 0; + color: var(--text-normal); + font-size: var(--font-ui-large); + font-weight: 600; + } + + .obsidian-confirm-dialog-body { + padding: 20px; + color: var(--text-normal); + font-size: var(--font-ui-medium); + line-height: 1.5; + } + + .obsidian-confirm-dialog-body p { + margin: 0 0 12px 0; + } + + .obsidian-confirm-dialog-warning { + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + padding: 12px; + margin: 12px 0; + color: var(--text-error); + font-size: var(--font-ui-small); + font-weight: 500; + } + + .obsidian-confirm-dialog-scope { + background-color: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + padding: 8px 12px; + margin: 12px 0 0 0; + font-size: var(--font-ui-small); + color: var(--text-muted); + } + + .obsidian-confirm-dialog-info { + background-color: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + padding: 12px; + margin: 12px 0; + } + + .obsidian-confirm-dialog-info-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: var(--font-ui-small); + } + + .obsidian-confirm-dialog-info-item:last-child { + margin-bottom: 0; + } + + .obsidian-confirm-dialog-info-item strong { + color: var(--text-normal); + margin-right: 12px; + flex-shrink: 0; + } + + .obsidian-confirm-dialog-model, + .obsidian-confirm-dialog-workspace { + color: var(--text-accent); + font-weight: 600; + font-family: var(--font-monospace); + text-align: right; + flex: 1; + word-break: break-all; + } + + .obsidian-confirm-dialog-footer { + padding: 16px 20px; + border-top: 1px solid var(--background-modifier-border); + background-color: var(--background-secondary); + display: flex; + justify-content: flex-end; + gap: 12px; + } + + .obsidian-confirm-dialog-cancel-btn { + padding: 8px 16px; + background-color: var(--interactive-normal); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + color: var(--text-normal); + font-size: var(--font-ui-small); + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; + } + + .obsidian-confirm-dialog-cancel-btn:hover { + background-color: var(--interactive-hover); + } + + .obsidian-confirm-dialog-confirm-btn { + padding: 8px 16px; + background-color: #dc3545; + border: 1px solid #dc3545; + border-radius: var(--radius-s); + color: white; + font-size: var(--font-ui-small); + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; + } + + .obsidian-confirm-dialog-confirm-btn:hover { + background-color: #c82333; + border-color: #c82333; + } `}
diff --git a/src/core/rag/rag-engine.ts b/src/core/rag/rag-engine.ts index e03c42e..d25b2ae 100644 --- a/src/core/rag/rag-engine.ts +++ b/src/core/rag/rag-engine.ts @@ -2,11 +2,13 @@ import { App, TFile } from 'obsidian' import { QueryProgressState } from '../../components/chat-view/QueryProgress' import { DBManager } from '../../database/database-manager' +import { Workspace } from '../../database/json/workspace/types' import { VectorManager } from '../../database/modules/vector/vector-manager' import { SelectVector } from '../../database/schema' import { EmbeddingModel } from '../../types/embedding' import { ApiProvider } from '../../types/llm/model' import { InfioSettings } from '../../types/settings' +import { getFilesWithTag } from '../../utils/glob-utils' import { getEmbeddingModel } from './embedding' @@ -103,6 +105,36 @@ export class RAGEngine { this.initialized = true } + async updateWorkspaceIndex( + workspace: Workspace, + options: { reindexAll: boolean }, + onQueryProgressChange?: (queryProgress: QueryProgressState) => void, + ): Promise { + if (!this.embeddingModel) { + throw new Error('Embedding model is not set') + } + await this.initializeDimension() + + await this.vectorManager.updateWorkspaceIndex( + this.embeddingModel, + workspace, + { + chunkSize: this.settings.ragOptions.chunkSize, + batchSize: this.settings.ragOptions.batchSize, + excludePatterns: this.settings.ragOptions.excludePatterns, + includePatterns: this.settings.ragOptions.includePatterns, + reindexAll: options.reindexAll, + }, + (indexProgress) => { + onQueryProgressChange?.({ + type: 'indexing', + indexProgress, + }) + }, + ) + this.initialized = true + } + async updateFileIndex(file: TFile) { if (!this.embeddingModel) { throw new Error('Embedding model is not set') @@ -185,4 +217,65 @@ export class RAGEngine { } return this.embeddingModel.getEmbedding(query) } + + async getWorkspaceStatistics(workspace?: Workspace): Promise<{ + totalFiles: number + totalChunks: number + }> { + if (!this.embeddingModel) { + throw new Error('Embedding model is not set') + } + await this.initializeDimension() + return await this.vectorManager.getWorkspaceStatistics(this.embeddingModel, workspace) + } + + async getVaultStatistics(): Promise<{ + totalFiles: number + totalChunks: number + }> { + if (!this.embeddingModel) { + throw new Error('Embedding model is not set') + } + await this.initializeDimension() + return await this.vectorManager.getVaultStatistics(this.embeddingModel) + } + + async clearWorkspaceIndex(workspace?: Workspace): Promise { + if (!this.embeddingModel) { + throw new Error('Embedding model is not set') + } + await this.initializeDimension() + + if (workspace) { + // 获取工作区中的所有文件路径 + const folders: string[] = [] + const files: string[] = [] + + for (const item of workspace.content) { + if (item.type === 'folder') { + const folderPath = item.content + + // 获取文件夹下的所有文件 + const folderFiles = this.app.vault.getMarkdownFiles().filter(file => + file.path.startsWith(folderPath === '/' ? '' : folderPath + '/') + ) + + files.push(...folderFiles.map(file => file.path)) + } else if (item.type === 'tag') { + // 获取标签对应的所有文件 + const tagFiles = getFilesWithTag(item.content, this.app) + files.push(...tagFiles) + } + } + + // 删除工作区相关的向量 + if (files.length > 0) { + // 通过 VectorManager 的私有 repository 访问 + await this.vectorManager['repository'].deleteVectorsForMultipleFiles(files, this.embeddingModel) + } + } else { + // 清除所有向量 + await this.vectorManager['repository'].clearAllVectors(this.embeddingModel) + } + } } diff --git a/src/database/modules/vector/vector-manager.ts b/src/database/modules/vector/vector-manager.ts index 5ccb058..6f72a1c 100644 --- a/src/database/modules/vector/vector-manager.ts +++ b/src/database/modules/vector/vector-manager.ts @@ -15,6 +15,8 @@ import { import { InsertVector, SelectVector } from '../../../database/schema'; import { EmbeddingModel } from '../../../types/embedding'; import { openSettingsModalWithError } from '../../../utils/open-settings-modal'; +import { getFilesWithTag } from '../../../utils/glob-utils'; +import { Workspace } from '../../json/workspace/types'; import { DBManager } from '../../database-manager'; import { VectorRepository } from './vector-repository'; @@ -53,6 +55,50 @@ export class VectorManager { ) } + async getWorkspaceStatistics( + embeddingModel: EmbeddingModel, + workspace?: Workspace + ): Promise<{ + totalFiles: number + totalChunks: number + }> { + // 构建工作区范围 + let scope: { files: string[], folders: string[] } | undefined + if (workspace) { + const folders: string[] = [] + const files: string[] = [] + + // 处理工作区中的文件夹和标签 + for (const item of workspace.content) { + if (item.type === 'folder') { + folders.push(item.content) + } else if (item.type === 'tag') { + // 获取标签对应的所有文件 + const tagFiles = getFilesWithTag(item.content, this.app) + files.push(...tagFiles) + } + } + + // 只有当有文件夹或文件时才设置 scope + if (folders.length > 0 || files.length > 0) { + scope = { files, folders } + } + } + + if (scope) { + return await this.repository.getWorkspaceStatistics(embeddingModel, scope) + } else { + return await this.repository.getVaultStatistics(embeddingModel) + } + } + + async getVaultStatistics(embeddingModel: EmbeddingModel): Promise<{ + totalFiles: number + totalChunks: number + }> { + return await this.repository.getVaultStatistics(embeddingModel) + } + // 强制垃圾回收的辅助方法 private forceGarbageCollection() { try { @@ -352,6 +398,289 @@ export class VectorManager { } } + async updateWorkspaceIndex( + embeddingModel: EmbeddingModel, + workspace: Workspace, + options: { + chunkSize: number + batchSize: number + excludePatterns: string[] + includePatterns: string[] + reindexAll?: boolean + }, + updateProgress?: (indexProgress: IndexProgress) => void, + ): Promise { + let filesToIndex: TFile[] + if (options.reindexAll) { + console.log("updateWorkspaceIndex reindexAll") + filesToIndex = await this.getFilesToIndexInWorkspace({ + embeddingModel: embeddingModel, + workspace: workspace, + excludePatterns: options.excludePatterns, + includePatterns: options.includePatterns, + reindexAll: true, + }) + // 只清理工作区相关的向量,而不是全部 + const workspaceFilePaths = filesToIndex.map((file) => file.path) + if (workspaceFilePaths.length > 0) { + await this.repository.deleteVectorsForMultipleFiles(workspaceFilePaths, embeddingModel) + } + } else { + console.log("updateWorkspaceIndex for update files") + await this.cleanVectorsForDeletedFiles(embeddingModel) + console.log("updateWorkspaceIndex cleanVectorsForDeletedFiles") + filesToIndex = await this.getFilesToIndexInWorkspace({ + embeddingModel: embeddingModel, + workspace: workspace, + excludePatterns: options.excludePatterns, + includePatterns: options.includePatterns, + }) + console.log("get workspace files to index: ", filesToIndex.length) + await this.repository.deleteVectorsForMultipleFiles( + filesToIndex.map((file) => file.path), + embeddingModel, + ) + console.log("delete vectors for workspace files: ", filesToIndex.length) + } + console.log("get workspace files to index: ", filesToIndex.length) + + if (filesToIndex.length === 0) { + return + } + + // Embed the files (使用与 updateVaultIndex 相同的逻辑) + const overlap = Math.floor(options.chunkSize * 0.15) + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: options.chunkSize, + chunkOverlap: overlap, + separators: [ + "\n\n", + "\n", + ".", + ",", + " ", + "\u200b", // Zero-width space + "\uff0c", // Fullwidth comma + "\u3001", // Ideographic comma + "\uff0e", // Fullwidth full stop + "\u3002", // Ideographic full stop + "", + ], + }); + console.log("textSplitter chunkSize: ", options.chunkSize, "overlap: ", overlap) + + const skippedFiles: string[] = [] + const contentChunks: InsertVector[] = ( + await Promise.all( + filesToIndex.map(async (file) => { + try { + let fileContent = await this.app.vault.cachedRead(file) + // 清理null字节,防止PostgreSQL UTF8编码错误 + fileContent = fileContent.replace(/\0/g, '') + const fileDocuments = await textSplitter.createDocuments([ + fileContent, + ]) + return fileDocuments + .map((chunk): InsertVector | null => { + // 保存原始内容,不在此处调用 removeMarkdown + const rawContent = chunk.pageContent.replace(/\0/g, '') + if (!rawContent || rawContent.trim().length === 0) { + return null + } + return { + path: file.path, + mtime: file.stat.mtime, + content: rawContent, // 保存原始内容 + embedding: [], + metadata: { + startLine: Number(chunk.metadata.loc.lines.from), + endLine: Number(chunk.metadata.loc.lines.to), + }, + } + }) + .filter((chunk): chunk is InsertVector => chunk !== null) + } catch (error) { + console.warn(`跳过文件 ${file.path}:`, error.message) + skippedFiles.push(file.path) + return [] + } + }), + ) + ).flat() + + console.log("contentChunks: ", contentChunks.length) + + if (skippedFiles.length > 0) { + console.warn(`跳过了 ${skippedFiles.length} 个有问题的文件:`, skippedFiles) + new Notice(`跳过了 ${skippedFiles.length} 个有问题的文件`) + } + + updateProgress?.({ + completedChunks: 0, + totalChunks: contentChunks.length, + totalFiles: filesToIndex.length, + }) + + const embeddingProgress = { completed: 0 } + // 减少批量大小以降低内存压力 + const batchSize = options.batchSize + let batchCount = 0 + + try { + if (embeddingModel.supportsBatch) { + // 支持批量处理的提供商:使用流式处理逻辑 + for (let i = 0; i < contentChunks.length; i += batchSize) { + batchCount++ + const batchChunks = contentChunks.slice(i, Math.min(i + batchSize, contentChunks.length)) + + const embeddedBatch: InsertVector[] = [] + + await backOff( + async () => { + // 在嵌入之前处理 markdown,只处理一次 + const cleanedBatchData = batchChunks.map(chunk => { + const cleanContent = removeMarkdown(chunk.content).replace(/\0/g, '') + return { chunk, cleanContent } + }).filter(({ cleanContent }) => cleanContent && cleanContent.trim().length > 0) + + if (cleanedBatchData.length === 0) { + return + } + + const batchTexts = cleanedBatchData.map(({ cleanContent }) => cleanContent) + const batchEmbeddings = await embeddingModel.getBatchEmbeddings(batchTexts) + + // 合并embedding结果到chunk数据 + for (let j = 0; j < cleanedBatchData.length; j++) { + const { chunk, cleanContent } = cleanedBatchData[j] + const embeddedChunk: InsertVector = { + path: chunk.path, + mtime: chunk.mtime, + content: cleanContent, // 使用已经清理过的内容 + embedding: batchEmbeddings[j], + metadata: chunk.metadata, + } + embeddedBatch.push(embeddedChunk) + } + }, + { + numOfAttempts: 3, // 减少重试次数 + startingDelay: 500, // 减少延迟 + timeMultiple: 1.5, + jitter: 'full', + }, + ) + + // 立即插入当前批次,避免内存累积 + if (embeddedBatch.length > 0) { + await this.repository.insertVectors(embeddedBatch, embeddingModel) + // 清理批次数据 + embeddedBatch.length = 0 + } + + embeddingProgress.completed += batchChunks.length + updateProgress?.({ + completedChunks: embeddingProgress.completed, + totalChunks: contentChunks.length, + totalFiles: filesToIndex.length, + }) + + // 定期内存清理 + await this.memoryCleanup(batchCount) + } + } else { + // 不支持批量处理的提供商:使用流式处理逻辑 + const limit = pLimit(32) // 从50降低到10,减少并发压力 + const abortController = new AbortController() + + // 流式处理:分批处理并立即插入 + for (let i = 0; i < contentChunks.length; i += batchSize) { + if (abortController.signal.aborted) { + throw new Error('Operation was aborted') + } + + batchCount++ + const batchChunks = contentChunks.slice(i, Math.min(i + batchSize, contentChunks.length)) + const embeddedBatch: InsertVector[] = [] + + const tasks = batchChunks.map((chunk) => + limit(async () => { + if (abortController.signal.aborted) { + throw new Error('Operation was aborted') + } + try { + await backOff( + async () => { + // 在嵌入之前处理 markdown + const cleanContent = removeMarkdown(chunk.content).replace(/\0/g, '') + // 跳过清理后为空的内容 + if (!cleanContent || cleanContent.trim().length === 0) { + return + } + + const embedding = await embeddingModel.getEmbedding(cleanContent) + const embeddedChunk = { + path: chunk.path, + mtime: chunk.mtime, + content: cleanContent, // 使用清理后的内容 + embedding, + metadata: chunk.metadata, + } + embeddedBatch.push(embeddedChunk) + }, + { + numOfAttempts: 3, // 减少重试次数 + startingDelay: 500, // 减少延迟 + timeMultiple: 1.5, + jitter: 'full', + }, + ) + } catch (error) { + abortController.abort() + throw error + } + }), + ) + + await Promise.all(tasks) + + // 立即插入当前批次 + if (embeddedBatch.length > 0) { + await this.repository.insertVectors(embeddedBatch, embeddingModel) + // 清理批次数据 + embeddedBatch.length = 0 + } + + embeddingProgress.completed += batchChunks.length + updateProgress?.({ + completedChunks: embeddingProgress.completed, + totalChunks: contentChunks.length, + totalFiles: filesToIndex.length, + }) + + // 定期内存清理 + await this.memoryCleanup(batchCount) + } + } + } catch (error) { + if ( + error instanceof LLMAPIKeyNotSetException || + error instanceof LLMAPIKeyInvalidException || + error instanceof LLMBaseUrlNotSetException + ) { + openSettingsModalWithError(this.app, error.message) + } else if (error instanceof LLMRateLimitExceededException) { + new Notice(error.message) + } else { + console.error('Error embedding chunks:', error) + throw error + } + } finally { + // 最终清理 + this.forceGarbageCollection() + } + } + async UpdateFileVectorIndex( embeddingModel: EmbeddingModel, chunkSize: number, @@ -615,4 +944,89 @@ export class VectorManager { return [] } } + + private async getFilesToIndexInWorkspace({ + embeddingModel, + workspace, + excludePatterns, + includePatterns, + reindexAll, + }: { + embeddingModel: EmbeddingModel + workspace: Workspace + excludePatterns: string[] + includePatterns: string[] + reindexAll?: boolean + }): Promise { + // 获取工作区中的所有文件 + const workspaceFiles = new Set() + + if (workspace) { + // 处理工作区中的文件夹和标签 + for (const item of workspace.content) { + if (item.type === 'folder') { + const folderPath = item.content + + // 获取文件夹下的所有文件 + const files = this.app.vault.getMarkdownFiles().filter(file => + file.path.startsWith(folderPath === '/' ? '' : folderPath + '/') + ) + + // 添加所有文件路径 + files.forEach(file => { + workspaceFiles.add(file.path) + }) + + } else if (item.type === 'tag') { + // 获取标签对应的所有文件 + const tagFiles = getFilesWithTag(item.content, this.app) + + tagFiles.forEach(filePath => { + workspaceFiles.add(filePath) + }) + } + } + } + + // 将路径转换为 TFile 对象 + let filesToIndex = Array.from(workspaceFiles) + .map(path => this.app.vault.getFileByPath(path)) + .filter((file): file is TFile => file !== null && file instanceof TFile) + + console.log("get workspace files: ", filesToIndex.length) + + // 应用排除和包含模式 + filesToIndex = filesToIndex.filter((file) => { + return !excludePatterns.some((pattern) => minimatch(file.path, pattern)) + }) + + if (includePatterns.length > 0) { + filesToIndex = filesToIndex.filter((file) => { + return includePatterns.some((pattern) => minimatch(file.path, pattern)) + }) + } + + if (reindexAll) { + return filesToIndex + } + + // 优化流程:使用数据库最大mtime来过滤需要更新的文件 + try { + const maxMtime = await this.repository.getMaxMtime(embeddingModel) + console.log("Database max mtime:", maxMtime) + + if (maxMtime === null) { + // 数据库中没有任何向量,需要索引所有文件 + return filesToIndex + } + + // 筛选出在数据库最后更新时间之后修改的文件 + return filesToIndex.filter((file) => { + return file.stat.mtime > maxMtime + }) + } catch (error) { + console.error("Error getting max mtime from database:", error) + return [] + } + } } diff --git a/src/database/modules/vector/vector-repository.ts b/src/database/modules/vector/vector-repository.ts index e7d07d8..c989ed3 100644 --- a/src/database/modules/vector/vector-repository.ts +++ b/src/database/modules/vector/vector-repository.ts @@ -188,4 +188,94 @@ export class VectorRepository { const result = await this.db.query(query, params) return result.rows } + + async getWorkspaceStatistics( + embeddingModel: EmbeddingModel, + scope?: { + files: string[] + folders: string[] + } + ): Promise<{ + totalFiles: number + totalChunks: number + }> { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + + let scopeCondition = '' + const params: unknown[] = [] + let paramIndex = 1 + + if (scope) { + const conditions: string[] = [] + + if (scope.files.length > 0) { + conditions.push(`path = ANY($${paramIndex})`) + params.push(scope.files) + paramIndex++ + } + + if (scope.folders.length > 0) { + const folderConditions = scope.folders.map((folder, idx) => { + params.push(`${folder}/%`) + return `path LIKE $${paramIndex + idx}` + }) + conditions.push(`(${folderConditions.join(' OR ')})`) + paramIndex += scope.folders.length + } + + if (conditions.length > 0) { + scopeCondition = `WHERE (${conditions.join(' OR ')})` + } + } + + const query = ` + SELECT + COUNT(DISTINCT path) as total_files, + COUNT(*) as total_chunks + FROM "${tableName}" + ${scopeCondition} + ` + + const result = await this.db.query<{ + total_files: number + total_chunks: number + }>(query, params) + + const row = result.rows[0] + return { + totalFiles: Number(row?.total_files || 0), + totalChunks: Number(row?.total_chunks || 0) + } + } + + async getVaultStatistics(embeddingModel: EmbeddingModel): Promise<{ + totalFiles: number + totalChunks: number + }> { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + + const query = ` + SELECT + COUNT(DISTINCT path) as total_files, + COUNT(*) as total_chunks + FROM "${tableName}" + ` + + const result = await this.db.query<{ + total_files: number + total_chunks: number + }>(query) + + const row = result.rows[0] + return { + totalFiles: Number(row?.total_files || 0), + totalChunks: Number(row?.total_chunks || 0) + } + } } diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 41f8304..dc0494f 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -239,6 +239,8 @@ export default { autocompleteModelDescription: 'Model used for code and text autocompletion, providing intelligent writing suggestions', embeddingModel: 'Embedding model:', embeddingModelDescription: 'Model used for document vectorization and semantic search, supporting RAG functionality', + insightModel: 'Insight model:', + insightModelDescription: 'Model used for generating intelligent insights and analysis, providing deep content understanding', }, // Model Provider Settings @@ -249,6 +251,7 @@ export default { oneClickConfig: 'One-Click Config', oneClickConfigTooltip: 'Automatically configure models to recommended models from providers with API keys set', chatModelConfigured: 'Chat model configured automatically: {provider}/{model}', + insightModelConfigured: 'Insight model configured automatically: {provider}/{model}', autocompleteModelConfigured: 'Autocomplete model configured automatically: {provider}/{model}', embeddingModelConfigured: 'Embedding model configured automatically: {provider}/{model}', provider: 'Provider', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 83f2a3d..a89a92a 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -241,6 +241,8 @@ export default { autocompleteModelDescription: '用于代码和文本自动补全的模型,提供智能写作建议', embeddingModel: '嵌入模型:', embeddingModelDescription: '用于文档向量化和语义搜索的模型,支持 RAG 功能', + insightModel: '洞察模型:', + insightModelDescription: '用于生成智能洞察和分析的模型,提供深度内容理解', }, // 模型提供商设置 @@ -251,6 +253,7 @@ export default { oneClickConfig: '一键配置', oneClickConfigTooltip: '自动配置模型为已设置 API Key 的提供商的推荐模型', chatModelConfigured: '已自动配置聊天模型:{provider}/{model}', + insightModelConfigured: '已自动配置洞察模型:{provider}/{model}', autocompleteModelConfigured: '已自动配置自动补全模型:{provider}/{model}', embeddingModelConfigured: '已自动配置嵌入模型:{provider}/{model}', provider: '提供商', diff --git a/src/settings/components/ModelProviderSettings.tsx b/src/settings/components/ModelProviderSettings.tsx index e1999ff..413f9ab 100644 --- a/src/settings/components/ModelProviderSettings.tsx +++ b/src/settings/components/ModelProviderSettings.tsx @@ -105,13 +105,19 @@ const CustomProviderSettings: React.FC = ({ plugin, if (selectedProvider) { const defaultModels = GetDefaultModelId(selectedProvider); - // 设置chat和autocomplete模型 + // 设置chat、insight和autocomplete模型 if (defaultModels.chat) { newSettings.chatModelProvider = selectedProvider; newSettings.chatModelId = defaultModels.chat; hasUpdates = true; console.debug(t("settings.ModelProvider.chatModelConfigured", { provider: selectedProvider, model: defaultModels.chat })); } + if (defaultModels.insight) { + newSettings.insightModelProvider = selectedProvider; + newSettings.insightModelId = defaultModels.insight; + hasUpdates = true; + console.debug(t("settings.ModelProvider.insightModelConfigured", { provider: selectedProvider, model: defaultModels.insight })); + } if (defaultModels.autoComplete) { newSettings.applyModelProvider = selectedProvider; newSettings.applyModelId = defaultModels.autoComplete; @@ -367,6 +373,28 @@ const CustomProviderSettings: React.FC = ({ plugin, }); }; + const updateInsightModelId = (provider: ApiProvider, modelId: string, isCustom: boolean = false) => { + console.debug(`updateInsightModelId: ${provider} -> ${modelId}, isCustom: ${isCustom}`) + const providerSettingKey = getProviderSettingKey(provider); + const providerSettings = settings[providerSettingKey] || {}; + const currentModels = providerSettings.models || []; + + // 如果是自定义模型且不在列表中,则添加 + const updatedModels = isCustom && !currentModels.includes(modelId) + ? [...currentModels, modelId] + : currentModels; + + handleSettingsUpdate({ + ...settings, + insightModelProvider: provider, + insightModelId: modelId, + [providerSettingKey]: { + ...providerSettings, + models: updatedModels + } + }); + }; + // 生成包含链接的API Key描述 const generateApiKeyDescription = (provider: ApiProvider): React.ReactNode => { const apiUrl = getProviderApiUrl(provider); @@ -493,6 +521,15 @@ const CustomProviderSettings: React.FC = ({ plugin, updateModel={updateChatModelId} /> + + > { // https://ai.google.dev/gemini-api/docs/models/gemini export type GeminiModelId = keyof typeof geminiModels export const geminiDefaultModelId: GeminiModelId = "gemini-2.5-pro-preview-05-06" +export const geminiDefaultInsightModelId: GeminiModelId = "gemini-2.5-flash-preview-05-20" export const geminiDefaultAutoCompleteModelId: GeminiModelId = "gemini-2.5-flash-preview-05-20" export const geminiDefaultEmbeddingModelId: keyof typeof geminiEmbeddingModels = "text-embedding-004" @@ -497,6 +501,7 @@ export const geminiEmbeddingModels = { // https://openai.com/api/pricing/ export type OpenAiNativeModelId = keyof typeof openAiNativeModels export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o" +export const openAiNativeDefaultInsightModelId: OpenAiNativeModelId = "gpt-4o-mini" export const openAiNativeDefaultAutoCompleteModelId: OpenAiNativeModelId = "gpt-4o-mini" export const openAiNativeDefaultEmbeddingModelId: keyof typeof openAINativeEmbeddingModels = "text-embedding-3-small" @@ -605,6 +610,7 @@ export const openAINativeEmbeddingModels = { // https://api-docs.deepseek.com/quick_start/pricing export type DeepSeekModelId = keyof typeof deepSeekModels export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat" +export const deepSeekDefaultInsightModelId: DeepSeekModelId = "deepseek-chat" export const deepSeekDefaultAutoCompleteModelId: DeepSeekModelId = "deepseek-chat" export const deepSeekDefaultEmbeddingModelId = null // this is not supported embedding model @@ -635,6 +641,7 @@ export const deepSeekModels = { // https://help.aliyun.com/zh/model-studio/getting-started/ export type QwenModelId = keyof typeof qwenModels export const qwenDefaultModelId: QwenModelId = "qwen3-235b-a22b" +export const qwenDefaultInsightModelId: QwenModelId = "qwen3-32b" export const qwenDefaultAutoCompleteModelId: QwenModelId = "qwen3-32b" export const qwenDefaultEmbeddingModelId: keyof typeof qwenEmbeddingModels = "text-embedding-v3" @@ -937,6 +944,7 @@ export const qwenEmbeddingModels = { // https://docs.siliconflow.cn/ export type SiliconFlowModelId = keyof typeof siliconFlowModels export const siliconFlowDefaultModelId: SiliconFlowModelId = "deepseek-ai/DeepSeek-V3" +export const siliconFlowDefaultInsightModelId: SiliconFlowModelId = "deepseek-ai/DeepSeek-V3" export const siliconFlowDefaultAutoCompleteModelId: SiliconFlowModelId = "deepseek-ai/DeepSeek-V3" export const siliconFlowDefaultEmbeddingModelId: keyof typeof siliconFlowEmbeddingModels = "BAAI/bge-m3" @@ -1420,6 +1428,7 @@ export const siliconFlowEmbeddingModels = { // https://console.groq.com/docs/overview export type GroqModelId = keyof typeof groqModels export const groqDefaultModelId: GroqModelId = "llama-3.3-70b-versatile" +export const groqDefaultInsightModelId: GroqModelId = "llama-3.3-70b-versatile" export const groqDefaultAutoCompleteModelId: GroqModelId = "llama-3.3-70b-versatile" export const groqDefaultEmbeddingModelId = null // this is not supported embedding model @@ -1581,6 +1590,7 @@ export const groqModels = { // https://docs.x.ai/docs/models export type GrokModelId = keyof typeof grokModels export const grokDefaultModelId: GrokModelId = "grok-3" +export const grokDefaultInsightModelId: GrokModelId = "grok-3-mini" export const grokDefaultAutoCompleteModelId: GrokModelId = "grok-3-mini-fast" export const grokDefaultEmbeddingModelId = null // this is not supported embedding model @@ -1637,6 +1647,7 @@ export const grokModels = { // LocalProvider (本地嵌入模型) export const localProviderDefaultModelId = null // this is not supported for chat/autocomplete +export const localProviderDefaultInsightModelId = null // this is not supported for insight export const localProviderDefaultAutoCompleteModelId = null // this is not supported for chat/autocomplete export const localProviderDefaultEmbeddingModelId: keyof typeof localProviderEmbeddingModels = "TaylorAI/bge-micro-v2" @@ -1805,77 +1816,103 @@ export const GetEmbeddingModelInfo = (provider: ApiProvider, modelId: string): E } // Get default model id for a provider -export const GetDefaultModelId = (provider: ApiProvider): { chat: string, autoComplete: string, embedding: string } => { +export const GetDefaultModelId = (provider: ApiProvider): { chat: string, insight: string, autoComplete: string, embedding: string } => { switch (provider) { case ApiProvider.Infio: return { "chat": infioDefaultModelId, + "insight": infioDefaultInsightModelId, "autoComplete": infioDefaultAutoCompleteModelId, "embedding": infioDefaultEmbeddingModelId, } case ApiProvider.OpenRouter: return { "chat": openRouterDefaultModelId, + "insight": openRouterDefaultInsightModelId, "autoComplete": openRouterDefaultAutoCompleteModelId, "embedding": openRouterDefaultEmbeddingModelId, } case ApiProvider.Anthropic: return { "chat": anthropicDefaultModelId, + "insight": anthropicDefaultInsightModelId, "autoComplete": anthropicDefaultAutoCompleteModelId, "embedding": anthropicDefaultEmbeddingModelId, } case ApiProvider.OpenAI: return { "chat": openAiNativeDefaultModelId, + "insight": openAiNativeDefaultInsightModelId, "autoComplete": openAiNativeDefaultAutoCompleteModelId, "embedding": openAiNativeDefaultEmbeddingModelId, } case ApiProvider.Deepseek: return { "chat": deepSeekDefaultModelId, + "insight": deepSeekDefaultInsightModelId, "autoComplete": deepSeekDefaultAutoCompleteModelId, "embedding": deepSeekDefaultEmbeddingModelId, } case ApiProvider.Google: return { "chat": geminiDefaultModelId, + "insight": geminiDefaultInsightModelId, "autoComplete": geminiDefaultAutoCompleteModelId, "embedding": geminiDefaultEmbeddingModelId, } case ApiProvider.AlibabaQwen: return { "chat": qwenDefaultModelId, + "insight": qwenDefaultInsightModelId, "autoComplete": qwenDefaultAutoCompleteModelId, "embedding": qwenDefaultEmbeddingModelId, } case ApiProvider.SiliconFlow: return { "chat": siliconFlowDefaultModelId, + "insight": siliconFlowDefaultInsightModelId, "autoComplete": siliconFlowDefaultAutoCompleteModelId, "embedding": siliconFlowDefaultEmbeddingModelId, } case ApiProvider.Groq: return { "chat": groqDefaultModelId, + "insight": groqDefaultInsightModelId, "autoComplete": groqDefaultAutoCompleteModelId, "embedding": groqDefaultEmbeddingModelId, } case ApiProvider.Grok: return { "chat": grokDefaultModelId, + "insight": grokDefaultInsightModelId, "autoComplete": grokDefaultAutoCompleteModelId, "embedding": grokDefaultEmbeddingModelId, } + case ApiProvider.Ollama: + return { + "chat": null, // user-configured + "insight": null, // user-configured + "autoComplete": null, // user-configured + "embedding": null, // not supported + } + case ApiProvider.OpenAICompatible: + return { + "chat": null, // user-configured + "insight": null, // user-configured + "autoComplete": null, // user-configured + "embedding": null, // user-configured + } case ApiProvider.LocalProvider: return { "chat": localProviderDefaultModelId, + "insight": localProviderDefaultInsightModelId, "autoComplete": localProviderDefaultAutoCompleteModelId, "embedding": localProviderDefaultEmbeddingModelId, } default: return { "chat": null, + "insight": null, "autoComplete": null, "embedding": null, }