From d0db8a27888b29421b8cf0a180c40c583e899037 Mon Sep 17 00:00:00 2001 From: Claire Froelich Date: Thu, 21 Mar 2024 08:35:36 -0400 Subject: [PATCH] bump version --- package-lock.json | 4 +- package.json | 52 ++-- src/main.ts | 587 ++++++++++++++++++++++---------------------- src/util/chatcbt.ts | 134 +++++----- versions.json | 2 +- 5 files changed, 381 insertions(+), 398 deletions(-) diff --git a/package-lock.json b/package-lock.json index 538f632..b293f32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-chat-cbt-plugin", - "version": "1.0.1", + "version": "1.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-chat-cbt-plugin", - "version": "1.0.1", + "version": "1.1.3", "license": "MIT", "dependencies": { "cryptr": "^6.3.0" diff --git a/package.json b/package.json index d50e11b..e4f4143 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,28 @@ { - "name": "obsidian-chat-cbt-plugin", - "version": "1.1.3", - "description": "Plugin to guide you in reframing negative thoughts and keep an organized record of your findings", - "main": "main.js", - "scripts": { - "dev": "node esbuild.config.mjs", - "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", - "version": "node version-bump.mjs && git add manifest.json versions.json" - }, - "keywords": [], - "author": "", - "license": "MIT", - "devDependencies": { - "@types/inputmask": "^5.0.4", - "@types/node": "^16.11.6", - "@typescript-eslint/eslint-plugin": "5.29.0", - "@typescript-eslint/parser": "5.29.0", - "builtin-modules": "3.3.0", - "esbuild": "0.17.3", - "obsidian": "latest", - "tslib": "2.4.0", - "typescript": "4.7.4" - }, - "dependencies": { - "cryptr": "^6.3.0" - } + "name": "obsidian-chat-cbt-plugin", + "version": "1.1.4", + "description": "Plugin to guide you in reframing negative thoughts and keep an organized record of your findings", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "@types/inputmask": "^5.0.4", + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "esbuild": "0.17.3", + "obsidian": "latest", + "tslib": "2.4.0", + "typescript": "4.7.4" + }, + "dependencies": { + "cryptr": "^6.3.0" + } } diff --git a/src/main.ts b/src/main.ts index 3fc72ad..24bb487 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,13 @@ import { - App, - Editor, - MarkdownView, - Modal, - Notice, - Plugin, - PluginSettingTab, - Setting, - Menu, + App, + Editor, + MarkdownView, + Modal, + Notice, + Plugin, + PluginSettingTab, + Setting, + Menu, } from 'obsidian'; import { crypt } from './util/crypt'; import { ChatCbt, Mode } from './util/chatcbt'; @@ -15,314 +15,303 @@ import { buildAssistantMsg, convertTextToMsg } from './util/messages'; /** Interfaces */ interface ChatCbtPluginSettings { - openAiApiKey: string; - mode: string; - model: string; - ollamaUrl: string; + openAiApiKey: string; + mode: string; + model: string; + ollamaUrl: string; } interface ChatCbtResponseInput { - isSummary: boolean; - mode: Mode; + isSummary: boolean; + mode: Mode; } /** Constants */ const VALID_MODES = ['openai', 'ollama']; const DEFAULT_SETTINGS: ChatCbtPluginSettings = { - openAiApiKey: '', - mode: 'openai', - model: '', - ollamaUrl: 'http://0.0.0.0:11434', + openAiApiKey: '', + mode: 'openai', + model: '', + ollamaUrl: 'http://0.0.0.0:11434', }; /** Initialize chat client */ const chatCbt = new ChatCbt(); export default class ChatCbtPlugin extends Plugin { - settings: ChatCbtPluginSettings; - statusBar: HTMLElement; - - async onload() { - console.log('loading plugin'); - - await this.loadSettings(); - - // This creates an icon in the left ribbon. - this.addRibbonIcon('heart-handshake', 'ChatCBT', (evt: MouseEvent) => { - const menu = new Menu(); - - menu.addItem((item) => - item - .setTitle('Chat') - .setIcon('message-circle') - .onClick(() => { - this.getChatCbtRepsonse({ - isSummary: false, - mode: 'openai', - }); - }), - ); - - menu.addItem((item) => - item - .setTitle('Summarize') - .setIcon('table') - .onClick(() => { - this.getChatCbtSummary(); - }), - ); - - menu.showAtMouseEvent(evt); - }); - - // This adds a status bar item to the bottom of the app. Does not work on mobile apps. - const statusBarItemEl = this.addStatusBarItem(); - this.statusBar = statusBarItemEl; - this.setStatusBarMode(this.settings.mode as Mode); - - // This adds an editor command that can perform some operation on the current editor instance - this.addCommand({ - id: 'chat', - name: 'Chat - submit the text in the active tab to ChatCBT', - editorCallback: (_editor: Editor, _view: MarkdownView) => { - this.getChatCbtRepsonse({ - isSummary: false, - mode: this.settings.mode as Mode, - }); - }, - }); - - this.addCommand({ - id: 'summarize', - name: 'Summarize - create a table that summarizes reframed thoughts from your conversation', - editorCallback: (_editor: Editor, _view: MarkdownView) => { - this.getChatCbtSummary(); - }, - }); - - // This adds a settings tab so the user can configure various aspects of the plugin - this.addSettingTab(new MySettingTab(this.app, this)); - } - - /** Run when plugin is disabled */ - onunload() { - console.log('unloading plugin'); - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveSettings() { - await this.saveData(this.settings); - } - - setStatusBarMode(mode: Mode) { - this.statusBar.setText(`ChatCBT - ${mode} mode`); - } - - async getChatCbtRepsonse({ - isSummary = false, - mode = 'openai', - }: ChatCbtResponseInput) { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - return; - } - - if (!VALID_MODES.includes(this.settings.mode)) { - new Notice( - `Inavlid mode '${this.settings.mode}' detected. Update in ChatCBT plugin settings and select a valid mode`, - ); - return; - } - - if (this.settings.mode === 'openai' && !this.settings.openAiApiKey) { - new Notice('Missing OpenAI API Key - update in ChatCBT plugin settings'); - return; - } - - if (this.settings.mode === 'ollama' && !this.settings.ollamaUrl) { - new Notice('Missing Ollama URL - update in ChatCBT plugin settings'); - return; - } - - const existingText = await this.app.vault.read(activeFile); - if (!existingText.trim()) { - new Notice('First, share how you are feeling'); - return; - } - - const messages = existingText - .split(/---+/) - .map((i) => i.trim()) - .map((i) => convertTextToMsg(i)); - - // TODO: refactor - const selectedModel = this.settings.model - ? this.settings.model - : this.settings.mode === 'openai' - ? 'gpt-3.5-turbo' - : 'mistral'; - const loadingModal = new TextModel( - this.app, - `Asking ChatCBT... (mode: '${this.settings.mode}', model: '${selectedModel}')`, - ); - loadingModal.open(); - - let response = ''; - - try { - const apiKey = this.settings.openAiApiKey - ? crypt.decrypt(this.settings.openAiApiKey) - : ''; - - const res = await chatCbt.chat({ - apiKey, - messages, - isSummary, - mode: this.settings.mode as Mode, - ollamaUrl: this.settings.ollamaUrl, - model: this.settings.model, - }); - response = res; - } catch (e) { - let msg = e.msg; - if (e.status === 404) { - msg = `Model named '${this.settings.model}' not found for ${this.settings.mode}. Update mode or model name in settings.`; - } - new Notice(`ChatCBT failed :(: ${msg}`); - console.error(e); - } finally { - loadingModal.close(); - } - - if (response) { - const MSG_PADDING = '\n\n'; - const appendMsg = isSummary - ? MSG_PADDING + response - : buildAssistantMsg(response); - await this.app.vault.append(activeFile, appendMsg); - } - } - - async getChatCbtSummary() { - await this.getChatCbtRepsonse({ isSummary: true, mode: 'openai' }); - } + settings: ChatCbtPluginSettings; + + async onload() { + console.log('loading plugin'); + + await this.loadSettings(); + + // This creates an icon in the left ribbon. + this.addRibbonIcon('heart-handshake', 'ChatCBT', (evt: MouseEvent) => { + const menu = new Menu(); + + menu.addItem((item) => + item + .setTitle('Chat') + .setIcon('message-circle') + .onClick(() => { + this.getChatCbtRepsonse({ + isSummary: false, + mode: 'openai', + }); + }), + ); + + menu.addItem((item) => + item + .setTitle('Summarize') + .setIcon('table') + .onClick(() => { + this.getChatCbtSummary(); + }), + ); + + menu.showAtMouseEvent(evt); + }); + + // This adds an editor command that can perform some operation on the current editor instance + this.addCommand({ + id: 'chat', + name: 'Chat - submit the text in the active tab to ChatCBT', + editorCallback: (_editor: Editor, _view: MarkdownView) => { + this.getChatCbtRepsonse({ + isSummary: false, + mode: this.settings.mode as Mode, + }); + }, + }); + + this.addCommand({ + id: 'summarize', + name: 'Summarize - create a table that summarizes reframed thoughts from your conversation', + editorCallback: (_editor: Editor, _view: MarkdownView) => { + this.getChatCbtSummary(); + }, + }); + + // This adds a settings tab so the user can configure various aspects of the plugin + this.addSettingTab(new MySettingTab(this.app, this)); + } + + /** Run when plugin is disabled */ + onunload() { + console.log('unloading plugin'); + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } + + async getChatCbtRepsonse({ + isSummary = false, + mode = 'openai', + }: ChatCbtResponseInput) { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + return; + } + + if (!VALID_MODES.includes(this.settings.mode)) { + new Notice( + `Inavlid mode '${this.settings.mode}' detected. Update in ChatCBT plugin settings and select a valid mode`, + ); + return; + } + + if (this.settings.mode === 'openai' && !this.settings.openAiApiKey) { + new Notice('Missing OpenAI API Key - update in ChatCBT plugin settings'); + return; + } + + if (this.settings.mode === 'ollama' && !this.settings.ollamaUrl) { + new Notice('Missing Ollama URL - update in ChatCBT plugin settings'); + return; + } + + const existingText = await this.app.vault.read(activeFile); + if (!existingText.trim()) { + new Notice('First, share how you are feeling'); + return; + } + + const messages = existingText + .split(/---+/) + .map((i) => i.trim()) + .map((i) => convertTextToMsg(i)); + + // TODO: refactor + const selectedModel = this.settings.model + ? this.settings.model + : this.settings.mode === 'openai' + ? 'gpt-3.5-turbo' + : 'mistral'; + const loadingModal = new TextModel( + this.app, + `Asking ChatCBT... (mode: '${this.settings.mode}', model: '${selectedModel}')`, + ); + loadingModal.open(); + + let response = ''; + + try { + const apiKey = this.settings.openAiApiKey + ? crypt.decrypt(this.settings.openAiApiKey) + : ''; + + const res = await chatCbt.chat({ + apiKey, + messages, + isSummary, + mode: this.settings.mode as Mode, + ollamaUrl: this.settings.ollamaUrl, + model: this.settings.model, + }); + response = res; + } catch (e) { + let msg = e.msg; + if (e.status === 404) { + msg = `Model named '${this.settings.model}' not found for ${this.settings.mode}. Update mode or model name in settings.`; + } + new Notice(`ChatCBT failed :(: ${msg}`); + console.error(e); + } finally { + loadingModal.close(); + } + + if (response) { + const MSG_PADDING = '\n\n'; + const appendMsg = isSummary + ? MSG_PADDING + response + : buildAssistantMsg(response); + await this.app.vault.append(activeFile, appendMsg); + } + } + + async getChatCbtSummary() { + await this.getChatCbtRepsonse({ isSummary: true, mode: 'openai' }); + } } class MySettingTab extends PluginSettingTab { - plugin: ChatCbtPlugin; - - constructor(app: App, plugin: ChatCbtPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - const { containerEl } = this; - - containerEl.empty(); - - containerEl.createEl('a', { - href: 'https://github.com/clairefro/obsidian-chat-cbt-plugin/blob/main/README.md', - text: 'Read the setup guide ↗️ ', - }); - containerEl.createEl('br'); - containerEl.createEl('br'); - - new Setting(containerEl) - .setName('OpenAI API Key') - .setDesc( - 'Create an OpenAI API Key from their website and paste here (Make sure you have added credits to your account!)', - ) - .addText((text) => - text - .setPlaceholder('Enter your API Key') - .setValue( - this.plugin.settings.openAiApiKey - ? crypt.decrypt(this.plugin.settings.openAiApiKey) - : '', - ) - .onChange(async (value) => { - if (!value.trim()) { - this.plugin.settings.openAiApiKey = ''; - } else { - this.plugin.settings.openAiApiKey = crypt.encrypt(value.trim()); - } - await this.plugin.saveSettings(); - }), - ); - - containerEl.createEl('br'); - containerEl.createEl('br'); - - new Setting(containerEl) - .setName('Ollama mode (local)') - .setDesc('Toggle on for a local experience if you are running Ollama') - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.mode === 'openai' ? false : true) - .onChange(async (value) => { - if (value) { - this.plugin.settings.mode = 'ollama'; - } else { - this.plugin.settings.mode = 'openai'; - } - await this.plugin.saveSettings(); - this.plugin.setStatusBarMode(this.plugin.settings.mode as Mode); - }), - ); - - new Setting(containerEl) - .setName('Ollama server URL') - .setDesc( - 'Edit this if you changed the default port for using Ollama. Requires Ollama v0.1.24 or higher.', - ) - .addText((text) => - text - .setPlaceholder('ex: http://0.0.0.0:11434') - .setValue(this.plugin.settings.ollamaUrl) - .onChange(async (value) => { - this.plugin.settings.ollamaUrl = value.trim(); - await this.plugin.saveSettings(); - }), - ); - - containerEl.createEl('br'); - containerEl.createEl('br'); - - new Setting(containerEl) - .setName('Model') - .setDesc( - "For OpenAI mode the default is 'gpt-3.5-turbo' model. For Ollama mode the default is 'mistral' model. If you prefer a different model, enter it here. Delete text here to restore defaults", - ) - .addText((text) => - text - .setPlaceholder('Override model name') - .setValue(this.plugin.settings.model) - .onChange(async (value) => { - this.plugin.settings.model = value.trim(); - await this.plugin.saveSettings(); - }), - ); - } + plugin: ChatCbtPlugin; + + constructor(app: App, plugin: ChatCbtPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + + containerEl.empty(); + + containerEl.createEl('a', { + href: 'https://github.com/clairefro/obsidian-chat-cbt-plugin/blob/main/README.md', + text: 'Read the setup guide ↗️ ', + }); + containerEl.createEl('br'); + containerEl.createEl('br'); + + new Setting(containerEl) + .setName('OpenAI API Key') + .setDesc( + 'Create an OpenAI API Key from their website and paste here (Make sure you have added credits to your account!)', + ) + .addText((text) => + text + .setPlaceholder('Enter your API Key') + .setValue( + this.plugin.settings.openAiApiKey + ? crypt.decrypt(this.plugin.settings.openAiApiKey) + : '', + ) + .onChange(async (value) => { + if (!value.trim()) { + this.plugin.settings.openAiApiKey = ''; + } else { + this.plugin.settings.openAiApiKey = crypt.encrypt(value.trim()); + } + await this.plugin.saveSettings(); + }), + ); + + containerEl.createEl('br'); + containerEl.createEl('br'); + + new Setting(containerEl) + .setName('Ollama mode (local)') + .setDesc('Toggle on for a local experience if you are running Ollama') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.mode === 'openai' ? false : true) + .onChange(async (value) => { + if (value) { + this.plugin.settings.mode = 'ollama'; + } else { + this.plugin.settings.mode = 'openai'; + } + await this.plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName('Ollama server URL') + .setDesc( + 'Edit this if you changed the default port for using Ollama. Requires Ollama v0.1.24 or higher.', + ) + .addText((text) => + text + .setPlaceholder('ex: http://0.0.0.0:11434') + .setValue(this.plugin.settings.ollamaUrl) + .onChange(async (value) => { + this.plugin.settings.ollamaUrl = value.trim(); + await this.plugin.saveSettings(); + }), + ); + + containerEl.createEl('br'); + containerEl.createEl('br'); + + new Setting(containerEl) + .setName('Model') + .setDesc( + "For OpenAI mode the default is 'gpt-3.5-turbo' model. For Ollama mode the default is 'mistral' model. If you prefer a different model, enter it here. Delete text here to restore defaults", + ) + .addText((text) => + text + .setPlaceholder('Override model name') + .setValue(this.plugin.settings.model) + .onChange(async (value) => { + this.plugin.settings.model = value.trim(); + await this.plugin.saveSettings(); + }), + ); + } } class TextModel extends Modal { - text: string; - constructor(app: App, _text: string) { - super(app); - this.text = _text; - } - - onOpen() { - const { contentEl } = this; - contentEl.setText(this.text); - } - - onClose() { - const { contentEl } = this; - contentEl.empty(); - } + text: string; + constructor(app: App, _text: string) { + super(app); + this.text = _text; + } + + onOpen() { + const { contentEl } = this; + contentEl.setText(this.text); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } } diff --git a/src/util/chatcbt.ts b/src/util/chatcbt.ts index 4739b67..853fbba 100644 --- a/src/util/chatcbt.ts +++ b/src/util/chatcbt.ts @@ -3,95 +3,89 @@ import systemPrompt from '../prompts/system'; import summaryPrompt from '../prompts/summary'; export interface Message { - role: string; - content: string; + role: string; + content: string; } export type Mode = 'openai' | 'ollama'; export interface ChatInput { - apiKey: string | undefined; - messages: Message[]; - isSummary: boolean | undefined; - mode: Mode; - ollamaUrl: string | undefined; - model: string | undefined; + apiKey: string | undefined; + messages: Message[]; + isSummary: boolean | undefined; + mode: Mode; + ollamaUrl: string | undefined; + model: string | undefined; } const SYSTEM_MSG = { role: 'system', content: systemPrompt }; const SUMMARY_MSG = { role: 'user', content: summaryPrompt }; export class ChatCbt { - constructor() {} + constructor() {} - async chat({ - apiKey, - messages, - isSummary = false, - mode = 'openai', - ollamaUrl, - model, - }: ChatInput): Promise { - const resolvedMsgs = [...messages]; + async chat({ + apiKey, + messages, + isSummary = false, + mode = 'openai', + ollamaUrl, + model, + }: ChatInput): Promise { + const resolvedMsgs = [...messages]; - if (isSummary) { - resolvedMsgs.push(SUMMARY_MSG); - } + if (isSummary) { + resolvedMsgs.push(SUMMARY_MSG); + } - let response = ''; - let resolvedModel = model; + let response = ''; - const msgs = [SYSTEM_MSG, ...resolvedMsgs]; + const msgs = [SYSTEM_MSG, ...resolvedMsgs]; - /** validations should be guaranteed from parent layer, based on mode. Re-validating here to appease typescript gods */ - if (mode === 'openai' && !!apiKey) { - const url = 'https://api.openai.com/v1/chat/completions'; - response = await this._chat( - url, - msgs, - apiKey, - resolvedModel || 'gpt-3.5-turbo', - ); - } else if (mode === 'ollama' && !!ollamaUrl) { - const url = ollamaUrl.replace(/\/$/, '') + '/v1/chat/completions'; - response = await this._chat( - url, - msgs, - 'ollama', // default API Key used by Ollama's OpenAI style chat endpoint (v0.1.24^) - resolvedModel || 'mistral', - ); - } + /** validations should be guaranteed from parent layer, based on mode. Re-validating here to appease typescript gods */ + if (mode === 'openai' && !!apiKey) { + const url = 'https://api.openai.com/v1/chat/completions'; + response = await this._chat(url, msgs, apiKey, model || 'gpt-3.5-turbo'); + } else if (mode === 'ollama' && !!ollamaUrl) { + const url = ollamaUrl.replace(/\/$/, '') + '/v1/chat/completions'; + response = await this._chat( + url, + msgs, + 'ollama', // default API Key used by Ollama's OpenAI style chat endpoint (v0.1.24^) + model || 'mistral', + ); + } - return response; - } + return response; + } - async _chat( - url: string, - messages: Message[], - apiKey: string, - model: string | undefined, - ): Promise { - const data = { - model, - messages, - temperature: 0.7, - }; + async _chat( + url: string, + messages: Message[], + apiKey: string, + model: string | undefined, + ): Promise { + const data = { + model, + messages, + temperature: 0.7, + }; - const headers = { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }; + const headers = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }; - const options = { - url, - method: 'POST', - body: JSON.stringify(data), - headers: headers as unknown as Record, - }; + const options = { + url, + method: 'POST', + body: JSON.stringify(data), + headers: headers as unknown as Record, + }; - const response: { - json: { choices: { message: { content: string } }[] }; - } = await requestUrl(options); + const response: { + json: { choices: { message: { content: string } }[] }; + } = await requestUrl(options); - return response.json.choices[0].message.content; - } + return response.json.choices[0].message.content; + } } diff --git a/versions.json b/versions.json index 2a34ffd..99a9ec0 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "1.1.3": "0.15.0" + "1.1.4": "0.15.0" }