From 321193993ce2c5be8ee5ab08c4a7907de45e2d81 Mon Sep 17 00:00:00 2001 From: Logan Yang Date: Wed, 21 Aug 2024 15:16:22 -0700 Subject: [PATCH] Format code (#521) --- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 9 +- .github/ISSUE_TEMPLATE/feature_request.md | 7 +- README.md | 56 +-- __mocks__/obsidian.js | 20 +- esbuild.config.mjs | 10 +- jest.config.js | 24 +- jest.setup.js | 2 +- local_copilot.md | 2 + manifest.json | 26 +- package.json | 4 +- src/LLMProviders/chainManager.ts | 188 +++------ src/LLMProviders/chatModelManager.ts | 21 +- src/LLMProviders/embeddingManager.ts | 40 +- src/LLMProviders/memoryManager.ts | 19 +- src/LLMProviders/promptManager.ts | 16 +- src/aiState.ts | 20 +- src/chainFactory.ts | 20 +- src/commands.ts | 5 +- src/components/AddPromptModal.tsx | 134 +++---- src/components/AdhocPromptModal.tsx | 106 +++-- src/components/Chat.tsx | 174 ++++---- src/components/ChatComponents/ChatIcons.tsx | 45 +-- src/components/ChatComponents/ChatInput.tsx | 28 +- .../ChatComponents/ChatMessages.tsx | 48 +-- src/components/ChatNoteContextModal.tsx | 103 +++-- src/components/CopilotView.tsx | 38 +- src/components/Icons.tsx | 170 ++++++-- src/components/ListPromptModal.tsx | 3 - ... NoteTitleModal.tsx => NoteTitleModal.tsx} | 8 +- src/components/QAExclusionModal.tsx | 49 +-- src/components/ToneModal.tsx | 9 +- src/context.ts | 5 +- src/customPromptProcessor.ts | 21 +- src/encryptionService.ts | 13 +- src/langchainStream.ts | 22 +- src/main.ts | 372 +++++++----------- src/search/hybridRetriever.ts | 28 +- src/sharedState.ts | 18 +- src/utils.ts | 127 ++---- src/vectorDBManager.ts | 84 ++-- tests/customPromptProcessor.test.ts | 123 +++--- tests/encryptionService.test.ts | 68 ++-- tests/utils.test.ts | 43 +- tsconfig.json | 13 +- typings/sse.d.ts | 2 +- versions.json | 94 ++--- wasmPlugin.mjs | 36 +- 48 files changed, 1059 insertions(+), 1416 deletions(-) rename src/components/{ NoteTitleModal.tsx => NoteTitleModal.tsx} (82%) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 77a28f42..be07f29a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: ['https://www.buymeacoffee.com/logancyang'] +custom: ["https://www.buymeacoffee.com/logancyang"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 01ff4d67..4f9ccd4f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - +title: "" +labels: "" +assignees: "" --- - [ ] Screenshot of note + Copilot chat pane + dev console added **(required)** @@ -16,7 +15,7 @@ A clear and concise description of what the bug is. Clear steps to reproduce the A clear and concise description of what you expected to happen. **Screenshots** -Add screenshots to help explain your problem. Please turn on debug mode in Copilot settings, turn off other plugins to leave only Copilot dev messages as necessary. +Add screenshots to help explain your problem. Please turn on debug mode in Copilot settings, turn off other plugins to leave only Copilot dev messages as necessary. **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 02145855..50a41c7c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,10 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - +title: "" +labels: "" +assignees: "" --- **Is your feature request related to a problem? Please describe.** diff --git a/README.md b/README.md index 3d7838bb..e551e975 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🔍 Copilot for Obsidian -![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/logancyang/obsidian-copilot?style=for-the-badge&sort=semver) ![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22copilot%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json&style=for-the-badge) +![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/logancyang/obsidian-copilot?style=for-the-badge&sort=semver) ![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&color=%23483699&label=downloads&query=%24%5B%22copilot%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json&style=for-the-badge) Copilot for Obsidian is a **free** and **open-source** ChatGPT interface right inside Obsidian. It has a minimalistic design and is straightforward to use. @@ -41,6 +41,7 @@ Since Claude 3 models are announced today (3/4/2024), I managed to add them in t **LM Studio** and **Ollama** are the 2 best choices for running local models on your own machine. Please check out the super simple setup guide [here](local_copilot.md). Don't forget to flex your creativity in custom prompts using local models! ## 🛠️ Features + - Chat with ChatGPT right inside Obsidian in the Copilot Chat window. - No repetitive login. Use your own API key (stored locally). - No monthly fee. Pay only for what you use. @@ -54,10 +55,11 @@ Since Claude 3 models are announced today (3/4/2024), I managed to add them in t - All QA modes are powered by retrieval augmentation with a **local vector store**. No sending your data to a cloud-based vector search service! - Easy commands to **simplify, emojify, summarize, translate, change tone, fix grammar, rewrite into a tweet/thread, count tokens** and more. - Set your own parameters like LLM temperature, max tokens, conversation context based on your need (**pls be mindful of the API cost**). -- **User custom prompt**! You can *add, apply, edit, delete* your custom prompts, persisted in your local Obsidian environment! Be creative with your own prompt templates, sky is the limit! +- **User custom prompt**! You can _add, apply, edit, delete_ your custom prompts, persisted in your local Obsidian environment! Be creative with your own prompt templates, sky is the limit! - **Local model** support for **offline chat** using LM Studio and Ollama. ## 🎬 Demos + #### 🤗 New to Copilot? Quick Guide for Beginners: @@ -106,11 +108,13 @@ Copilot for Obsidian is now available in **Obsidian Community Plugin**! Now you can see the chat icon in your leftside ribbon, clicking on it will open the chat panel on the right! Don't forget to check out the Copilot commands available in the commands palette! #### ⛓️ Manual Installation + - Go to the latest release - Download `main.js`, `manifest.json`, `styles.css` and put them under `.obsidian/plugins/obsidian-copilot/` in your vault - Open your Obsidian settings > Community plugins, and turn on `Copilot`. ## 🔔 Note + - The chat history is not saved by default. Please use "**Save as Note**" to save it. The note will have a title `Chat-Year_Month_Day-Hour_Minute_Second`, you can change its name as needed. - "**New Chat**" clears all previous chat history. Again, please use "**Save as Note**" if you would like to save the chat. - "**Use Long Note as Context**" creates a local vector index for the active long note so that you can chat with note longer than the model's context window! To start the QA, please switch from "Chat" to "QA" in the Mode Selection dropdown. @@ -123,54 +127,55 @@ Now you can see the chat icon in your leftside ribbon, clicking on it will open
"You do not have access to this model" - - You need to have access to some of the models like GPT-4 or Azure ones to use them. If you don't, sign up on their waitlist! - - A common misunderstanding I see is that some think they have access to GPT-4 API when they get ChatGPT Plus subscription. It was not always true. *You need to have access to GPT-4 API to use the GPT-4 model in this plugin*. Please check if you can successfully use your model in the OpenAI playground first https://platform.openai.com/playground?mode=chat. If not, you can apply for GPT-4 API access here https://openai.com/waitlist/gpt-4-api. Once you have access to the API, you can use GPT-4 with this plugin without the ChatGPT Plus subscription! - - Reference issue: https://github.com/logancyang/obsidian-copilot/issues/3#issuecomment-1544583676 +- You need to have access to some of the models like GPT-4 or Azure ones to use them. If you don't, sign up on their waitlist! +- A common misunderstanding I see is that some think they have access to GPT-4 API when they get ChatGPT Plus subscription. It was not always true. _You need to have access to GPT-4 API to use the GPT-4 model in this plugin_. Please check if you can successfully use your model in the OpenAI playground first https://platform.openai.com/playground?mode=chat. If not, you can apply for GPT-4 API access here https://openai.com/waitlist/gpt-4-api. Once you have access to the API, you can use GPT-4 with this plugin without the ChatGPT Plus subscription! +- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/3#issuecomment-1544583676
It's not using my note as context - - Please don't forget to switch to "**QA**" in the Mode Selection dropdown in order to start the QA. Copilot does not have your note as context in "Chat" mode. - Settings - - In fact, you don't have to click the button on the right before starting the QA. Switching to QA mode in the dropdown directly is enough for Copilot to read the note as context. The button on the right is only for when you'd like to manually rebuild the index for the active note, like, when you'd like to switch context to another note, or you think the current index is corrupted because you switched the embedding provider, etc. - - Reference issue: https://github.com/logancyang/obsidian-copilot/issues/51 +- Please don't forget to switch to "**QA**" in the Mode Selection dropdown in order to start the QA. Copilot does not have your note as context in "Chat" mode. + Settings +- In fact, you don't have to click the button on the right before starting the QA. Switching to QA mode in the dropdown directly is enough for Copilot to read the note as context. The button on the right is only for when you'd like to manually rebuild the index for the active note, like, when you'd like to switch context to another note, or you think the current index is corrupted because you switched the embedding provider, etc. +- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/51
Unresponsive QA when using Huggingface as the Embedding Provider - - Huggingface Inference API is free to use. It can give errors such as 503 or 504 frequently at times because their server has issues. If it's an issue for you, please consider using OpenAI or CohereAI as the embedding provider. Just keep in mind that OpenAI costs more, especially with very long notes as context. +- Huggingface Inference API is free to use. It can give errors such as 503 or 504 frequently at times because their server has issues. If it's an issue for you, please consider using OpenAI or CohereAI as the embedding provider. Just keep in mind that OpenAI costs more, especially with very long notes as context.
"insufficient_quota" - - It might be because you haven't set up payment for your OpenAI account, or you exceeded your max monthly limit. OpenAI has a cap on how much you can use their API, usually $120 for individual users. - - Reference issue: https://github.com/logancyang/obsidian-copilot/issues/11 +- It might be because you haven't set up payment for your OpenAI account, or you exceeded your max monthly limit. OpenAI has a cap on how much you can use their API, usually $120 for individual users. +- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/11
"context_length_exceeded" - - GPT-3.5 has a 4096 context token limit, GPT-4 has 8K (there is a 32K one available to the public soon per OpenAI). **So if you set a big token limit in your Copilot setting, you could get this error.** Note that the prompts behind the scenes for Copilot commands can also take up tokens, so please limit your message length and max tokens to avoid this error. (For QA with Unlimited Context, use the "QA" mode in the dropdown! Requires Copilot v2.1.0.) - - Reference issue: https://github.com/logancyang/obsidian-copilot/issues/1#issuecomment-1542934569 +- GPT-3.5 has a 4096 context token limit, GPT-4 has 8K (there is a 32K one available to the public soon per OpenAI). **So if you set a big token limit in your Copilot setting, you could get this error.** Note that the prompts behind the scenes for Copilot commands can also take up tokens, so please limit your message length and max tokens to avoid this error. (For QA with Unlimited Context, use the "QA" mode in the dropdown! Requires Copilot v2.1.0.) +- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/1#issuecomment-1542934569
Azure issue - - It's a bit tricky to get all Azure credentials right in the first try. My suggestion is to use `curl` to test in your terminal first, make sure it gets response back, and then set the correct params in Copilot settings. Example: - ``` - curl https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/completions?api-version=VERSION\ - -H "Content-Type: application/json" \ - -H "api-key: YOUR_API_KEY" \ - -d "{ - \"prompt\": \"Once upon a time\", - \"max_tokens\": 5 - }" - ``` - - Reference issue: https://github.com/logancyang/obsidian-copilot/issues/98 +- It's a bit tricky to get all Azure credentials right in the first try. My suggestion is to use `curl` to test in your terminal first, make sure it gets response back, and then set the correct params in Copilot settings. Example: + ``` + curl https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/completions?api-version=VERSION\ + -H "Content-Type: application/json" \ + -H "api-key: YOUR_API_KEY" \ + -d "{ + \"prompt\": \"Once upon a time\", + \"max_tokens\": 5 + }" + ``` +- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/98
When opening an issue, please include relevant console logs. You can go to Copilot's settings and turn on "Debug mode" at the bottom for more console messages! ## 📝 Planned features (feedback welcome) + - New modes - **Chat mode** (originally Conversation mode): You can now provide multiple notes at once as context in conversations, for LLMs with an extended context window. - **QA mode**: You can **index any folder** and perform question and answer sessions using a **local** search index and Retrieval-Augmented Generation (RAG) system. @@ -178,6 +183,7 @@ When opening an issue, please include relevant console logs. You can go to Copil - Interact with a **powerful AI agent** that knows your vault who can search, filter and use your notes as context to work with. Explore, brainstorm and research like never before! ## 🙏 Thank You + Did you know that [even the timer on Alexa needs internet access](https://twitter.com/logancyang/status/1720929870635802738)? In this era of corporate-dominated internet, I still believe there's room for powerful tech that's focused on privacy. A great **local** AI agent in Obsidian is the ultimate form of this plugin. If you share my vision, please consider [sponsoring this project](https://github.com/sponsors/logancyang) or buying me coffees! Buy Me A Coffee diff --git a/__mocks__/obsidian.js b/__mocks__/obsidian.js index 4d64b560..9468d7ef 100644 --- a/__mocks__/obsidian.js +++ b/__mocks__/obsidian.js @@ -1,6 +1,6 @@ // __mocks__/obsidian.js /* eslint-disable no-undef */ -import yaml from 'js-yaml'; +import yaml from "js-yaml"; module.exports = { Vault: jest.fn().mockImplementation(() => { @@ -8,19 +8,19 @@ module.exports = { getMarkdownFiles: jest.fn().mockImplementation(() => { // Return an array of mock markdown file objects return [ - { path: 'test/test2/note1.md' }, - { path: 'test/note2.md' }, - { path: 'test2/note3.md' }, - { path: 'note4.md' }, + { path: "test/test2/note1.md" }, + { path: "test/note2.md" }, + { path: "test2/note3.md" }, + { path: "note4.md" }, ]; }), cachedRead: jest.fn().mockImplementation((file) => { // Simulate reading file contents. You can adjust the content as needed for your tests. const fileContents = { - 'test/test2/note1.md': '---\ntags: [Tag1, tag2]\n---\nContent of note1', - 'test/note2.md': '---\ntags: [tag2, tag3]\n---\nContent of note2', - 'test2/note3.md': 'something else ---\ntags: [false_tag]\n---\nContent of note3', - 'note4.md': '---\ntags: [tag1, Tag4]\n---\nContent of note4', + "test/test2/note1.md": "---\ntags: [Tag1, tag2]\n---\nContent of note1", + "test/note2.md": "---\ntags: [tag2, tag3]\n---\nContent of note2", + "test2/note3.md": "something else ---\ntags: [false_tag]\n---\nContent of note3", + "note4.md": "---\ntags: [tag1, Tag4]\n---\nContent of note4", }; return Promise.resolve(fileContents[file.path]); }), @@ -32,4 +32,4 @@ module.exports = { parseYaml: jest.fn().mockImplementation((content) => { return yaml.load(content); }), -}; \ No newline at end of file +}; diff --git a/esbuild.config.mjs b/esbuild.config.mjs index dc22258e..90f3bfe7 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -4,14 +4,13 @@ import svgPlugin from "esbuild-plugin-svg"; import process from "process"; import wasmPlugin from "./wasmPlugin.mjs"; -const banner = -`/* +const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ `; -const prod = (process.argv[2] === "production"); +const prod = process.argv[2] === "production"; const context = await esbuild.context({ banner: { @@ -33,7 +32,8 @@ const context = await esbuild.context({ "@lezer/common", "@lezer/highlight", "@lezer/lr", - ...builtins], + ...builtins, + ], format: "cjs", target: "es2018", logLevel: "info", @@ -48,4 +48,4 @@ if (prod) { process.exit(0); } else { await context.watch(); -} \ No newline at end of file +} diff --git a/jest.config.js b/jest.config.js index c353527b..a39d024d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,17 +1,17 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'jsdom', - roots: ['/src', '/tests'], + preset: "ts-jest", + testEnvironment: "jsdom", + roots: ["/src", "/tests"], transform: { - '^.+\\.(js|jsx|ts|tsx)$': 'ts-jest', + "^.+\\.(js|jsx|ts|tsx)$": "ts-jest", }, moduleNameMapper: { - '\\.(css|less|scss|sass)$': 'identity-obj-proxy', - '^@/(.*)$': '/src/$1', - '^obsidian$': '/__mocks__/obsidian.js' + "\\.(css|less|scss|sass)$": "identity-obj-proxy", + "^@/(.*)$": "/src/$1", + "^obsidian$": "/__mocks__/obsidian.js", }, - testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testPathIgnorePatterns: ['/node_modules/'], - setupFiles: ['/jest.setup.js'], -}; \ No newline at end of file + testRegex: "(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + testPathIgnorePatterns: ["/node_modules/"], + setupFiles: ["/jest.setup.js"], +}; diff --git a/jest.setup.js b/jest.setup.js index 245db08c..3e9b34ab 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1 +1 @@ -import 'web-streams-polyfill/dist/polyfill.min.js'; +import "web-streams-polyfill/dist/polyfill.min.js"; diff --git a/local_copilot.md b/local_copilot.md index d6123bc1..d46ca7bd 100644 --- a/local_copilot.md +++ b/local_copilot.md @@ -5,6 +5,7 @@ [LM Studio](https://lmstudio.ai/) has the best UI for running local models, it has support for Apple Silicon, Windows, and Linux (in beta). After you download the correct version of LM Studio to your machine, the first thing is to download a model. Find something small to start with, such as Mistral 7B, and work your way up if you have a beefy machine. A rule of thumb to determine how large a model you can run: + - If you are on an Apple Silicon Mac, look at your RAM - If you are on a Windows PC with a GPU, look at your VRAM. @@ -77,6 +78,7 @@ ollama serve ``` ## Ollama for Local Embeddings + Ollama has added support for local embeddings for RAG since v0.1.26! It's super easy to setup, just run ``` diff --git a/manifest.json b/manifest.json index 35e8024c..492cdcea 100644 --- a/manifest.json +++ b/manifest.json @@ -1,14 +1,14 @@ { - "id": "copilot", - "name": "Copilot", - "version": "2.5.4", - "minAppVersion": "0.15.0", - "description": "A ChatGPT Copilot in Obsidian.", - "author": "Logan Yang", - "authorUrl": "https://twitter.com/logancyang", - "fundingUrl": { - "Buy Me a Coffee": "https://www.buymeacoffee.com/logancyang", - "GitHub Sponsor": "https://github.com/sponsors/logancyang" - }, - "isDesktopOnly": true -} \ No newline at end of file + "id": "copilot", + "name": "Copilot", + "version": "2.5.4", + "minAppVersion": "0.15.0", + "description": "A ChatGPT Copilot in Obsidian.", + "author": "Logan Yang", + "authorUrl": "https://twitter.com/logancyang", + "fundingUrl": { + "Buy Me a Coffee": "https://www.buymeacoffee.com/logancyang", + "GitHub Sponsor": "https://github.com/sponsors/logancyang" + }, + "isDesktopOnly": true +} diff --git a/package.json b/package.json index 5182eaa7..385c8ed2 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", + "format": "prettier --write 'src/**/*.{js,ts,tsx,md}'", + "format:check": "prettier --check 'src/**/*.{js,ts,tsx,md}'", "version": "node version-bump.mjs && git add manifest.json versions.json", "test": "jest", "prepare": "husky" diff --git a/src/LLMProviders/chainManager.ts b/src/LLMProviders/chainManager.ts index 07b89343..ae36e910 100644 --- a/src/LLMProviders/chainManager.ts +++ b/src/LLMProviders/chainManager.ts @@ -12,11 +12,7 @@ import { getModelName, isSupportedChain, } from "@/utils"; -import VectorDBManager, { - MemoryVector, - NoteFile, - VectorStoreDocument, -} from "@/vectorDBManager"; +import VectorDBManager, { MemoryVector, NoteFile, VectorStoreDocument } from "@/vectorDBManager"; import { ChatOllama } from "@langchain/community/chat_models/ollama"; import { RunnableSequence } from "@langchain/core/runnables"; import { BaseChatMemory } from "langchain/memory"; @@ -64,7 +60,7 @@ export default class ChainManager { encryptionService: EncryptionService, settings: CopilotSettings, // Ensure ChainManager always has the up-to-date dbVectorStores - getDbVectorStores: () => PouchDB.Database, + getDbVectorStores: () => PouchDB.Database ) { // Instantiate singletons this.app = app; @@ -72,10 +68,7 @@ export default class ChainManager { this.settings = settings; this.memoryManager = MemoryManager.getInstance(this.langChainParams); this.encryptionService = encryptionService; - this.chatModelManager = ChatModelManager.getInstance( - this.langChainParams, - encryptionService, - ); + this.chatModelManager = ChatModelManager.getInstance(this.langChainParams, encryptionService); this.promptManager = PromptManager.getInstance(this.langChainParams); this.getDbVectorStores = getDbVectorStores; this.createChainWithNewModel(this.langChainParams.modelDisplayName); @@ -86,8 +79,7 @@ export default class ChainManager { } private validateChainType(chainType: ChainType): void { - if (chainType === undefined || chainType === null) - throw new Error("No chain type set"); + if (chainType === undefined || chainType === null) throw new Error("No chain type set"); } static storeRetrieverDocuments(documents: Document[]) { @@ -102,8 +94,7 @@ export default class ChainManager { * @return {void} */ createChainWithNewModel(newModelDisplayName: string): void { - ChainManager.isOllamaModelActive = - newModelDisplayName === ChatModelDisplayNames.OLLAMA; + ChainManager.isOllamaModelActive = newModelDisplayName === ChatModelDisplayNames.OLLAMA; ChainManager.isOpenRouterModelActive = newModelDisplayName === ChatModelDisplayNames.OPENROUTERAI; // model and model display name must be update at the same time! @@ -150,28 +141,18 @@ export default class ChainManager { } } - async setChain( - chainType: ChainType, - options: SetChainOptions = {}, - ): Promise { - if ( - !this.chatModelManager.validateChatModel( - this.chatModelManager.getChatModel(), - ) - ) { + async setChain(chainType: ChainType, options: SetChainOptions = {}): Promise { + if (!this.chatModelManager.validateChatModel(this.chatModelManager.getChatModel())) { // No need to throw error and trigger multiple Notices to user console.error("setChain failed: No chat model set."); return; } this.validateChainType(chainType); // MUST set embeddingsManager when switching to QA mode - if ( - chainType === ChainType.LONG_NOTE_QA_CHAIN || - chainType === ChainType.VAULT_QA_CHAIN - ) { + if (chainType === ChainType.LONG_NOTE_QA_CHAIN || chainType === ChainType.VAULT_QA_CHAIN) { this.embeddingsManager = EmbeddingsManager.getInstance( this.langChainParams, - this.encryptionService, + this.encryptionService ); } @@ -188,8 +169,7 @@ export default class ChainManager { if (ChainManager.isOllamaModelActive) { (chatModel as ChatOllama).model = this.langChainParams.ollamaModel; } else if (ChainManager.isOpenRouterModelActive) { - (chatModel as ProxyChatOpenAI).modelName = - this.langChainParams.openRouterModel; + (chatModel as ProxyChatOpenAI).modelName = this.langChainParams.openRouterModel; } ChainManager.chain = ChainFactory.createNewLLMChain({ @@ -220,36 +200,30 @@ export default class ChainManager { this.setNoteFile(options.noteFile); const docHash = VectorDBManager.getDocumentHash(options.noteFile.path); const parsedMemoryVectors: MemoryVector[] | undefined = - await VectorDBManager.getMemoryVectors( - this.getDbVectorStores(), - docHash, - ); + await VectorDBManager.getMemoryVectors(this.getDbVectorStores(), docHash); const embeddingsAPI = this.embeddingsManager.getEmbeddingsAPI(); if (!embeddingsAPI) { - console.error( - "Error getting embeddings API. Please check your settings.", - ); + console.error("Error getting embeddings API. Please check your settings."); return; } if (parsedMemoryVectors) { // Index already exists const vectorStore = await VectorDBManager.rebuildMemoryVectorStore( parsedMemoryVectors, - embeddingsAPI, + embeddingsAPI ); // Create new conversational retrieval chain - ChainManager.retrievalChain = - ChainFactory.createConversationalRetrievalChain( - { - llm: chatModel, - retriever: vectorStore.asRetriever(undefined, (doc) => { - return doc.metadata.path === options.noteFile?.path; - }), - }, - ChainManager.storeRetrieverDocuments.bind(ChainManager), - options.debug, - ); + ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain( + { + llm: chatModel, + retriever: vectorStore.asRetriever(undefined, (doc) => { + return doc.metadata.path === options.noteFile?.path; + }), + }, + ChainManager.storeRetrieverDocuments.bind(ChainManager), + options.debug + ); console.log("Existing vector store for document hash: ", docHash); } else { // Index doesn't exist @@ -257,7 +231,7 @@ export default class ChainManager { this.vectorStore = await VectorDBManager.getMemoryVectorStore( this.getDbVectorStores(), embeddingsAPI, - vectorStoreDoc?._id, + vectorStoreDoc?._id ); if (!this.vectorStore) { console.error("Error creating vector store."); @@ -272,19 +246,17 @@ export default class ChainManager { verbose: false, }); - ChainManager.retrievalChain = - ChainFactory.createConversationalRetrievalChain( - { - llm: chatModel, - retriever: retriever, - }, - ChainManager.storeRetrieverDocuments.bind(ChainManager), - options.debug, - ); + ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain( + { + llm: chatModel, + retriever: retriever, + }, + ChainManager.storeRetrieverDocuments.bind(ChainManager), + options.debug + ); console.log( - "New Long Note QA chain with multi-query retriever created for " + - "document hash: ", - docHash, + "New Long Note QA chain with multi-query retriever created for " + "document hash: ", + docHash ); } @@ -296,14 +268,12 @@ export default class ChainManager { case ChainType.VAULT_QA_CHAIN: { const embeddingsAPI = this.embeddingsManager.getEmbeddingsAPI(); if (!embeddingsAPI) { - console.error( - "Error getting embeddings API. Please check your settings.", - ); + console.error("Error getting embeddings API. Please check your settings."); return; } const vectorStore = await VectorDBManager.getMemoryVectorStore( this.getDbVectorStores(), - embeddingsAPI, + embeddingsAPI ); const retriever = new HybridRetriever( this.getDbVectorStores(), @@ -314,22 +284,19 @@ export default class ChainManager { maxK: this.settings.maxSourceChunks, // The maximum number of docs (chunks) to retrieve kIncrement: 2, }, - options.debug, + options.debug ); // Create new conversational retrieval chain - ChainManager.retrievalChain = - ChainFactory.createConversationalRetrievalChain( - { - llm: chatModel, - retriever: retriever, - }, - ChainManager.storeRetrieverDocuments.bind(ChainManager), - options.debug, - ); - console.log( - "New Vault QA chain with hybrid retriever created for entire vault", + ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain( + { + llm: chatModel, + retriever: retriever, + }, + ChainManager.storeRetrieverDocuments.bind(ChainManager), + options.debug ); + console.log("New Vault QA chain with hybrid retriever created for entire vault"); this.langChainParams.chainType = ChainType.VAULT_QA_CHAIN; console.log("Set chain:", ChainType.VAULT_QA_CHAIN); @@ -351,16 +318,12 @@ export default class ChainManager { debug?: boolean; ignoreSystemMessage?: boolean; updateLoading?: (loading: boolean) => void; - } = {}, + } = {} ) { const { debug = false, ignoreSystemMessage = false } = options; // Check if chat model is initialized - if ( - !this.chatModelManager.validateChatModel( - this.chatModelManager.getChatModel(), - ) - ) { + if (!this.chatModelManager.validateChatModel(this.chatModelManager.getChatModel())) { const errorMsg = "Chat model is not initialized properly, check your API key in Copilot setting and make sure you have API access."; new Notice(errorMsg); @@ -371,21 +334,13 @@ export default class ChainManager { if (!ChainManager.chain || !isSupportedChain(ChainManager.chain)) { console.error( "Chain is not initialized properly, re-initializing chain: ", - this.langChainParams.chainType, - ); - this.setChain( - this.langChainParams.chainType, - this.langChainParams.options, + this.langChainParams.chainType ); + this.setChain(this.langChainParams.chainType, this.langChainParams.options); } - const { - temperature, - maxTokens, - systemMessage, - chatContextTurns, - chainType, - } = this.langChainParams; + const { temperature, maxTokens, systemMessage, chatContextTurns, chainType } = + this.langChainParams; const memory = this.memoryManager.getMemory(); const chatPrompt = this.promptManager.getChatPrompt(); @@ -429,7 +384,7 @@ export default class ChainManager { `temperature: ${temperature}\n` + `maxTokens: ${maxTokens}\n` + `system prompt: ${systemPrompt}\n` + - `chat context turns: ${chatContextTurns}\n`, + `chat context turns: ${chatContextTurns}\n` ); console.log("chain RunnableSequence:", ChainManager.chain); console.log("Chat memory:", memory); @@ -452,27 +407,21 @@ export default class ChainManager { `temperature: ${temperature}\n` + `maxTokens: ${maxTokens}\n` + `system prompt: ${systemPrompt}\n` + - `chat context turns: ${chatContextTurns}\n`, + `chat context turns: ${chatContextTurns}\n` ); console.log("chain RunnableSequence:", ChainManager.chain); - console.log( - "embedding model:", - this.langChainParams.embeddingModel, - ); + console.log("embedding model:", this.langChainParams.embeddingModel); } fullAIResponse = await this.runRetrievalChain( userMessage, memory, updateCurrentAiMessage, abortController, - { debug }, + { debug } ); break; default: - console.error( - "Chain type not supported:", - this.langChainParams.chainType, - ); + console.error("Chain type not supported:", this.langChainParams.chainType); } } catch (error) { const errorData = error?.response?.data?.error || error; @@ -489,10 +438,7 @@ export default class ChainManager { } finally { if (fullAIResponse) { // This line is a must for memory to work with RunnableSequence! - await memory.saveContext( - { input: userMessage }, - { output: fullAIResponse }, - ); + await memory.saveContext({ input: userMessage }, { output: fullAIResponse }); addMessage({ message: fullAIResponse, sender: AI_SENDER, @@ -511,7 +457,7 @@ export default class ChainManager { abortController: AbortController, options: { debug?: boolean; - } = {}, + } = {} ): Promise { const memoryVariables = await memory.loadMemoryVariables({}); const chatHistory = extractChatHistory(memoryVariables); @@ -539,15 +485,13 @@ export default class ChainManager { // to show specific chunks in the future. E.g. collapsed note title when clicked, // expand and reveal the chunk if (this.langChainParams.chainType === ChainType.VAULT_QA_CHAIN) { - const docTitles = extractUniqueTitlesFromDocs( - ChainManager.retrievedDocuments, - ); + const docTitles = extractUniqueTitlesFromDocs(ChainManager.retrievedDocuments); const markdownLinks = docTitles .map( (title) => `[${title}](obsidian://open?vault=${encodeURIComponent(this.app.vault.getName())}&file=${encodeURIComponent( - title, - )})`, + title + )})` ) .join("\n"); fullAIResponse += "\n\n**Sources**:\n" + markdownLinks; @@ -556,9 +500,7 @@ export default class ChainManager { return fullAIResponse; } - async indexFile( - noteFile: NoteFile, - ): Promise { + async indexFile(noteFile: NoteFile): Promise { const embeddingsAPI = this.embeddingsManager.getEmbeddingsAPI(); if (!embeddingsAPI) { const errorMsg = @@ -567,10 +509,6 @@ export default class ChainManager { console.error(errorMsg); return; } - return await VectorDBManager.indexFile( - this.getDbVectorStores(), - embeddingsAPI, - noteFile, - ); + return await VectorDBManager.indexFile(this.getDbVectorStores(), embeddingsAPI, noteFile); } } diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts index 9f898c58..029c3630 100644 --- a/src/LLMProviders/chatModelManager.ts +++ b/src/LLMProviders/chatModelManager.ts @@ -37,7 +37,7 @@ export default class ChatModelManager { private constructor( private langChainParams: LangChainParams, - encryptionService: EncryptionService, + encryptionService: EncryptionService ) { this.encryptionService = encryptionService; this.buildModelMap(); @@ -45,20 +45,16 @@ export default class ChatModelManager { static getInstance( langChainParams: LangChainParams, - encryptionService: EncryptionService, + encryptionService: EncryptionService ): ChatModelManager { if (!ChatModelManager.instance) { - ChatModelManager.instance = new ChatModelManager( - langChainParams, - encryptionService, - ); + ChatModelManager.instance = new ChatModelManager(langChainParams, encryptionService); } return ChatModelManager.instance; } private getModelConfig(chatModelProvider: string): ModelConfig { - const decrypt = (key: string) => - this.encryptionService.getDecryptedKey(key); + const decrypt = (key: string) => this.encryptionService.getDecryptedKey(key); const params = this.langChainParams; const baseConfig: ModelConfig = { modelName: params.model, @@ -70,10 +66,7 @@ export default class ChatModelManager { const providerConfig = { [ModelProviders.OPENAI]: { - modelName: - params.openAIProxyModelName || - params.openAICustomModel || - params.model, + modelName: params.openAIProxyModelName || params.openAICustomModel || params.model, openAIApiKey: decrypt(params.openAIApiKey), openAIOrgId: decrypt(params.openAIOrgId), maxTokens: params.maxTokens, @@ -136,9 +129,7 @@ export default class ChatModelManager { ChatModelManager.modelMap = {}; const modelMap = ChatModelManager.modelMap; - const OpenAIChatModel = this.langChainParams.openAIProxyBaseUrl - ? ProxyChatOpenAI - : ChatOpenAI; + const OpenAIChatModel = this.langChainParams.openAIProxyBaseUrl ? ProxyChatOpenAI : ChatOpenAI; const modelConfigurations = [ { diff --git a/src/LLMProviders/embeddingManager.ts b/src/LLMProviders/embeddingManager.ts index 59417d04..250db033 100644 --- a/src/LLMProviders/embeddingManager.ts +++ b/src/LLMProviders/embeddingManager.ts @@ -1,10 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { LangChainParams } from "@/aiParams"; -import { - EMBEDDING_MODEL_TO_PROVIDERS, - ModelProviders, - NOMIC_EMBED_TEXT, -} from "@/constants"; +import { EMBEDDING_MODEL_TO_PROVIDERS, ModelProviders, NOMIC_EMBED_TEXT } from "@/constants"; import EncryptionService from "@/encryptionService"; import { ProxyOpenAIEmbeddings } from "@/langchainWrappers"; import { CohereEmbeddings } from "@langchain/cohere"; @@ -16,18 +12,15 @@ export default class EmbeddingManager { private static instance: EmbeddingManager; private constructor( private langChainParams: LangChainParams, - private encryptionService: EncryptionService, + private encryptionService: EncryptionService ) {} static getInstance( langChainParams: LangChainParams, - encryptionService: EncryptionService, + encryptionService: EncryptionService ): EmbeddingManager { if (!EmbeddingManager.instance) { - EmbeddingManager.instance = new EmbeddingManager( - langChainParams, - encryptionService, - ); + EmbeddingManager.instance = new EmbeddingManager(langChainParams, encryptionService); } return EmbeddingManager.instance; } @@ -40,14 +33,13 @@ export default class EmbeddingManager { return emb.modelName as string; } else { throw new Error( - `Embeddings instance missing model or modelName properties: ${embeddingsInstance}`, + `Embeddings instance missing model or modelName properties: ${embeddingsInstance}` ); } } getOpenAIEmbeddingAPI(): OpenAIEmbeddings | undefined { - const decrypt = (key: string) => - this.encryptionService.getDecryptedKey(key); + const decrypt = (key: string) => this.encryptionService.getDecryptedKey(key); const { openAIApiKey, @@ -78,8 +70,7 @@ export default class EmbeddingManager { } getEmbeddingsAPI(): Embeddings | undefined { - const decrypt = (key: string) => - this.encryptionService.getDecryptedKey(key); + const decrypt = (key: string) => this.encryptionService.getDecryptedKey(key); const { azureOpenAIApiKey, azureOpenAIApiInstanceName, @@ -90,17 +81,14 @@ export default class EmbeddingManager { const OpenAIEmbeddingsAPI = this.getOpenAIEmbeddingAPI(); - const embeddingProvder = - EMBEDDING_MODEL_TO_PROVIDERS[this.langChainParams.embeddingModel]; + const embeddingProvder = EMBEDDING_MODEL_TO_PROVIDERS[this.langChainParams.embeddingModel]; switch (embeddingProvder) { case ModelProviders.OPENAI: if (OpenAIEmbeddingsAPI) { return OpenAIEmbeddingsAPI; } - console.error( - "OpenAI API key is not provided for the embedding model.", - ); + console.error("OpenAI API key is not provided for the embedding model."); break; case ModelProviders.COHEREAI: return new CohereEmbeddings({ @@ -119,9 +107,7 @@ export default class EmbeddingManager { maxConcurrency: 3, }); } - console.error( - "Azure OpenAI API key is not provided for the embedding model.", - ); + console.error("Azure OpenAI API key is not provided for the embedding model."); break; case ModelProviders.OLLAMA: return new OllamaEmbeddings({ @@ -133,14 +119,12 @@ export default class EmbeddingManager { }); default: console.error( - "No embedding provider set or no valid API key provided. Defaulting to OpenAI.", + "No embedding provider set or no valid API key provided. Defaulting to OpenAI." ); return ( OpenAIEmbeddingsAPI || new OpenAIEmbeddings({ - modelName: - openAIEmbeddingProxyModelName || - this.langChainParams.embeddingModel, + modelName: openAIEmbeddingProxyModelName || this.langChainParams.embeddingModel, openAIApiKey: "default-key", maxRetries: 3, maxConcurrency: 3, diff --git a/src/LLMProviders/memoryManager.ts b/src/LLMProviders/memoryManager.ts index 1ab990e1..0e410544 100644 --- a/src/LLMProviders/memoryManager.ts +++ b/src/LLMProviders/memoryManager.ts @@ -1,19 +1,15 @@ -import { LangChainParams } from '@/aiParams'; +import { LangChainParams } from "@/aiParams"; import { BaseChatMemory, BufferWindowMemory } from "langchain/memory"; export default class MemoryManager { private static instance: MemoryManager; private memory: BaseChatMemory; - private constructor( - private langChainParams: LangChainParams - ) { + private constructor(private langChainParams: LangChainParams) { this.initMemory(); } - static getInstance( - langChainParams: LangChainParams - ): MemoryManager { + static getInstance(langChainParams: LangChainParams): MemoryManager { if (!MemoryManager.instance) { MemoryManager.instance = new MemoryManager(langChainParams); } @@ -23,8 +19,8 @@ export default class MemoryManager { private initMemory(): void { this.memory = new BufferWindowMemory({ k: this.langChainParams.chatContextTurns * 2, - memoryKey: 'history', - inputKey: 'input', + memoryKey: "history", + inputKey: "input", returnMessages: true, }); } @@ -34,8 +30,7 @@ export default class MemoryManager { } clearChatMemory(): void { - console.log('clearing chat memory'); + console.log("clearing chat memory"); this.memory.clear(); } - -} \ No newline at end of file +} diff --git a/src/LLMProviders/promptManager.ts b/src/LLMProviders/promptManager.ts index 452eea88..dc627010 100644 --- a/src/LLMProviders/promptManager.ts +++ b/src/LLMProviders/promptManager.ts @@ -1,4 +1,4 @@ -import { LangChainParams } from '@/aiParams'; +import { LangChainParams } from "@/aiParams"; import { ChatPromptTemplate, HumanMessagePromptTemplate, @@ -10,15 +10,11 @@ export default class PromptManager { private static instance: PromptManager; private chatPrompt: ChatPromptTemplate; - private constructor( - private langChainParams: LangChainParams - ) { + private constructor(private langChainParams: LangChainParams) { this.initChatPrompt(); } - static getInstance( - langChainParams: LangChainParams - ): PromptManager { + static getInstance(langChainParams: LangChainParams): PromptManager { if (!PromptManager.instance) { PromptManager.instance = new PromptManager(langChainParams); } @@ -27,9 +23,7 @@ export default class PromptManager { private initChatPrompt(): void { this.chatPrompt = ChatPromptTemplate.fromMessages([ - SystemMessagePromptTemplate.fromTemplate( - this.langChainParams.systemMessage - ), + SystemMessagePromptTemplate.fromTemplate(this.langChainParams.systemMessage), new MessagesPlaceholder("history"), HumanMessagePromptTemplate.fromTemplate("{input}"), ]); @@ -38,4 +32,4 @@ export default class PromptManager { getChatPrompt(): ChatPromptTemplate { return this.chatPrompt; } -} \ No newline at end of file +} diff --git a/src/aiState.ts b/src/aiState.ts index ae8b996d..4dedaa03 100644 --- a/src/aiState.ts +++ b/src/aiState.ts @@ -1,14 +1,14 @@ -import ChainManager from '@/LLMProviders/chainManager'; -import { SetChainOptions } from '@/aiParams'; -import { ChainType } from '@/chainFactory'; +import ChainManager from "@/LLMProviders/chainManager"; +import { SetChainOptions } from "@/aiParams"; +import { ChainType } from "@/chainFactory"; import { BaseChatMemory } from "langchain/memory"; -import { useState } from 'react'; +import { useState } from "react"; /** * React hook to manage state related to model, chain and memory in Chat component. -*/ + */ export function useAIState( - chainManager: ChainManager, + chainManager: ChainManager ): [ string, (model: string) => void, @@ -36,11 +36,5 @@ export function useAIState( setCurrentChain(newChain); }; - return [ - currentModel, - setModel, - currentChain, - setChain, - clearChatMemory, - ]; + return [currentModel, setModel, currentChain, setChain, clearChatMemory]; } diff --git a/src/chainFactory.ts b/src/chainFactory.ts index 196850e5..c0f76510 100644 --- a/src/chainFactory.ts +++ b/src/chainFactory.ts @@ -1,9 +1,6 @@ import { StringOutputParser } from "@langchain/core/output_parsers"; import { BaseRetriever } from "@langchain/core/retrievers"; -import { - RunnablePassthrough, - RunnableSequence, -} from "@langchain/core/runnables"; +import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables"; import { BaseLanguageModel } from "langchain/base_language"; import { BaseChatMemory } from "langchain/memory"; import { ChatPromptTemplate, PromptTemplate } from "langchain/prompts"; @@ -129,7 +126,7 @@ class ChainFactory { public static createConversationalRetrievalChain( args: ConversationalRetrievalChainParams, onDocumentsRetrieved: (documents: Document[]) => void, - debug?: boolean, + debug?: boolean ): RunnableSequence { const { llm, retriever } = args; @@ -144,9 +141,7 @@ class ChainFactory { {chat_history} Follow Up Input: {question} Standalone question:`; - const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate( - condenseQuestionTemplate, - ); + const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate(condenseQuestionTemplate); const answerTemplate = `Answer the question with as detailed as possible based only on the following context: {context} @@ -157,8 +152,7 @@ class ChainFactory { const formatChatHistory = (chatHistory: [string, string][]) => { const formattedDialogueTurns = chatHistory.map( - (dialogueTurn) => - `Human: ${dialogueTurn[0]}\nAssistant: ${dialogueTurn[1]}`, + (dialogueTurn) => `Human: ${dialogueTurn[0]}\nAssistant: ${dialogueTurn[1]}` ); return formattedDialogueTurns.join("\n"); }; @@ -171,8 +165,7 @@ class ChainFactory { }, chat_history: (input: ConversationalRetrievalQAChainInput) => { const formattedChatHistory = formatChatHistory(input.chat_history); - if (debug) - console.log("Formatted Chat History: ", formattedChatHistory); + if (debug) console.log("Formatted Chat History: ", formattedChatHistory); return formattedChatHistory; }, }, @@ -196,8 +189,7 @@ class ChainFactory { llm, ]); - const conversationalRetrievalQAChain = - standaloneQuestionChain.pipe(answerChain); + const conversationalRetrievalQAChain = standaloneQuestionChain.pipe(answerChain); return conversationalRetrievalQAChain as RunnableSequence; } } diff --git a/src/commands.ts b/src/commands.ts index 1a6e114f..461b551b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -5,10 +5,7 @@ import { Editor, Notice } from "obsidian"; import { COMMAND_IDS } from "./constants"; export function registerBuiltInCommands(plugin: CopilotPlugin) { - const addCommandIfEnabled = ( - id: string, - callback: (editor: Editor) => void, - ) => { + const addCommandIfEnabled = (id: string, callback: (editor: Editor) => void) => { const commandSettings = plugin.settings.enabledCommands[id]; if (commandSettings && commandSettings.enabled) { plugin.addCommand({ diff --git a/src/components/AddPromptModal.tsx b/src/components/AddPromptModal.tsx index 74eee2a1..b8fe6c8e 100644 --- a/src/components/AddPromptModal.tsx +++ b/src/components/AddPromptModal.tsx @@ -1,121 +1,101 @@ import { App, Modal, Notice } from "obsidian"; - export class AddPromptModal extends Modal { constructor( app: App, onSave: (title: string, prompt: string) => void, - initialTitle = '', - initialPrompt = '', - disabledTitle?: boolean, + initialTitle = "", + initialPrompt = "", + disabledTitle?: boolean ) { super(app); - this.contentEl.createEl('h2', { text: 'User Custom Prompt' }); + this.contentEl.createEl("h2", { text: "User Custom Prompt" }); - const formContainer = this.contentEl.createEl('div', { cls: 'copilot-command-modal' }); + const formContainer = this.contentEl.createEl("div", { cls: "copilot-command-modal" }); - const titleContainer = formContainer.createEl( - 'div', - { cls: 'copilot-command-input-container' } - ); + const titleContainer = formContainer.createEl("div", { + cls: "copilot-command-input-container", + }); - titleContainer.createEl( - 'h3', { text: 'Title', cls: 'copilot-command-header' } - ); - titleContainer.createEl( - 'p', - { - text: 'The title of the prompt, must be unique.', - cls: 'copilot-command-input-description', - } - ); + titleContainer.createEl("h3", { text: "Title", cls: "copilot-command-header" }); + titleContainer.createEl("p", { + text: "The title of the prompt, must be unique.", + cls: "copilot-command-input-description", + }); - const titleField = titleContainer.createEl('input', { type: 'text' }); + const titleField = titleContainer.createEl("input", { type: "text" }); if (disabledTitle) { - titleField.setAttribute('disabled', 'true'); + titleField.setAttribute("disabled", "true"); } if (initialTitle) { titleField.value = initialTitle; } - const promptContainer = formContainer.createEl( - 'div', - { cls: 'copilot-command-input-container' } - ); + const promptContainer = formContainer.createEl("div", { + cls: "copilot-command-input-container", + }); - promptContainer.createEl( - 'h3', { text: 'Prompt', cls: 'copilot-command-header' } - ); + promptContainer.createEl("h3", { text: "Prompt", cls: "copilot-command-header" }); const promptDescFragment = createFragment((frag) => { - frag.createEl( - 'strong', - { text: '- {} represents the selected text (not required). ' } - ); - frag.createEl('br'); - frag.createEl( - 'strong', - { text: '- {[[Note Title]]} represents a note. ' } - ); - frag.createEl('br'); - frag.createEl( - 'strong', - { text: '- {FolderPath} represents a folder of notes. ' } - ); - frag.createEl('br'); - frag.createEl( - 'strong', - { text: '- {#tag1, #tag2} represents ALL notes with ANY of the specified tags in their property (an OR operation). ' } - ); - frag.createEl('br'); - frag.createEl('br'); - frag.appendText('Tip: turn on debug mode to show the processed prompt in the chat window.'); - frag.createEl('br'); - frag.createEl('br'); + frag.createEl("strong", { text: "- {} represents the selected text (not required). " }); + frag.createEl("br"); + frag.createEl("strong", { text: "- {[[Note Title]]} represents a note. " }); + frag.createEl("br"); + frag.createEl("strong", { text: "- {FolderPath} represents a folder of notes. " }); + frag.createEl("br"); + frag.createEl("strong", { + text: "- {#tag1, #tag2} represents ALL notes with ANY of the specified tags in their property (an OR operation). ", + }); + frag.createEl("br"); + frag.createEl("br"); + frag.appendText("Tip: turn on debug mode to show the processed prompt in the chat window."); + frag.createEl("br"); + frag.createEl("br"); }); promptContainer.appendChild(promptDescFragment); - const promptField = promptContainer.createEl('textarea'); + const promptField = promptContainer.createEl("textarea"); if (initialPrompt) { promptField.value = initialPrompt; } const descFragment = createFragment((frag) => { - frag.appendText('Save the prompt to the local prompt library. You can then use it with the Copilot command: '); - frag.createEl( - 'strong', - { text: 'Apply custom prompt to selection.' } + frag.appendText( + "Save the prompt to the local prompt library. You can then use it with the Copilot command: " ); - frag.createEl('br'); - frag.appendText('Check out the '); - frag.createEl('a', { - href: 'https://github.com/f/awesome-chatgpt-prompts', - text: 'awesome chatGPT prompts', - }).setAttr('target', '_blank'); - frag.appendText(' for inspiration.'); + frag.createEl("strong", { text: "Apply custom prompt to selection." }); + frag.createEl("br"); + frag.appendText("Check out the "); + frag + .createEl("a", { + href: "https://github.com/f/awesome-chatgpt-prompts", + text: "awesome chatGPT prompts", + }) + .setAttr("target", "_blank"); + frag.appendText(" for inspiration."); }); - const descContainer = promptContainer.createEl('p', { - cls: 'copilot-command-input-description', + const descContainer = promptContainer.createEl("p", { + cls: "copilot-command-input-description", }); descContainer.appendChild(descFragment); - const saveButtonContainer = formContainer.createEl( - 'div', - { cls: 'copilot-command-save-btn-container' } - ); - const saveButton = saveButtonContainer.createEl( - 'button', - { text: 'Save', cls: 'copilot-command-save-btn' } - ); - saveButton.addEventListener('click', () => { + const saveButtonContainer = formContainer.createEl("div", { + cls: "copilot-command-save-btn-container", + }); + const saveButton = saveButtonContainer.createEl("button", { + text: "Save", + cls: "copilot-command-save-btn", + }); + saveButton.addEventListener("click", () => { if (titleField.value && promptField.value) { onSave(titleField.value, promptField.value); this.close(); } else { - new Notice('Please fill in both fields: Title and Prompt.'); + new Notice("Please fill in both fields: Title and Prompt."); } }); } diff --git a/src/components/AdhocPromptModal.tsx b/src/components/AdhocPromptModal.tsx index e1ac248a..d0ec2c60 100644 --- a/src/components/AdhocPromptModal.tsx +++ b/src/components/AdhocPromptModal.tsx @@ -1,68 +1,60 @@ -import { App, Modal } from 'obsidian'; +import { App, Modal } from "obsidian"; export class AdhocPromptModal extends Modal { - result: string; - onSubmit: (result: string) => void; + result: string; + onSubmit: (result: string) => void; - private placeholderText = 'Please enter your custom ad-hoc prompt here, press enter to send.'; + private placeholderText = "Please enter your custom ad-hoc prompt here, press enter to send."; - constructor(app: App, onSubmit: (result: string) => void) { - super(app); - this.onSubmit = onSubmit; - } + constructor(app: App, onSubmit: (result: string) => void) { + super(app); + this.onSubmit = onSubmit; + } - onOpen() { - const { contentEl } = this; + onOpen() { + const { contentEl } = this; - const promptDescFragment = createFragment((frag) => { - frag.createEl( - 'strong', - { text: '- {} represents the selected text (not required). ' } - ); - frag.createEl('br'); - frag.createEl( - 'strong', - { text: '- {[[Note Title]]} represents a note. ' } - ); - frag.createEl('br'); - frag.createEl( - 'strong', - { text: '- {FolderPath} represents a folder of notes. ' } - ); - frag.createEl('br'); - frag.createEl( - 'strong', - { text: '- {#tag1, #tag2} represents ALL notes with ANY of the specified tags in their property (an OR operation). ' } - ); - frag.createEl('br'); - frag.createEl('br'); - frag.appendText('Tip: turn on debug mode to show the processed prompt in the chat window.'); - frag.createEl('br'); - frag.createEl('br'); - }); - contentEl.appendChild(promptDescFragment); + const promptDescFragment = createFragment((frag) => { + frag.createEl("strong", { text: "- {} represents the selected text (not required). " }); + frag.createEl("br"); + frag.createEl("strong", { text: "- {[[Note Title]]} represents a note. " }); + frag.createEl("br"); + frag.createEl("strong", { text: "- {FolderPath} represents a folder of notes. " }); + frag.createEl("br"); + frag.createEl("strong", { + text: "- {#tag1, #tag2} represents ALL notes with ANY of the specified tags in their property (an OR operation). ", + }); + frag.createEl("br"); + frag.createEl("br"); + frag.appendText("Tip: turn on debug mode to show the processed prompt in the chat window."); + frag.createEl("br"); + frag.createEl("br"); + }); + contentEl.appendChild(promptDescFragment); - const textareaEl = contentEl.createEl('textarea', { attr: { placeholder: this.placeholderText } }); - textareaEl.style.width = '100%'; - textareaEl.style.height = '100px'; // Set the desired height - textareaEl.style.padding = '10px'; - textareaEl.style.resize = 'vertical'; // Allow vertical resizing + const textareaEl = contentEl.createEl("textarea", { + attr: { placeholder: this.placeholderText }, + }); + textareaEl.style.width = "100%"; + textareaEl.style.height = "100px"; // Set the desired height + textareaEl.style.padding = "10px"; + textareaEl.style.resize = "vertical"; // Allow vertical resizing - textareaEl.addEventListener('input', (evt) => { - this.result = (evt.target as HTMLTextAreaElement).value; - }); + textareaEl.addEventListener("input", (evt) => { + this.result = (evt.target as HTMLTextAreaElement).value; + }); - textareaEl.addEventListener('keydown', (evt) => { - if (evt.key === 'Enter' && !evt.shiftKey) { - evt.preventDefault(); // Prevent line break unless Shift key is pressed - this.close(); - this.onSubmit(this.result); - } - }); - } + textareaEl.addEventListener("keydown", (evt) => { + if (evt.key === "Enter" && !evt.shiftKey) { + evt.preventDefault(); // Prevent line break unless Shift key is pressed + this.close(); + this.onSubmit(this.result); + } + }); + } - onClose() { - const { contentEl } = this; - contentEl.empty(); - } + onClose() { + const { contentEl } = this; + contentEl.empty(); + } } diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 68100e87..f2feac4d 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -1,18 +1,16 @@ -import ChainManager from '@/LLMProviders/chainManager'; -import { useAIState } from '@/aiState'; -import { ChainType } from '@/chainFactory'; -import ChatIcons from '@/components/ChatComponents/ChatIcons'; -import ChatInput from '@/components/ChatComponents/ChatInput'; -import ChatMessages from '@/components/ChatComponents/ChatMessages'; -import { AI_SENDER, USER_SENDER } from '@/constants'; -import { AppContext } from '@/context'; -import { CustomPromptProcessor } from '@/customPromptProcessor'; -import { getAIResponse } from '@/langchainStream'; -import CopilotPlugin from '@/main'; -import { CopilotSettings } from '@/settings/SettingsPage'; -import SharedState, { - ChatMessage, useSharedState, -} from '@/sharedState'; +import ChainManager from "@/LLMProviders/chainManager"; +import { useAIState } from "@/aiState"; +import { ChainType } from "@/chainFactory"; +import ChatIcons from "@/components/ChatComponents/ChatIcons"; +import ChatInput from "@/components/ChatComponents/ChatInput"; +import ChatMessages from "@/components/ChatComponents/ChatMessages"; +import { AI_SENDER, USER_SENDER } from "@/constants"; +import { AppContext } from "@/context"; +import { CustomPromptProcessor } from "@/customPromptProcessor"; +import { getAIResponse } from "@/langchainStream"; +import CopilotPlugin from "@/main"; +import { CopilotSettings } from "@/settings/SettingsPage"; +import SharedState, { ChatMessage, useSharedState } from "@/sharedState"; import { createChangeToneSelectionPrompt, createTranslateSelectionPrompt, @@ -39,14 +37,10 @@ import { simplifyPrompt, summarizePrompt, tocPrompt, -} from '@/utils'; -import { EventEmitter } from 'events'; -import { Notice, TFile } from 'obsidian'; -import React, { - useContext, - useEffect, - useState, -} from 'react'; +} from "@/utils"; +import { EventEmitter } from "events"; +import { Notice, TFile } from "obsidian"; +import React, { useContext, useEffect, useState } from "react"; interface CreateEffectOptions { custom_temperature?: number; @@ -73,16 +67,13 @@ const Chat: React.FC = ({ getChatVisibility, defaultSaveFolder, plugin, - debug + debug, }) => { - const [ - chatHistory, addMessage, clearMessages, - ] = useSharedState(sharedState); - const [ - currentModel, setModel, currentChain, setChain, clearChatMemory, - ] = useAIState(chainManager); - const [currentAiMessage, setCurrentAiMessage] = useState(''); - const [inputMessage, setInputMessage] = useState(''); + const [chatHistory, addMessage, clearMessages] = useSharedState(sharedState); + const [currentModel, setModel, currentChain, setChain, clearChatMemory] = + useAIState(chainManager); + const [currentAiMessage, setCurrentAiMessage] = useState(""); + const [inputMessage, setInputMessage] = useState(""); const [abortController, setAbortController] = useState(null); const [loading, setLoading] = useState(false); @@ -121,7 +112,7 @@ const Chat: React.FC = ({ addMessage(userMessage); addMessage(promptMessageHidden); // Clear input - setInputMessage(''); + setInputMessage(""); // Display running dots to indicate loading setLoading(true); @@ -131,14 +122,14 @@ const Chat: React.FC = ({ addMessage, setCurrentAiMessage, setAbortController, - { debug }, + { debug } ); setLoading(false); }; const handleKeyDown = (event: React.KeyboardEvent) => { if (event.nativeEvent.isComposing) return; - if (event.key === 'Enter' && !event.shiftKey) { + if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); // Prevents adding a newline to the textarea handleSendMessage(); } @@ -146,16 +137,16 @@ const Chat: React.FC = ({ const handleSaveAsNote = async () => { if (!app) { - console.error('App instance is not available.'); + console.error("App instance is not available."); return; } // Save the chat history as a new note in the vault // Only visible messages are included const chatContent = chatHistory - .filter(message => message.isVisible) + .filter((message) => message.isVisible) .map((message) => `**${message.sender}**: ${message.message}`) - .join('\n\n'); + .join("\n\n"); try { // Check if the default folder exists or create it @@ -170,20 +161,20 @@ const Chat: React.FC = ({ const leaf = app.workspace.getLeaf(); leaf.openFile(newNote); } catch (error) { - console.error('Error saving chat as note:', error); + console.error("Error saving chat as note:", error); } }; const handleSendActiveNoteToPrompt = async () => { if (!app) { - console.error('App instance is not available.'); + console.error("App instance is not available."); return; } let noteFiles: TFile[] = []; if (debug) { - console.log('Chat note context path:', settings.chatNoteContextPath); - console.log('Chat note context tags:', settings.chatNoteContextTags); + console.log("Chat note context path:", settings.chatNoteContextPath); + console.log("Chat note context tags:", settings.chatNoteContextTags); } if (settings.chatNoteContextPath) { // Recursively get all note TFiles in the path @@ -199,11 +190,11 @@ const Chat: React.FC = ({ // If no note context provided, default to the active note if (noteFiles.length === 0) { if (!file) { - new Notice('No active note found.'); - console.error('No active note found.'); + new Notice("No active note found."); + console.error("No active note found."); return; } - new Notice('No valid Chat context provided. Defaulting to the active note.'); + new Notice("No valid Chat context provided. Defaulting to the active note."); noteFiles = [file]; } @@ -229,7 +220,7 @@ const Chat: React.FC = ({ const sendNoteContentUserMessage = getSendChatContextNotesPrompt( notes, settings.chatNoteContextPath, - settings.chatNoteContextTags, + settings.chatNoteContextTags ); const promptMessageVisible: ChatMessage = { message: sendNoteContentUserMessage, @@ -247,32 +238,32 @@ const Chat: React.FC = ({ addMessage, setCurrentAiMessage, setAbortController, - { debug }, + { debug } ); setLoading(false); }; const forceRebuildActiveNoteContext = async () => { if (!app) { - console.error('App instance is not available.'); + console.error("App instance is not available."); return; } const file = app.workspace.getActiveFile(); if (!file) { - new Notice('No active note found.'); - console.error('No active note found.'); + new Notice("No active note found."); + console.error("No active note found."); return; } const noteContent = await getFileContent(file, app.vault); const noteName = getFileName(file); if (!noteContent) { - new Notice('No note content found.'); - console.error('No note content found.'); + new Notice("No note content found."); + console.error("No note content found."); return; } - const fileMetadata = app.metadataCache.getFileCache(file) + const fileMetadata = app.metadataCache.getFileCache(file); const noteFile = { path: file.path, basename: file.basename, @@ -296,16 +287,16 @@ const Chat: React.FC = ({ const refreshVaultContext = async () => { if (!app) { - console.error('App instance is not available.'); + console.error("App instance is not available."); return; } await plugin.indexVaultToVectorStore(); - new Notice('Vault index refreshed.'); - } + new Notice("Vault index refreshed."); + }; const clearCurrentAiMessage = () => { - setCurrentAiMessage(''); + setCurrentAiMessage(""); }; const handleStopGenerating = () => { @@ -317,7 +308,7 @@ const Chat: React.FC = ({ useEffect(() => { async function handleSelection(selectedText: string) { - const wordCount = selectedText.split(' ').length; + const wordCount = selectedText.split(" ").length; const tokenCount = await chainManager.chatModelManager.countTokens(selectedText); const tokenCountMessage: ChatMessage = { sender: AI_SENDER, @@ -327,11 +318,11 @@ const Chat: React.FC = ({ addMessage(tokenCountMessage); } - emitter.on('countTokensSelection', handleSelection); + emitter.on("countTokensSelection", handleSelection); // Cleanup function to remove the event listener when the component unmounts return () => { - emitter.removeListener('countTokensSelection', handleSelection); + emitter.removeListener("countTokensSelection", handleSelection); }; }, []); @@ -339,13 +330,13 @@ const Chat: React.FC = ({ const createEffect = ( eventType: string, promptFn: (selectedText: string, eventSubtype?: string) => string | Promise, - options: CreateEffectOptions = {}, + options: CreateEffectOptions = {} ) => { return () => { const { custom_temperature, isVisible = false, - ignoreSystemMessage = true, // Ignore system message by default for commands + ignoreSystemMessage = true, // Ignore system message by default for commands } = options; const handleSelection = async (selectedText: string, eventSubtype?: string) => { const messageWithPrompt = await promptFn(selectedText, eventSubtype); @@ -390,37 +381,35 @@ const Chat: React.FC = ({ }; }; - useEffect(createEffect('fixGrammarSpellingSelection', fixGrammarSpellingSelectionPrompt), []); - useEffect(createEffect('summarizeSelection', summarizePrompt), []); - useEffect(createEffect('tocSelection', tocPrompt), []); - useEffect(createEffect('glossarySelection', glossaryPrompt), []); - useEffect(createEffect('simplifySelection', simplifyPrompt), []); - useEffect(createEffect('emojifySelection', emojifyPrompt), []); - useEffect(createEffect('removeUrlsFromSelection', removeUrlsFromSelectionPrompt), []); + useEffect(createEffect("fixGrammarSpellingSelection", fixGrammarSpellingSelectionPrompt), []); + useEffect(createEffect("summarizeSelection", summarizePrompt), []); + useEffect(createEffect("tocSelection", tocPrompt), []); + useEffect(createEffect("glossarySelection", glossaryPrompt), []); + useEffect(createEffect("simplifySelection", simplifyPrompt), []); + useEffect(createEffect("emojifySelection", emojifyPrompt), []); + useEffect(createEffect("removeUrlsFromSelection", removeUrlsFromSelectionPrompt), []); useEffect( - createEffect( - 'rewriteTweetSelection', rewriteTweetSelectionPrompt, { custom_temperature: 0.2 }, - ), + createEffect("rewriteTweetSelection", rewriteTweetSelectionPrompt, { custom_temperature: 0.2 }), [] ); useEffect( - createEffect( - 'rewriteTweetThreadSelection', rewriteTweetThreadSelectionPrompt, { custom_temperature: 0.2 }, - ), + createEffect("rewriteTweetThreadSelection", rewriteTweetThreadSelectionPrompt, { + custom_temperature: 0.2, + }), [] ); - useEffect(createEffect('rewriteShorterSelection', rewriteShorterSelectionPrompt), []); - useEffect(createEffect('rewriteLongerSelection', rewriteLongerSelectionPrompt), []); - useEffect(createEffect('eli5Selection', eli5SelectionPrompt), []); - useEffect(createEffect('rewritePressReleaseSelection', rewritePressReleaseSelectionPrompt), []); + useEffect(createEffect("rewriteShorterSelection", rewriteShorterSelectionPrompt), []); + useEffect(createEffect("rewriteLongerSelection", rewriteLongerSelectionPrompt), []); + useEffect(createEffect("eli5Selection", eli5SelectionPrompt), []); + useEffect(createEffect("rewritePressReleaseSelection", rewritePressReleaseSelectionPrompt), []); useEffect( - createEffect('translateSelection', (selectedText, language) => + createEffect("translateSelection", (selectedText, language) => createTranslateSelectionPrompt(language)(selectedText) ), [] ); useEffect( - createEffect('changeToneSelection', (selectedText, tone) => + createEffect("changeToneSelection", (selectedText, tone) => createChangeToneSelectionPrompt(tone)(selectedText) ), [] @@ -429,33 +418,32 @@ const Chat: React.FC = ({ const customPromptProcessor = CustomPromptProcessor.getInstance(app.vault); useEffect( createEffect( - 'applyCustomPrompt', + "applyCustomPrompt", async (selectedText, customPrompt) => { if (!customPrompt) { return selectedText; } return await customPromptProcessor.processCustomPrompt(customPrompt, selectedText); }, - { isVisible: debug, ignoreSystemMessage: true, custom_temperature: 0.1 }, + { isVisible: debug, ignoreSystemMessage: true, custom_temperature: 0.1 } ), [] ); useEffect( createEffect( - 'applyAdhocPrompt', + "applyAdhocPrompt", async (selectedText, customPrompt) => { if (!customPrompt) { return selectedText; } return await customPromptProcessor.processCustomPrompt(customPrompt, selectedText); }, - { isVisible: debug, ignoreSystemMessage: true, custom_temperature: 0.1 }, + { isVisible: debug, ignoreSystemMessage: true, custom_temperature: 0.1 } ), [] ); - return (
= ({ currentAiMessage={currentAiMessage} loading={loading} /> -
+
{ - clearMessages(); - clearChatMemory(); - clearCurrentAiMessage(); - } - } + onNewChat={() => { + clearMessages(); + clearChatMemory(); + clearCurrentAiMessage(); + }} onSaveAsNote={handleSaveAsNote} onSendActiveNoteToPrompt={handleSendActiveNoteToPrompt} onForceRebuildActiveNoteContext={forceRebuildActiveNoteContext} diff --git a/src/components/ChatComponents/ChatIcons.tsx b/src/components/ChatComponents/ChatIcons.tsx index 5a62a461..d1ac1dcb 100644 --- a/src/components/ChatComponents/ChatIcons.tsx +++ b/src/components/ChatComponents/ChatIcons.tsx @@ -1,9 +1,5 @@ import { SetChainOptions } from "@/aiParams"; -import { - AI_SENDER, - ChatModelDisplayNames, - VAULT_VECTOR_STORE_STRATEGY, -} from "@/constants"; +import { AI_SENDER, ChatModelDisplayNames, VAULT_VECTOR_STORE_STRATEGY } from "@/constants"; import { ProxyServer } from "@/proxyServer"; import { ChatMessage } from "@/sharedState"; import { getFileContent, getFileName } from "@/utils"; @@ -64,7 +60,10 @@ const ChatIcons: React.FC = ({ // Start proxy server based on the selected model & settings const proxyServerURL = proxyServer.getProxyURL(selectedModel); if (proxyServerURL) { - await proxyServer.startProxyServer(proxyServerURL, selectedModel !== ChatModelDisplayNames.CLAUDE); + await proxyServer.startProxyServer( + proxyServerURL, + selectedModel !== ChatModelDisplayNames.CLAUDE + ); } else { await proxyServer.stopProxyServer(); } @@ -72,7 +71,10 @@ const ChatIcons: React.FC = ({ useEffect(() => { const startProxyServerForClaude = async (proxyServerURL: string) => { - await proxyServer.startProxyServer(proxyServerURL, currentModel !== ChatModelDisplayNames.CLAUDE); + await proxyServer.startProxyServer( + proxyServerURL, + currentModel !== ChatModelDisplayNames.CLAUDE + ); }; // Call the function on component mount @@ -87,9 +89,7 @@ const ChatIcons: React.FC = ({ }; }, []); - const handleChainChange = async ( - event: React.ChangeEvent - ) => { + const handleChainChange = async (event: React.ChangeEvent) => { setSelectedChain(stringToChainType(event.target.value)); }; @@ -158,12 +158,8 @@ const ChatIcons: React.FC = ({ value={currentModel} onChange={handleModelChange} > - - + + @@ -176,9 +172,7 @@ const ChatIcons: React.FC = ({ - + @@ -188,15 +182,11 @@ const ChatIcons: React.FC = ({ - + - + Model Selection
@@ -247,10 +237,7 @@ const ChatIcons: React.FC = ({ )} {selectedChain === "long_note_qa" && ( -