mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 16:31:56 +00:00
update trans
This commit is contained in:
parent
98bc810b86
commit
a269258353
@ -2,7 +2,7 @@ import * as path from 'path'
|
||||
|
||||
import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { Box, CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react'
|
||||
import { Box, Brain, CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react'
|
||||
import { App, Notice, TFile, TFolder, WorkspaceLeaf } from 'obsidian'
|
||||
import {
|
||||
forwardRef,
|
||||
@ -51,7 +51,7 @@ import {
|
||||
MentionableCurrentFile,
|
||||
} from '../../types/mentionable'
|
||||
import { ApplyEditToFile, SearchAndReplace } from '../../utils/apply'
|
||||
import { listFilesAndFolders } from '../../utils/glob-utils'
|
||||
import { listFilesAndFolders, semanticSearchFiles } from '../../utils/glob-utils'
|
||||
import {
|
||||
getMentionableKey,
|
||||
serializeMentionable,
|
||||
@ -70,6 +70,7 @@ import CommandsView from './CommandsView'
|
||||
import CustomModeView from './CustomModeView'
|
||||
import FileReadResults from './FileReadResults'
|
||||
import HelloInfo from './HelloInfo'
|
||||
import InsightView from './InsightView'
|
||||
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
|
||||
import McpHubView from './McpHubView'; // Moved after MarkdownReasoningBlock
|
||||
import QueryProgress, { QueryProgressState } from './QueryProgress'
|
||||
@ -192,7 +193,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history' | 'workspace'>('chat')
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history' | 'workspace' | 'insights'>('chat')
|
||||
|
||||
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
|
||||
|
||||
@ -704,24 +705,25 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
}
|
||||
} else if (toolArgs.type === 'semantic_search_files') {
|
||||
const scope_folders = toolArgs.filepath
|
||||
&& toolArgs.filepath !== ''
|
||||
&& toolArgs.filepath !== '.'
|
||||
&& toolArgs.filepath !== '/'
|
||||
? { files: [], folders: [toolArgs.filepath] }
|
||||
: undefined
|
||||
const results = await (await getRAGEngine()).processQuery({
|
||||
query: toolArgs.query,
|
||||
scope: scope_folders,
|
||||
})
|
||||
let snippets = results.map(({ path, content, metadata }) => {
|
||||
const contentWithLineNumbers = addLineNumbers(content, metadata.startLine)
|
||||
return `<file_block_content location="${path}#L${metadata.startLine}-${metadata.endLine}">\n${contentWithLineNumbers}\n</file_block_content>`
|
||||
}).join('\n\n')
|
||||
if (snippets.length === 0) {
|
||||
snippets = `No results found for '${toolArgs.query}'`
|
||||
// 获取当前工作区
|
||||
let currentWorkspace: Workspace | null = null
|
||||
if (settings.workspace && settings.workspace !== 'vault') {
|
||||
currentWorkspace = await workspaceManager.findByName(String(settings.workspace))
|
||||
}
|
||||
const formattedContent = `[semantic_search_files for '${toolArgs.filepath}'] Result:\n${snippets}\n`;
|
||||
|
||||
const snippets = await semanticSearchFiles(
|
||||
await getRAGEngine(),
|
||||
toolArgs.query,
|
||||
toolArgs.filepath,
|
||||
currentWorkspace || undefined,
|
||||
app,
|
||||
await getTransEngine()
|
||||
)
|
||||
|
||||
const contextInfo = currentWorkspace
|
||||
? `workspace '${currentWorkspace.name}'`
|
||||
: toolArgs.filepath || 'vault'
|
||||
const formattedContent = `[semantic_search_files for '${contextInfo}'] Result:\n${snippets}\n`;
|
||||
return {
|
||||
type: 'semantic_search_files',
|
||||
applyMsgId,
|
||||
@ -866,11 +868,12 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
try {
|
||||
console.log("call_transformations", toolArgs)
|
||||
// Validate that the transformation type is a valid enum member
|
||||
if (!Object.values(TransformationType).includes(toolArgs.transformation as TransformationType)) {
|
||||
const validTransformationTypes = Object.values(TransformationType) as string[]
|
||||
if (!validTransformationTypes.includes(toolArgs.transformation)) {
|
||||
throw new Error(`Unsupported transformation type: ${toolArgs.transformation}`);
|
||||
}
|
||||
|
||||
const transformationType = toolArgs.transformation as TransformationType;
|
||||
const transformationType = toolArgs.transformation;
|
||||
const transEngine = await getTransEngine();
|
||||
|
||||
// Execute the transformation using the TransEngine
|
||||
@ -1030,7 +1033,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
break;
|
||||
|
||||
default:
|
||||
results.push(`❌ 不支持的操作类型: ${operation.action}`);
|
||||
results.push(`❌ 不支持的操作类型: ${String(operation.action)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1067,7 +1070,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
} else {
|
||||
// 处理未知的工具类型
|
||||
throw new Error(`Unsupported tool type: ${toolArgs.type}`);
|
||||
throw new Error(`Unsupported tool type: ${(toolArgs as any).type || 'unknown'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply changes', error)
|
||||
@ -1369,6 +1372,18 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
>
|
||||
<Server size={18} color={tab === 'mcp' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (tab === 'insights') {
|
||||
setTab('chat')
|
||||
} else {
|
||||
setTab('insights')
|
||||
}
|
||||
}}
|
||||
className="infio-chat-list-dropdown"
|
||||
>
|
||||
<Brain size={18} color={tab === 'insights' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* main view */}
|
||||
@ -1569,6 +1584,10 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
<div className="infio-chat-commands">
|
||||
<WorkspaceView />
|
||||
</div>
|
||||
) : tab === 'insights' ? (
|
||||
<div className="infio-chat-commands">
|
||||
<InsightView />
|
||||
</div>
|
||||
) : (
|
||||
<div className="infio-chat-commands">
|
||||
<McpHubView />
|
||||
|
||||
1107
src/components/chat-view/InsightView.tsx
Normal file
1107
src/components/chat-view/InsightView.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,17 @@
|
||||
import { SerializedEditorState } from 'lexical'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { useRAG } from '../../contexts/RAGContext'
|
||||
import { useSettings } from '../../contexts/SettingsContext'
|
||||
import { useTrans } from '../../contexts/TransContext'
|
||||
import { Workspace } from '../../database/json/workspace/types'
|
||||
import { WorkspaceManager } from '../../database/json/workspace/WorkspaceManager'
|
||||
import { SelectVector } from '../../database/schema'
|
||||
import { Mentionable } from '../../types/mentionable'
|
||||
import { getFilesWithTag } from '../../utils/glob-utils'
|
||||
import { openMarkdownFile } from '../../utils/obsidian'
|
||||
|
||||
import SearchInputWithActions, { SearchInputRef } from './chat-input/SearchInputWithActions'
|
||||
@ -20,18 +25,49 @@ interface FileGroup {
|
||||
blocks: (Omit<SelectVector, 'embedding'> & { similarity: number })[]
|
||||
}
|
||||
|
||||
// 洞察文件分组结果接口
|
||||
interface InsightFileGroup {
|
||||
path: string
|
||||
fileName: string
|
||||
maxSimilarity: number
|
||||
insights: Array<{
|
||||
id: string
|
||||
insight: string
|
||||
insight_type: string
|
||||
similarity: number
|
||||
source_path: string
|
||||
}>
|
||||
}
|
||||
|
||||
const SearchView = () => {
|
||||
const { getRAGEngine } = useRAG()
|
||||
const { getTransEngine } = useTrans()
|
||||
const app = useApp()
|
||||
const { settings } = useSettings()
|
||||
const searchInputRef = useRef<SearchInputRef>(null)
|
||||
|
||||
// 工作区管理器
|
||||
const workspaceManager = useMemo(() => {
|
||||
return new WorkspaceManager(app)
|
||||
}, [app])
|
||||
const [searchResults, setSearchResults] = useState<(Omit<SelectVector, 'embedding'> & { similarity: number })[]>([])
|
||||
const [insightResults, setInsightResults] = useState<Array<{
|
||||
id: string
|
||||
insight: string
|
||||
insight_type: string
|
||||
similarity: number
|
||||
source_path: string
|
||||
}>>([])
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [hasSearched, setHasSearched] = useState(false)
|
||||
const [searchMode, setSearchMode] = useState<'notes' | 'insights'>('notes') // 搜索模式:笔记或洞察
|
||||
// 展开状态管理 - 默认全部展开
|
||||
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set())
|
||||
// 新增:mentionables 状态管理
|
||||
const [mentionables, setMentionables] = useState<Mentionable[]>([])
|
||||
const [searchEditorState, setSearchEditorState] = useState<SerializedEditorState | null>(null)
|
||||
// 当前搜索范围信息
|
||||
const [currentSearchScope, setCurrentSearchScope] = useState<string>('')
|
||||
|
||||
const handleSearch = useCallback(async (editorState?: SerializedEditorState) => {
|
||||
let searchTerm = ''
|
||||
@ -43,7 +79,9 @@ const SearchView = () => {
|
||||
|
||||
if (!searchTerm.trim()) {
|
||||
setSearchResults([])
|
||||
setInsightResults([])
|
||||
setHasSearched(false)
|
||||
setCurrentSearchScope('')
|
||||
return
|
||||
}
|
||||
|
||||
@ -51,23 +89,87 @@ const SearchView = () => {
|
||||
setHasSearched(true)
|
||||
|
||||
try {
|
||||
const ragEngine = await getRAGEngine()
|
||||
const results = await ragEngine.processQuery({
|
||||
query: searchTerm,
|
||||
limit: 50, // 使用用户选择的限制数量
|
||||
})
|
||||
// 获取当前工作区
|
||||
let currentWorkspace: Workspace | null = null
|
||||
if (settings.workspace && settings.workspace !== 'vault') {
|
||||
currentWorkspace = await workspaceManager.findByName(String(settings.workspace))
|
||||
}
|
||||
|
||||
setSearchResults(results)
|
||||
// 默认展开所有文件
|
||||
// const uniquePaths = new Set(results.map(r => r.path))
|
||||
// setExpandedFiles(new Set(uniquePaths))
|
||||
// 设置搜索范围信息
|
||||
let scopeDescription = ''
|
||||
if (currentWorkspace) {
|
||||
scopeDescription = `工作区: ${currentWorkspace.name}`
|
||||
} else {
|
||||
scopeDescription = '整个 Vault'
|
||||
}
|
||||
setCurrentSearchScope(scopeDescription)
|
||||
|
||||
// 构建搜索范围
|
||||
let scope: { files: string[], folders: string[] } | undefined
|
||||
if (currentWorkspace) {
|
||||
const folders: string[] = []
|
||||
const files: string[] = []
|
||||
|
||||
// 处理工作区中的文件夹和标签
|
||||
for (const item of currentWorkspace.content) {
|
||||
if (item.type === 'folder') {
|
||||
folders.push(item.content)
|
||||
} else if (item.type === 'tag') {
|
||||
// 获取标签对应的所有文件
|
||||
const tagFiles = getFilesWithTag(item.content, app)
|
||||
files.push(...tagFiles)
|
||||
}
|
||||
}
|
||||
|
||||
// 只有当有文件夹或文件时才设置 scope
|
||||
if (folders.length > 0 || files.length > 0) {
|
||||
scope = { files, folders }
|
||||
}
|
||||
}
|
||||
|
||||
if (searchMode === 'notes') {
|
||||
// 搜索原始笔记
|
||||
const ragEngine = await getRAGEngine()
|
||||
const results = await ragEngine.processQuery({
|
||||
query: searchTerm,
|
||||
scope: scope,
|
||||
limit: 50,
|
||||
})
|
||||
|
||||
setSearchResults(results)
|
||||
setInsightResults([])
|
||||
} else {
|
||||
// 搜索洞察
|
||||
const transEngine = await getTransEngine()
|
||||
const results = await transEngine.processQuery({
|
||||
query: searchTerm,
|
||||
scope: scope,
|
||||
limit: 50,
|
||||
minSimilarity: 0.3,
|
||||
})
|
||||
|
||||
setInsightResults(results)
|
||||
setSearchResults([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error)
|
||||
setSearchResults([])
|
||||
setInsightResults([])
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
}
|
||||
}, [getRAGEngine])
|
||||
}, [getRAGEngine, getTransEngine, settings, workspaceManager, app, searchMode])
|
||||
|
||||
// 当搜索模式切换时,如果已经搜索过,重新执行搜索
|
||||
useEffect(() => {
|
||||
if (hasSearched && searchEditorState) {
|
||||
// 延迟执行避免状态更新冲突
|
||||
const timer = setTimeout(() => {
|
||||
handleSearch(searchEditorState)
|
||||
}, 100)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [searchMode, handleSearch]) // 监听搜索模式变化
|
||||
|
||||
const handleResultClick = (result: Omit<SelectVector, 'embedding'> & { similarity: number }) => {
|
||||
// 如果用户正在选择文本,不触发点击事件
|
||||
@ -170,7 +272,7 @@ const SearchView = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// 按文件分组并排序
|
||||
// 按文件分组并排序 - 原始笔记
|
||||
const groupedResults = useMemo(() => {
|
||||
if (!searchResults.length) return []
|
||||
|
||||
@ -209,6 +311,45 @@ const SearchView = () => {
|
||||
return Array.from(fileGroups.values()).sort((a, b) => b.maxSimilarity - a.maxSimilarity)
|
||||
}, [searchResults])
|
||||
|
||||
// 按文件分组并排序 - 洞察
|
||||
const insightGroupedResults = useMemo(() => {
|
||||
if (!insightResults.length) return []
|
||||
|
||||
// 按文件路径分组
|
||||
const fileGroups = new Map<string, InsightFileGroup>()
|
||||
|
||||
insightResults.forEach(result => {
|
||||
const filePath = result.source_path
|
||||
const fileName = filePath.split('/').pop() || filePath
|
||||
|
||||
if (!fileGroups.has(filePath)) {
|
||||
fileGroups.set(filePath, {
|
||||
path: filePath,
|
||||
fileName,
|
||||
maxSimilarity: result.similarity,
|
||||
insights: []
|
||||
})
|
||||
}
|
||||
|
||||
const group = fileGroups.get(filePath)
|
||||
if (group) {
|
||||
group.insights.push(result)
|
||||
// 更新最高相似度
|
||||
if (result.similarity > group.maxSimilarity) {
|
||||
group.maxSimilarity = result.similarity
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 对每个文件内的洞察按相似度排序
|
||||
fileGroups.forEach(group => {
|
||||
group.insights.sort((a, b) => b.similarity - a.similarity)
|
||||
})
|
||||
|
||||
// 将文件按最高相似度排序
|
||||
return Array.from(fileGroups.values()).sort((a, b) => b.maxSimilarity - a.maxSimilarity)
|
||||
}, [insightResults])
|
||||
|
||||
const totalBlocks = searchResults.length
|
||||
const totalFiles = groupedResults.length
|
||||
|
||||
@ -227,12 +368,41 @@ const SearchView = () => {
|
||||
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>
|
||||
|
||||
{/* 结果统计 */}
|
||||
{hasSearched && !isSearching && (
|
||||
<div className="obsidian-search-stats">
|
||||
{totalFiles} 个文件,{totalBlocks} 个块
|
||||
<div className="obsidian-search-stats-line">
|
||||
{searchMode === 'notes' ? (
|
||||
`${totalFiles} 个文件,${totalBlocks} 个块`
|
||||
) : (
|
||||
`${insightGroupedResults.length} 个文件,${insightResults.length} 个洞察`
|
||||
)}
|
||||
</div>
|
||||
{currentSearchScope && (
|
||||
<div className="obsidian-search-scope">
|
||||
搜索范围: {currentSearchScope}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -245,70 +415,127 @@ const SearchView = () => {
|
||||
|
||||
{/* 搜索结果 */}
|
||||
<div className="obsidian-search-results">
|
||||
{!isSearching && groupedResults.length > 0 && (
|
||||
<div className="obsidian-results-list">
|
||||
{groupedResults.map((fileGroup) => (
|
||||
<div key={fileGroup.path} className="obsidian-file-group">
|
||||
{/* 文件头部 */}
|
||||
<div
|
||||
className="obsidian-file-header"
|
||||
onClick={() => toggleFileExpansion(fileGroup.path)}
|
||||
>
|
||||
<div className="obsidian-file-header-content">
|
||||
<div className="obsidian-file-header-top">
|
||||
<div className="obsidian-file-header-left">
|
||||
{expandedFiles.has(fileGroup.path) ? (
|
||||
<ChevronDown size={16} className="obsidian-expand-icon" />
|
||||
) : (
|
||||
<ChevronRight size={16} className="obsidian-expand-icon" />
|
||||
)}
|
||||
{/* <span className="obsidian-file-index">{fileIndex + 1}</span> */}
|
||||
<span className="obsidian-file-name">{fileGroup.fileName}</span>
|
||||
{searchMode === 'notes' ? (
|
||||
// 原始笔记搜索结果
|
||||
!isSearching && groupedResults.length > 0 && (
|
||||
<div className="obsidian-results-list">
|
||||
{groupedResults.map((fileGroup) => (
|
||||
<div key={fileGroup.path} className="obsidian-file-group">
|
||||
{/* 文件头部 */}
|
||||
<div
|
||||
className="obsidian-file-header"
|
||||
onClick={() => toggleFileExpansion(fileGroup.path)}
|
||||
>
|
||||
<div className="obsidian-file-header-content">
|
||||
<div className="obsidian-file-header-top">
|
||||
<div className="obsidian-file-header-left">
|
||||
{expandedFiles.has(fileGroup.path) ? (
|
||||
<ChevronDown size={16} className="obsidian-expand-icon" />
|
||||
) : (
|
||||
<ChevronRight size={16} className="obsidian-expand-icon" />
|
||||
)}
|
||||
<span className="obsidian-file-name">{fileGroup.fileName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="obsidian-file-header-right">
|
||||
{/* <span className="obsidian-file-blocks">{fileGroup.blocks.length} 块</span> */}
|
||||
{/* <span className="obsidian-file-similarity">
|
||||
{fileGroup.maxSimilarity.toFixed(3)}
|
||||
</span> */}
|
||||
<div className="obsidian-file-path-row">
|
||||
<span className="obsidian-file-path">{fileGroup.path}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="obsidian-file-path-row">
|
||||
<span className="obsidian-file-path">{fileGroup.path}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件块列表 */}
|
||||
{expandedFiles.has(fileGroup.path) && (
|
||||
<div className="obsidian-file-blocks">
|
||||
{fileGroup.blocks.map((result, blockIndex) => (
|
||||
<div
|
||||
key={result.id}
|
||||
className="obsidian-result-item"
|
||||
onClick={() => handleResultClick(result)}
|
||||
>
|
||||
<div className="obsidian-result-header">
|
||||
<span className="obsidian-result-index">{blockIndex + 1}</span>
|
||||
<span className="obsidian-result-location">
|
||||
L{result.metadata.startLine}-{result.metadata.endLine}
|
||||
</span>
|
||||
<span className="obsidian-result-similarity">
|
||||
{result.similarity.toFixed(3)}
|
||||
</span>
|
||||
{/* 文件块列表 */}
|
||||
{expandedFiles.has(fileGroup.path) && (
|
||||
<div className="obsidian-file-blocks">
|
||||
{fileGroup.blocks.map((result, blockIndex) => (
|
||||
<div
|
||||
key={result.id}
|
||||
className="obsidian-result-item"
|
||||
onClick={() => handleResultClick(result)}
|
||||
>
|
||||
<div className="obsidian-result-header">
|
||||
<span className="obsidian-result-index">{blockIndex + 1}</span>
|
||||
<span className="obsidian-result-location">
|
||||
L{result.metadata.startLine}-{result.metadata.endLine}
|
||||
</span>
|
||||
<span className="obsidian-result-similarity">
|
||||
{result.similarity.toFixed(3)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="obsidian-result-content">
|
||||
{renderMarkdownContent(result.content)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="obsidian-result-content">
|
||||
{renderMarkdownContent(result.content)}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
// AI 洞察搜索结果
|
||||
!isSearching && insightGroupedResults.length > 0 && (
|
||||
<div className="obsidian-results-list">
|
||||
{insightGroupedResults.map((fileGroup) => (
|
||||
<div key={fileGroup.path} className="obsidian-file-group">
|
||||
{/* 文件头部 */}
|
||||
<div
|
||||
className="obsidian-file-header"
|
||||
onClick={() => toggleFileExpansion(fileGroup.path)}
|
||||
>
|
||||
<div className="obsidian-file-header-content">
|
||||
<div className="obsidian-file-header-top">
|
||||
<div className="obsidian-file-header-left">
|
||||
{expandedFiles.has(fileGroup.path) ? (
|
||||
<ChevronDown size={16} className="obsidian-expand-icon" />
|
||||
) : (
|
||||
<ChevronRight size={16} className="obsidian-expand-icon" />
|
||||
)}
|
||||
<span className="obsidian-file-name">{fileGroup.fileName}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="obsidian-file-path-row">
|
||||
<span className="obsidian-file-path">{fileGroup.path}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 洞察列表 */}
|
||||
{expandedFiles.has(fileGroup.path) && (
|
||||
<div className="obsidian-file-blocks">
|
||||
{fileGroup.insights.map((insight, insightIndex) => (
|
||||
<div
|
||||
key={insight.id}
|
||||
className="obsidian-result-item"
|
||||
>
|
||||
<div className="obsidian-result-header">
|
||||
<span className="obsidian-result-index">{insightIndex + 1}</span>
|
||||
<span className="obsidian-result-insight-type">
|
||||
{insight.insight_type.toUpperCase()}
|
||||
</span>
|
||||
<span className="obsidian-result-similarity">
|
||||
{insight.similarity.toFixed(3)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="obsidian-result-content">
|
||||
<div className="obsidian-insight-content">
|
||||
{insight.insight}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{!isSearching && hasSearched && groupedResults.length === 0 && (
|
||||
{!isSearching && hasSearched && (
|
||||
(searchMode === 'notes' && groupedResults.length === 0) ||
|
||||
(searchMode === 'insights' && insightGroupedResults.length === 0)
|
||||
) && (
|
||||
<div className="obsidian-no-results">
|
||||
<p>未找到相关结果</p>
|
||||
</div>
|
||||
@ -329,12 +556,54 @@ const SearchView = () => {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.obsidian-search-mode-toggle {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding: 4px;
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
.obsidian-search-mode-btn {
|
||||
flex: 1;
|
||||
padding: 6px 12px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.obsidian-search-mode-btn:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.obsidian-search-mode-btn.active {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-search-stats {
|
||||
padding: 8px 12px;
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.obsidian-search-stats-line {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.obsidian-search-scope {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.obsidian-search-loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
@ -594,6 +863,27 @@ const SearchView = () => {
|
||||
margin: 0;
|
||||
font-size: var(--font-ui-medium);
|
||||
}
|
||||
|
||||
/* 洞察结果特殊样式 */
|
||||
.obsidian-result-insight-type {
|
||||
color: var(--text-accent);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-family: var(--font-monospace);
|
||||
font-weight: 600;
|
||||
background-color: var(--background-modifier-border);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-s);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.obsidian-insight-content {
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-medium);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
13
src/core/prompts/transformations/concise-dense-summary.ts
Normal file
13
src/core/prompts/transformations/concise-dense-summary.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const CONCISE_DENSE_SUMMARY_PROMPT = `# MISSION
|
||||
You are a Sparse Priming Representation (SPR) writer. Your goal is to render the user's input as an extremely concise and distilled SPR.
|
||||
|
||||
# THEORY
|
||||
LLMs are a kind of deep neural network. They have been demonstrated to embed knowledge, abilities, and concepts, ranging from reasoning to planning, and even to theory of mind. These are called latent abilities and latent content, collectively referred to as latent space. The latent space of an LLM can be activated with the correct series of words as inputs, which will create a useful internal state of the neural network. You need to provide the most potent and succinct cues to prime another model effectively. Less is more.
|
||||
|
||||
# METHODOLOGY
|
||||
Render the input as a distilled list of the **most critical** assertions, concepts, and associations. The idea is to capture the absolute essence with minimal words. Use complete sentences.
|
||||
|
||||
**!! CRITICAL INSTRUCTION !!**
|
||||
**Your output MUST BE EXTREMELY CONCISE. Aim for a dense paragraph of no more than 3-5 sentences OR a bulleted list of 3-5 key points. Focus only on the highest-level insights and most essential concepts.**`;
|
||||
|
||||
export const CONCISE_DENSE_SUMMARY_DESCRIPTION = "Creates an extremely concise, rich summary of the content focusing on the most essential concepts";
|
||||
@ -1,10 +1,13 @@
|
||||
export const DENSE_SUMMARY_PROMPT = `# MISSION
|
||||
You are a Sparse Priming Representation (SPR) writer. An SPR is a particular kind of use of language for advanced NLP, NLU, and NLG tasks, particularly useful for the latest generation of Large Language Models (LLMs). You will be given information by the USER which you are to render as an SPR.
|
||||
You are a Sparse Priming Representation (SPR) writer. Your goal is to render the user's input as an extremely concise and distilled SPR.
|
||||
|
||||
# THEORY
|
||||
LLMs are a kind of deep neural network. They have been demonstrated to embed knowledge, abilities, and concepts, ranging from reasoning to planning, and even to theory of mind. These are called latent abilities and latent content, collectively referred to as latent space. The latent space of an LLM can be activated with the correct series of words as inputs, which will create a useful internal state of the neural network. This is not unlike how the right shorthand cues can prime a human mind to think in a certain way. Like human minds, LLMs are associative, meaning you only need to use the correct associations to 'prime' another model to think in the same way.
|
||||
LLMs are a kind of deep neural network. They have been demonstrated to embed knowledge, abilities, and concepts, ranging from reasoning to planning, and even to theory of mind. These are called latent abilities and latent content, collectively referred to as latent space. The latent space of an LLM can be activated with the correct series of words as inputs, which will create a useful internal state of the neural network. You need to provide the most potent and succinct cues to prime another model effectively. Less is more.
|
||||
|
||||
# METHODOLOGY
|
||||
Render the input as a distilled list of succinct statements, assertions, associations, concepts, analogies, and metaphors. The idea is to capture as much, conceptually, as possible but with as few words as possible. Write it in a way that makes sense to you, as the future audience will be another language model, not a human. Use complete sentences.`;
|
||||
Render the input as a distilled list of the **most critical** assertions, concepts, and associations. The idea is to capture the absolute essence with minimal words. Use complete sentences.
|
||||
|
||||
export const DENSE_SUMMARY_DESCRIPTION = "Creates a rich, deep summary of the content";
|
||||
**!! CRITICAL INSTRUCTION !!**
|
||||
**Your output MUST BE EXTREMELY CONCISE. Aim for a dense paragraph of no more than 3-5 sentences OR a bulleted list of 3-5 key points. Focus only on the highest-level insights and most essential concepts.**`;
|
||||
|
||||
export const DENSE_SUMMARY_DESCRIPTION = "Creates an extremely concise, rich summary of the content focusing on the most essential concepts";
|
||||
|
||||
14
src/core/prompts/transformations/hierarchical-summary.ts
Normal file
14
src/core/prompts/transformations/hierarchical-summary.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const HIERARCHICAL_SUMMARY_PROMPT = `# MISSION
|
||||
You are an expert knowledge architect responsible for creating hierarchical summaries of a knowledge base. You will be given a collection of summaries from files and sub-folders within a specific directory. Your mission is to synthesize these individual summaries into a single, cohesive, and abstract summary for the parent directory.
|
||||
|
||||
# METHODOLOGY
|
||||
1. **Identify Core Themes**: Analyze the provided summaries to identify the main topics, recurring concepts, and overarching themes present in the directory.
|
||||
2. **Synthesize, Don't Just List**: Do not simply concatenate or list the child summaries. Instead, integrate them. Explain what this collection of information represents as a whole. For example, instead of "This folder contains a summary of A and a summary of B," write "This folder explores the relationship between A and B, focusing on..."
|
||||
3. **Capture Structure**: Briefly mention the types of content within (e.g., "Contains technical specifications, meeting notes, and final reports related to Project X.").
|
||||
4. **Be Abstract and Concise**: The goal is to create a higher-level understanding. The output should be a dense, short paragraph that gives a bird's-eye view of the directory's contents and purpose.
|
||||
5. **Focus on Relationships**: Highlight how the different pieces of content relate to each other and what they collectively achieve or represent.
|
||||
|
||||
**!! CRITICAL INSTRUCTION !!**
|
||||
**Your output MUST BE CONCISE. Aim for 2-4 sentences that capture the essence and purpose of this directory as a cohesive unit. Focus on the highest-level insights and connections.**`;
|
||||
|
||||
export const HIERARCHICAL_SUMMARY_DESCRIPTION = "Creates a concise, high-level summary that synthesizes content from multiple files and folders into a cohesive understanding of the directory's purpose and themes";
|
||||
File diff suppressed because it is too large
Load Diff
@ -207,6 +207,16 @@ export class InsightManager {
|
||||
await this.repository.clearAllInsights(embeddingModel)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定ID的洞察
|
||||
*/
|
||||
async deleteInsightById(
|
||||
id: number,
|
||||
embeddingModel: EmbeddingModel,
|
||||
): Promise<void> {
|
||||
await this.repository.deleteInsightById(id, embeddingModel)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件删除时清理相关洞察
|
||||
*/
|
||||
|
||||
@ -29,7 +29,8 @@ export class InsightRepository {
|
||||
const tableName = this.getTableName(embeddingModel)
|
||||
const result = await this.db.query<SelectSourceInsight>(
|
||||
`SELECT * FROM "${tableName}" ORDER BY created_at DESC`
|
||||
)
|
||||
)
|
||||
console.log(result.rows)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
@ -128,6 +129,20 @@ export class InsightRepository {
|
||||
await this.db.query(`DELETE FROM "${tableName}"`)
|
||||
}
|
||||
|
||||
async deleteInsightById(
|
||||
id: number,
|
||||
embeddingModel: EmbeddingModel,
|
||||
): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new DatabaseNotInitializedException()
|
||||
}
|
||||
const tableName = this.getTableName(embeddingModel)
|
||||
await this.db.query(
|
||||
`DELETE FROM "${tableName}" WHERE id = $1`,
|
||||
[id]
|
||||
)
|
||||
}
|
||||
|
||||
async insertInsights(
|
||||
data: InsertSourceInsight[],
|
||||
embeddingModel: EmbeddingModel,
|
||||
|
||||
@ -903,6 +903,10 @@ export const qwenModels = {
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
export const qwenEmbeddingModels = {
|
||||
"text-embedding-v4": {
|
||||
dimensions: 1024,
|
||||
description: "支持50+主流语种,包括中文、英语、西班牙语、法语、葡萄牙语、印尼语、日语、韩语、德语、俄罗斯语等。最大行数20,单行最大处理8,192 Token。支持可选维度:1,024(默认)、768或512。单价:0.0007元/千Token。免费额度:50万Token(有效期180天)。"
|
||||
},
|
||||
"text-embedding-v3": {
|
||||
dimensions: 1024,
|
||||
description: "支持50+主流语种,包括中文、英语、西班牙语、法语、葡萄牙语、印尼语、日语、韩语、德语、俄罗斯语等。最大行数20,单行最大处理8,192 Token。支持可选维度:1,024(默认)、768或512。单价:0.0007元/千Token。免费额度:50万Token(有效期180天)。"
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { minimatch } from 'minimatch'
|
||||
import { App, TFile, TFolder, Vault } from 'obsidian'
|
||||
|
||||
import { RAGEngine } from '../core/rag/rag-engine'
|
||||
import { TRANSFORMATIONS, TransEngine } from '../core/transformations/trans-engine'
|
||||
import { Workspace } from '../database/json/workspace/types'
|
||||
|
||||
import { addLineNumbers } from './prompt-generator'
|
||||
|
||||
export const findFilesMatchingPatterns = async (
|
||||
patterns: string[],
|
||||
vault: Vault,
|
||||
@ -226,3 +230,115 @@ export const matchSearchFiles = async (vault: Vault, path: string, query: string
|
||||
export const regexSearchFiles = async (vault: Vault, path: string, regex: string, file_pattern: string) => {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 语义搜索文件(同时查询原始笔记和抽象洞察)
|
||||
*/
|
||||
export const semanticSearchFiles = async (
|
||||
ragEngine: RAGEngine, // RAG 引擎实例 - 原始笔记数据库
|
||||
query: string,
|
||||
path?: string,
|
||||
workspace?: Workspace,
|
||||
app?: App,
|
||||
transEngine?: TransEngine // Trans 引擎实例 - 抽象洞察数据库
|
||||
): Promise<string> => {
|
||||
let scope: { files: string[], folders: string[] } | undefined
|
||||
|
||||
// 如果指定了路径,使用该路径
|
||||
if (path && path !== '' && path !== '.' && path !== '/') {
|
||||
scope = { files: [], folders: [path] }
|
||||
}
|
||||
// 如果没有指定路径但有工作区,使用工作区范围
|
||||
else if (workspace && app) {
|
||||
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, app)
|
||||
files.push(...tagFiles)
|
||||
}
|
||||
}
|
||||
|
||||
// 只有当有文件夹或文件时才设置 scope
|
||||
if (folders.length > 0 || files.length > 0) {
|
||||
scope = { files, folders }
|
||||
}
|
||||
}
|
||||
|
||||
const resultSections: string[] = []
|
||||
|
||||
// 1. 查询原始笔记数据库 (RAGEngine)
|
||||
try {
|
||||
const ragResults = await ragEngine.processQuery({
|
||||
query: query,
|
||||
scope: scope,
|
||||
})
|
||||
|
||||
if (ragResults.length > 0) {
|
||||
resultSections.push('## 📝 原始笔记内容')
|
||||
const ragSnippets = ragResults.map(({ path, content, metadata }: any) => {
|
||||
const contentWithLineNumbers = addLineNumbers(content, metadata.startLine)
|
||||
return `<file_block_content location="${path}#L${metadata.startLine}-${metadata.endLine}">\n${contentWithLineNumbers}\n</file_block_content>`
|
||||
}).join('\n\n')
|
||||
resultSections.push(ragSnippets)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('RAG 搜索失败:', error)
|
||||
resultSections.push('## 📝 原始笔记内容\n⚠️ 原始笔记搜索失败')
|
||||
}
|
||||
|
||||
// 2. 查询抽象洞察数据库 (TransEngine) - 使用新的 processQuery 接口
|
||||
if (transEngine) {
|
||||
try {
|
||||
const insightResults = await transEngine.processQuery({
|
||||
query: query,
|
||||
scope: scope,
|
||||
limit: 20,
|
||||
minSimilarity: 0.3,
|
||||
})
|
||||
|
||||
if (insightResults.length > 0) {
|
||||
resultSections.push('\n## 🧠 AI 抽象洞察')
|
||||
|
||||
// 按转换类型分组
|
||||
const groupedInsights: { [key: string]: any[] } = {}
|
||||
insightResults.forEach(insight => {
|
||||
if (!groupedInsights[insight.insight_type]) {
|
||||
groupedInsights[insight.insight_type] = []
|
||||
}
|
||||
groupedInsights[insight.insight_type].push(insight)
|
||||
})
|
||||
|
||||
// 渲染每种类型的洞察
|
||||
for (const [insightType, insights] of Object.entries(groupedInsights)) {
|
||||
const transformationConfig = TRANSFORMATIONS[insightType as keyof typeof TRANSFORMATIONS]
|
||||
const typeName = transformationConfig ? transformationConfig.description : insightType
|
||||
|
||||
resultSections.push(`\n### ${typeName}`)
|
||||
|
||||
insights.forEach((insight, index) => {
|
||||
const similarity = (insight.similarity * 100).toFixed(1)
|
||||
resultSections.push(
|
||||
`<insight_block source="${insight.source_path}" type="${insightType}" similarity="${similarity}%">\n${insight.insight}\n</insight_block>`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('TransEngine 搜索失败:', error)
|
||||
resultSections.push('\n## 🧠 AI 抽象洞察\n⚠️ 洞察搜索失败: ' + (error instanceof Error ? error.message : String(error)))
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 合并结果
|
||||
if (resultSections.length === 0) {
|
||||
return `No results found for '${query}'`
|
||||
}
|
||||
|
||||
return resultSections.join('\n\n')
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user