From f84a3bc56b4a84993cd82842e9f10862f9161f17 Mon Sep 17 00:00:00 2001 From: Logan Yang Date: Sat, 20 Jan 2024 19:12:34 -0800 Subject: [PATCH] [2.4.11] Refactor settings to React components (#248) * Add ollama custom base url setting --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/aiParams.ts | 3 + src/aiState.ts | 11 +- src/constants.ts | 48 +- src/main.ts | 33 +- src/settings.ts | 667 ------------------ src/settings/SettingsPage.tsx | 92 +++ src/settings/components/AdvancedSettings.tsx | 43 ++ src/settings/components/ApiSetting.tsx | 27 + src/settings/components/ApiSettings.tsx | 163 +++++ src/settings/components/Collapsible.tsx | 54 ++ .../components/LocalCopilotSettings.tsx | 66 ++ src/settings/components/QASettings.tsx | 97 +++ src/settings/components/SettingBlocks.tsx | 136 ++++ src/settings/components/SettingsMain.tsx | 212 ++++++ src/utils.ts | 10 +- styles.css | 28 + versions.json | 3 +- 20 files changed, 962 insertions(+), 739 deletions(-) delete mode 100644 src/settings.ts create mode 100644 src/settings/SettingsPage.tsx create mode 100644 src/settings/components/AdvancedSettings.tsx create mode 100644 src/settings/components/ApiSetting.tsx create mode 100644 src/settings/components/ApiSettings.tsx create mode 100644 src/settings/components/Collapsible.tsx create mode 100644 src/settings/components/LocalCopilotSettings.tsx create mode 100644 src/settings/components/QASettings.tsx create mode 100644 src/settings/components/SettingBlocks.tsx create mode 100644 src/settings/components/SettingsMain.tsx diff --git a/manifest.json b/manifest.json index 3ec3a69c..618ecf81 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "copilot", "name": "Copilot", - "version": "2.4.10", + "version": "2.4.11", "minAppVersion": "0.15.0", "description": "A ChatGPT Copilot in Obsidian.", "author": "Logan Yang", diff --git a/package-lock.json b/package-lock.json index 8d831a9b..54871fb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-copilot", - "version": "2.4.10", + "version": "2.4.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-copilot", - "version": "2.4.10", + "version": "2.4.11", "license": "AGPL-3.0", "dependencies": { "@huggingface/inference": "^2.6.4", diff --git a/package.json b/package.json index c5bfce8e..ce144322 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-copilot", - "version": "2.4.10", + "version": "2.4.11", "description": "ChatGPT integration for Obsidian", "main": "main.js", "scripts": { diff --git a/src/aiParams.ts b/src/aiParams.ts index 4874200c..14918488 100644 --- a/src/aiParams.ts +++ b/src/aiParams.ts @@ -18,6 +18,8 @@ export interface ModelConfig { apiKey?: string, openAIProxyBaseUrl?: string, ollamaModel?: string, + // OllamaBaseUrl + baseUrl?: string, openRouterModel?: string, lmStudioPort?: string, } @@ -44,6 +46,7 @@ export interface LangChainParams { chainType: ChainType, // Default ChainType is set in main.ts getAIStateParams options: SetChainOptions, ollamaModel: string, + ollamaBaseUrl: string, openRouterModel: string, lmStudioPort: string, openAIProxyBaseUrl?: string, diff --git a/src/aiState.ts b/src/aiState.ts index fd6e9b8e..4a5e11c7 100644 --- a/src/aiState.ts +++ b/src/aiState.ts @@ -6,7 +6,6 @@ import { ANTHROPIC, AZURE_MODELS, AZURE_OPENAI, - CLAUDE_MODELS, COHEREAI, ChatModelDisplayNames, DEFAULT_SYSTEM_PROMPT, @@ -178,6 +177,7 @@ class AIState { googleApiKey, openRouterAiApiKey, ollamaModel, + ollamaBaseUrl, openRouterModel, } = this.langChainParams; @@ -239,6 +239,7 @@ class AIState { case OLLAMA: config = { ...config, + ...(ollamaBaseUrl ? { baseUrl: ollamaBaseUrl } : {}), modelName: ollamaModel, }; break; @@ -269,14 +270,6 @@ class AIState { }; } - for (const modelDisplayNameKey of CLAUDE_MODELS) { - modelMap[modelDisplayNameKey] = { - hasApiKey: Boolean(this.langChainParams.anthropicApiKey), - AIConstructor: ChatAnthropic, - vendor: ANTHROPIC, - }; - } - for (const modelDisplayNameKey of AZURE_MODELS) { modelMap[modelDisplayNameKey] = { hasApiKey: Boolean(this.langChainParams.azureOpenAIApiKey), diff --git a/src/constants.ts b/src/constants.ts index e7966b55..14e22e8a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,4 @@ -import { CopilotSettings } from '@/main'; +import { CopilotSettings } from '@/settings/SettingsPage'; export const CHAT_VIEWTYPE = 'copilot-chat-view'; export const USER_SENDER = 'user'; @@ -11,10 +11,10 @@ export enum ChatModels { GPT_4 = 'gpt-4', GPT_4_TURBO = 'gpt-4-1106-preview', GPT_4_32K = 'gpt-4-32k', - CLAUDE_1 = 'claude-1', - CLAUDE_1_100K = 'claude-1-100k', - CLAUDE_INSTANT_1 = 'claude-instant-1', - CLAUDE_INSTANT_1_100K = 'claude-instant-1-100k', + // CLAUDE_1 = 'claude-1', + // CLAUDE_1_100K = 'claude-1-100k', + // CLAUDE_INSTANT_1 = 'claude-instant-1', + // CLAUDE_INSTANT_1_100K = 'claude-instant-1-100k', AZURE_GPT_35_TURBO = 'gpt-35-turbo', AZURE_GPT_35_TURBO_16K = 'gpt-35-turbo-16k', GEMINI_PRO = 'gemini-pro', @@ -27,10 +27,10 @@ export enum ChatModelDisplayNames { GPT_4 = 'GPT-4', GPT_4_TURBO = 'GPT-4 TURBO', GPT_4_32K = 'GPT-4 32K', - CLAUDE_1 = 'CLAUDE-1', - CLAUDE_1_100K = 'CLAUDE-1-100K', - CLAUDE_INSTANT_1 = 'CLAUDE-INSTANT', - CLAUDE_INSTANT_1_100K = 'CLAUDE-INSTANT-100K', + // CLAUDE_1 = 'CLAUDE-1', + // CLAUDE_1_100K = 'CLAUDE-1-100K', + // CLAUDE_INSTANT_1 = 'CLAUDE-INSTANT', + // CLAUDE_INSTANT_1_100K = 'CLAUDE-INSTANT-100K', AZURE_GPT_35_TURBO = 'AZURE GPT-3.5', AZURE_GPT_35_TURBO_16K = 'AZURE GPT-3.5-16K', AZURE_GPT_4 = 'AZURE GPT-4', @@ -57,12 +57,12 @@ export const AZURE_MODELS = new Set([ ChatModelDisplayNames.AZURE_GPT_4_32K, ]); -export const CLAUDE_MODELS = new Set([ - ChatModelDisplayNames.CLAUDE_1, - ChatModelDisplayNames.CLAUDE_1_100K, - ChatModelDisplayNames.CLAUDE_INSTANT_1, - ChatModelDisplayNames.CLAUDE_INSTANT_1_100K, -]); +// export const CLAUDE_MODELS = new Set([ +// ChatModelDisplayNames.CLAUDE_1, +// ChatModelDisplayNames.CLAUDE_1_100K, +// ChatModelDisplayNames.CLAUDE_INSTANT_1, +// ChatModelDisplayNames.CLAUDE_INSTANT_1_100K, +// ]); export const GOOGLE_MODELS = new Set([ ChatModelDisplayNames.GEMINI_PRO, @@ -86,10 +86,10 @@ export const DISPLAY_NAME_TO_MODEL: Record = { [ChatModelDisplayNames.GPT_4]: ChatModels.GPT_4, [ChatModelDisplayNames.GPT_4_TURBO]: ChatModels.GPT_4_TURBO, [ChatModelDisplayNames.GPT_4_32K]: ChatModels.GPT_4_32K, - [ChatModelDisplayNames.CLAUDE_1]: ChatModels.CLAUDE_1, - [ChatModelDisplayNames.CLAUDE_1_100K]: ChatModels.CLAUDE_1_100K, - [ChatModelDisplayNames.CLAUDE_INSTANT_1]: ChatModels.CLAUDE_INSTANT_1, - [ChatModelDisplayNames.CLAUDE_INSTANT_1_100K]: ChatModels.CLAUDE_INSTANT_1_100K, + // [ChatModelDisplayNames.CLAUDE_1]: ChatModels.CLAUDE_1, + // [ChatModelDisplayNames.CLAUDE_1_100K]: ChatModels.CLAUDE_1_100K, + // [ChatModelDisplayNames.CLAUDE_INSTANT_1]: ChatModels.CLAUDE_INSTANT_1, + // [ChatModelDisplayNames.CLAUDE_INSTANT_1_100K]: ChatModels.CLAUDE_INSTANT_1_100K, [ChatModelDisplayNames.AZURE_GPT_35_TURBO]: ChatModels.AZURE_GPT_35_TURBO, [ChatModelDisplayNames.AZURE_GPT_35_TURBO_16K]: ChatModels.AZURE_GPT_35_TURBO_16K, [ChatModelDisplayNames.AZURE_GPT_4]: ChatModels.GPT_4, @@ -111,13 +111,20 @@ export const OLLAMA = 'ollama'; export const VENDOR_MODELS: Record> = { [OPENAI]: OPENAI_MODELS, [AZURE_OPENAI]: AZURE_MODELS, - [ANTHROPIC]: CLAUDE_MODELS, + // [ANTHROPIC]: CLAUDE_MODELS, [GOOGLE]: GOOGLE_MODELS, [OPENROUTERAI]: OPENROUTERAI_MODELS, [OLLAMA]: OLLAMA_MODELS, [LM_STUDIO]: LM_STUDIO_MODELS, }; +export const EMBEDDING_PROVIDERS = [ + OPENAI, + AZURE_OPENAI, + COHEREAI, + HUGGINGFACE, +]; + // Embedding Models export const DISTILBERT_NLI = 'sentence-transformers/distilbert-base-nli-mean-tokens'; export const INSTRUCTOR_XL = 'hkunlp/instructor-xl'; // Inference API is off for this @@ -144,6 +151,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = { userSystemPrompt: '', openAIProxyBaseUrl: '', ollamaModel: 'llama2', + ollamaBaseUrl: '', lmStudioPort: '1234', ttlDays: 30, stream: true, diff --git a/src/main.ts b/src/main.ts index 12676a37..438c1540 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ import { ToneModal } from "@/components/ToneModal"; import { CHAT_VIEWTYPE, DEFAULT_SETTINGS, DEFAULT_SYSTEM_PROMPT, } from '@/constants'; -import { CopilotSettingTab } from '@/settings'; +import { CopilotSettingTab, CopilotSettings } from '@/settings/SettingsPage'; import SharedState from '@/sharedState'; import { sanitizeSettings } from "@/utils"; import VectorDBManager, { VectorStoreDocument } from '@/vectorDBManager'; @@ -18,35 +18,6 @@ import { Editor, Notice, Plugin, WorkspaceLeaf } from 'obsidian'; import PouchDB from 'pouchdb'; -export interface CopilotSettings { - openAIApiKey: string; - huggingfaceApiKey: string; - cohereApiKey: string; - anthropicApiKey: string; - azureOpenAIApiKey: string; - azureOpenAIApiInstanceName: string; - azureOpenAIApiDeploymentName: string; - azureOpenAIApiVersion: string; - azureOpenAIApiEmbeddingDeploymentName: string; - googleApiKey: string; - openRouterAiApiKey: string; - openRouterModel: string; - defaultModel: string; - defaultModelDisplayName: string; - temperature: number; - maxTokens: number; - contextTurns: number; - userSystemPrompt: string; - openAIProxyBaseUrl: string; - ollamaModel: string; - lmStudioPort: string; - ttlDays: number; - stream: boolean; - embeddingProvider: string; - defaultSaveFolder: string; - debug: boolean; -} - interface CustomPrompt { _id: string; _rev?: string; @@ -495,6 +466,7 @@ export default class CopilotPlugin extends Plugin { contextTurns, embeddingProvider, ollamaModel, + ollamaBaseUrl, lmStudioPort, } = sanitizeSettings(this.settings); return { @@ -511,6 +483,7 @@ export default class CopilotPlugin extends Plugin { openRouterAiApiKey, openRouterModel: openRouterModel || DEFAULT_SETTINGS.openRouterModel, ollamaModel: ollamaModel || DEFAULT_SETTINGS.ollamaModel, + ollamaBaseUrl: ollamaBaseUrl || DEFAULT_SETTINGS.ollamaBaseUrl, lmStudioPort: lmStudioPort || DEFAULT_SETTINGS.lmStudioPort, model: this.settings.defaultModel, modelDisplayName: this.settings.defaultModelDisplayName, diff --git a/src/settings.ts b/src/settings.ts deleted file mode 100644 index 61abf4fa..00000000 --- a/src/settings.ts +++ /dev/null @@ -1,667 +0,0 @@ -import { - AZURE_OPENAI, - COHEREAI, - ChatModelDisplayNames, - DEFAULT_SETTINGS, - DISPLAY_NAME_TO_MODEL, - HUGGINGFACE, - OPENAI -} from "@/constants"; -import CopilotPlugin from "@/main"; -import { App, DropdownComponent, Notice, PluginSettingTab, Setting } from "obsidian"; - -export class CopilotSettingTab extends PluginSettingTab { - plugin: CopilotPlugin; - - constructor(app: App, plugin: CopilotPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - async reloadPlugin() { - try { - // Save the settings before reloading - await this.plugin.saveSettings(); - - // Reload the plugin - const app = (this.plugin.app as any); - await app.plugins.disablePlugin("copilot"); - await app.plugins.enablePlugin("copilot"); - - app.setting.openTabById("copilot").display(); - new Notice('Plugin reloaded successfully.'); - } catch (error) { - new Notice('Failed to reload the plugin. Please reload manually.'); - console.error('Error reloading plugin:', error); - } - } - - display(): void { - const { containerEl } = this; - - containerEl.empty(); - containerEl.style.userSelect = 'text'; - containerEl.createEl('h2', { text: 'Copilot Settings' }); - - const buttonContainer = containerEl.createDiv({ cls: 'button-container' }); - buttonContainer.createEl('button', { - text: 'Save and Reload', - type: 'button', - cls: 'mod-cta', - }).addEventListener('click', async () => { - await this.plugin.saveSettings(); - await this.reloadPlugin(); - new Notice('Settings have been saved and the plugin has been reloaded.'); - }); - - buttonContainer.createEl('button', { - text: 'Reset to Default Settings', - type: 'button', - cls: 'mod-cta', - }).addEventListener('click', async () => { - this.plugin.settings = DEFAULT_SETTINGS; - await this.plugin.saveSettings(); - await this.reloadPlugin(); - new Notice('Settings have been reset to their default values.'); - }); - - containerEl.createEl('div', { - text: 'Please Save and Reload the plugin when you change any setting below!', - cls: 'warning-message' - }); - - - const modelDisplayNames = [ - ChatModelDisplayNames.GPT_35_TURBO, - ChatModelDisplayNames.GPT_35_TURBO_16K, - ChatModelDisplayNames.GPT_4, - ChatModelDisplayNames.GPT_4_TURBO, - ChatModelDisplayNames.GPT_4_32K, - // ChatModelDisplayNames.CLAUDE_1, - // ChatModelDisplayNames.CLAUDE_1_100K, - // ChatModelDisplayNames.CLAUDE_INSTANT_1, - // ChatModelDisplayNames.CLAUDE_INSTANT_1_100K, - ChatModelDisplayNames.AZURE_GPT_35_TURBO, - ChatModelDisplayNames.AZURE_GPT_35_TURBO_16K, - ChatModelDisplayNames.AZURE_GPT_4, - ChatModelDisplayNames.AZURE_GPT_4_32K, - ChatModelDisplayNames.GEMINI_PRO, - ChatModelDisplayNames.OPENROUTERAI, - ChatModelDisplayNames.LM_STUDIO, - ChatModelDisplayNames.OLLAMA, - ]; - - new Setting(containerEl) - .setName("Default Model") - .setDesc( - createFragment((frag) => { - frag.appendText("The default model to use"); - }) - ) - .addDropdown((dropdown: DropdownComponent) => { - modelDisplayNames.forEach(displayName => { - dropdown.addOption(displayName, displayName); - }); - dropdown - .setValue(this.plugin.settings.defaultModelDisplayName) - .onChange(async (value: string) => { - this.plugin.settings.defaultModelDisplayName = value; - this.plugin.settings.defaultModel = DISPLAY_NAME_TO_MODEL[this.plugin.settings.defaultModelDisplayName]; - await this.plugin.saveSettings(); - }); - }); - - new Setting(containerEl) - .setName("Default Conversation Folder Name") - .setDesc("The default folder name where chat conversations will be saved. Default is 'copilot-conversations'") - .addText(text => text - .setPlaceholder("copilot-conversations") - .setValue(this.plugin.settings.defaultSaveFolder) - .onChange(async (value: string) => { - this.plugin.settings.defaultSaveFolder = value; - await this.plugin.saveSettings(); - }) - ); - - containerEl.createEl('h4', { text: 'API Settings' }); - containerEl.createEl('h6', { text: 'OpenAI API' }); - - new Setting(containerEl) - .setName("Your OpenAI API key") - .setDesc( - createFragment((frag) => { - frag.appendText("You can find your API key at "); - frag.createEl('a', { - text: "https://platform.openai.com/api-keys", - href: "https://platform.openai.com/api-keys" - }); - frag.createEl('br'); - frag.appendText( - "It is stored locally in your vault at " - ); - frag.createEl('br'); - frag.createEl( - 'strong', - { text: "path_to_your_vault/.obsidian/plugins/obsidian-copilot/data.json" } - ); - frag.createEl('br'); - frag.appendText("and it is only used to make requests to OpenAI."); - }) - ) - .addText((text) => { - text.inputEl.type = "password"; - text.inputEl.style.width = "100%"; - text - .setPlaceholder("OpenAI API key") - .setValue(this.plugin.settings.openAIApiKey) - .onChange(async (value) => { - this.plugin.settings.openAIApiKey = value; - await this.plugin.saveSettings(); - }) - } - ); - - const warningMessage = containerEl.createEl('div', { cls: 'warning-message' }); - - warningMessage.createEl('span', { - text: 'If errors occur, pls re-enter the API key, save and reload the plugin to see if it resolves the issue.' - }); - - warningMessage.createEl('br'); - - warningMessage.createEl('span', { - text: 'If you are a new user, try ' - }); - - warningMessage.createEl('a', { - text: 'OpenAI playground', - href: 'https://platform.openai.com/playground?mode=chat' - }); - - warningMessage.createEl('span', { - text: ' to see if you have correct API access first.' - }); - - // containerEl.createEl('h6', { text: 'Anthropic' }); - - // new Setting(containerEl) - // .setName("Your Anthropic API key") - // .setDesc( - // createFragment((frag) => { - // frag.appendText("This is for Claude models. Sign up on their waitlist if you don't have access."); - // frag.createEl('a', { - // text: "https://docs.anthropic.com/claude/docs/getting-access-to-claude", - // href: "https://docs.anthropic.com/claude/docs/getting-access-to-claude" - // }); - // }) - // ) - // .addText((text) => { - // text.inputEl.type = "password"; - // text.inputEl.style.width = "100%"; - // text - // .setPlaceholder("Anthropic API key") - // .setValue(this.plugin.settings.anthropicApiKey) - // .onChange(async (value) => { - // this.plugin.settings.anthropicApiKey = value; - // await this.plugin.saveSettings(); - // }) - // } - // ); - - containerEl.createEl('h6', { text: 'Google Gemini API' }); - - new Setting(containerEl) - .setName("Your Google API key") - .setDesc( - createFragment((frag) => { - frag.appendText("If you have Google Cloud, you can get Gemini API key "); - frag.createEl('a', { - text: "here", - href: "https://makersuite.google.com/app/apikey" - }); - }) - ) - .addText((text) => { - text.inputEl.type = "password"; - text.inputEl.style.width = "100%"; - text - .setPlaceholder("Google API key") - .setValue(this.plugin.settings.googleApiKey) - .onChange(async (value) => { - this.plugin.settings.googleApiKey = value; - await this.plugin.saveSettings(); - }) - } - ); - - containerEl.createEl('h6', { text: 'OpenRouter.ai API' }); - - new Setting(containerEl) - .setName("Your OpenRouterAI API key") - .setDesc( - createFragment((frag) => { - frag.appendText("You can get your OpenRouterAI key "); - frag.createEl('a', { - text: "here", - href: "https://openrouter.ai/keys" - }); - }) - ) - .addText((text) => { - text.inputEl.type = "password"; - text.inputEl.style.width = "100%"; - text - .setPlaceholder("OpenRouterAI API key") - .setValue(this.plugin.settings.openRouterAiApiKey) - .onChange(async (value) => { - this.plugin.settings.openRouterAiApiKey = value; - await this.plugin.saveSettings(); - }) - }); - - new Setting(containerEl) - .setName("OpenRouterAI model") - .setDesc("Default: cognitivecomputations/dolphin-mixtral-8x7b") - .addText(text => { - text.inputEl.style.width = "100%"; - text - .setPlaceholder("cognitivecomputations/dolphin-mixtral-8x7b") - .setValue(this.plugin.settings.openRouterModel) - .onChange(async (value: string) => { - this.plugin.settings.openRouterModel = value; - await this.plugin.saveSettings(); - }) - }); - - containerEl.createEl('h6', { text: 'Azure OpenAI API' }); - - new Setting(containerEl) - .setName("Your Azure OpenAI API key") - .setDesc( - createFragment((frag) => { - frag.appendText("This is for Azure OpenAI APIs. Sign up on their waitlist if you don't have access."); - }) - ) - .addText((text) => { - text.inputEl.type = "password"; - text.inputEl.style.width = "100%"; - text - .setPlaceholder("Azure OpenAI API key") - .setValue(this.plugin.settings.azureOpenAIApiKey) - .onChange(async (value) => { - this.plugin.settings.azureOpenAIApiKey = value; - await this.plugin.saveSettings(); - }) - } - ); - - new Setting(containerEl) - .setName("Your Azure OpenAI instance name") - .addText((text) => { - text.inputEl.style.width = "100%"; - text - .setPlaceholder("Azure OpenAI instance name") - .setValue(this.plugin.settings.azureOpenAIApiInstanceName) - .onChange(async (value) => { - this.plugin.settings.azureOpenAIApiInstanceName = value; - await this.plugin.saveSettings(); - }) - } - ); - - new Setting(containerEl) - .setName("Your Azure OpenAI deployment name") - .addText((text) => { - text.inputEl.style.width = "100%"; - text - .setPlaceholder("Azure OpenAI deployment name") - .setValue(this.plugin.settings.azureOpenAIApiDeploymentName) - .onChange(async (value) => { - this.plugin.settings.azureOpenAIApiDeploymentName = value; - await this.plugin.saveSettings(); - }) - } - ); - - new Setting(containerEl) - .setName("Your Azure OpenAI API version") - .addText((text) => { - text.inputEl.style.width = "100%"; - text - .setPlaceholder("Azure OpenAI API version") - .setValue(this.plugin.settings.azureOpenAIApiVersion) - .onChange(async (value) => { - this.plugin.settings.azureOpenAIApiVersion = value; - await this.plugin.saveSettings(); - }) - } - ); - - new Setting(containerEl) - .setName("Your Azure OpenAI embedding model deployment name (Optional)") - .setDesc( - createFragment((frag) => { - frag.appendText("Only if you'd like to use Azure as the embedding provider."); - }) - ) - .addText((text) => { - text.inputEl.style.width = "100%"; - text - .setPlaceholder("Azure OpenAI embedding model deployment name") - .setValue(this.plugin.settings.azureOpenAIApiEmbeddingDeploymentName) - .onChange(async (value) => { - this.plugin.settings.azureOpenAIApiEmbeddingDeploymentName = value; - await this.plugin.saveSettings(); - }) - } - ); - - containerEl.createEl( - 'h6', - { - text: 'Please be mindful of the number of tokens and context conversation turns you set here, as they will affect the cost of your API requests.' - } - ); - - new Setting(containerEl) - .setName("Temperature") - .setDesc( - createFragment((frag) => { - frag.appendText( - "Default is 0.7. Higher values will result in more creativeness, but also more mistakes. Set to 0 for no randomness." - ); - }) - ) - .addSlider(slider => - slider - .setLimits(0, 2, 0.05) - .setValue( - this.plugin.settings.temperature !== undefined && - this.plugin.settings.temperature !== null ? - this.plugin.settings.temperature : 0.7 - ) - .setDynamicTooltip() - .onChange(async value => { - this.plugin.settings.temperature = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl) - .setName("Token limit") - .setDesc( - createFragment((frag) => { - frag.appendText( - "The maximum number of output tokens to generate. Default is 1000." - ); - frag.createEl( - 'strong', - { - text: 'This number plus the length of your prompt (input tokens) must be smaller than the context window of the model.' - } - ) - }) - ) - .addSlider(slider => - slider - .setLimits(0, 10000, 100) - .setValue( - this.plugin.settings.maxTokens !== undefined && - this.plugin.settings.maxTokens !== null ? - this.plugin.settings.maxTokens : 1000 - ) - .setDynamicTooltip() - .onChange(async value => { - this.plugin.settings.maxTokens = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl) - .setName("Conversation turns in context") - .setDesc( - createFragment((frag) => { - frag.appendText( - "The number of previous conversation turns to include in the context. Default is 3 turns, i.e. 6 messages." - ); - }) - ) - .addSlider(slider => - slider - .setLimits(1, 30, 1) - .setValue( - this.plugin.settings.contextTurns !== undefined && - this.plugin.settings.contextTurns !== null ? - this.plugin.settings.contextTurns : 3 - ) - .setDynamicTooltip() - .onChange(async value => { - this.plugin.settings.contextTurns = value; - await this.plugin.saveSettings(); - }) - ); - - containerEl.createEl('h4', { text: 'Vector-based QA Settings (Beta). No context limit!' }); - containerEl.createEl('p', { text: 'To start the QA session, use the Mode Selection dropdown and select "QA". Switch back to "Chat" when you are done!' }); - containerEl.createEl( - 'p', - { - text: 'NOTE: OpenAI embeddings are not free but may give better QA results. CohereAI offers trial API for FREE and the quality is very good! It is more stable than Huggingface Inference API (more timeouts).' - } - ); - - new Setting(containerEl) - .setName("Embedding Provider") - .setDesc( - createFragment((frag) => { - frag.appendText("The embedding provider to use"); - }) - ) - .addDropdown((dropdown: DropdownComponent) => { - dropdown - .addOption(OPENAI, 'OpenAI') - .addOption(COHEREAI, 'CohereAI') - .addOption(AZURE_OPENAI, 'Azure OpenAI') - .addOption(HUGGINGFACE, 'Huggingface') - .setValue(this.plugin.settings.embeddingProvider) - .onChange(async (value: string) => { - this.plugin.settings.embeddingProvider = value; - await this.plugin.saveSettings(); - }); - }); - - new Setting(containerEl) - .setName("TTL (Days)") - .setDesc("Specify the Time To Live (TTL) for the saved embeddings in days. Default is 30 days. Embeddings older than the TTL will be deleted automatically to save storage space.") - .addText((text) => { - text - .setPlaceholder("30") - .setValue(this.plugin.settings.ttlDays ? this.plugin.settings.ttlDays.toString() : '') - .onChange(async (value: string) => { - const intValue = parseInt(value); - if (!isNaN(intValue)) { - this.plugin.settings.ttlDays = intValue; - await this.plugin.saveSettings(); - } - }); - }); - - - new Setting(containerEl) - .setName("Your CohereAI trial API key") - .setDesc( - createFragment((frag) => { - frag.appendText("You can sign up at CohereAI and find your API key at "); - frag.createEl('a', { - text: "https://dashboard.cohere.ai/api-keys", - href: "https://dashboard.cohere.ai/api-keys" - }); - frag.createEl('br'); - frag.appendText("It is used to make requests to CohereAI trial API for free embeddings."); - }) - ) - .addText((text) => { - text.inputEl.type = "password"; - text.inputEl.style.width = "80%"; - text - .setPlaceholder("CohereAI trial API key") - .setValue(this.plugin.settings.cohereApiKey) - .onChange(async (value) => { - this.plugin.settings.cohereApiKey = value; - await this.plugin.saveSettings(); - }) - } - ); - - new Setting(containerEl) - .setName("Your Huggingface Inference API key") - .setDesc( - createFragment((frag) => { - frag.appendText("You can find your API key at "); - frag.createEl('a', { - text: "https://hf.co/settings/tokens", - href: "https://hf.co/settings/tokens" - }); - frag.createEl('br'); - frag.appendText("It is used to make requests to Huggingface Inference API for free embeddings."); - frag.createEl('br'); - frag.createEl('strong', { - text: "Please note that the quality may be worse than OpenAI embeddings," - }); - frag.createEl('br'); - frag.createEl('strong', { - text: "and may have more API timeout errors." - }); - }) - ) - .addText((text) => { - text.inputEl.type = "password"; - text.inputEl.style.width = "80%"; - text - .setPlaceholder("Huggingface Inference API key") - .setValue(this.plugin.settings.huggingfaceApiKey) - .onChange(async (value) => { - this.plugin.settings.huggingfaceApiKey = value; - await this.plugin.saveSettings(); - }) - } - ); - - containerEl.createEl('h4', { text: 'Advanced Settings' }); - - new Setting(containerEl) - .setName("User custom system prompt") - .setDesc( - createFragment((frag) => { - frag.appendText("You can set your own system prompt here. ") - frag.createEl( - 'strong', - { text: "Warning: It will override the default system prompt for all messages! " } - ); - frag.appendText( - "Use with caution! Also note that OpenAI can return error codes for some system prompts." - ); - }) - ) - .addTextArea(text => { - text.inputEl.style.width = "200px"; - text.inputEl.style.height = "100px"; - text - .setPlaceholder("User system prompt") - .setValue(this.plugin.settings.userSystemPrompt) - .onChange(async (value) => { - this.plugin.settings.userSystemPrompt = value; - await this.plugin.saveSettings(); - }); - }); - - new Setting(containerEl) - .setName("OpenAI Proxy Base URL (3rd-party providers)") - .setDesc( - createFragment((frag) => { - frag.createEl( - 'strong', - { text: "CAUTION: This overrides the OpenAI API URL " }, - ); - frag.createEl('br'); - frag.createEl('strong', { - text: "for both chat and embedding models when OpenAI models are picked!" - }); - frag.createEl('br'); - frag.appendText(" Leave blank to use the official OpenAI API."); - }) - ) - .addText((text) => { - text.inputEl.style.width = "100%"; - text - .setPlaceholder("https://openai.example.com/v1") - .setValue(this.plugin.settings.openAIProxyBaseUrl) - .onChange(async (value) => { - this.plugin.settings.openAIProxyBaseUrl = value; - await this.plugin.saveSettings(); - }) - }); - - containerEl.createEl('h4', { text: 'Local Copilot (No Internet Required!)' }); - containerEl.createEl('div', { - text: 'Please check the doc to set up LM Studio or Ollama server on your device.', - cls: 'warning-message' - }); - containerEl.createEl('p', { text: 'Local models can be limited in capabilities and may not work for some use cases at this time. Keep in mind that it is still in early experimental phase. But some 13B even 7B models are already quite capable!' }); - - - containerEl.createEl('h5', { text: 'LM Studio' }); - containerEl.createEl('p', { text: 'To use Local Copilot with LM Studio:' }); - containerEl.createEl('p', { text: '1. Start LM Studio server with CORS on. Default port is 1234 but if you change it, you can provide it below.' }); - containerEl.createEl('p', { text: '2. Pick LM Studio in the Copilot Chat model selection dropdown to chat with it!' }); - - new Setting(containerEl) - .setName("LM Studio server port") - .setDesc("The default is 1234") - .addText(text => text - .setPlaceholder("1234") - .setValue(this.plugin.settings.lmStudioPort) - .onChange(async (value: string) => { - this.plugin.settings.lmStudioPort = value; - await this.plugin.saveSettings(); - }) - ); - - containerEl.createEl('h5', { text: 'Ollama' }); - containerEl.createEl('p', { text: 'To use Local Copilot with Ollama, pick Ollama in the Copilot Chat model selection dropdown.' }); - containerEl.createEl('p', { text: 'Run the local Ollama server by running this in your terminal:' }); - containerEl.createEl( - 'strong', - { text: "OLLAMA_ORIGINS=app://obsidian.md* ollama serve" } - ); - - new Setting(containerEl) - .setName("Ollama model") - .setDesc("The default is llama2") - .addText(text => text - .setPlaceholder("llama2") - .setValue(this.plugin.settings.ollamaModel) - .onChange(async (value: string) => { - this.plugin.settings.ollamaModel = value; - await this.plugin.saveSettings(); - }) - ); - - containerEl.createEl('h4', { text: 'Development mode' }); - - new Setting(containerEl) - .setName("Debug mode") - .setDesc( - createFragment((frag) => { - frag.appendText("Debug mode will log all API requests and prompts to the console."); - }) - ) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.debug) - .onChange(async (value) => { - this.plugin.settings.debug = value; - await this.plugin.saveSettings(); - }) - ); - } -} \ No newline at end of file diff --git a/src/settings/SettingsPage.tsx b/src/settings/SettingsPage.tsx new file mode 100644 index 00000000..d03820ef --- /dev/null +++ b/src/settings/SettingsPage.tsx @@ -0,0 +1,92 @@ +import CopilotPlugin from "@/main"; +import { App, Notice, PluginSettingTab, Setting } from "obsidian"; +import React from 'react'; +import { createRoot } from "react-dom/client"; +import SettingsMain from "./components/SettingsMain"; + +export interface CopilotSettings { + openAIApiKey: string; + huggingfaceApiKey: string; + cohereApiKey: string; + anthropicApiKey: string; + azureOpenAIApiKey: string; + azureOpenAIApiInstanceName: string; + azureOpenAIApiDeploymentName: string; + azureOpenAIApiVersion: string; + azureOpenAIApiEmbeddingDeploymentName: string; + googleApiKey: string; + openRouterAiApiKey: string; + openRouterModel: string; + defaultModel: string; + defaultModelDisplayName: string; + temperature: number; + maxTokens: number; + contextTurns: number; + userSystemPrompt: string; + openAIProxyBaseUrl: string; + ollamaModel: string; + ollamaBaseUrl: string; + lmStudioPort: string; + ttlDays: number; + stream: boolean; + embeddingProvider: string; + defaultSaveFolder: string; + debug: boolean; +} + +export class CopilotSettingTab extends PluginSettingTab { + plugin: CopilotPlugin; + + constructor(app: App, plugin: CopilotPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + async reloadPlugin() { + try { + // Save the settings before reloading + await this.plugin.saveSettings(); + + // Reload the plugin + const app = (this.plugin.app as any); + await app.plugins.disablePlugin("copilot"); + await app.plugins.enablePlugin("copilot"); + + app.setting.openTabById("copilot").display(); + new Notice('Plugin reloaded successfully.'); + } catch (error) { + new Notice('Failed to reload the plugin. Please reload manually.'); + console.error('Error reloading plugin:', error); + } + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + containerEl.style.userSelect = 'text'; + const div = containerEl.createDiv("div") + const sections = createRoot(div); + + sections.render( + + ); + + const devModeHeader = containerEl.createEl('h1', { text: 'Development mode' }); + devModeHeader.style.marginTop = '40px'; + + new Setting(containerEl) + .setName("Debug mode") + .setDesc( + createFragment((frag) => { + frag.appendText("Debug mode will log all API requests and prompts to the console."); + }) + ) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.debug) + .onChange(async (value) => { + this.plugin.settings.debug = value; + await this.plugin.saveSettings(); + }) + ); + } +} \ No newline at end of file diff --git a/src/settings/components/AdvancedSettings.tsx b/src/settings/components/AdvancedSettings.tsx new file mode 100644 index 00000000..97d7d648 --- /dev/null +++ b/src/settings/components/AdvancedSettings.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { TextAreaComponent, TextComponent } from './SettingBlocks'; + +interface AdvancedSettingsProps { + openAIProxyBaseUrl: string; + setOpenAIProxyBaseUrl: (value: string) => void; + userSystemPrompt: string; + setUserSystemPrompt: (value: string) => void; +} + +const AdvancedSettings: React.FC = ({ + openAIProxyBaseUrl, + setOpenAIProxyBaseUrl, + userSystemPrompt, + setUserSystemPrompt, +}) => { + return ( +
+
+
+

Advanced Settings

+
+ OpenAI Proxy Base URL overrides the default OpenAI base URL, meaning now your OpenAI models are routed to this provider instead! Clear this field to use OpenAI again. +
+ + +
+ ); +}; + +export default AdvancedSettings; diff --git a/src/settings/components/ApiSetting.tsx b/src/settings/components/ApiSetting.tsx new file mode 100644 index 00000000..3313b9d5 --- /dev/null +++ b/src/settings/components/ApiSetting.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { TextComponent } from './SettingBlocks'; + +const ApiSetting: React.FC<{ + title: string; + description?: string; + value: string; + setValue: (value: string) => void; + placeholder?: string; + type?: string; +}> = ({ title, description, value, setValue, placeholder, type }) => { + return ( +
+ +
+ ); +}; + +export default ApiSetting; diff --git a/src/settings/components/ApiSettings.tsx b/src/settings/components/ApiSettings.tsx new file mode 100644 index 00000000..55d33a3f --- /dev/null +++ b/src/settings/components/ApiSettings.tsx @@ -0,0 +1,163 @@ +import { DEFAULT_SETTINGS } from "@/constants"; +import React from 'react'; +import ApiSetting from './ApiSetting'; +import Collapsible from './Collapsible'; + +interface ApiSettingsProps { + openAIApiKey: string; + setOpenAIApiKey: (value: string) => void; + googleApiKey: string; + setGoogleApiKey: (value: string) => void; + openRouterAiApiKey: string; + setOpenRouterAiApiKey: (value: string) => void; + openRouterModel: string; + setOpenRouterModel: (value: string) => void; + azureOpenAIApiKey: string; + setAzureOpenAIApiKey: (value: string) => void; + azureOpenAIApiInstanceName: string; + setAzureOpenAIApiInstanceName: (value: string) => void; + azureOpenAIApiDeploymentName: string; + setAzureOpenAIApiDeploymentName: (value: string) => void; + azureOpenAIApiVersion: string; + setAzureOpenAIApiVersion: (value: string) => void; +} + +const ApiSettings: React.FC = ({ + openAIApiKey, + setOpenAIApiKey, + googleApiKey, + setGoogleApiKey, + openRouterAiApiKey, + setOpenRouterAiApiKey, + openRouterModel, + setOpenRouterModel, + azureOpenAIApiKey, + setAzureOpenAIApiKey, + azureOpenAIApiInstanceName, + setAzureOpenAIApiInstanceName, + azureOpenAIApiDeploymentName, + setAzureOpenAIApiDeploymentName, + azureOpenAIApiVersion, + setAzureOpenAIApiVersion, +}) => { + return ( +
+
+
+

API Settings

+

+ All your API keys are stored locally. +

+
+ Make sure you have access to the model and the correct API key. +
+ If errors occur, please re-enter the API key, save and reload the plugin to see if it resolves the issue. +
+
+
+ +

+ You can find your API key at{' '} + + https://platform.openai.com/api-keys + +

+
+
+ If you are a new user, try + + OpenAI playground + + to see if you have correct API access first. +
+
+
+ +
+ +

+ If you have Google Cloud, you can get Gemini API key {' '} + + here + . +
+ Your API key is stored locally and is only used to make requests to Google's services. +

+
+
+ + +
+ + +

+ You can get your OpenRouterAI key {' '} + + here + . +
+ Find models + here + . +

+
+
+ + +
+ + + + +
+
+
+ ); +}; + +export default ApiSettings; \ No newline at end of file diff --git a/src/settings/components/Collapsible.tsx b/src/settings/components/Collapsible.tsx new file mode 100644 index 00000000..515eea3e --- /dev/null +++ b/src/settings/components/Collapsible.tsx @@ -0,0 +1,54 @@ +import React, { ReactNode, useState } from 'react'; + +interface CollapsibleProps { + title: ReactNode; + children: ReactNode; +} + +const Collapsible: React.FC = ({ title, children }) => { + const [isOpen, setIsOpen] = useState(false); + + const titleStyle = { + fontWeight: 'bold', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: '10px', + borderRadius: '5px', + }; + + const contentStyle = { + padding: '10px 10px', + borderRadius: '8px', + marginTop: '10px', + }; + + const ChevronDown = () => ( + + + + ); + + const ChevronRight = () => ( + + + + ); + + return ( +
+
setIsOpen(!isOpen)}> + {title} + {isOpen ? : } +
+ {isOpen && ( +
+ {children} +
+ )} +
+ ); +}; + +export default Collapsible; diff --git a/src/settings/components/LocalCopilotSettings.tsx b/src/settings/components/LocalCopilotSettings.tsx new file mode 100644 index 00000000..f11e8d3e --- /dev/null +++ b/src/settings/components/LocalCopilotSettings.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { TextComponent } from './SettingBlocks'; + +interface LocalCopilotSettingsProps { + lmStudioPort: string; + setLmStudioPort: (value: string) => void; + ollamaModel: string; + setOllamaModel: (value: string) => void; + ollamaBaseUrl: string; + setOllamaBaseUrl: (value: string) => void; +} + +const LocalCopilotSettings: React.FC = ({ + lmStudioPort, + setLmStudioPort, + ollamaModel, + setOllamaModel, + ollamaBaseUrl, + setOllamaBaseUrl, +}) => { + return ( +
+
+

Local Copilot (No Internet Required!)

+
+ Please check the doc to set up LM Studio or Ollama server on your device. +
+

+ Local models can be limited in capabilities and may not work for some use cases at this time. Keep in mind that it is still in early experimental phase. But some 13B even 7B models are already quite capable! +

+

LM Studio

+

+ To use Local Copilot with LM Studio:
+ 1. Start LM Studio server with CORS on. Default port is 1234 but if you change it, you can provide it below.
+ 2. Pick LM Studio in the Copilot Chat model selection dropdown to chat with it! +

+ +

Ollama

+

To use Local Copilot with Ollama, pick Ollama in the Copilot Chat model selection dropdown.

+

Run the local Ollama server by running this in your terminal:

+

OLLAMA_ORIGINS=app://obsidian.md* ollama serve

+ + +
+ ); +}; + +export default LocalCopilotSettings; diff --git a/src/settings/components/QASettings.tsx b/src/settings/components/QASettings.tsx new file mode 100644 index 00000000..e244606b --- /dev/null +++ b/src/settings/components/QASettings.tsx @@ -0,0 +1,97 @@ +import { EMBEDDING_PROVIDERS } from '@/constants'; +import React from 'react'; +import ApiSetting from './ApiSetting'; +import Collapsible from './Collapsible'; +import { DropdownComponent, SliderComponent } from './SettingBlocks'; + +interface QASettingsProps { + embeddingProvider: string; + setEmbeddingProvider: (value: string) => void; + ttlDays: number; + setTtlDays: (value: number) => void; + cohereApiKey: string; + setCohereApiKey: (value: string) => void; + huggingfaceApiKey: string; + setHuggingfaceApiKey: (value: string) => void; +} + +const QASettings: React.FC = ({ + embeddingProvider, + setEmbeddingProvider, + ttlDays, + setTtlDays, + cohereApiKey, + setCohereApiKey, + huggingfaceApiKey, + setHuggingfaceApiKey, +}) => { + return ( +
+
+
+

QA Settings

+
+ YOU MUST REBUILD YOUR INDEX AFTER SWITCHING EMBEDDING PROVIDERS! +
+

QA mode relies a local vector index (experimental) +
+ OpenAI embeddings currently has the best retrieval quality. CohereAI embeddings are free during trial and are decent. With Huggingface Inference API, your mileage may vary. +

+ + +
+ + +

+ Get your free Cohere API key{' '} + + here + +

+
+ + +

+ Get your Huggingface Inference API key{' '} + + here + +

+
+
+ ); +}; + +export default QASettings; \ No newline at end of file diff --git a/src/settings/components/SettingBlocks.tsx b/src/settings/components/SettingBlocks.tsx new file mode 100644 index 00000000..7f8cd75e --- /dev/null +++ b/src/settings/components/SettingBlocks.tsx @@ -0,0 +1,136 @@ +import React from 'react'; + +type DropdownComponentProps = { + name: string; + description?: string; + options: string[]; + value: string; + onChange: (value: string) => void; +}; + +type TextComponentProps = { + name: string; + description?: string; + placeholder: string; + value: string; + type?: string; + onChange: (value: string) => void; +}; + +type TextAreaComponentProps = { + name: string; + description?: string; + placeholder: string; + value: string; + onChange: (value: string) => void; +}; + +type SliderComponentProps = { + name: string; + description?: React.ReactNode; // This allows for JSX elements, strings, etc. + min: number; + max: number; + step: number; + value: number; + onChange: (value: number) => void; +}; + +type ToggleComponentProps = { + name: string; + description?: string; + value: boolean; + onChange: (value: boolean) => void; +} + +const DropdownComponent: React.FC = ({ name, description, options, value, onChange }) => { + return ( +
+
{name}
+
{description}
+ +
+ ); +}; + +const TextComponent: React.FC = ({ name, description, placeholder, value, type, onChange }) => { + return ( +
+
{name}
+
{description}
+ onChange(e.target.value)} + /> +
+ ); +}; + +const TextAreaComponent: React.FC = ({ name, description, placeholder, value, onChange }) => { + return ( +
+
{name}
+
{description}
+