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.

This commit is contained in:
duanfuxiang
2025-07-07 09:47:37 +08:00
parent 51f8620815
commit 3db334c6e8
10 changed files with 1532 additions and 39 deletions

View File

@@ -723,7 +723,7 @@ const InsightView = () => {
<div className="obsidian-confirm-dialog-info-item">
<strong>{t('insights.initConfirm.modelLabel')}</strong>
<span className="obsidian-confirm-dialog-model">
{settings.chatModelProvider} / {settings.chatModelId || t('insights.initConfirm.defaultModel')}
{settings.chatModelProvider}/{settings.chatModelId || t('insights.initConfirm.defaultModel')}
</span>
</div>
<div className="obsidian-confirm-dialog-info-item">

View File

@@ -69,6 +69,35 @@ const SearchView = () => {
// 当前搜索范围信息
const [currentSearchScope, setCurrentSearchScope] = useState<string>('')
// 统计信息状态
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<SelectVector, 'embedding'> & { similarity: number }) => {
// 如果用户正在选择文本,不触发点击事件
const selection = window.getSelection()
@@ -355,36 +526,80 @@ const SearchView = () => {
return (
<div className="obsidian-search-container">
{/* 搜索输入框 */}
<div className="obsidian-search-header">
<SearchInputWithActions
ref={searchInputRef}
initialSerializedEditorState={searchEditorState}
onChange={setSearchEditorState}
onSubmit={handleSearch}
mentionables={mentionables}
setMentionables={setMentionables}
placeholder="语义搜索(按回车键搜索)..."
autoFocus={true}
disabled={isSearching}
/>
{/* 搜索模式切换 */}
<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 className="obsidian-search-header-wrapper">
<div className="obsidian-search-title">
<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>
{/* 统计信息 */}
{!isLoadingStats && statisticsInfo && (
<div className="obsidian-search-stats">
<div className="obsidian-search-stats-overview">
<div className="obsidian-search-stats-main">
<span className="obsidian-search-stats-number">{statisticsInfo.totalChunks}</span>
<span className="obsidian-search-stats-label"></span>
</div>
<div className="obsidian-search-stats-breakdown">
<div className="obsidian-search-stats-item">
<span className="obsidian-search-stats-item-icon">📄</span>
<span className="obsidian-search-stats-item-value">{statisticsInfo.totalFiles}</span>
<span className="obsidian-search-stats-item-label"></span>
</div>
</div>
</div>
</div>
)}
{/* 搜索输入框 */}
<div className="obsidian-search-input-section">
<SearchInputWithActions
ref={searchInputRef}
initialSerializedEditorState={searchEditorState}
onChange={setSearchEditorState}
onSubmit={handleSearch}
mentionables={mentionables}
setMentionables={setMentionables}
placeholder="语义搜索(按回车键搜索)..."
autoFocus={true}
disabled={isSearching}
/>
{/* 搜索模式切换 */}
<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>
@@ -398,11 +613,6 @@ const SearchView = () => {
`${insightGroupedResults.length} 个文件,${insightResults.length} 个洞察`
)}
</div>
{currentSearchScope && (
<div className="obsidian-search-scope">
: {currentSearchScope}
</div>
)}
</div>
)}
@@ -413,6 +623,151 @@ const SearchView = () => {
</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 && (
<div className="obsidian-confirm-dialog-overlay">
<div className="obsidian-confirm-dialog">
<div className="obsidian-confirm-dialog-header">
<h3></h3>
</div>
<div className="obsidian-confirm-dialog-body">
<p>
</p>
<p className="obsidian-confirm-dialog-warning">
</p>
<div className="obsidian-confirm-dialog-scope">
<strong>:</strong> {settings.workspace === 'vault' ? '整个 Vault' : settings.workspace}
</div>
</div>
<div className="obsidian-confirm-dialog-footer">
<button
onClick={cancelDeleteConfirm}
className="obsidian-confirm-dialog-cancel-btn"
>
</button>
<button
onClick={confirmDeleteWorkspaceIndex}
className="obsidian-confirm-dialog-confirm-btn"
>
</button>
</div>
</div>
</div>
)}
{/* 确认初始化对话框 */}
{showRAGInitConfirm && (
<div className="obsidian-confirm-dialog-overlay">
<div className="obsidian-confirm-dialog">
<div className="obsidian-confirm-dialog-header">
<h3>{statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '更新工作区索引' : '初始化工作区索引'}</h3>
</div>
<div className="obsidian-confirm-dialog-body">
<p>
{statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0)
? '将更新当前工作区的向量索引,重新处理所有文件以确保索引最新。'
: '将为当前工作区的所有文件建立向量索引,这将提高语义搜索的准确性。'
}
</p>
<div className="obsidian-confirm-dialog-info">
<div className="obsidian-confirm-dialog-info-item">
<strong>:</strong>
<span className="obsidian-confirm-dialog-model">
{settings.embeddingModelProvider} / {settings.embeddingModelId || '默认模型'}
</span>
</div>
<div className="obsidian-confirm-dialog-info-item">
<strong>:</strong>
<span className="obsidian-confirm-dialog-workspace">
{settings.workspace === 'vault' ? '整个 Vault' : settings.workspace}
</span>
</div>
</div>
<p className="obsidian-confirm-dialog-warning">
</p>
</div>
<div className="obsidian-confirm-dialog-footer">
<button
onClick={cancelRAGInitConfirm}
className="obsidian-confirm-dialog-cancel-btn"
>
</button>
<button
onClick={confirmInitWorkspaceRAG}
className="obsidian-confirm-dialog-confirm-btn"
>
{statisticsInfo && (statisticsInfo.totalFiles > 0 || statisticsInfo.totalChunks > 0) ? '开始更新' : '开始初始化'}
</button>
</div>
</div>
</div>
)}
{/* 搜索结果 */}
<div className="obsidian-search-results">
{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;
}
`}
</style>
</div>

View File

@@ -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<void> {
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<void> {
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)
}
}
}

View File

@@ -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<void> {
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<TFile[]> {
// 获取工作区中的所有文件
const workspaceFiles = new Set<string>()
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 []
}
}
}

View File

@@ -188,4 +188,94 @@ export class VectorRepository {
const result = await this.db.query<SearchResult>(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)
}
}
}

View File

@@ -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',

View File

@@ -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: '提供商',

View File

@@ -105,13 +105,19 @@ const CustomProviderSettings: React.FC<CustomProviderSettingsProps> = ({ 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<CustomProviderSettingsProps> = ({ 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<CustomProviderSettingsProps> = ({ plugin,
updateModel={updateChatModelId}
/>
<ComboBoxComponent
name={t("settings.Models.insightModel")}
description={t("settings.Models.insightModelDescription")}
settings={settings}
provider={settings.insightModelProvider || ApiProvider.Infio}
modelId={settings.insightModelId}
updateModel={updateInsightModelId}
/>
<ComboBoxComponent
name={t("settings.Models.autocompleteModel")}
description={t("settings.Models.autocompleteModelDescription")}

View File

@@ -289,6 +289,10 @@ export const InfioSettingsSchema = z.object({
chatModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
chatModelId: z.string().catch(''),
// Insight Model
insightModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
insightModelId: z.string().catch(''),
// Apply Model
applyModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
applyModelId: z.string().catch(''),

View File

@@ -35,6 +35,7 @@ export interface EmbeddingModelInfo {
// https://docs.anthropic.com/en/docs/about-claude/models
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-20250514"
export const anthropicDefaultInsightModelId: AnthropicModelId = "claude-sonnet-4-20250514"
export const anthropicDefaultAutoCompleteModelId: AnthropicModelId = "claude-3-5-haiku-20241022"
export const anthropicDefaultEmbeddingModelId: AnthropicModelId = null // this is not supported embedding model
export const anthropicModels = {
@@ -131,6 +132,7 @@ export const anthropicModels = {
// Infio
export const infioDefaultModelId = "gemini/gemini-2.5-pro-preview-06-05" // for chat
export const infioDefaultInsightModelId = "deepseek/deepseek-v3" // for insight
export const infioDefaultAutoCompleteModelId = "groq/llama-3.3-70b-versatile" // for auto complete
export const infioDefaultEmbeddingModelId = "openai/text-embedding-3-small" // for embedding
export const infioDefaultModelInfo: ModelInfo = {
@@ -214,6 +216,7 @@ export const infioEmbeddingModels = {
// OpenRouter
// https://openrouter.ai/models?order=newest&supported_parameters=tools
export const openRouterDefaultModelId = "google/gemini-2.5-pro-preview" // for chat
export const openRouterDefaultInsightModelId = "deepseek/deepseek-chat-v3-0324" // for insight
export const openRouterDefaultAutoCompleteModelId = "google/gemini-2.5-flash-preview-05-20" // for auto complete
export const openRouterDefaultEmbeddingModelId = null // this is not supported embedding model
export const openRouterDefaultModelInfo: ModelInfo = {
@@ -268,6 +271,7 @@ async function fetchOpenRouterModels(): Promise<Record<string, ModelInfo>> {
// 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,
}