infio-copilot-dev/src/settings/SettingTab.tsx
travertexg 9984527e85 feat: Enhance file search with core plugin and Omnisearch integration
- Introduces a new match_search_files tool for fuzzy/keyword search, integrating with Obsidian's core search plugin and updating Omnisearch integration for improved file search capabilities.
- Adds settings for selecting search backends (core plugin, Omnisearch, ripgrep) for both regex and match searches.
- Updates language files, prompts, and types to support the new functionality.
- Restructures search-related files for better organization.
2025-06-09 15:15:16 +00:00

753 lines
22 KiB
TypeScript

import {
App,
Modal,
Notice,
PluginSettingTab,
Setting,
TFile
} from 'obsidian';
import * as React from "react";
import { createRoot } from "react-dom/client";
import { t } from '../lang/helpers';
import InfioPlugin from '../main';
import { InfioSettings } from '../types/settings';
import { findFilesMatchingPatterns } from '../utils/glob-utils';
// import AdvancedSettings from './components/AdvancedSettings';
import BasicAutoCompleteSettings from './components/BasicAutoCompleteSettings';
// import DangerZoneSettings from './components/DangerZoneSettings';
import CustomProviderSettings from './components/ModelProviderSettings';
import PostprocessingSettings from './components/PostprocessingSettings';
import PreprocessingSettings from './components/PreprocessingSettings';
import PrivacySettings from './components/PrivacySettings';
import TriggerSettingsSection from './components/TriggerSettingsSection';
export class InfioSettingTab extends PluginSettingTab {
plugin: InfioPlugin;
private autoCompleteContainer: HTMLElement | null = null;
private modelsContainer: HTMLElement | null = null;
constructor(app: App, plugin: InfioPlugin) {
super(app, plugin)
this.plugin = plugin
}
display(): void {
const { containerEl } = this
containerEl.empty()
this.renderModelsSection(containerEl)
this.renderModelParametersSection(containerEl)
this.renderFilesSearchSection(containerEl)
this.renderChatBehaviorSection(containerEl)
this.renderDeepResearchSection(containerEl)
this.renderRAGSection(containerEl)
this.renderAutoCompleteSection(containerEl)
}
private renderModelsContent(containerEl: HTMLElement): void {
const div = containerEl.createDiv("div");
const sections = createRoot(div);
sections.render(
<CustomProviderSettings
plugin={this.plugin}
onSettingsUpdate={() => {
if (this.modelsContainer) {
this.modelsContainer.empty();
this.renderModelsContent(this.modelsContainer);
}
}}
/>
);
}
private renderModelParametersSection(containerEl: HTMLElement): void {
new Setting(containerEl).setHeading().setName(t('settings.ModelParameters.title'));
new Setting(containerEl)
.setName(t('settings.ModelParameters.temperature'))
.setDesc(t('settings.ModelParameters.temperatureDescription'))
.addText((text) => {
text
.setValue(String(this.plugin.settings.modelOptions.temperature))
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
modelOptions: {
...this.plugin.settings.modelOptions,
temperature: parseFloat(value),
},
});
})
});
new Setting(containerEl)
.setName(t('settings.ModelParameters.topP'))
.setDesc(t('settings.ModelParameters.topPDescription'))
.addText((text) => {
text
.setValue(String(this.plugin.settings.modelOptions.top_p))
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
modelOptions: {
...this.plugin.settings.modelOptions,
top_p: parseFloat(value),
},
});
})
});
new Setting(containerEl)
.setName(t('settings.ModelParameters.frequencyPenalty'))
.setDesc(t('settings.ModelParameters.frequencyPenaltyDescription'))
.addText((text) => {
text
.setValue(String(this.plugin.settings.modelOptions.frequency_penalty))
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
modelOptions: {
...this.plugin.settings.modelOptions,
frequency_penalty: parseFloat(value),
},
});
})
});
new Setting(containerEl)
.setName(t('settings.ModelParameters.presencePenalty'))
.setDesc(t('settings.ModelParameters.presencePenaltyDescription'))
.addText((text) => {
text
.setValue(String(this.plugin.settings.modelOptions.presence_penalty))
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
modelOptions: {
...this.plugin.settings.modelOptions,
presence_penalty: parseFloat(value),
},
});
})
});
new Setting(containerEl)
.setName(t('settings.ModelParameters.maxTokens'))
.setDesc(t('settings.ModelParameters.maxTokensDescription'))
.addText((text) => {
text
.setValue(String(this.plugin.settings.modelOptions.max_tokens))
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
modelOptions: {
...this.plugin.settings.modelOptions,
max_tokens: parseInt(value),
},
});
})
});
}
private renderFilesSearchSection(containerEl: HTMLElement): void {
new Setting(containerEl).setHeading().setName(t('settings.FilesSearch.title'))
new Setting(containerEl)
.setName(t('settings.FilesSearch.method'))
.setDesc(t('settings.FilesSearch.methodDescription'))
.addDropdown((dropdown) =>
dropdown
.addOption('auto', t('settings.FilesSearch.auto'))
.addOption('semantic', t('settings.FilesSearch.semantic'))
.addOption('regex', t('settings.FilesSearch.regex'))
.addOption('match', t('settings.FilesSearch.match'))
.setValue(this.plugin.settings.filesSearchMethod)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
filesSearchMethod: value as 'match' | 'regex' | 'semantic' | 'auto',
})
}),
)
new Setting(containerEl)
.setName(t('settings.FilesSearch.regexBackend'))
.setDesc(t('settings.FilesSearch.regexBackendDescription'))
.addDropdown((dropdown) =>
dropdown
.addOption('ripgrep', t('settings.FilesSearch.ripgrep'))
.addOption('coreplugin', t('settings.FilesSearch.coreplugin'))
.setValue(this.plugin.settings.regexSearchBackend)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
regexSearchBackend: value as 'ripgrep' | 'coreplugin',
})
}),
)
new Setting(containerEl)
.setName(t('settings.FilesSearch.matchBackend'))
.setDesc(t('settings.FilesSearch.matchBackendDescription'))
.addDropdown((dropdown) =>
dropdown
.addOption('coreplugin', t('settings.FilesSearch.coreplugin'))
.addOption('omnisearch', t('settings.FilesSearch.omnisearch'))
.setValue(this.plugin.settings.matchSearchBackend)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
matchSearchBackend: value as 'coreplugin' | 'omnisearch',
})
}),
)
new Setting(containerEl)
.setName(t('settings.FilesSearch.ripgrepPath'))
.setDesc(t('settings.FilesSearch.ripgrepPathDescription'))
.addText((text) =>
text
.setPlaceholder('/opt/homebrew/bin/')
.setValue(this.plugin.settings.ripgrepPath)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
ripgrepPath: value,
})
}),
)
}
private renderChatBehaviorSection(containerEl: HTMLElement): void {
new Setting(containerEl).setHeading().setName(t('settings.ChatBehavior.title'));
new Setting(containerEl)
.setName(t('settings.ChatBehavior.defaultMention'))
.setDesc(t('settings.ChatBehavior.defaultMentionDescription'))
.addDropdown((dropdown) =>
dropdown
.addOption('none', t('settings.ChatBehavior.none'))
.addOption('current-file', t('settings.ChatBehavior.currentFile'))
.addOption('vault', t('settings.ChatBehavior.vault'))
.setValue(this.plugin.settings.defaultMention || 'none')
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
defaultMention: value as 'none' | 'current-file' | 'vault',
});
}),
);
}
renderModelsSection(containerEl: HTMLElement): void {
const modelsDiv = containerEl.createDiv("models-section");
this.modelsContainer = modelsDiv;
this.renderModelsContent(modelsDiv);
}
renderDeepResearchSection(containerEl: HTMLElement): void {
new Setting(containerEl)
.setHeading()
.setName(t('settings.WebSearch.title'))
new Setting(containerEl)
.setName(t('settings.WebSearch.serperApiKey'))
.setDesc(createFragment(el => {
el.appendText(t('settings.WebSearch.serperApiKeyDescription') + ' ');
const a = el.createEl('a', {
href: 'https://serpapi.com/manage-api-key',
text: 'https://serpapi.com/manage-api-key'
});
a.setAttr('target', '_blank');
a.setAttr('rel', 'noopener');
}))
.setClass('setting-item-heading-smaller')
.addText((text) => {
const t = text
.setValue(this.plugin.settings.serperApiKey)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
serperApiKey: value,
})
});
if (t.inputEl) {
t.inputEl.type = "password";
}
return t;
})
new Setting(containerEl)
.setName(t('settings.WebSearch.searchEngine'))
.setDesc(t('settings.WebSearch.searchEngineDescription'))
.addDropdown((dropdown) =>
dropdown
.addOption('google', t('settings.WebSearch.google'))
.addOption('duckduckgo', t('settings.WebSearch.duckDuckGo'))
.addOption('bing', t('settings.WebSearch.bing'))
.setValue(this.plugin.settings.serperSearchEngine)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
// @ts-ignore
serperSearchEngine: value,
})
}),
)
new Setting(containerEl)
.setName(t('settings.WebSearch.jinaApiKey'))
.setDesc(createFragment(el => {
el.appendText(t('settings.WebSearch.jinaApiKeyDescription') + ' ');
const a = el.createEl('a', {
href: 'https://jina.ai/api-key',
text: 'https://jina.ai/api-key'
});
a.setAttr('target', '_blank');
a.setAttr('rel', 'noopener');
}))
.setClass('setting-item-heading-smaller')
.addText((text) => {
const t = text
.setValue(this.plugin.settings.jinaApiKey)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
jinaApiKey: value,
})
});
if (t.inputEl) {
t.inputEl.type = "password";
}
return t;
})
}
renderRAGSection(containerEl: HTMLElement): void {
// 创建一个折叠区域的容器
const ragContainer = containerEl.createDiv("rag-settings-container");
// 创建标题元素,添加折叠控件
const headerEl = ragContainer.createEl("div", { cls: "infio-collapsible-heading" });
// 添加展开/折叠指示器
const toggleIcon = headerEl.createEl("span", { cls: "infio-toggle-icon" });
toggleIcon.textContent = "▶"; // 默认为折叠状态,使用右箭头
// 添加标题文本
const titleEl = headerEl.createEl("h3", { text: t('settings.RAG.title') });
// 创建内容容器
const contentContainer = ragContainer.createEl("div", { cls: "infio-collapsible-content" });
// 默认设置为隐藏状态
contentContainer.style.display = "none";
// 添加点击事件处理
headerEl.addEventListener("click", () => {
if (contentContainer.style.display === "none") {
contentContainer.style.display = "block";
toggleIcon.textContent = "▼"; // 展开状态使用下箭头
toggleIcon.style.transform = "rotate(0deg)";
} else {
contentContainer.style.display = "none";
toggleIcon.textContent = "▶"; // 折叠状态使用右箭头
toggleIcon.style.transform = "rotate(0deg)";
}
});
// 添加样式
headerEl.style.cursor = "pointer";
headerEl.style.display = "flex";
headerEl.style.alignItems = "center";
headerEl.style.marginBottom = "10px";
headerEl.style.padding = "6px 0";
toggleIcon.style.marginRight = "5px";
toggleIcon.style.fontSize = "10px";
toggleIcon.style.transition = "transform 0.15s ease";
titleEl.style.margin = "0";
titleEl.style.fontSize = "16px";
titleEl.style.fontWeight = "600";
// 以下是原有的设置内容,移动到内容容器中
new Setting(contentContainer)
.setName(t('settings.RAG.includePatterns'))
.setDesc(
t('settings.RAG.includePatternsDescription'),
)
.addButton((button) =>
button.setButtonText(t('settings.RAG.testPatterns')).onClick(async () => {
const patterns = this.plugin.settings.ragOptions.includePatterns
const includedFiles = await findFilesMatchingPatterns(
patterns,
this.plugin.app.vault,
)
new IncludedFilesModal(this.app, includedFiles, patterns).open()
}),
)
new Setting(contentContainer)
.setClass('infio-chat-settings-textarea')
.addTextArea((text) =>
text
.setValue(this.plugin.settings.ragOptions.includePatterns.join('\n'))
.onChange(async (value) => {
const patterns = value
.split('\n')
.map((p) => p.trim())
.filter((p) => p.length > 0)
await this.plugin.setSettings({
...this.plugin.settings,
ragOptions: {
...this.plugin.settings.ragOptions,
includePatterns: patterns,
},
})
}),
)
new Setting(contentContainer)
.setName(t('settings.RAG.excludePatterns'))
.setDesc(
t('settings.RAG.excludePatternsDescription'),
)
.addButton((button) =>
button.setButtonText(t('settings.RAG.testPatterns')).onClick(async () => {
const patterns = this.plugin.settings.ragOptions.excludePatterns
const excludedFiles = await findFilesMatchingPatterns(
patterns,
this.plugin.app.vault,
)
new ExcludedFilesModal(this.app, excludedFiles).open()
}),
)
new Setting(contentContainer)
.setClass('infio-chat-settings-textarea')
.addTextArea((text) =>
text
.setValue(this.plugin.settings.ragOptions.excludePatterns.join('\n'))
.onChange(async (value) => {
const patterns = value
.split('\n')
.map((p) => p.trim())
.filter((p) => p.length > 0)
await this.plugin.setSettings({
...this.plugin.settings,
ragOptions: {
...this.plugin.settings.ragOptions,
excludePatterns: patterns,
},
})
}),
)
new Setting(contentContainer)
.setName(t('settings.RAG.chunkSize'))
.setDesc(
t('settings.RAG.chunkSizeDescription'),
)
.addText((text) =>
text
.setPlaceholder('1000')
.setValue(String(this.plugin.settings.ragOptions.chunkSize))
.onChange(async (value) => {
const chunkSize = parseInt(value, 10)
if (!isNaN(chunkSize)) {
await this.plugin.setSettings({
...this.plugin.settings,
ragOptions: {
...this.plugin.settings.ragOptions,
chunkSize,
},
})
}
}),
)
new Setting(contentContainer)
.setName(t('settings.RAG.thresholdTokens'))
.setDesc(
t('settings.RAG.thresholdTokensDescription'),
)
.addText((text) =>
text
.setPlaceholder('8192')
.setValue(String(this.plugin.settings.ragOptions.thresholdTokens))
.onChange(async (value) => {
const thresholdTokens = parseInt(value, 10)
if (!isNaN(thresholdTokens)) {
await this.plugin.setSettings({
...this.plugin.settings,
ragOptions: {
...this.plugin.settings.ragOptions,
thresholdTokens,
},
})
}
}),
)
new Setting(contentContainer)
.setName(t('settings.RAG.minSimilarity'))
.setDesc(
t('settings.RAG.minSimilarityDescription'),
)
.addText((text) =>
text
.setPlaceholder('0.0')
.setValue(String(this.plugin.settings.ragOptions.minSimilarity))
.onChange(async (value) => {
const minSimilarity = parseFloat(value)
if (!isNaN(minSimilarity)) {
await this.plugin.setSettings({
...this.plugin.settings,
ragOptions: {
...this.plugin.settings.ragOptions,
minSimilarity,
},
})
}
}),
)
new Setting(contentContainer)
.setName(t('settings.RAG.limit'))
.setDesc(
t('settings.RAG.limitDescription'),
)
.addText((text) =>
text
.setPlaceholder('10')
.setValue(String(this.plugin.settings.ragOptions.limit))
.onChange(async (value) => {
const limit = parseInt(value, 10)
if (!isNaN(limit)) {
await this.plugin.setSettings({
...this.plugin.settings,
ragOptions: {
...this.plugin.settings.ragOptions,
limit,
},
})
}
}),
)
}
renderAutoCompleteSection(containerEl: HTMLElement): void {
// 创建一个折叠区域的容器
const autoCompleteContainer = containerEl.createDiv("auto-complete-settings-container");
// 创建标题元素,添加折叠控件
const headerEl = autoCompleteContainer.createEl("div", { cls: "infio-collapsible-heading" });
// 添加展开/折叠指示器
const toggleIcon = headerEl.createEl("span", { cls: "infio-toggle-icon" });
toggleIcon.textContent = "▶"; // 默认为折叠状态,使用右箭头
// 添加标题文本
const titleEl = headerEl.createEl("h3", { text: t('settings.AutoComplete.title') });
// 创建内容容器
const contentContainer = autoCompleteContainer.createEl("div", { cls: "infio-collapsible-content" });
// 保存容器引用
this.autoCompleteContainer = contentContainer;
// 默认设置为隐藏状态
contentContainer.style.display = "none";
// 添加点击事件处理
headerEl.addEventListener("click", () => {
if (contentContainer.style.display === "none") {
contentContainer.style.display = "block";
toggleIcon.textContent = "▼"; // 展开状态使用下箭头
toggleIcon.style.transform = "rotate(0deg)";
} else {
contentContainer.style.display = "none";
toggleIcon.textContent = "▶"; // 折叠状态使用右箭头
toggleIcon.style.transform = "rotate(0deg)";
}
});
// 添加样式
headerEl.style.cursor = "pointer";
headerEl.style.display = "flex";
headerEl.style.alignItems = "center";
headerEl.style.marginBottom = "10px";
headerEl.style.padding = "6px 0";
toggleIcon.style.marginRight = "5px";
toggleIcon.style.fontSize = "10px";
toggleIcon.style.transition = "transform 0.15s ease";
titleEl.style.margin = "0";
titleEl.style.fontSize = "16px";
titleEl.style.fontWeight = "600";
// 在内容容器中渲染AutoComplete设置
this.renderAutoCompleteContent(contentContainer);
}
private renderAutoCompleteContent(containerEl: HTMLElement): void {
const updateSettings = async (update: Partial<InfioSettings>) => {
await this.plugin.setSettings({
...this.plugin.settings,
...update
});
// 只重新渲染 AutoComplete 部分
if (this.autoCompleteContainer) {
this.autoCompleteContainer.empty();
this.renderAutoCompleteContent(this.autoCompleteContainer);
}
};
const errors = new Map();
// AutoComplete base
// new Setting(containerEl).setName(t('settings.AutoComplete.title')).setHeading();
this.renderComponent(containerEl,
<BasicAutoCompleteSettings
settings={this.plugin.settings}
updateSettings={updateSettings}
/>
);
// Preprocessing
new Setting(containerEl).setName(t('settings.AutoComplete.preprocessing.title')).setHeading();
this.renderComponent(containerEl,
<PreprocessingSettings
settings={this.plugin.settings}
updateSettings={updateSettings}
errors={errors}
/>
);
// Postprocessing
new Setting(containerEl).setName(t('settings.AutoComplete.postprocessing.title')).setHeading();
this.renderComponent(containerEl,
<PostprocessingSettings
settings={this.plugin.settings}
updateSettings={updateSettings}
/>
);
// Trigger
new Setting(containerEl).setName(t('settings.AutoComplete.trigger.title')).setHeading();
this.renderComponent(containerEl,
<TriggerSettingsSection
settings={this.plugin.settings}
updateSettings={updateSettings}
errors={errors}
/>
);
// Privacy
new Setting(containerEl).setName(t('settings.AutoComplete.privacy.title')).setHeading();
this.renderComponent(containerEl,
<PrivacySettings
settings={this.plugin.settings}
updateSettings={updateSettings}
errors={errors}
/>
);
// // Danger zone
// new Setting(containerEl).setName(t('settings.AutoComplete.dangerZone.title')).setHeading();
// this.renderComponent(containerEl,
// <DangerZoneSettings
// settings={this.plugin.settings}
// updateSettings={updateSettings}
// onReset={() => {
// new Notice(t('settings.AutoComplete.dangerZone.resetComplete'));
// }}
// />
// );
// // Advanced
// if (this.plugin.settings.advancedMode) {
// new Setting(containerEl).setName(t('settings.AutoComplete.advanced.title')).setHeading();
// this.renderComponent(containerEl,
// <AdvancedSettings
// settings={this.plugin.settings}
// updateSettings={updateSettings}
// errors={errors}
// />
// );
// }
}
private renderComponent(containerEl: HTMLElement, component: React.ReactNode) {
const div = containerEl.createDiv("div");
const root = createRoot(div);
root.render(component);
}
}
class ExcludedFilesModal extends Modal {
private files: TFile[]
constructor(app: App, files: TFile[]) {
super(app)
this.files = files
}
onOpen() {
const { contentEl } = this
contentEl.empty()
this.titleEl.setText(`Excluded Files (${this.files.length})`)
if (this.files.length === 0) {
contentEl.createEl('p', { text: t('settings.RAG.noExcludedFiles') })
return
}
const list = contentEl.createEl('ul')
this.files.forEach((file) => {
list.createEl('li', { text: file.path })
})
}
onClose() {
const { contentEl } = this
contentEl.empty()
}
}
class IncludedFilesModal extends Modal {
private files: TFile[]
private patterns: string[]
constructor(app: App, files: TFile[], patterns: string[]) {
super(app)
this.files = files
this.patterns = patterns
}
onOpen() {
const { contentEl } = this
contentEl.empty()
this.titleEl.setText(`Included Files (${this.files.length})`)
if (this.patterns.length === 0) {
contentEl.createEl('p', {
text: t('settings.RAG.noInclusionPatterns'),
})
return
}
if (this.files.length === 0) {
contentEl.createEl('p', {
text: t('settings.RAG.noMatchingFiles'),
})
return
}
const list = contentEl.createEl('ul')
this.files.forEach((file) => {
list.createEl('li', { text: file.path })
})
}
onClose() {
const { contentEl } = this
contentEl.empty()
}
}