[fix review issue]: update setting name

This commit is contained in:
duanfuxiang 2025-02-17 19:07:33 +08:00
parent 173f2b7fa5
commit dc520535fc
22 changed files with 721 additions and 1269 deletions

View File

@ -652,7 +652,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
{submitMutation.isPending && ( {submitMutation.isPending && (
<button onClick={abortActiveStreams} className="infio-stop-gen-btn"> <button onClick={abortActiveStreams} className="infio-stop-gen-btn">
<CircleStop size={16} /> <CircleStop size={16} />
<div>Stop Generation</div> <div>Stop generation</div>
</button> </button>
)} )}
</div> </div>

View File

@ -30,9 +30,9 @@ export default function LLMResponseInfoPopover({
</Popover.Trigger> </Popover.Trigger>
{usage ? ( {usage ? (
<Popover.Content className="infio-chat-popover-content infio-llm-info-content"> <Popover.Content className="infio-chat-popover-content infio-llm-info-content">
<div className="infio-llm-info-header">LLM Response Information</div> <div className="infio-llm-info-header">LLM response information</div>
<div className="infio-llm-info-tokens"> <div className="infio-llm-info-tokens">
<div className="infio-llm-info-tokens-header">Token Count</div> <div className="infio-llm-info-tokens-header">Token count</div>
<div className="infio-llm-info-tokens-grid"> <div className="infio-llm-info-tokens-grid">
<div className="infio-llm-info-token-row"> <div className="infio-llm-info-token-row">
<ArrowUp className="infio-llm-info-icon--input" /> <ArrowUp className="infio-llm-info-icon--input" />
@ -59,7 +59,7 @@ export default function LLMResponseInfoPopover({
</div> </div>
<div className="infio-llm-info-footer-row"> <div className="infio-llm-info-footer-row">
<Coins className="infio-llm-info-icon--footer" /> <Coins className="infio-llm-info-icon--footer" />
<span>Estimated Price:</span> <span>Estimated price:</span>
<span className="infio-llm-info-footer-value"> <span className="infio-llm-info-footer-value">
{estimatedPrice === null {estimatedPrice === null
? 'Not available' ? 'Not available'

View File

@ -150,7 +150,7 @@ function CurrentFileBadge({
<span>{mentionable.file.name}</span> <span>{mentionable.file.name}</span>
</div> </div>
<div className="infio-chat-user-input-file-badge-name-block-suffix"> <div className="infio-chat-user-input-file-badge-name-block-suffix">
{' (Current File)'} {' (Current file)'}
</div> </div>
</BadgeBase> </BadgeBase>
) : null ) : null

View File

@ -208,7 +208,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'autocomplete-accept', id: 'autocomplete-accept',
name: 'Autocomplete Accept', name: 'Autocomplete accept',
editorCheckCallback: ( editorCheckCallback: (
checking: boolean, checking: boolean,
editor: Editor, editor: Editor,
@ -228,7 +228,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: 'autocomplete-predict', id: 'autocomplete-predict',
name: 'Autocomplete Predict', name: 'Autocomplete predict',
editorCheckCallback: ( editorCheckCallback: (
checking: boolean, checking: boolean,
editor: Editor, editor: Editor,
@ -251,7 +251,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: "autocomplete-toggle", id: "autocomplete-toggle",
name: "Autocomplete Toggle", name: "Autocomplete toggle",
callback: () => { callback: () => {
const newValue = !this.settings.autocompleteEnabled; const newValue = !this.settings.autocompleteEnabled;
this.setSettings({ this.setSettings({
@ -262,8 +262,8 @@ export default class InfioPlugin extends Plugin {
}); });
this.addCommand({ this.addCommand({
id: "infio-autocomplete-enable", id: "autocomplete-enable",
name: "Infio Autocomplete Enable", name: "Autocomplete enable",
checkCallback: (checking) => { checkCallback: (checking) => {
if (checking) { if (checking) {
return !this.settings.autocompleteEnabled; return !this.settings.autocompleteEnabled;
@ -279,7 +279,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: "autocomplete-disable", id: "autocomplete-disable",
name: "Autocomplete Disable", name: "Autocomplete disable",
checkCallback: (checking) => { checkCallback: (checking) => {
if (checking) { if (checking) {
return this.settings.autocompleteEnabled; return this.settings.autocompleteEnabled;
@ -295,7 +295,7 @@ export default class InfioPlugin extends Plugin {
this.addCommand({ this.addCommand({
id: "ai-inline-edit", id: "ai-inline-edit",
name: "Inline Edit", name: "Inline edit",
// hotkeys: [ // hotkeys: [
// { // {
// modifiers: ['Mod', 'Shift'], // modifiers: ['Mod', 'Shift'],

View File

@ -1,610 +0,0 @@
import { Notice } from "obsidian";
import * as React from "react";
import { useState } from "react";
import {
InfioSettings,
} from '../types/settings';
import { checkForErrors } from "../utils/auto-complete";
import CheckBoxSettingItem from "./components/CheckBoxSettingItem";
import FewShotExampleSettings from "./components/FewShotExampleSettings";
import SettingsItem from "./components/SettingsItem";
import SliderSettingsItem from "./components/SliderSettingsItem";
import TextSettingItem from "./components/TextSettingItem";
import TriggerSettings from "./components/TriggerSettings";
import {
MAX_DELAY,
MAX_FREQUENCY_PENALTY,
MAX_MAX_CHAR_LIMIT,
MAX_MAX_TOKENS,
MAX_PRESENCE_PENALTY,
MAX_TEMPERATURE,
MAX_TOP_P,
MIN_DELAY,
MIN_FREQUENCY_PENALTY,
MIN_MAX_CHAR_LIMIT,
MIN_MAX_TOKENS,
MIN_PRESENCE_PENALTY,
MIN_TEMPERATURE,
MIN_TOP_P
} from "./versions";
type IProps = {
onSettingsChanged(settings: InfioSettings): void;
settings: InfioSettings;
}
export default function AutoCompleteSettings(props: IProps): React.JSX.Element {
const [settings, _setSettings] = useState<InfioSettings>(props.settings);
const errors = checkForErrors(settings);
React.useEffect(() => {
_setSettings(props.settings);
}, [props.settings]);
const updateSettings = (update: Partial<InfioSettings>) => {
_setSettings((settings: InfioSettings) => {
const newSettings = { ...settings, ...update };
props.onSettingsChanged(newSettings);
return newSettings;
});
};
const resetSettings = () => {
// const azureOAIApiSettings = settings.azureOAIApiSettings,
// const openAIApiSettings = settings.openAIApiSettings,
// const ollamaApiSettings = settings.ollamaApiSettings
// const newSettings: SmartCopilotSettings = {
// ...settings
// azureOAIApiSettings,
// openAIApiSettings,
// ollamaApiSettings,
// advancedMode: settings.advancedMode,
// };
// updateSettings(newSettings);
new Notice("Factory reset complete.");
};
// const renderAPISettings = () => {
// if (settings.apiProvider === "azure") {
// return (
// <>
// <TextSettingItem
// name={"Azure OAI API URL"}
// description={
// "The Azure OpenAI services API URL is used in the requests."
// }
// placeholder={"Your API URL..."}
// value={settings.azureOAIApiSettings.url}
// errorMessage={errors.get("azureOAIApiSettings.url")}
// setValue={(value: string) =>
// updateSettings({
// azureOAIApiSettings: {
// ...settings.azureOAIApiSettings,
// url: value,
// },
// })
// }
// />
// <TextSettingItem
// name={"Azure API key"}
// description={
// "The Azure OpenAI services API key used in the requests."
// }
// placeholder={"Your API key..."}
// password
// value={settings.azureOAIApiSettings.key}
// errorMessage={errors.get("azureOAIApiSettings.key")}
// setValue={(value: string) =>
// updateSettings({
// azureOAIApiSettings: {
// ...settings.azureOAIApiSettings,
// key: value,
// },
// })
// }
// />
// <ConnectivityCheck key={"azure"} settings={settings} />
// </>
// );
// }
// if (settings.apiProvider === "openai") {
// return (
// <>
// <TextSettingItem
// name={"OpenAI API URL"}
// description={
// "The URL used in the requests."
// }
// placeholder={"Your API URL..."}
// value={settings.openAIApiSettings.url}
// errorMessage={errors.get("openAIApiSettings.url")}
// setValue={(value: string) =>
// updateSettings({
// openAIApiSettings: {
// ...settings.openAIApiSettings,
// url: value,
// },
// })
// }
// />
// <TextSettingItem
// name={"OpenAI API key"}
// description={"The API key used in the requests."}
// placeholder={"Your API key..."}
// password
// value={settings.openAIApiSettings.key}
// errorMessage={errors.get("openAIApiSettings.key")}
// setValue={(value: string) =>
// updateSettings({
// openAIApiSettings: {
// ...settings.openAIApiSettings,
// key: value,
// },
// })
// }
// />
// <TextSettingItem
// name={"Model"}
// description={"The value of the model parameter in the request body."}
// placeholder="gpt-3.5-turbo"
// value={settings.openAIApiSettings.model}
// setValue={(value: string) =>
// updateSettings({
// openAIApiSettings: {
// ...settings.openAIApiSettings,
// model: value,
// }
// })
// }
// errorMessage={errors.get("openAIApiSettings.model")}
// />
// <ConnectivityCheck key={"openai"} settings={settings} />
// </>
// );
// }
// if (settings.apiProvider === "ollama") {
// return (
// <>
// <TextSettingItem
// name={"API URL"}
// description={
// "The URL used in the requests."
// }
// placeholder={"Your API URL..."}
// value={settings.ollamaApiSettings.url}
// errorMessage={errors.get("ollamaApiSettings.url")}
// setValue={(value: string) =>
// updateSettings({
// ollamaApiSettings: {
// ...settings.ollamaApiSettings,
// url: value,
// },
// })
// }
// />
// <TextSettingItem
// name={"Model"}
// description={"The model you have locally running using OLLAMA."}
// placeholder="Your model name..."
// value={settings.ollamaApiSettings.model}
// setValue={(value: string) =>
// updateSettings({
// ollamaApiSettings: {
// ...settings.ollamaApiSettings,
// model: value,
// }
// })
// }
// errorMessage={errors.get("ollamaApiSettings.model")}
// />
// <ConnectivityCheck key={"openai"} settings={settings} />
// </>
// );
// }
// };
return (
<div>
<h2>AutoComplete</h2>
<CheckBoxSettingItem
name={"Enable"}
description={
"If disabled, nothing will trigger the extension or can result in an API call."
}
enabled={settings.autocompleteEnabled}
setEnabled={(value) => updateSettings({ autocompleteEnabled: value })}
/>
<CheckBoxSettingItem
name={"Cache completions"}
description={
"If disabled, the plugin will not cache the completions. After accepting or rejecting a completion, the plugin will not remember it. This might result in more API calls."
}
enabled={settings.cacheSuggestions}
setEnabled={(value) => updateSettings({ cacheSuggestions: value })}
/>
{/* <DropDownSettingItem
name={"API provider"}
description={
"The plugin supports multiple API providers. Each provider might require different settings."
}
value={settings.apiProvider}
setValue={(value: string) => {
if (value === "openai" || value === "azure" || value === "ollama") {
updateSettings({ apiProvider: value });
}
}}
options={{
openai: "OpenAI API",
azure: "Azure OAI API",
ollama: "Self-hosted OLLAMA API"
}}
errorMessage={errors.get("apiProvider")}
/> */}
<CheckBoxSettingItem
name={"Debug mode"}
description={
"If enabled, various debug messages will be logged to the console, such as the complete response from the API, including the chain of thought tokens."
}
enabled={settings.debugMode}
setEnabled={(value) => updateSettings({ debugMode: value })}
/>
{/* <h2>API</h2>
{renderAPISettings()} */}
<h2>Model Options</h2>
<SliderSettingsItem
name={"Temperature"}
description={
"This parameter affects randomness in the sampling. Lower values result in more repetitive and deterministic responses. Higher temperatures will result in more unexpected or creative responses."
}
value={settings.modelOptions.temperature}
errorMessage={errors.get("modelOptions.temperature")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
temperature: value,
},
})
}
min={MIN_TEMPERATURE}
max={MAX_TEMPERATURE}
step={0.05}
/>
<SliderSettingsItem
name={"TopP"}
description={
"Like the temperature parameter, the Top P parameter affects the randomness in sampling. Lowering the value will limit the model's token selection to likelier tokens while increasing the value expands the model's token selection with lower likelihood tokens."
}
value={settings.modelOptions.top_p}
errorMessage={errors.get("modelOptions.top_p")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
top_p: value,
},
})
}
min={MIN_TOP_P}
max={MAX_TOP_P}
step={0.05}
/>
{settings.apiProvider !== "ollama" && (<>
<SliderSettingsItem
name={"Frequency Penalty"}
description={
"This parameter reduces the chance of repeating a token proportionally based on how often it has appeared in the text so far. This decreases the likelihood of repeating the exact same text in a response."
}
value={settings.modelOptions.frequency_penalty}
errorMessage={errors.get("modelOptions.frequency_penalty")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
frequency_penalty: value,
},
})
}
min={MIN_FREQUENCY_PENALTY}
max={MAX_FREQUENCY_PENALTY}
step={0.05}
/>
<SliderSettingsItem
name={"Presence Penalty"}
description={
"This parameter reduces the chance of repeating any token that has appeared in the text so far. This increases the likelihood of introducing new topics in a response."
}
value={settings.modelOptions.presence_penalty}
errorMessage={errors.get("modelOptions.presence_penalty")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
presence_penalty: value,
},
})
}
min={MIN_PRESENCE_PENALTY}
max={MAX_PRESENCE_PENALTY}
step={0.05}
/>
<SliderSettingsItem
name={"Max Tokens"}
description={
"This parameter changes the maximum number of tokens the model is allowed to generate. This includes the chain of thought tokens before the answer."
}
value={settings.modelOptions.max_tokens}
errorMessage={errors.get("modelOptions.max_tokens")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
max_tokens: value,
},
})
}
min={MIN_MAX_TOKENS}
max={MAX_MAX_TOKENS}
step={10}
/>
</>)}
<h2>Preprocessing</h2>
<CheckBoxSettingItem
name={"Don't include dataviews"}
description={
"Dataview(js) blocks can be quite long while not providing much value to the AI. If this setting is enabled, data view blocks will be removed promptly to reduce the number of tokens. This could save you some money in the long run."
}
enabled={settings.dontIncludeDataviews}
setEnabled={(value) =>
updateSettings({ dontIncludeDataviews: value })
}
/>
<SliderSettingsItem
name={"Maximum Prefix Length"}
description={
"The maximum number of characters that will be included in the prefix. A larger value will increase the context for the completion, but it can also increase the cost or push you over the token limit."
}
value={settings.maxPrefixCharLimit}
errorMessage={errors.get("maxPrefixCharLimit")}
setValue={(value: number) =>
updateSettings({ maxPrefixCharLimit: value })
}
min={MIN_MAX_CHAR_LIMIT}
max={MAX_MAX_CHAR_LIMIT}
step={100}
suffix={" chars"}
/>
<SliderSettingsItem
name={"Maximum Suffix Length"}
description={
"The maximum number of characters that will be included in the suffix. A larger value will increase the context for the completion, but it can also increase the cost or push you over the token limit."
}
value={settings.maxSuffixCharLimit}
errorMessage={errors.get("maxSuffixCharLimit")}
setValue={(value: number) =>
updateSettings({ maxSuffixCharLimit: value })
}
min={MIN_MAX_CHAR_LIMIT}
max={MAX_MAX_CHAR_LIMIT}
step={100}
suffix={" chars"}
/>
<h2>Postprocessing</h2>
<CheckBoxSettingItem
name={"Auto remove duplicate mat block indicators"}
description={
"The AI model might eagerly add a math block indicator ($), even though the cursor is already inside a math block. If this setting is enabled, the plugin will automatically remove these duplicate indicators from the completion."
}
enabled={settings.removeDuplicateMathBlockIndicator}
setEnabled={(value) =>
updateSettings({ removeDuplicateMathBlockIndicator: value })
}
/>
<CheckBoxSettingItem
name={"Auto remove duplicate mat block indicators"}
description={
"The AI model might eagerly add a code block indicator (`), even though the cursor is already inside a code block. If this setting is enabled, the plugin will automatically remove these duplicate indicators from the completion."
}
enabled={settings.removeDuplicateCodeBlockIndicator}
setEnabled={(value) =>
updateSettings({ removeDuplicateCodeBlockIndicator: value })
}
/>
<h2>Trigger</h2>
<SliderSettingsItem
name={"Delay"}
description={
"Delay in ms between the last character typed and the completion request."
}
value={settings.delay}
errorMessage={errors.get("delay")}
setValue={(value: number) => updateSettings({ delay: value })}
min={MIN_DELAY}
max={MAX_DELAY}
step={100}
suffix={"ms"}
/>
<TriggerSettings
name={"Trigger words"}
description={
"Completions will be triggered if the text before the matches any of these words or characters. This can either be a direct string match or a regex match. When using a regex, make sure to include the end of line character ($)."
}
triggers={settings.triggers}
setValues={(triggers) => updateSettings({ triggers })}
errorMessage={errors.get("triggerWords")}
errorMessages={errors}
/>
<h2>Privacy</h2>
<SettingsItem
name={"Ignored files"}
description={
<div>
<p>This field enables you to specify files and directories that the plugin should ignore. When
you open any of these files, the plugin will automatically disable itself and display a
'disabled' status in the bottom menu. Enter one pattern per line. These patterns function
similar to glob patterns. Here are some frequently used patterns:</p>
<ul>
<li><code>path/to/folder/**</code>: This pattern ignores all files and sub folders within
this folder.
</li>
<li><code>"**/secret/**"</code>: This pattern ignores any file located inside a 'secret'
directory,
regardless of its location in the path.
</li>
<li><code>!path/to/folder/example.md</code>: This pattern explicitly undoes an ignore,
making this file noticeable to the plugin.
</li>
<li><code>**/*Python*.md</code>: This pattern ignores any file with 'Python' in its name,
irrespective of its location.
</li>
</ul>
</div>
}
display={"block"}
errorMessage={errors.get("ignoredFilePatterns")}
>
<textarea
className="infio-autocomplete-setting-item-textarea"
rows={10}
placeholder="Your file patterns, e.g., **/secret/**"
value={settings.ignoredFilePatterns}
onChange={(e) =>
updateSettings({
ignoredFilePatterns: e.target.value
})
}
/>
</SettingsItem>
<SettingsItem
name={"Ignored tags"}
description={
<div>
<p>Files containing any of these tags will be ignored. When you open a file containing a
tag listed here, the plugin will automatically disable itself and display a 'disabled'
status in the bottom menu. Enter one tag per line.
</p>
</div>
}
display={"block"}
errorMessage={errors.get("ignoredTags")}
>
<textarea
className="infio-autocomplete-setting-item-textarea"
rows={10}
placeholder="Your file tags, e.g., secret"
value={settings.ignoredTags}
onChange={(e) =>
updateSettings({
ignoredTags: e.target.value
})
}
/>
</SettingsItem>
<h2>Danger zone</h2>
<SettingsItem
name={"Factory Reset"}
description={
"Messed-up the settings? No worries, press this button! After that, the plugin will go back to the default settings. The URL and API key will remain unchanged."
}
>
<button
aria-label="Reset to default settings"
onClick={resetSettings}
>
Reset
</button>
</SettingsItem>
<CheckBoxSettingItem
name={"Advanced mode"}
description={
"If you are familiar with prompt engineering, you can enable this setting to view the prompt generation and a few shot example settings. Turn off this button. It will not reset your changes; use the factory reset button for that."
}
enabled={settings.advancedMode}
setEnabled={(value) => updateSettings({ advancedMode: value })}
/>
{settings.advancedMode && (
<>
<h2>Advanced</h2>
<TextSettingItem
name={"Chain of thought removal regex"}
description={
"This regex is used to remove the chain of thought tokens from the generated answer. If it is not implemented correctly, the chain of thought tokens will be included in the suggested completion."
}
placeholder={"your regex..."}
value={settings.chainOfThoughRemovalRegex}
errorMessage={errors.get("chainOfThoughRemovalRegex")}
setValue={(value: string) =>
updateSettings({
chainOfThoughRemovalRegex: value,
})
}
/>
<SettingsItem
name={"System Message"}
description={
"This system message gives the models all the context and instructions they need to complete the answer generation tasks. You can edit this message to your liking. If you edit the chain of thought formatting, make sure to update the extract regex and examples accordingly."
}
display={"block"}
errorMessage={errors.get("systemMessage")}
>
<textarea
className="infio-autocomplete-setting-item-textarea"
rows={10}
placeholder="Your system message..."
value={settings.systemMessage}
onChange={(e) =>
updateSettings({
systemMessage: e.target.value,
})
}
/>
</SettingsItem>
<SettingsItem
name={"User Message template"}
description={
"This template defines how the prefix and suffix are formatted to create the user message. You have access to two variables: {{prefix}} and {{suffix}}. If you edit this, make sure to update the examples accordingly."
}
display={"block"}
errorMessage={errors.get("userMessageTemplate")}
>
<textarea
className="infio-autocomplete-setting-item-textarea"
rows={3}
placeholder="{{prefix}}<mask/>{{suffix}}"
value={settings.userMessageTemplate}
onChange={(e) =>
updateSettings({
userMessageTemplate: e.target.value,
})
}
/>
</SettingsItem>
<FewShotExampleSettings
fewShotExamples={settings.fewShotExamples}
name={"Few Shot Examples"}
description={
"The model uses these examples to learn the expected answer format. Not all examples are sent at the same time. We only send the relevant examples, given the current cursor location. For example, the CodeBlock examples are only sent if the cursor is in a code block. If no special context is detected, we send the Text examples. Each context has a default of 2 examples, but you can add or remove examples if there is at least one per context. You can add more examples, but this will increase the inference costs."
}
setFewShotExamples={(value) =>
updateSettings({ fewShotExamples: value })
}
errorMessages={errors}
/>
</>
)}
</div>
);
}

View File

@ -1,34 +0,0 @@
import React from "react";
import InfioPlugin from "../main";
import { InfioSettings } from "../types/settings";
// import ModelsSettings from "./ModelsSettings";
import ProviderSettings from "./ProviderSettings";
type CustomSettingsProps = {
plugin: InfioPlugin;
}
const CustomSettings: React.FC<CustomSettingsProps> = ({ plugin }) => {
const settings = plugin.settings;
const handleSettingsUpdate = async (newSettings: InfioSettings) => {
await plugin.setSettings(newSettings);
// Force refresh the settings page to update dropdowns
plugin.settingTab.display();
};
return (
<div>
<h1 className="infio-llm-setting-title">
<div>
Infio Settings <small>v{settings.version}</small>
</div>
</h1>
<ProviderSettings settings={settings} setSettings={handleSettingsUpdate} />
</div>
);
};
export default CustomSettings;

View File

@ -1,26 +1,32 @@
import { import {
App, App,
DropdownComponent,
Modal, Modal,
Notice,
PluginSettingTab, PluginSettingTab,
Setting, Setting,
TFile, TFile
} from 'obsidian'; } from 'obsidian';
import * as React from "react"; import * as React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
// import {
// EMBEDDING_MODEL_OPTIONS,
// } from '../constants'
import InfioPlugin from '../main'; import InfioPlugin from '../main';
import { InfioSettings } from '../types/settings';
import { findFilesMatchingPatterns } from '../utils/glob-utils'; import { findFilesMatchingPatterns } from '../utils/glob-utils';
import { getOllamaModels } from '../utils/ollama';
import AutoCompleteSettings from './AutoCompleteSettings'; import AdvancedSettings from './components/AdvancedSettings';
import CustomSettings from './CustomSettings'; import BasicAutoCompleteSettings from './components/BasicAutoCompleteSettings';
import DangerZoneSettings from './components/DangerZoneSettings';
import ModelParametersSettings from './components/ModelParametersSettings';
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 { export class InfioSettingTab extends PluginSettingTab {
plugin: InfioPlugin plugin: InfioPlugin;
private autoCompleteContainer: HTMLElement | null = null;
private modelsContainer: HTMLElement | null = null;
constructor(app: App, plugin: InfioPlugin) { constructor(app: App, plugin: InfioPlugin) {
super(app, plugin) super(app, plugin)
@ -35,522 +41,30 @@ export class InfioSettingTab extends PluginSettingTab {
this.renderAutoCompleteSection(containerEl) this.renderAutoCompleteSection(containerEl)
} }
renderModelsSection(containerEl: HTMLElement): void { private renderModelsContent(containerEl: HTMLElement): void {
const div = containerEl.createDiv("div"); const div = containerEl.createDiv("div");
const sections = createRoot(div); const sections = createRoot(div);
sections.render(<CustomSettings plugin={this.plugin} />); sections.render(
<CustomProviderSettings
plugin={this.plugin}
onSettingsUpdate={() => {
if (this.modelsContainer) {
this.modelsContainer.empty();
this.renderModelsContent(this.modelsContainer);
}
}}
/>
);
} }
renderAPIKeysSection(containerEl: HTMLElement): void { renderModelsSection(containerEl: HTMLElement): void {
new Setting(containerEl) const modelsDiv = containerEl.createDiv("models-section");
.setHeading() this.modelsContainer = modelsDiv;
.setName('API keys') this.renderModelsContent(modelsDiv);
new Setting(containerEl)
.setName('Infio API key')
.setClass("infio-chat-setting-item-container")
.addText((text) =>
text
.setPlaceholder('Enter your API key')
.setValue(this.plugin.settings.infioApiKey)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
infioApiKey: value,
})
}),
)
new Setting(containerEl)
.setName('Anthropic API key')
.setClass("infio-chat-setting-item-container-append")
.addText((text) =>
text
.setPlaceholder('Enter your API key')
.setValue(this.plugin.settings.anthropicApiKey)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
anthropicApiKey: value,
})
}),
)
new Setting(containerEl)
.setName('Deepseek API key')
.setClass("infio-chat-setting-item-container-append")
.addText((text) =>
text
.setPlaceholder('Enter your API key')
.setValue(this.plugin.settings.deepseekApiKey)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
deepseekApiKey: value,
})
}),
)
new Setting(containerEl)
.setName('OpenAI API key')
.setClass("infio-chat-setting-item-container-append")
.addText((text) =>
text
.setPlaceholder('Enter your API key')
.setValue(this.plugin.settings.openAIApiKey)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
openAIApiKey: value,
})
}),
)
new Setting(containerEl)
.setName('Google API key')
.setClass("infio-chat-setting-item-container-append")
.addText((text) =>
text
.setPlaceholder('Enter your API key')
.setValue(this.plugin.settings.geminiApiKey)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
geminiApiKey: value,
})
}),
)
new Setting(containerEl)
.setName('Groq API key')
.setClass("infio-chat-setting-item-container-append")
.addText((text) =>
text
.setPlaceholder('Enter your API key')
.setValue(this.plugin.settings.groqApiKey)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
groqApiKey: value,
})
}),
)
}
renderDefaultModelSection(containerEl: HTMLElement): void {
new Setting(containerEl).setHeading().setName('Default Model')
new Setting(containerEl)
.setName('Default chat model')
.setClass("infio-chat-setting-item-container")
.addDropdown((dropdown) =>
dropdown
.addOptions(
this.plugin.settings.activeModels
.reduce<Record<string, string>>((acc, option) => {
if (!option.isEmbeddingModel && option.enabled) {
acc[option.name] = option.name
}
return acc
}, {}),
)
.setValue(this.plugin.settings.chatModelId)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
chatModelId: value,
})
// Force refresh to show/hide Ollama and OpenAI-compatible settings
this.display()
}),
)
if (this.plugin.settings.chatModelId === 'ollama') {
this.renderOllamaChatModelSettings(containerEl)
}
if (this.plugin.settings.chatModelId === 'openai-compatible') {
this.renderOpenAICompatibleChatModelSettings(containerEl)
}
new Setting(containerEl)
.setName('Default apply model')
.setClass("infio-chat-setting-item-container-append")
.addDropdown((dropdown) =>
dropdown
.addOptions(
this.plugin.settings.activeModels
.reduce<Record<string, string>>(
(acc, option) => {
if (!option.isEmbeddingModel && option.enabled) {
acc[option.name] = option.name
}
return acc
},
{},
),
)
.setValue(this.plugin.settings.applyModelId)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
applyModelId: value,
})
// Force refresh to show/hide Ollama and OpenAI-compatible settings
this.display()
}),
)
if (this.plugin.settings.applyModelId === 'ollama') {
this.renderOllamaApplyModelSettings(containerEl)
}
if (this.plugin.settings.applyModelId === 'openai-compatible') {
this.renderOpenAICompatibleApplyModelSettings(containerEl)
}
new Setting(containerEl)
.setName('Default embedding model')
.setClass("infio-chat-setting-item-container-append")
.addDropdown((dropdown) =>
dropdown
.addOptions(
this.plugin.settings.activeModels
.reduce<Record<string, string>>(
(acc, option) => {
if (option.isEmbeddingModel && option.enabled) {
acc[option.name] = option.name
}
return acc
},
{},
),
)
.setValue(this.plugin.settings.embeddingModelId)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
embeddingModelId: value,
})
// Force refresh to show/hide Ollama settings
this.display()
}),
)
if (this.plugin.settings.embeddingModelId.startsWith('ollama/')) {
this.renderOllamaEmbeddingModelSettings(containerEl)
}
new Setting(containerEl)
.setHeading()
.setName('System prompt')
.setDesc('This prompt will be added to the beginning of every chat.')
new Setting(containerEl)
.setClass('infio-chat-settings-textarea')
.addTextArea((text) =>
text
.setValue(this.plugin.settings.systemPrompt)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
systemPrompt: value,
})
}),
)
}
renderOllamaChatModelSettings(containerEl: HTMLElement): void {
const ollamaContainer = containerEl.createDiv(
'infio-chat-settings-model-container',
)
let modelDropdown: DropdownComponent | null = null // Store reference to the dropdown
// Base URL Setting
new Setting(ollamaContainer)
.setName('Base URL')
.setClass("infio-chat-setting-item-container-append")
.setDesc(
'The API endpoint for your Ollama service (e.g., http://127.0.0.1:11434)',
)
.addText((text) => {
text
.setPlaceholder('http://127.0.0.1:11434')
.setValue(this.plugin.settings.ollamaChatModel.baseUrl || '')
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
ollamaChatModel: {
...this.plugin.settings.ollamaChatModel,
baseUrl: value,
},
})
if (modelDropdown) {
await this.updateOllamaModelOptions({
baseUrl: value,
dropdown: modelDropdown,
onModelChange: async (model: string) => {
await this.plugin.setSettings({
...this.plugin.settings,
ollamaChatModel: {
...this.plugin.settings.ollamaChatModel,
model,
},
})
},
})
}
})
})
// Model Setting
new Setting(ollamaContainer)
.setName('Model Name')
.setDesc('Select a model from your Ollama instance')
.addDropdown(async (dropdown) => {
const currentModel = this.plugin.settings.ollamaChatModel.model
modelDropdown = dropdown
.addOption(currentModel, currentModel)
.setValue(currentModel)
await this.updateOllamaModelOptions({
baseUrl: this.plugin.settings.ollamaChatModel.baseUrl,
dropdown,
onModelChange: async (model: string) => {
await this.plugin.setSettings({
...this.plugin.settings,
ollamaChatModel: {
...this.plugin.settings.ollamaChatModel,
model,
},
})
},
})
})
}
renderOpenAICompatibleChatModelSettings(containerEl: HTMLElement): void {
const openAICompatContainer = containerEl.createDiv(
'infio-chat-settings-model-container',
)
new Setting(openAICompatContainer)
.setName('Base URL')
.setDesc(
'The API endpoint for your OpenAI-compatible service (e.g., https://api.example.com/v1)',
)
.addText((text) =>
text
.setPlaceholder('https://api.example.com/v1')
.setValue(
this.plugin.settings.openAICompatibleChatModel.baseUrl || '',
)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
openAICompatibleChatModel: {
...this.plugin.settings.openAICompatibleChatModel,
baseUrl: value,
},
})
}),
)
new Setting(openAICompatContainer)
.setName('API Key')
.setDesc('Your authentication key for the OpenAI-compatible service')
.addText((text) =>
text
.setPlaceholder('Enter your API key')
.setValue(this.plugin.settings.openAICompatibleChatModel.apiKey || '')
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
openAICompatibleChatModel: {
...this.plugin.settings.openAICompatibleChatModel,
apiKey: value,
},
})
}),
)
new Setting(openAICompatContainer)
.setName('Model Name')
.setDesc(
'The specific model to use with your service (e.g., llama-3.1-70b, mixtral-8x7b)',
)
.addText((text) =>
text
.setPlaceholder('llama-3.1-70b')
.setValue(this.plugin.settings.openAICompatibleChatModel.model || '')
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
openAICompatibleChatModel: {
...this.plugin.settings.openAICompatibleChatModel,
model: value,
},
})
}),
)
}
renderOllamaApplyModelSettings(containerEl: HTMLElement): void {
const ollamaContainer = containerEl.createDiv(
'infio-chat-settings-model-container',
)
let modelDropdown: DropdownComponent | null = null // Store reference to the dropdown
// Base URL Setting
new Setting(ollamaContainer)
.setName('Base URL')
.setDesc(
'The API endpoint for your Ollama service (e.g., http://127.0.0.1:11434)',
)
.addText((text) => {
text
.setPlaceholder('http://127.0.0.1:11434')
.setValue(this.plugin.settings.ollamaApplyModel.baseUrl || '')
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
ollamaApplyModel: {
...this.plugin.settings.ollamaApplyModel,
baseUrl: value,
},
})
if (modelDropdown) {
await this.updateOllamaModelOptions({
baseUrl: value,
dropdown: modelDropdown,
onModelChange: async (model: string) => {
await this.plugin.setSettings({
...this.plugin.settings,
ollamaApplyModel: {
...this.plugin.settings.ollamaApplyModel,
model,
},
})
},
})
}
})
})
// Model Setting
new Setting(ollamaContainer)
.setName('Model Name')
.setDesc('Select a model from your Ollama instance')
.addDropdown(async (dropdown) => {
const currentModel = this.plugin.settings.ollamaApplyModel.model
modelDropdown = dropdown
.addOption(currentModel, currentModel)
.setValue(currentModel)
await this.updateOllamaModelOptions({
baseUrl: this.plugin.settings.ollamaApplyModel.baseUrl,
dropdown,
onModelChange: async (model: string) => {
await this.plugin.setSettings({
...this.plugin.settings,
ollamaApplyModel: {
...this.plugin.settings.ollamaApplyModel,
model,
},
})
},
})
})
}
renderOpenAICompatibleApplyModelSettings(containerEl: HTMLElement): void {
const openAICompatContainer = containerEl.createDiv(
'infio-chat-settings-model-container',
)
new Setting(openAICompatContainer)
.setName('Base URL')
.setDesc(
'The API endpoint for your OpenAI-compatible service (e.g., https://api.example.com/v1)',
)
.addText((text) =>
text
.setPlaceholder('https://api.example.com/v1')
.setValue(
this.plugin.settings.openAICompatibleApplyModel.baseUrl || '',
)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
openAICompatibleApplyModel: {
...this.plugin.settings.openAICompatibleApplyModel,
baseUrl: value,
},
})
}),
)
new Setting(openAICompatContainer)
.setName('API Key')
.setDesc('Your authentication key for the OpenAI-compatible service')
.addText((text) =>
text
.setPlaceholder('Enter your API key')
.setValue(
this.plugin.settings.openAICompatibleApplyModel.apiKey || '',
)
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
openAICompatibleApplyModel: {
...this.plugin.settings.openAICompatibleApplyModel,
apiKey: value,
},
})
}),
)
new Setting(openAICompatContainer)
.setName('Model Name')
.setDesc(
'The specific model to use with your service (e.g., llama-3.1-70b, mixtral-8x7b)',
)
.addText((text) =>
text
.setPlaceholder('llama-3.1-70b')
.setValue(this.plugin.settings.openAICompatibleApplyModel.model || '')
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
openAICompatibleApplyModel: {
...this.plugin.settings.openAICompatibleApplyModel,
model: value,
},
})
}),
)
}
renderOllamaEmbeddingModelSettings(containerEl: HTMLElement): void {
const ollamaContainer = containerEl.createDiv(
'infio-chat-settings-model-container',
)
new Setting(ollamaContainer)
.setName('Base URL')
.setDesc(
'The API endpoint for your Ollama service (e.g., http://127.0.0.1:11434)',
)
.addText((text) =>
text
.setPlaceholder('http://127.0.0.1:11434')
.setValue(this.plugin.settings.ollamaEmbeddingModel.baseUrl || '')
.onChange(async (value) => {
await this.plugin.setSettings({
...this.plugin.settings,
ollamaEmbeddingModel: {
...this.plugin.settings.ollamaEmbeddingModel,
baseUrl: value,
},
})
}),
)
} }
renderRAGSection(containerEl: HTMLElement): void { renderRAGSection(containerEl: HTMLElement): void {
new Setting(containerEl).setHeading().setName('RAG') new Setting(containerEl).setHeading().setName('RAG')
new Setting(containerEl) new Setting(containerEl)
.setName('Include patterns') .setName('Include patterns')
.setDesc( .setDesc(
@ -715,69 +229,115 @@ export class InfioSettingTab extends PluginSettingTab {
} }
renderAutoCompleteSection(containerEl: HTMLElement): void { renderAutoCompleteSection(containerEl: HTMLElement): void {
const div = containerEl.createDiv("div"); // 创建一个专门的容器来存放 AutoComplete 相关的组件
const autoCompleteDiv = containerEl.createDiv("auto-complete-section");
this.autoCompleteContainer = autoCompleteDiv;
this.renderAutoCompleteContent(autoCompleteDiv);
}
const sections = createRoot(div); private renderAutoCompleteContent(containerEl: HTMLElement): void {
sections.render( const updateSettings = async (update: Partial<InfioSettings>) => {
<React.StrictMode> await this.plugin.setSettings({
<AutoCompleteSettings ...this.plugin.settings,
onSettingsChanged={async (settings) => { ...update
this.plugin.setSettings(settings); });
// Force refresh the settings page to update dropdowns
this.plugin.settingTab.display(); // 只重新渲染 AutoComplete 部分
}} if (this.autoCompleteContainer) {
this.autoCompleteContainer.empty();
this.renderAutoCompleteContent(this.autoCompleteContainer);
}
};
const errors = new Map();
// AutoComplete base
new Setting(containerEl).setName('AutoComplete').setHeading();
this.renderComponent(containerEl,
<BasicAutoCompleteSettings
settings={this.plugin.settings} settings={this.plugin.settings}
updateSettings={updateSettings}
/>
);
// Model parameters
new Setting(containerEl).setName('Model parameters').setHeading();
this.renderComponent(containerEl,
<ModelParametersSettings
settings={this.plugin.settings}
updateSettings={updateSettings}
errors={errors}
/>
);
// Preprocessing
new Setting(containerEl).setName('Preprocessing').setHeading();
this.renderComponent(containerEl,
<PreprocessingSettings
settings={this.plugin.settings}
updateSettings={updateSettings}
errors={errors}
/>
);
// Postprocessing
new Setting(containerEl).setName('Postprocessing').setHeading();
this.renderComponent(containerEl,
<PostprocessingSettings
settings={this.plugin.settings}
updateSettings={updateSettings}
/>
);
// Trigger
new Setting(containerEl).setName('Trigger').setHeading();
this.renderComponent(containerEl,
<TriggerSettingsSection
settings={this.plugin.settings}
updateSettings={updateSettings}
errors={errors}
/>
);
// Privacy
new Setting(containerEl).setName('Privacy').setHeading();
this.renderComponent(containerEl,
<PrivacySettings
settings={this.plugin.settings}
updateSettings={updateSettings}
errors={errors}
/>
);
// Danger zone
new Setting(containerEl).setName('Danger zone').setHeading();
this.renderComponent(containerEl,
<DangerZoneSettings
settings={this.plugin.settings}
updateSettings={updateSettings}
onReset={() => {
new Notice("Factory reset complete.");
}}
/>
);
// Advanced
if (this.plugin.settings.advancedMode) {
new Setting(containerEl).setName('Advanced').setHeading();
this.renderComponent(containerEl,
<AdvancedSettings
settings={this.plugin.settings}
updateSettings={updateSettings}
errors={errors}
/> />
</React.StrictMode>
); );
} }
private async updateOllamaModelOptions({
baseUrl,
dropdown,
onModelChange,
}: {
baseUrl: string
dropdown: DropdownComponent
onModelChange: (model: string) => Promise<void>
}): Promise<void> {
const currentValue = dropdown.getValue()
dropdown.selectEl.empty()
try {
const models = await getOllamaModels(baseUrl)
if (models.length > 0) {
const modelOptions = models.reduce<Record<string, string>>(
(acc, model) => {
acc[model] = model
return acc
},
{},
)
dropdown.addOptions(modelOptions)
if (models.includes(currentValue)) {
dropdown.setValue(currentValue)
} else {
dropdown.setValue(models[0])
await onModelChange(models[0])
}
} else {
dropdown.addOption('', 'No models found - check base URL')
dropdown.setValue('')
await onModelChange('')
}
} catch (error) {
console.error('Failed to fetch Ollama models:', error)
dropdown.addOption('', 'No models found - check base URL')
dropdown.setValue('')
await onModelChange('')
} }
dropdown.onChange(async (value) => { private renderComponent(containerEl: HTMLElement, component: React.ReactNode) {
await onModelChange(value) const div = containerEl.createDiv("div");
}) const root = createRoot(div);
root.render(component);
} }
} }

View File

@ -0,0 +1,91 @@
import * as React from "react";
import { InfioSettings } from '../../types/settings';
import SettingsItem from "./SettingsItem";
import TextSettingItem from "./TextSettingItem";
import FewShotExampleSettings from "./FewShotExampleSettings";
type Props = {
settings: InfioSettings;
updateSettings: (update: Partial<InfioSettings>) => void;
errors: Map<string, string>;
}
export default function AdvancedSettings({ settings, updateSettings, errors }: Props): React.JSX.Element {
if (!settings.advancedMode) {
return null;
}
return (
<>
<TextSettingItem
name={"Chain of thought removal regex"}
description={
"This regex is used to remove the chain of thought tokens from the generated answer. If it is not implemented correctly, the chain of thought tokens will be included in the suggested completion."
}
placeholder={"your regex..."}
value={settings.chainOfThoughRemovalRegex}
errorMessage={errors.get("chainOfThoughRemovalRegex")}
setValue={(value: string) =>
updateSettings({
chainOfThoughRemovalRegex: value,
})
}
/>
<SettingsItem
name={"System message"}
description={
"This system message gives the models all the context and instructions they need to complete the answer generation tasks. You can edit this message to your liking. If you edit the chain of thought formatting, make sure to update the extract regex and examples accordingly."
}
display={"block"}
errorMessage={errors.get("systemMessage")}
>
<textarea
className="infio-autocomplete-setting-item-textarea"
rows={10}
placeholder="Your system message..."
value={settings.systemMessage}
onChange={(e) =>
updateSettings({
systemMessage: e.target.value,
})
}
/>
</SettingsItem>
<SettingsItem
name={"User message template"}
description={
"This template defines how the prefix and suffix are formatted to create the user message. You have access to two variables: {{prefix}} and {{suffix}}. If you edit this, make sure to update the examples accordingly."
}
display={"block"}
errorMessage={errors.get("userMessageTemplate")}
>
<textarea
className="infio-autocomplete-setting-item-textarea"
rows={3}
placeholder="{{prefix}}<mask/>{{suffix}}"
value={settings.userMessageTemplate}
onChange={(e) =>
updateSettings({
userMessageTemplate: e.target.value,
})
}
/>
</SettingsItem>
<FewShotExampleSettings
fewShotExamples={settings.fewShotExamples}
name={"Few shot examples"}
description={
"The model uses these examples to learn the expected answer format. Not all examples are sent at the same time. We only send the relevant examples, given the current cursor location. For example, the CodeBlock examples are only sent if the cursor is in a code block. If no special context is detected, we send the Text examples. Each context has a default of 2 examples, but you can add or remove examples if there is at least one per context. You can add more examples, but this will increase the inference costs."
}
setFewShotExamples={(value) =>
updateSettings({ fewShotExamples: value })
}
errorMessages={errors}
/>
</>
);
}

View File

@ -0,0 +1,41 @@
import * as React from "react";
import { InfioSettings } from '../../types/settings';
import CheckBoxSettingItem from "./CheckBoxSettingItem";
type Props = {
settings: InfioSettings;
updateSettings: (update: Partial<InfioSettings>) => void;
}
export default function BasicAutoCompleteSettings({ settings, updateSettings }: Props): React.JSX.Element {
return (
<>
<CheckBoxSettingItem
name={"Enable"}
description={
"If disabled, nothing will trigger the extension or can result in an API call."
}
enabled={settings.autocompleteEnabled}
setEnabled={(value) => updateSettings({ autocompleteEnabled: value })}
/>
<CheckBoxSettingItem
name={"Cache completions"}
description={
"If disabled, the plugin will not cache the completions. After accepting or rejecting a completion, the plugin will not remember it. This might result in more API calls."
}
enabled={settings.cacheSuggestions}
setEnabled={(value) => updateSettings({ cacheSuggestions: value })}
/>
<CheckBoxSettingItem
name={"Debug mode"}
description={
"If enabled, various debug messages will be logged to the console, such as the complete response from the API, including the chain of thought tokens."
}
enabled={settings.debugMode}
setEnabled={(value) => updateSettings({ debugMode: value })}
/>
</>
);
}

View File

@ -0,0 +1,40 @@
import * as React from "react";
import { InfioSettings } from '../../types/settings';
import CheckBoxSettingItem from "./CheckBoxSettingItem";
import SettingsItem from "./SettingsItem";
type Props = {
settings: InfioSettings;
updateSettings: (update: Partial<InfioSettings>) => void;
onReset: () => void;
}
export default function DangerZoneSettings({ settings, updateSettings, onReset }: Props): React.JSX.Element {
return (
<>
<SettingsItem
name={"Factory reset"}
description={
"Messed-up the settings? No worries, press this button! After that, the plugin will go back to the default settings. The URL and API key will remain unchanged."
}
>
<button
aria-label="Reset to default settings"
onClick={onReset}
>
Reset
</button>
</SettingsItem>
<CheckBoxSettingItem
name={"Advanced mode"}
description={
"If you are familiar with prompt engineering, you can enable this setting to view the prompt generation and a few shot example settings. Turn off this button. It will not reset your changes; use the factory reset button for that."
}
enabled={settings.advancedMode}
setEnabled={(value) => updateSettings({ advancedMode: value })}
/>
</>
);
}

View File

@ -187,7 +187,7 @@ export default function FewShotExampleSettings(
className="setting-item-name" className="setting-item-name"
style={{ width: "100%", textAlign: "left" }} style={{ width: "100%", textAlign: "left" }}
> >
Human Message Human message
</div> </div>
{props.errorMessages.get(`fewShotExamples.${index}.input`) !== undefined && ( {props.errorMessages.get(`fewShotExamples.${index}.input`) !== undefined && (
<div className="setting-item-description" style={{ width: "100%", textAlign: "left" }}> <div className="setting-item-description" style={{ width: "100%", textAlign: "left" }}>
@ -207,7 +207,7 @@ export default function FewShotExampleSettings(
className="setting-item-name" className="setting-item-name"
style={{ width: "100%", textAlign: "left" }} style={{ width: "100%", textAlign: "left" }}
> >
Assistant Message Assistant message
</div> </div>
{props.errorMessages.get(`fewShotExamples.${index}.answer`) !== undefined && ( {props.errorMessages.get(`fewShotExamples.${index}.answer`) !== undefined && (
<div className="setting-item-description" style={{ width: "100%", textAlign: "left" }}> <div className="setting-item-description" style={{ width: "100%", textAlign: "left" }}>

View File

@ -0,0 +1,129 @@
import * as React from "react";
import { InfioSettings } from '../../types/settings';
import {
MAX_FREQUENCY_PENALTY,
MAX_MAX_TOKENS,
MAX_PRESENCE_PENALTY,
MAX_TEMPERATURE,
MAX_TOP_P,
MIN_FREQUENCY_PENALTY,
MIN_MAX_TOKENS,
MIN_PRESENCE_PENALTY,
MIN_TEMPERATURE,
MIN_TOP_P
} from "../versions";
import SliderSettingsItem from "./SliderSettingsItem";
type Props = {
settings: InfioSettings;
updateSettings: (update: Partial<InfioSettings>) => void;
errors: Map<string, string>;
}
export default function ModelParametersSettings({ settings, updateSettings, errors }: Props): React.JSX.Element {
return (
<>
<SliderSettingsItem
name={"Temperature"}
description={
"This parameter affects randomness in the sampling. Lower values result in more repetitive and deterministic responses. Higher temperatures will result in more unexpected or creative responses."
}
value={settings.modelOptions.temperature}
errorMessage={errors.get("modelOptions.temperature")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
temperature: value,
},
})
}
min={MIN_TEMPERATURE}
max={MAX_TEMPERATURE}
step={0.05}
/>
<SliderSettingsItem
name={"TopP"}
description={
"Like the temperature parameter, the Top P parameter affects the randomness in sampling. Lowering the value will limit the model's token selection to likelier tokens while increasing the value expands the model's token selection with lower likelihood tokens."
}
value={settings.modelOptions.top_p}
errorMessage={errors.get("modelOptions.top_p")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
top_p: value,
},
})
}
min={MIN_TOP_P}
max={MAX_TOP_P}
step={0.05}
/>
{settings.apiProvider !== "ollama" && (
<>
<SliderSettingsItem
name={"Frequency penalty"}
description={
"This parameter reduces the chance of repeating a token proportionally based on how often it has appeared in the text so far. This decreases the likelihood of repeating the exact same text in a response."
}
value={settings.modelOptions.frequency_penalty}
errorMessage={errors.get("modelOptions.frequency_penalty")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
frequency_penalty: value,
},
})
}
min={MIN_FREQUENCY_PENALTY}
max={MAX_FREQUENCY_PENALTY}
step={0.05}
/>
<SliderSettingsItem
name={"Presence penalty"}
description={
"This parameter reduces the chance of repeating any token that has appeared in the text so far. This increases the likelihood of introducing new topics in a response."
}
value={settings.modelOptions.presence_penalty}
errorMessage={errors.get("modelOptions.presence_penalty")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
presence_penalty: value,
},
})
}
min={MIN_PRESENCE_PENALTY}
max={MAX_PRESENCE_PENALTY}
step={0.05}
/>
<SliderSettingsItem
name={"Max tokens"}
description={
"This parameter changes the maximum number of tokens the model is allowed to generate. This includes the chain of thought tokens before the answer."
}
value={settings.modelOptions.max_tokens}
errorMessage={errors.get("modelOptions.max_tokens")}
setValue={(value: number) =>
updateSettings({
modelOptions: {
...settings.modelOptions,
max_tokens: value,
},
})
}
min={MIN_MAX_TOKENS}
max={MAX_MAX_TOKENS}
step={10}
/>
</>
)}
</>
);
}

View File

@ -1,14 +1,18 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
// import { PROVIDERS } from '../constants'; import InfioPlugin from "../../main";
import { ApiProvider } from '../types/llm/model'; import { ApiProvider } from '../../types/llm/model';
import { InfioSettings } from '../types/settings'; import { InfioSettings } from '../../types/settings';
import { GetAllProviders } from '../utils/api'; import { GetAllProviders } from '../../utils/api';
// import { siliconFlowDefaultModelId } from '../utils/api';
import { DropdownComponent, TextComponent, ToggleComponent } from './FormComponents'; import { DropdownComponent, TextComponent, ToggleComponent } from './FormComponents';
import { ComboBoxComponent } from './ProviderModelsPicker'; import { ComboBoxComponent } from './ProviderModelsPicker';
type CustomProviderSettingsProps = {
plugin: InfioPlugin;
onSettingsUpdate?: () => void;
}
type ProviderSettingKey = type ProviderSettingKey =
| 'infioProvider' | 'infioProvider'
| 'openrouterProvider' | 'openrouterProvider'
@ -22,11 +26,6 @@ type ProviderSettingKey =
| 'ollamaProvider' | 'ollamaProvider'
| 'openaicompatibleProvider'; | 'openaicompatibleProvider';
interface ProviderSettingsProps {
settings: InfioSettings;
setSettings: (settings: InfioSettings) => Promise<void>;
}
const keyMap: Record<ApiProvider, ProviderSettingKey> = { const keyMap: Record<ApiProvider, ProviderSettingKey> = {
'Infio': 'infioProvider', 'Infio': 'infioProvider',
'OpenRouter': 'openrouterProvider', 'OpenRouter': 'openrouterProvider',
@ -45,19 +44,26 @@ const getProviderSettingKey = (provider: ApiProvider): ProviderSettingKey => {
return keyMap[provider]; return keyMap[provider];
}; };
const PROVIDERS = GetAllProviders(); const CustomProviderSettings: React.FC<CustomProviderSettingsProps> = ({ plugin, onSettingsUpdate }) => {
const settings = plugin.settings;
const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettings }) => {
const [currProvider, setCurrProvider] = useState(settings.defaultProvider); const [currProvider, setCurrProvider] = useState(settings.defaultProvider);
const handleSettingsUpdate = async (newSettings: InfioSettings) => {
await plugin.setSettings(newSettings);
// 使用父组件传入的回调函数来刷新整个容器
onSettingsUpdate?.();
};
const providerSetting = useMemo(() => { const providerSetting = useMemo(() => {
const providerKey = getProviderSettingKey(currProvider); const providerKey = getProviderSettingKey(currProvider);
return settings[providerKey] || {}; return settings[providerKey] || {};
}, [currProvider, settings]); }, [currProvider, settings]);
const providers = GetAllProviders();
const updateProvider = (provider: ApiProvider) => { const updateProvider = (provider: ApiProvider) => {
setCurrProvider(provider); setCurrProvider(provider);
setSettings({ handleSettingsUpdate({
...settings, ...settings,
defaultProvider: provider defaultProvider: provider
}); });
@ -67,7 +73,7 @@ const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettin
const providerKey = getProviderSettingKey(currProvider); const providerKey = getProviderSettingKey(currProvider);
const providerSettings = settings[providerKey]; const providerSettings = settings[providerKey];
setSettings({ handleSettingsUpdate({
...settings, ...settings,
[providerKey]: { [providerKey]: {
...providerSettings, ...providerSettings,
@ -80,7 +86,7 @@ const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettin
const providerKey = getProviderSettingKey(currProvider); const providerKey = getProviderSettingKey(currProvider);
const providerSettings = settings[providerKey]; const providerSettings = settings[providerKey];
setSettings({ handleSettingsUpdate({
...settings, ...settings,
[providerKey]: { [providerKey]: {
...providerSettings, ...providerSettings,
@ -93,7 +99,7 @@ const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettin
const providerKey = getProviderSettingKey(currProvider); const providerKey = getProviderSettingKey(currProvider);
const providerSettings = settings[providerKey]; const providerSettings = settings[providerKey];
setSettings({ handleSettingsUpdate({
...settings, ...settings,
[providerKey]: { [providerKey]: {
...providerSettings, ...providerSettings,
@ -103,7 +109,7 @@ const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettin
}; };
const updateChatModelId = (provider: ApiProvider, modelId: string) => { const updateChatModelId = (provider: ApiProvider, modelId: string) => {
setSettings({ handleSettingsUpdate({
...settings, ...settings,
chatModelProvider: provider, chatModelProvider: provider,
chatModelId: modelId chatModelId: modelId
@ -111,7 +117,7 @@ const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettin
}; };
const updateApplyModelId = (provider: ApiProvider, modelId: string) => { const updateApplyModelId = (provider: ApiProvider, modelId: string) => {
setSettings({ handleSettingsUpdate({
...settings, ...settings,
applyModelProvider: provider, applyModelProvider: provider,
applyModelId: modelId applyModelId: modelId
@ -119,7 +125,7 @@ const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettin
}; };
const updateEmbeddingModelId = (provider: ApiProvider, modelId: string) => { const updateEmbeddingModelId = (provider: ApiProvider, modelId: string) => {
setSettings({ handleSettingsUpdate({
...settings, ...settings,
embeddingModelProvider: provider, embeddingModelProvider: provider,
embeddingModelId: modelId embeddingModelId: modelId
@ -129,28 +135,28 @@ const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettin
return ( return (
<div className="infio-llm-setting-provider"> <div className="infio-llm-setting-provider">
<DropdownComponent <DropdownComponent
name="API Provider:" name="Api provider:"
value={currProvider} value={currProvider}
options={PROVIDERS} options={providers}
onChange={updateProvider} onChange={updateProvider}
/> />
<div className="infio-llm-setting-divider"></div> <div className="infio-llm-setting-divider"></div>
<TextComponent <TextComponent
name={currProvider + " API Key:"} name={currProvider + " api key:"}
placeholder="Enter your API key" placeholder="Enter your api key"
value={providerSetting.apiKey || ''} value={providerSetting.apiKey || ''}
onChange={updateProviderApiKey} onChange={updateProviderApiKey}
type="password" type="password"
/> />
<div className="infio-llm-setting-divider"></div> <div className="infio-llm-setting-divider"></div>
<ToggleComponent <ToggleComponent
name="Use custom base URL" name="Use custom base url"
value={providerSetting.useCustomUrl || false} value={providerSetting.useCustomUrl || false}
onChange={updateProviderUseCustomUrl} onChange={updateProviderUseCustomUrl}
/> />
{providerSetting.useCustomUrl && ( {providerSetting.useCustomUrl && (
<TextComponent <TextComponent
placeholder="Enter your custom API endpoint URL" placeholder="Enter your custom api endpoint url"
value={providerSetting.baseUrl || ''} value={providerSetting.baseUrl || ''}
onChange={updateProviderBaseUrl} onChange={updateProviderBaseUrl}
/> />
@ -159,21 +165,21 @@ const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettin
<div className="infio-llm-setting-divider"></div> <div className="infio-llm-setting-divider"></div>
<div className="infio-llm-setting-divider"></div> <div className="infio-llm-setting-divider"></div>
<ComboBoxComponent <ComboBoxComponent
name="Chat Model:" name="Chat model:"
provider={settings.chatModelProvider || currProvider} provider={settings.chatModelProvider || currProvider}
modelId={settings.chatModelId} modelId={settings.chatModelId}
updateModel={updateChatModelId} updateModel={updateChatModelId}
/> />
<div className="infio-llm-setting-divider"></div> <div className="infio-llm-setting-divider"></div>
<ComboBoxComponent <ComboBoxComponent
name="Autocomplete Model:" name="Autocomplete model:"
provider={settings.applyModelProvider || currProvider} provider={settings.applyModelProvider || currProvider}
modelId={settings.applyModelId} modelId={settings.applyModelId}
updateModel={updateApplyModelId} updateModel={updateApplyModelId}
/> />
<div className="infio-llm-setting-divider"></div> <div className="infio-llm-setting-divider"></div>
<ComboBoxComponent <ComboBoxComponent
name="Embedding Model:" name="Embedding model:"
provider={settings.embeddingModelProvider || ApiProvider.Google} provider={settings.embeddingModelProvider || ApiProvider.Google}
modelId={settings.embeddingModelId} modelId={settings.embeddingModelId}
isEmbedding={true} isEmbedding={true}
@ -185,4 +191,4 @@ const ProviderSettings: React.FC<ProviderSettingsProps> = ({ settings, setSettin
); );
}; };
export default ProviderSettings; export default CustomProviderSettings;

View File

@ -0,0 +1,37 @@
import * as React from "react";
import { InfioSettings } from '../../types/settings';
import CheckBoxSettingItem from "./CheckBoxSettingItem";
type Props = {
settings: InfioSettings;
updateSettings: (update: Partial<InfioSettings>) => void;
}
export default function PostprocessingSettings({ settings, updateSettings }: Props): React.JSX.Element {
return (
<>
<CheckBoxSettingItem
name={"Auto remove duplicate mat block indicators"}
description={
"The AI model might eagerly add a math block indicator ($), even though the cursor is already inside a math block. If this setting is enabled, the plugin will automatically remove these duplicate indicators from the completion."
}
enabled={settings.removeDuplicateMathBlockIndicator}
setEnabled={(value) =>
updateSettings({ removeDuplicateMathBlockIndicator: value })
}
/>
<CheckBoxSettingItem
name={"Auto remove duplicate code block indicators"}
description={
"The AI model might eagerly add a code block indicator (`), even though the cursor is already inside a code block. If this setting is enabled, the plugin will automatically remove these duplicate indicators from the completion."
}
enabled={settings.removeDuplicateCodeBlockIndicator}
setEnabled={(value) =>
updateSettings({ removeDuplicateCodeBlockIndicator: value })
}
/>
</>
);
}

View File

@ -0,0 +1,63 @@
import * as React from "react";
import { InfioSettings } from '../../types/settings';
import {
MAX_MAX_CHAR_LIMIT,
MIN_MAX_CHAR_LIMIT,
} from "../versions";
import CheckBoxSettingItem from "./CheckBoxSettingItem";
import SliderSettingsItem from "./SliderSettingsItem";
type Props = {
settings: InfioSettings;
updateSettings: (update: Partial<InfioSettings>) => void;
errors: Map<string, string>;
}
export default function PreprocessingSettings({ settings, updateSettings, errors }: Props): React.JSX.Element {
return (
<>
<CheckBoxSettingItem
name={"Don't include Dataview"}
description={
"Dataview(js) blocks can be quite long while not providing much value to the AI. If this setting is enabled, data view blocks will be removed promptly to reduce the number of tokens. This could save you some money in the long run."
}
enabled={settings.dontIncludeDataviews}
setEnabled={(value) =>
updateSettings({ dontIncludeDataviews: value })
}
/>
<SliderSettingsItem
name={"Maximum prefix length"}
description={
"The maximum number of characters that will be included in the prefix. A larger value will increase the context for the completion, but it can also increase the cost or push you over the token limit."
}
value={settings.maxPrefixCharLimit}
errorMessage={errors.get("maxPrefixCharLimit")}
setValue={(value: number) =>
updateSettings({ maxPrefixCharLimit: value })
}
min={MIN_MAX_CHAR_LIMIT}
max={MAX_MAX_CHAR_LIMIT}
step={100}
suffix={" chars"}
/>
<SliderSettingsItem
name={"Maximum suffix length"}
description={
"The maximum number of characters that will be included in the suffix. A larger value will increase the context for the completion, but it can also increase the cost or push you over the token limit."
}
value={settings.maxSuffixCharLimit}
errorMessage={errors.get("maxSuffixCharLimit")}
setValue={(value: number) =>
updateSettings({ maxSuffixCharLimit: value })
}
min={MIN_MAX_CHAR_LIMIT}
max={MAX_MAX_CHAR_LIMIT}
step={100}
suffix={" chars"}
/>
</>
);
}

View File

@ -0,0 +1,83 @@
import * as React from "react";
import { InfioSettings } from '../../types/settings';
import SettingsItem from "./SettingsItem";
type Props = {
settings: InfioSettings;
updateSettings: (update: Partial<InfioSettings>) => void;
errors: Map<string, string>;
}
export default function PrivacySettings({ settings, updateSettings, errors }: Props): React.JSX.Element {
return (
<>
<SettingsItem
name={"Ignored files"}
description={
<div>
<p>This field enables you to specify files and directories that the plugin should ignore. When
you open any of these files, the plugin will automatically disable itself and display a
'disabled' status in the bottom menu. Enter one pattern per line. These patterns function
similar to glob patterns. Here are some frequently used patterns:</p>
<ul>
<li><code>path/to/folder/**</code>: This pattern ignores all files and sub folders within
this folder.
</li>
<li><code>"**/secret/**"</code>: This pattern ignores any file located inside a 'secret'
directory,
regardless of its location in the path.
</li>
<li><code>!path/to/folder/example.md</code>: This pattern explicitly undoes an ignore,
making this file noticeable to the plugin.
</li>
<li><code>**/*Python*.md</code>: This pattern ignores any file with 'Python' in its name,
irrespective of its location.
</li>
</ul>
</div>
}
display={"block"}
errorMessage={errors.get("ignoredFilePatterns")}
>
<textarea
className="infio-autocomplete-setting-item-textarea"
rows={10}
placeholder="Your file patterns, e.g., **/secret/**"
value={settings.ignoredFilePatterns}
onChange={(e) =>
updateSettings({
ignoredFilePatterns: e.target.value
})
}
/>
</SettingsItem>
<SettingsItem
name={"Ignored tags"}
description={
<div>
<p>Files containing any of these tags will be ignored. When you open a file containing a
tag listed here, the plugin will automatically disable itself and display a 'disabled'
status in the bottom menu. Enter one tag per line.
</p>
</div>
}
display={"block"}
errorMessage={errors.get("ignoredTags")}
>
<textarea
className="infio-autocomplete-setting-item-textarea"
rows={10}
placeholder="Your file tags, e.g., secret"
value={settings.ignoredTags}
onChange={(e) =>
updateSettings({
ignoredTags: e.target.value
})
}
/>
</SettingsItem>
</>
);
}

View File

@ -2,9 +2,9 @@ import * as Popover from "@radix-ui/react-popover";
import Fuse, { FuseResult } from "fuse.js"; import Fuse, { FuseResult } from "fuse.js";
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { ApiProvider } from "../types/llm/model"; import { ApiProvider } from "../../types/llm/model";
// import { PROVIDERS } from '../constants'; // import { PROVIDERS } from '../constants';
import { GetAllProviders, GetEmbeddingProviderModelIds, GetEmbeddingProviders, GetProviderModelIds } from "../utils/api"; import { GetAllProviders, GetEmbeddingProviderModelIds, GetEmbeddingProviders, GetProviderModelIds } from "../../utils/api";
type TextSegment = { type TextSegment = {
text: string; text: string;

View File

@ -0,0 +1,46 @@
import * as React from "react";
import { InfioSettings } from '../../types/settings';
import {
MAX_DELAY,
MIN_DELAY,
} from "../versions";
import SliderSettingsItem from "./SliderSettingsItem";
import TriggerSettings from "./TriggerSettings";
type Props = {
settings: InfioSettings;
updateSettings: (update: Partial<InfioSettings>) => void;
errors: Map<string, string>;
}
export default function TriggerSettingsSection({ settings, updateSettings, errors }: Props): React.JSX.Element {
return (
<>
<SliderSettingsItem
name={"Delay"}
description={
"Delay in ms between the last character typed and the completion request."
}
value={settings.delay}
errorMessage={errors.get("delay")}
setValue={(value: number) => updateSettings({ delay: value })}
min={MIN_DELAY}
max={MAX_DELAY}
step={100}
suffix={"ms"}
/>
<TriggerSettings
name={"Trigger words"}
description={
"Completions will be triggered if the text before the matches any of these words or characters. This can either be a direct string match or a regex match. When using a regex, make sure to include the end of line character ($)."
}
triggers={settings.triggers}
setValues={(triggers) => updateSettings({ triggers })}
errorMessage={errors.get("triggerWords")}
errorMessages={errors}
/>
</>
);
}

View File

@ -50,8 +50,8 @@ export const modelOptionsSchema = z.object({
export const fewShotExampleSchema = z.object({ export const fewShotExampleSchema = z.object({
// TODO: figure out how to make this compatible with the context enum and its namespace. // TODO: figure out how to make this compatible with the context enum and its namespace.
context: z.enum(["Text", "Heading", "BlockQuotes", "UnorderedList", "NumberedList", "CodeBlock", "MathBlock", "TaskList"]), context: z.enum(["Text", "Heading", "BlockQuotes", "UnorderedList", "NumberedList", "CodeBlock", "MathBlock", "TaskList"]),
input: z.string().min(3, { message: "The Input must be at least 3 characters long" }), input: z.string().min(3, { message: "The input must be at least 3 characters long" }),
answer: z.string().min(3, { message: "The Answer must be at least 3 characters long" }), answer: z.string().min(3, { message: "The answer must be at least 3 characters long" }),
}).strict(); }).strict();
export type FewShotExample = z.infer<typeof fewShotExampleSchema>; export type FewShotExample = z.infer<typeof fewShotExampleSchema>;

View File

@ -153,7 +153,7 @@ export function getMentionableName(mentionable: Mentionable): string {
case 'vault': case 'vault':
return 'Vault' return 'Vault'
case 'current-file': case 'current-file':
return mentionable.file?.name ?? 'Current File' return mentionable.file?.name ?? 'Current file'
case 'block': case 'block':
return `${mentionable.file.name} (${mentionable.startLine}:${mentionable.endLine})` return `${mentionable.file.name} (${mentionable.startLine}:${mentionable.endLine})`
case 'url': case 'url':

View File

@ -209,7 +209,7 @@ export class PromptGenerator {
}, },
onQueryProgressChange: onQueryProgressChange, onQueryProgressChange: onQueryProgressChange,
}) })
filePrompt = `## Potentially Relevant Snippets from the current vault filePrompt = `## Potentially relevant snippets from the current vault
${similaritySearchResults ${similaritySearchResults
.map(({ path, content, metadata }) => { .map(({ path, content, metadata }) => {
const contentWithLineNumbers = this.addLineNumbersToContent({ const contentWithLineNumbers = this.addLineNumbersToContent({
@ -242,7 +242,7 @@ ${similaritySearchResults
const urlPrompt = const urlPrompt =
urls.length > 0 urls.length > 0
? `## Potentially Relevant Websearch Results ? `## Potentially relevant web search results
${( ${(
await Promise.all( await Promise.all(
urls.map( urls.map(
@ -387,7 +387,7 @@ ${customInstruction}
return { return {
role: 'user', role: 'user',
content: `# Inputs content: `# Inputs
## Current File ## Current file
Here is the file I'm looking at. Here is the file I'm looking at.
\`\`\`${currentFile.path} \`\`\`${currentFile.path}
${fileContent} ${fileContent}