mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
Format code (#521)
This commit is contained in:
parent
0191f66e8d
commit
321193993c
48 changed files with 1059 additions and 1416 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -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"]
|
||||
|
|
|
|||
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -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)**
|
||||
|
|
|
|||
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -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.**
|
||||
|
|
|
|||
36
README.md
36
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# 🔍 Copilot for Obsidian
|
||||
 
|
||||
|
||||
 
|
||||
|
||||
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:
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=jRCDAg2sck8" target="_blank"><img src="./images/thumbnail.png" width="700" /></a>
|
||||
|
|
@ -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,39 +127,39 @@ Now you can see the chat icon in your leftside ribbon, clicking on it will open
|
|||
<details>
|
||||
<summary>"You do not have access to this model"</summary>
|
||||
|
||||
- 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
|
||||
</details>
|
||||
<details>
|
||||
<summary>It's not using my note as context</summary>
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
<img src="./images/faq-mode-switch.png" alt="Settings" width="500">
|
||||
- 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
|
||||
- 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
|
||||
</details>
|
||||
<details>
|
||||
<summary>Unresponsive QA when using Huggingface as the Embedding Provider</summary>
|
||||
|
||||
- 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.
|
||||
</details>
|
||||
<details>
|
||||
<summary>"insufficient_quota"</summary>
|
||||
|
||||
- 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
|
||||
</details>
|
||||
<details>
|
||||
<summary>"context_length_exceeded"</summary>
|
||||
|
||||
- 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
|
||||
</details>
|
||||
<details>
|
||||
<summary>Azure issue</summary>
|
||||
|
||||
- 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:
|
||||
- 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" \
|
||||
|
|
@ -165,12 +169,13 @@ Now you can see the chat icon in your leftside ribbon, clicking on it will open
|
|||
\"max_tokens\": 5
|
||||
}"
|
||||
```
|
||||
- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/98
|
||||
- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/98
|
||||
</details>
|
||||
|
||||
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!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/logancyang" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 150px !important;" ></a>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
roots: ['<rootDir>/src', '<rootDir>/tests'],
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
roots: ["<rootDir>/src", "<rootDir>/tests"],
|
||||
transform: {
|
||||
'^.+\\.(js|jsx|ts|tsx)$': 'ts-jest',
|
||||
"^.+\\.(js|jsx|ts|tsx)$": "ts-jest",
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^obsidian$': '<rootDir>/__mocks__/obsidian.js'
|
||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
"^obsidian$": "<rootDir>/__mocks__/obsidian.js",
|
||||
},
|
||||
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
testRegex: "(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||
testPathIgnorePatterns: ["/node_modules/"],
|
||||
setupFiles: ["<rootDir>/jest.setup.js"],
|
||||
};
|
||||
|
|
@ -1 +1 @@
|
|||
import 'web-streams-polyfill/dist/polyfill.min.js';
|
||||
import "web-streams-polyfill/dist/polyfill.min.js";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<VectorStoreDocument>,
|
||||
getDbVectorStores: () => PouchDB.Database<VectorStoreDocument>
|
||||
) {
|
||||
// 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<void> {
|
||||
if (
|
||||
!this.chatModelManager.validateChatModel(
|
||||
this.chatModelManager.getChatModel(),
|
||||
)
|
||||
) {
|
||||
async setChain(chainType: ChainType, options: SetChainOptions = {}): Promise<void> {
|
||||
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,27 +200,21 @@ 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(
|
||||
ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain(
|
||||
{
|
||||
llm: chatModel,
|
||||
retriever: vectorStore.asRetriever(undefined, (doc) => {
|
||||
|
|
@ -248,7 +222,7 @@ export default class ChainManager {
|
|||
}),
|
||||
},
|
||||
ChainManager.storeRetrieverDocuments.bind(ChainManager),
|
||||
options.debug,
|
||||
options.debug
|
||||
);
|
||||
console.log("Existing vector store for document hash: ", docHash);
|
||||
} else {
|
||||
|
|
@ -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(
|
||||
ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain(
|
||||
{
|
||||
llm: chatModel,
|
||||
retriever: retriever,
|
||||
},
|
||||
ChainManager.storeRetrieverDocuments.bind(ChainManager),
|
||||
options.debug,
|
||||
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(
|
||||
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",
|
||||
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<string> {
|
||||
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<VectorStoreDocument | undefined> {
|
||||
async indexFile(noteFile: NoteFile): Promise<VectorStoreDocument | undefined> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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}"),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { App, Modal } from 'obsidian';
|
||||
import { App, Modal } from "obsidian";
|
||||
|
||||
export class AdhocPromptModal extends Modal {
|
||||
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);
|
||||
|
|
@ -15,45 +15,37 @@ export class AdhocPromptModal extends Modal {
|
|||
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');
|
||||
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) => {
|
||||
textareaEl.addEventListener("input", (evt) => {
|
||||
this.result = (evt.target as HTMLTextAreaElement).value;
|
||||
});
|
||||
|
||||
textareaEl.addEventListener('keydown', (evt) => {
|
||||
if (evt.key === 'Enter' && !evt.shiftKey) {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<ChatProps> = ({
|
|||
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<AbortController | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
|
@ -121,7 +112,7 @@ const Chat: React.FC<ChatProps> = ({
|
|||
addMessage(userMessage);
|
||||
addMessage(promptMessageHidden);
|
||||
// Clear input
|
||||
setInputMessage('');
|
||||
setInputMessage("");
|
||||
|
||||
// Display running dots to indicate loading
|
||||
setLoading(true);
|
||||
|
|
@ -131,14 +122,14 @@ const Chat: React.FC<ChatProps> = ({
|
|||
addMessage,
|
||||
setCurrentAiMessage,
|
||||
setAbortController,
|
||||
{ debug },
|
||||
{ debug }
|
||||
);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
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<ChatProps> = ({
|
|||
|
||||
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<ChatProps> = ({
|
|||
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<ChatProps> = ({
|
|||
// 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<ChatProps> = ({
|
|||
const sendNoteContentUserMessage = getSendChatContextNotesPrompt(
|
||||
notes,
|
||||
settings.chatNoteContextPath,
|
||||
settings.chatNoteContextTags,
|
||||
settings.chatNoteContextTags
|
||||
);
|
||||
const promptMessageVisible: ChatMessage = {
|
||||
message: sendNoteContentUserMessage,
|
||||
|
|
@ -247,32 +238,32 @@ const Chat: React.FC<ChatProps> = ({
|
|||
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<ChatProps> = ({
|
|||
|
||||
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<ChatProps> = ({
|
|||
|
||||
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<ChatProps> = ({
|
|||
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,7 +330,7 @@ const Chat: React.FC<ChatProps> = ({
|
|||
const createEffect = (
|
||||
eventType: string,
|
||||
promptFn: (selectedText: string, eventSubtype?: string) => string | Promise<string>,
|
||||
options: CreateEffectOptions = {},
|
||||
options: CreateEffectOptions = {}
|
||||
) => {
|
||||
return () => {
|
||||
const {
|
||||
|
|
@ -390,37 +381,35 @@ const Chat: React.FC<ChatProps> = ({
|
|||
};
|
||||
};
|
||||
|
||||
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<ChatProps> = ({
|
|||
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 (
|
||||
<div className="chat-container">
|
||||
<ChatMessages
|
||||
|
|
@ -463,20 +451,18 @@ const Chat: React.FC<ChatProps> = ({
|
|||
currentAiMessage={currentAiMessage}
|
||||
loading={loading}
|
||||
/>
|
||||
<div className='bottom-container'>
|
||||
<div className="bottom-container">
|
||||
<ChatIcons
|
||||
currentModel={currentModel}
|
||||
setCurrentModel={setModel}
|
||||
currentChain={currentChain}
|
||||
setCurrentChain={setChain}
|
||||
onStopGenerating={handleStopGenerating}
|
||||
onNewChat={
|
||||
() => {
|
||||
onNewChat={() => {
|
||||
clearMessages();
|
||||
clearChatMemory();
|
||||
clearCurrentAiMessage();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onSaveAsNote={handleSaveAsNote}
|
||||
onSendActiveNoteToPrompt={handleSendActiveNoteToPrompt}
|
||||
onForceRebuildActiveNoteContext={forceRebuildActiveNoteContext}
|
||||
|
|
|
|||
|
|
@ -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<ChatIconsProps> = ({
|
|||
// 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<ChatIconsProps> = ({
|
|||
|
||||
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<ChatIconsProps> = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
const handleChainChange = async (
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
) => {
|
||||
const handleChainChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedChain(stringToChainType(event.target.value));
|
||||
};
|
||||
|
||||
|
|
@ -158,12 +158,8 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
|||
value={currentModel}
|
||||
onChange={handleModelChange}
|
||||
>
|
||||
<option value={ChatModelDisplayNames.GPT_4}>
|
||||
{ChatModelDisplayNames.GPT_4}
|
||||
</option>
|
||||
<option value={ChatModelDisplayNames.GPT_4o}>
|
||||
{ChatModelDisplayNames.GPT_4o}
|
||||
</option>
|
||||
<option value={ChatModelDisplayNames.GPT_4}>{ChatModelDisplayNames.GPT_4}</option>
|
||||
<option value={ChatModelDisplayNames.GPT_4o}>{ChatModelDisplayNames.GPT_4o}</option>
|
||||
<option value={ChatModelDisplayNames.GPT_4o_mini}>
|
||||
{ChatModelDisplayNames.GPT_4o_mini}
|
||||
</option>
|
||||
|
|
@ -176,9 +172,7 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
|||
<option value={ChatModelDisplayNames.AZURE_OPENAI}>
|
||||
{ChatModelDisplayNames.AZURE_OPENAI}
|
||||
</option>
|
||||
<option value={ChatModelDisplayNames.CLAUDE}>
|
||||
{ChatModelDisplayNames.CLAUDE}
|
||||
</option>
|
||||
<option value={ChatModelDisplayNames.CLAUDE}>{ChatModelDisplayNames.CLAUDE}</option>
|
||||
<option value={ChatModelDisplayNames.GEMINI_PRO}>
|
||||
{ChatModelDisplayNames.GEMINI_PRO}
|
||||
</option>
|
||||
|
|
@ -188,15 +182,11 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
|||
<option value={ChatModelDisplayNames.OPENROUTERAI}>
|
||||
{ChatModelDisplayNames.OPENROUTERAI}
|
||||
</option>
|
||||
<option value={ChatModelDisplayNames.GROQ}>
|
||||
{ChatModelDisplayNames.GROQ}
|
||||
</option>
|
||||
<option value={ChatModelDisplayNames.GROQ}>{ChatModelDisplayNames.GROQ}</option>
|
||||
<option value={ChatModelDisplayNames.LM_STUDIO}>
|
||||
{ChatModelDisplayNames.LM_STUDIO}
|
||||
</option>
|
||||
<option value={ChatModelDisplayNames.OLLAMA}>
|
||||
{ChatModelDisplayNames.OLLAMA}
|
||||
</option>
|
||||
<option value={ChatModelDisplayNames.OLLAMA}>{ChatModelDisplayNames.OLLAMA}</option>
|
||||
</select>
|
||||
<span className="tooltip-text">Model Selection</span>
|
||||
</div>
|
||||
|
|
@ -247,10 +237,7 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
|||
</button>
|
||||
)}
|
||||
{selectedChain === "long_note_qa" && (
|
||||
<button
|
||||
className="chat-icon-button"
|
||||
onClick={onForceRebuildActiveNoteContext}
|
||||
>
|
||||
<button className="chat-icon-button" onClick={onForceRebuildActiveNoteContext}>
|
||||
<UseActiveNoteAsContextIcon className="icon-scaler" />
|
||||
<span className="tooltip-text">
|
||||
Refresh Index
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NoteTitleModal } from '@/components/ NoteTitleModal';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { NoteTitleModal } from "@/components/NoteTitleModal";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface ChatInputProps {
|
||||
inputMessage: string;
|
||||
|
|
@ -10,7 +10,11 @@ interface ChatInputProps {
|
|||
}
|
||||
|
||||
const ChatInput: React.FC<ChatInputProps> = ({
|
||||
inputMessage, setInputMessage, handleKeyDown, handleSendMessage, getChatVisibility,
|
||||
inputMessage,
|
||||
setInputMessage,
|
||||
handleKeyDown,
|
||||
handleSendMessage,
|
||||
getChatVisibility,
|
||||
}) => {
|
||||
const [rows, setRows] = useState(1);
|
||||
const [shouldFocus, setShouldFocus] = useState(false);
|
||||
|
|
@ -22,22 +26,18 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
updateRows(inputValue);
|
||||
|
||||
// Check if the user typed `[[`
|
||||
if (inputValue.slice(-2) === '[[') {
|
||||
if (inputValue.slice(-2) === "[[") {
|
||||
showNoteTitleModal();
|
||||
}
|
||||
};
|
||||
|
||||
const showNoteTitleModal = () => {
|
||||
const fetchNoteTitles = async () => {
|
||||
const noteTitles = app.vault.getMarkdownFiles().map(file => file.basename);
|
||||
const noteTitles = app.vault.getMarkdownFiles().map((file) => file.basename);
|
||||
|
||||
new NoteTitleModal(
|
||||
app,
|
||||
noteTitles,
|
||||
(noteTitle: string) => {
|
||||
new NoteTitleModal(app, noteTitles, (noteTitle: string) => {
|
||||
setInputMessage(inputMessage.slice(0, -2) + ` [[${noteTitle}]]`);
|
||||
}
|
||||
).open();
|
||||
}).open();
|
||||
};
|
||||
|
||||
fetchNoteTitles();
|
||||
|
|
@ -49,7 +49,8 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
const minRows = 1;
|
||||
|
||||
const rowsNeeded = Math.min(
|
||||
Math.max(text.split('\n').length, minRows), Math.floor(maxHeight / lineHeight)
|
||||
Math.max(text.split("\n").length, minRows),
|
||||
Math.floor(maxHeight / lineHeight)
|
||||
);
|
||||
setRows(rowsNeeded);
|
||||
};
|
||||
|
|
@ -87,4 +88,3 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
};
|
||||
|
||||
export default ChatInput;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import ChatSingleMessage from '@/components/ChatComponents/ChatSingleMessage';
|
||||
import { BotIcon } from '@/components/Icons';
|
||||
import ReactMarkdown from '@/components/Markdown/MemoizedReactMarkdown';
|
||||
import { ChatMessage } from '@/sharedState';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ChatSingleMessage from "@/components/ChatComponents/ChatSingleMessage";
|
||||
import { BotIcon } from "@/components/Icons";
|
||||
import ReactMarkdown from "@/components/Markdown/MemoizedReactMarkdown";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface ChatMessagesProps {
|
||||
chatHistory: ChatMessage[];
|
||||
|
|
@ -10,37 +10,37 @@ interface ChatMessagesProps {
|
|||
loading?: boolean;
|
||||
}
|
||||
|
||||
const ChatMessages: React.FC<ChatMessagesProps> = ({
|
||||
chatHistory, currentAiMessage, loading
|
||||
}) => {
|
||||
const [loadingDots, setLoadingDots] = useState('');
|
||||
const ChatMessages: React.FC<ChatMessagesProps> = ({ chatHistory, currentAiMessage, loading }) => {
|
||||
const [loadingDots, setLoadingDots] = useState("");
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const chatMessagesContainer = document.querySelector('.chat-messages');
|
||||
const chatMessagesContainer = document.querySelector(".chat-messages");
|
||||
if (chatMessagesContainer) {
|
||||
chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {scrollToBottom()}, [chatHistory]);
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [chatHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId: NodeJS.Timeout;
|
||||
if (loading) {
|
||||
intervalId = setInterval(() => {
|
||||
setLoadingDots(dots => dots.length < 6 ? dots + '.' : '');
|
||||
setLoadingDots((dots) => (dots.length < 6 ? dots + "." : ""));
|
||||
}, 200);
|
||||
} else {
|
||||
setLoadingDots('');
|
||||
setLoadingDots("");
|
||||
}
|
||||
return () => clearInterval(intervalId);
|
||||
}, [loading]);
|
||||
|
||||
return (
|
||||
<div className="chat-messages">
|
||||
{chatHistory.map((message, index) => (
|
||||
message.isVisible && <ChatSingleMessage key={index} message={message} />
|
||||
))}
|
||||
{chatHistory.map(
|
||||
(message, index) => message.isVisible && <ChatSingleMessage key={index} message={message} />
|
||||
)}
|
||||
{currentAiMessage ? (
|
||||
<div className="message bot-message" key={`ai_message_${currentAiMessage}`}>
|
||||
<div className="message-icon">
|
||||
|
|
@ -51,7 +51,8 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
loading && <div className="message bot-message" key={`ai_message_${currentAiMessage}`}>
|
||||
loading && (
|
||||
<div className="message bot-message" key={`ai_message_${currentAiMessage}`}>
|
||||
<div className="message-icon">
|
||||
<BotIcon />
|
||||
</div>
|
||||
|
|
@ -59,6 +60,7 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
|
|||
<ReactMarkdown>{loadingDots}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,83 +5,80 @@ export class ChatNoteContextModal extends Modal {
|
|||
private settings: CopilotSettings;
|
||||
private onSubmit: (path: string, tags: string[]) => void;
|
||||
|
||||
constructor(app: App, settings: CopilotSettings, onSubmit: (path: string, tags: string[]) => void) {
|
||||
constructor(
|
||||
app: App,
|
||||
settings: CopilotSettings,
|
||||
onSubmit: (path: string, tags: string[]) => void
|
||||
) {
|
||||
super(app);
|
||||
this.settings = settings;
|
||||
this.onSubmit = onSubmit;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const formContainer = this.contentEl.createEl('div', { cls: 'copilot-command-modal' });
|
||||
const pathContainer = formContainer.createEl('div', { cls: 'copilot-command-input-container' });
|
||||
const formContainer = this.contentEl.createEl("div", { cls: "copilot-command-modal" });
|
||||
const pathContainer = formContainer.createEl("div", { cls: "copilot-command-input-container" });
|
||||
|
||||
pathContainer.createEl('h3', { text: 'Filter by Folder Path', cls: 'copilot-command-header' });
|
||||
pathContainer.createEl("h3", { text: "Filter by Folder Path", cls: "copilot-command-header" });
|
||||
const descFragment = createFragment((frag) => {
|
||||
frag.appendText('All notes under the path will be sent to the prompt when the ');
|
||||
frag.createEl(
|
||||
'strong',
|
||||
{ text: 'Send Note(s) to Prompt' }
|
||||
);
|
||||
frag.appendText(' button is clicked in Chat mode. ');
|
||||
frag.appendText('If none provided, ');
|
||||
frag.createEl(
|
||||
'strong',
|
||||
{ text: 'default context is the active note' }
|
||||
);
|
||||
frag.appendText("All notes under the path will be sent to the prompt when the ");
|
||||
frag.createEl("strong", { text: "Send Note(s) to Prompt" });
|
||||
frag.appendText(" button is clicked in Chat mode. ");
|
||||
frag.appendText("If none provided, ");
|
||||
frag.createEl("strong", { text: "default context is the active note" });
|
||||
});
|
||||
pathContainer.appendChild(descFragment);
|
||||
|
||||
const pathField = pathContainer.createEl(
|
||||
'input',
|
||||
{
|
||||
type: 'text',
|
||||
cls: 'copilot-command-input',
|
||||
const pathField = pathContainer.createEl("input", {
|
||||
type: "text",
|
||||
cls: "copilot-command-input",
|
||||
value: this.settings.chatNoteContextPath,
|
||||
}
|
||||
);
|
||||
pathField.setAttribute('name', 'folderPath');
|
||||
});
|
||||
pathField.setAttribute("name", "folderPath");
|
||||
|
||||
pathContainer.createEl('h3', { text: 'Filter by Tags', cls: 'copilot-command-header' });
|
||||
pathContainer.createEl("h3", { text: "Filter by Tags", cls: "copilot-command-header" });
|
||||
const descTagsFragment = createFragment((frag) => {
|
||||
frag.createEl(
|
||||
'strong',
|
||||
{ text: 'Only tags in note property are used, tags in note content are not used.' }
|
||||
)
|
||||
frag.createEl('p', { text: 'All notes under the path above are further filtered by the specified tags. If no path is provided, only tags are used. Multiple tags should be separated by commas. ' });
|
||||
frag.createEl(
|
||||
'strong',
|
||||
{ text: 'Tags function as an OR filter, ' }
|
||||
frag.createEl("strong", {
|
||||
text: "Only tags in note property are used, tags in note content are not used.",
|
||||
});
|
||||
frag.createEl("p", {
|
||||
text: "All notes under the path above are further filtered by the specified tags. If no path is provided, only tags are used. Multiple tags should be separated by commas. ",
|
||||
});
|
||||
frag.createEl("strong", { text: "Tags function as an OR filter, " });
|
||||
frag.appendText(
|
||||
" any note that matches one of the tags will be sent to the prompt when button is clicked in Chat mode."
|
||||
);
|
||||
frag.appendText(' any note that matches one of the tags will be sent to the prompt when button is clicked in Chat mode.');
|
||||
});
|
||||
pathContainer.appendChild(descTagsFragment);
|
||||
|
||||
const tagsField = pathContainer.createEl(
|
||||
'input',
|
||||
{
|
||||
type: 'text',
|
||||
cls: 'copilot-command-input',
|
||||
value: this.settings.chatNoteContextTags.join(','),
|
||||
}
|
||||
);
|
||||
tagsField.setAttribute('name', 'tags');
|
||||
const tagsField = pathContainer.createEl("input", {
|
||||
type: "text",
|
||||
cls: "copilot-command-input",
|
||||
value: this.settings.chatNoteContextTags.join(","),
|
||||
});
|
||||
tagsField.setAttribute("name", "tags");
|
||||
|
||||
const submitButtonContainer = formContainer.createEl('div', { cls: 'copilot-command-save-btn-container' });
|
||||
const submitButton = submitButtonContainer.createEl('button', { text: 'Submit', cls: 'copilot-command-save-btn' });
|
||||
const submitButtonContainer = formContainer.createEl("div", {
|
||||
cls: "copilot-command-save-btn-container",
|
||||
});
|
||||
const submitButton = submitButtonContainer.createEl("button", {
|
||||
text: "Submit",
|
||||
cls: "copilot-command-save-btn",
|
||||
});
|
||||
|
||||
submitButton.addEventListener('click', () => {
|
||||
submitButton.addEventListener("click", () => {
|
||||
// Remove the leading slash if it exists
|
||||
let pathValue = pathField.value;
|
||||
if (pathValue.startsWith('/') && pathValue.length > 1) {
|
||||
if (pathValue.startsWith("/") && pathValue.length > 1) {
|
||||
pathValue = pathValue.slice(1);
|
||||
}
|
||||
|
||||
const tagsValue = tagsField.value
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.map(tag => tag.toLowerCase())
|
||||
.map(tag => tag.replace('#', ''))
|
||||
.filter(tag => tag !== '');
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.map((tag) => tag.toLowerCase())
|
||||
.map((tag) => tag.replace("#", ""))
|
||||
.filter((tag) => tag !== "");
|
||||
|
||||
this.onSubmit(pathValue, tagsValue);
|
||||
this.close();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import ChainManager from '@/LLMProviders/chainManager';
|
||||
import Chat from '@/components/Chat';
|
||||
import { CHAT_VIEWTYPE } from '@/constants';
|
||||
import { AppContext } from '@/context';
|
||||
import CopilotPlugin from '@/main';
|
||||
import { CopilotSettings } from '@/settings/SettingsPage';
|
||||
import SharedState from '@/sharedState';
|
||||
import { EventEmitter } from 'events';
|
||||
import { ItemView, WorkspaceLeaf } from 'obsidian';
|
||||
import * as React from 'react';
|
||||
import { Root, createRoot } from 'react-dom/client';
|
||||
|
||||
import ChainManager from "@/LLMProviders/chainManager";
|
||||
import Chat from "@/components/Chat";
|
||||
import { CHAT_VIEWTYPE } from "@/constants";
|
||||
import { AppContext } from "@/context";
|
||||
import CopilotPlugin from "@/main";
|
||||
import { CopilotSettings } from "@/settings/SettingsPage";
|
||||
import SharedState from "@/sharedState";
|
||||
import { EventEmitter } from "events";
|
||||
import { ItemView, WorkspaceLeaf } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { Root, createRoot } from "react-dom/client";
|
||||
|
||||
export default class CopilotView extends ItemView {
|
||||
private sharedState: SharedState;
|
||||
|
|
@ -19,9 +18,12 @@ export default class CopilotView extends ItemView {
|
|||
private defaultSaveFolder: string;
|
||||
private debug = false;
|
||||
emitter: EventEmitter;
|
||||
userSystemPrompt = '';
|
||||
userSystemPrompt = "";
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, private plugin: CopilotPlugin) {
|
||||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
private plugin: CopilotPlugin
|
||||
) {
|
||||
super(leaf);
|
||||
this.sharedState = plugin.sharedState;
|
||||
this.settings = plugin.settings;
|
||||
|
|
@ -41,19 +43,19 @@ export default class CopilotView extends ItemView {
|
|||
|
||||
// Return an icon for this view
|
||||
getIcon(): string {
|
||||
return 'message-square';
|
||||
return "message-square";
|
||||
}
|
||||
|
||||
// Return a title for this view
|
||||
getTitle(): string {
|
||||
return 'Copilot Chat';
|
||||
return "Copilot Chat";
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'Copilot';
|
||||
return "Copilot";
|
||||
}
|
||||
|
||||
async getChatVisibility(){
|
||||
async getChatVisibility() {
|
||||
if (this.plugin.activateViewPromise) {
|
||||
await this.plugin.activateViewPromise;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,38 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
type IconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const UserIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const BotIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="3" y="11" width="18" height="10" rx="2"></rect>
|
||||
<circle cx="12" cy="5" r="2"></circle>
|
||||
<path d="M12 7v4"></path>
|
||||
|
|
@ -24,67 +42,145 @@ export const BotIcon = () => (
|
|||
);
|
||||
|
||||
export const CopyClipboardIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CheckIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const RefreshIcon: React.FC<IconProps> = ({ className }) => (
|
||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 2v6h6"></path><path d="M21 12A9 9 0 0 0 6 5.3L3 8"></path>
|
||||
<path d="M21 22v-6h-6"></path><path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"></path>
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 2v6h6"></path>
|
||||
<path d="M21 12A9 9 0 0 0 6 5.3L3 8"></path>
|
||||
<path d="M21 22v-6h-6"></path>
|
||||
<path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"></path>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const StopIcon: React.FC<IconProps> = ({ className }) => (
|
||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle><rect x="9" y="9" width="6" height="6"></rect></svg>
|
||||
)
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<rect x="9" y="9" width="6" height="6"></rect>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const NewChatIcon: React.FC<IconProps> = ({ className }) => (
|
||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24" fill="none"
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12" y2="16"></line>
|
||||
<line x1="8" y1="12" x2="16" y2="12"></line>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SaveAsNoteIcon: React.FC<IconProps> = ({ className }) => (
|
||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 3v12"></path><path d="m8 11 4 4 4-4"></path>
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 3v12"></path>
|
||||
<path d="m8 11 4 4 4-4"></path>
|
||||
<path d="M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4"></path>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const UseActiveNoteAsContextIcon: React.FC<IconProps> = ({ className }) => (
|
||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.062-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 1.998c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z"></path>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const SendActiveNoteToPromptIcon: React.FC<IconProps> = ({ className }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<circle cx={12} cy={12} r={10} />
|
||||
<path d="m16 12-4-4-4 4M12 16V8" />
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { App, FuzzySuggestModal } from "obsidian";
|
||||
|
||||
|
||||
export class ListPromptModal extends FuzzySuggestModal<string> {
|
||||
private onChoosePromptTitle: (promptTitle: string) => void;
|
||||
private promptTitles: string[];
|
||||
|
|
@ -27,5 +26,3 @@ export class ListPromptModal extends FuzzySuggestModal<string> {
|
|||
this.onChoosePromptTitle(promptTitle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@ export class NoteTitleModal extends FuzzySuggestModal<string> {
|
|||
private onChooseNoteTitle: (noteTitle: string) => void;
|
||||
private noteTitles: string[];
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
noteTitles: string[],
|
||||
onChooseNoteTitle: (noteTitle: string) => void
|
||||
) {
|
||||
constructor(app: App, noteTitles: string[], onChooseNoteTitle: (noteTitle: string) => void) {
|
||||
super(app);
|
||||
this.noteTitles = noteTitles;
|
||||
this.onChooseNoteTitle = onChooseNoteTitle;
|
||||
|
|
@ -12,36 +12,41 @@ export class QAExclusionModal extends Modal {
|
|||
}
|
||||
|
||||
onOpen() {
|
||||
const formContainer = this.contentEl.createEl('div', { cls: 'copilot-command-modal' });
|
||||
const pathContainer = formContainer.createEl('div', { cls: 'copilot-command-input-container' });
|
||||
const formContainer = this.contentEl.createEl("div", { cls: "copilot-command-modal" });
|
||||
const pathContainer = formContainer.createEl("div", { cls: "copilot-command-input-container" });
|
||||
|
||||
pathContainer.createEl('h3', { text: 'Exclude by Folder Path or Note Title', cls: 'copilot-command-header' });
|
||||
pathContainer.createEl("h3", {
|
||||
text: "Exclude by Folder Path or Note Title",
|
||||
cls: "copilot-command-header",
|
||||
});
|
||||
const descFragment = createFragment((frag) => {
|
||||
frag.appendText('All notes under the paths will be excluded from indexing');
|
||||
frag.appendText("All notes under the paths will be excluded from indexing");
|
||||
});
|
||||
pathContainer.appendChild(descFragment);
|
||||
|
||||
const pathField = pathContainer.createEl(
|
||||
'input',
|
||||
{
|
||||
type: 'text',
|
||||
cls: 'copilot-command-input',
|
||||
const pathField = pathContainer.createEl("input", {
|
||||
type: "text",
|
||||
cls: "copilot-command-input",
|
||||
value: this.settings.qaExclusionPaths,
|
||||
placeholder: 'Enter /folderPath, [[note title]] separated by commas',
|
||||
}
|
||||
);
|
||||
pathField.setAttribute('name', 'folderPath');
|
||||
placeholder: "Enter /folderPath, [[note title]] separated by commas",
|
||||
});
|
||||
pathField.setAttribute("name", "folderPath");
|
||||
|
||||
const submitButtonContainer = formContainer.createEl('div', { cls: 'copilot-command-save-btn-container' });
|
||||
const submitButton = submitButtonContainer.createEl('button', { text: 'Submit', cls: 'copilot-command-save-btn' });
|
||||
const submitButtonContainer = formContainer.createEl("div", {
|
||||
cls: "copilot-command-save-btn-container",
|
||||
});
|
||||
const submitButton = submitButtonContainer.createEl("button", {
|
||||
text: "Submit",
|
||||
cls: "copilot-command-save-btn",
|
||||
});
|
||||
|
||||
submitButton.addEventListener('click', () => {
|
||||
submitButton.addEventListener("click", () => {
|
||||
// Parse the input list
|
||||
const pathsValue = pathField.value
|
||||
.split(',')
|
||||
.map(pathValue => pathValue.trim())
|
||||
.filter(pathValue => pathValue !== '')
|
||||
.join(',');
|
||||
.split(",")
|
||||
.map((pathValue) => pathValue.trim())
|
||||
.filter((pathValue) => pathValue !== "")
|
||||
.join(",");
|
||||
|
||||
this.onSubmit(pathsValue);
|
||||
this.close();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
import { App, FuzzySuggestModal } from "obsidian";
|
||||
|
||||
|
||||
const TONES: string[] = [
|
||||
"Professional",
|
||||
"Casual",
|
||||
"Straightforward",
|
||||
"Confident",
|
||||
"Friendly",
|
||||
];
|
||||
const TONES: string[] = ["Professional", "Casual", "Straightforward", "Confident", "Friendly"];
|
||||
|
||||
export class ToneModal extends FuzzySuggestModal<string> {
|
||||
private onChooseTone: (tone: string) => void;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { App } from 'obsidian';
|
||||
import * as React from 'react';
|
||||
import { App } from "obsidian";
|
||||
import * as React from "react";
|
||||
|
||||
// App context
|
||||
export const AppContext = React.createContext<App | undefined>(undefined);
|
||||
|
||||
|
|
|
|||
|
|
@ -56,12 +56,8 @@ export class CustomPromptProcessor {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
const processedVariableName =
|
||||
processVariableNameForNotePath(variableName);
|
||||
const noteFiles = await getNotesFromPath(
|
||||
this.vault,
|
||||
processedVariableName
|
||||
);
|
||||
const processedVariableName = processVariableNameForNotePath(variableName);
|
||||
const noteFiles = await getNotesFromPath(this.vault, processedVariableName);
|
||||
for (const file of noteFiles) {
|
||||
const content = await getFileContent(file, this.vault);
|
||||
if (content) {
|
||||
|
|
@ -73,22 +69,15 @@ export class CustomPromptProcessor {
|
|||
if (notes.length > 0) {
|
||||
variablesWithContent.push(JSON.stringify(notes));
|
||||
} else {
|
||||
new Notice(
|
||||
`Warning: No valid notes found for the provided path '${variableName}'.`
|
||||
);
|
||||
new Notice(`Warning: No valid notes found for the provided path '${variableName}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
return variablesWithContent;
|
||||
}
|
||||
|
||||
async processCustomPrompt(
|
||||
customPrompt: string,
|
||||
selectedText: string
|
||||
): Promise<string> {
|
||||
const variablesWithContent = await this.extractVariablesFromPrompt(
|
||||
customPrompt
|
||||
);
|
||||
async processCustomPrompt(customPrompt: string, selectedText: string): Promise<string> {
|
||||
const variablesWithContent = await this.extractVariablesFromPrompt(customPrompt);
|
||||
let processedPrompt = customPrompt;
|
||||
const matches = [...processedPrompt.matchAll(/\{([^}]+)\}/g)];
|
||||
|
||||
|
|
|
|||
|
|
@ -29,9 +29,7 @@ export default class EncryptionService {
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private isDecrypted(keyBuffer: any): boolean {
|
||||
return (
|
||||
keyBuffer.startsWith(EncryptionService.DECRYPTION_PREFIX)
|
||||
);
|
||||
return keyBuffer.startsWith(EncryptionService.DECRYPTION_PREFIX);
|
||||
}
|
||||
|
||||
public encryptAllKeys(): void {
|
||||
|
|
@ -65,9 +63,7 @@ export default class EncryptionService {
|
|||
}
|
||||
const encryptedBuffer = safeStorage.encryptString(apiKey) as Buffer;
|
||||
// Convert the encrypted buffer to a Base64 string and prepend the prefix
|
||||
return (
|
||||
EncryptionService.ENCRYPTION_PREFIX + encryptedBuffer.toString("base64")
|
||||
);
|
||||
return EncryptionService.ENCRYPTION_PREFIX + encryptedBuffer.toString("base64");
|
||||
}
|
||||
|
||||
// Get the actual key for use in the LLM chain
|
||||
|
|
@ -83,10 +79,7 @@ export default class EncryptionService {
|
|||
}
|
||||
|
||||
// Remove the prefix and convert from Base64 to a buffer before decryption
|
||||
const base64Data = apiKey.replace(
|
||||
EncryptionService.ENCRYPTION_PREFIX,
|
||||
""
|
||||
);
|
||||
const base64Data = apiKey.replace(EncryptionService.ENCRYPTION_PREFIX, "");
|
||||
try {
|
||||
const buffer = Buffer.from(base64Data, "base64");
|
||||
return safeStorage.decryptString(buffer) as string;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import ChainManager from '@/LLMProviders/chainManager';
|
||||
import { ChatMessage } from '@/sharedState';
|
||||
import { Notice } from 'obsidian';
|
||||
import ChainManager from "@/LLMProviders/chainManager";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
export type Role = 'assistant' | 'user' | 'system';
|
||||
export type Role = "assistant" | "user" | "system";
|
||||
|
||||
export const getAIResponse = async (
|
||||
userMessage: ChatMessage,
|
||||
|
|
@ -11,10 +11,10 @@ export const getAIResponse = async (
|
|||
updateCurrentAiMessage: (message: string) => void,
|
||||
updateShouldAbort: (abortController: AbortController | null) => void,
|
||||
options: {
|
||||
debug?: boolean,
|
||||
ignoreSystemMessage?: boolean,
|
||||
updateLoading?: (loading: boolean) => void
|
||||
} = {},
|
||||
debug?: boolean;
|
||||
ignoreSystemMessage?: boolean;
|
||||
updateLoading?: (loading: boolean) => void;
|
||||
} = {}
|
||||
) => {
|
||||
const abortController = new AbortController();
|
||||
updateShouldAbort(abortController);
|
||||
|
|
@ -24,10 +24,10 @@ export const getAIResponse = async (
|
|||
abortController,
|
||||
updateCurrentAiMessage,
|
||||
addMessage,
|
||||
options,
|
||||
options
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Model request failed:', error);
|
||||
new Notice('Model request failed:', error);
|
||||
console.error("Model request failed:", error);
|
||||
new Notice("Model request failed:", error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
236
src/main.ts
236
src/main.ts
|
|
@ -29,15 +29,7 @@ import {
|
|||
} from "@/utils";
|
||||
import VectorDBManager, { VectorStoreDocument } from "@/vectorDBManager";
|
||||
import { MD5 } from "crypto-js";
|
||||
import {
|
||||
Editor,
|
||||
MarkdownView,
|
||||
Menu,
|
||||
Notice,
|
||||
Plugin,
|
||||
TFile,
|
||||
WorkspaceLeaf,
|
||||
} from "obsidian";
|
||||
import { Editor, MarkdownView, Menu, Notice, Plugin, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import PouchDB from "pouchdb";
|
||||
|
||||
export default class CopilotPlugin extends Plugin {
|
||||
|
|
@ -65,30 +57,24 @@ export default class CopilotPlugin extends Plugin {
|
|||
const langChainParams = this.getChainManagerParams();
|
||||
this.encryptionService = new EncryptionService(this.settings);
|
||||
this.dbVectorStores = new PouchDB<VectorStoreDocument>(
|
||||
`copilot_vector_stores_${this.getVaultIdentifier()}`,
|
||||
`copilot_vector_stores_${this.getVaultIdentifier()}`
|
||||
);
|
||||
this.chainManager = new ChainManager(
|
||||
this.app,
|
||||
langChainParams,
|
||||
this.encryptionService,
|
||||
this.settings,
|
||||
() => this.dbVectorStores,
|
||||
() => this.dbVectorStores
|
||||
);
|
||||
|
||||
if (this.settings.enableEncryption) {
|
||||
await this.saveSettings();
|
||||
}
|
||||
|
||||
this.embeddingsManager = EmbeddingsManager.getInstance(
|
||||
langChainParams,
|
||||
this.encryptionService,
|
||||
);
|
||||
this.embeddingsManager = EmbeddingsManager.getInstance(langChainParams, this.encryptionService);
|
||||
this.dbPrompts = new PouchDB<CustomPrompt>("copilot_custom_prompts");
|
||||
|
||||
this.registerView(
|
||||
CHAT_VIEWTYPE,
|
||||
(leaf: WorkspaceLeaf) => new CopilotView(leaf, this),
|
||||
);
|
||||
this.registerView(CHAT_VIEWTYPE, (leaf: WorkspaceLeaf) => new CopilotView(leaf, this));
|
||||
|
||||
this.addCommand({
|
||||
id: "chat-toggle-window",
|
||||
|
|
@ -122,9 +108,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
await this.dbPrompts.put({ _id: title, prompt: prompt });
|
||||
new Notice("Custom prompt saved successfully.");
|
||||
} catch (e) {
|
||||
new Notice(
|
||||
"Error saving custom prompt. Please check if the title already exists.",
|
||||
);
|
||||
new Notice("Error saving custom prompt. Please check if the title already exists.");
|
||||
console.error(e);
|
||||
}
|
||||
}).open();
|
||||
|
|
@ -136,37 +120,27 @@ export default class CopilotPlugin extends Plugin {
|
|||
name: "Apply custom prompt",
|
||||
callback: () => {
|
||||
this.fetchPromptTitles().then((promptTitles: string[]) => {
|
||||
new ListPromptModal(
|
||||
this.app,
|
||||
promptTitles,
|
||||
async (promptTitle: string) => {
|
||||
new ListPromptModal(this.app, promptTitles, async (promptTitle: string) => {
|
||||
if (!promptTitle) {
|
||||
new Notice("Please select a prompt title.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const doc = (await this.dbPrompts.get(
|
||||
promptTitle,
|
||||
)) as CustomPrompt;
|
||||
const doc = (await this.dbPrompts.get(promptTitle)) as CustomPrompt;
|
||||
if (!doc.prompt) {
|
||||
new Notice(
|
||||
`No prompt found with the title "${promptTitle}".`,
|
||||
);
|
||||
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||
return;
|
||||
}
|
||||
this.processCustomPrompt("applyCustomPrompt", doc.prompt);
|
||||
} catch (err) {
|
||||
if (err.name === "not_found") {
|
||||
new Notice(
|
||||
`No prompt found with the title "${promptTitle}".`,
|
||||
);
|
||||
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||
} else {
|
||||
console.error(err);
|
||||
new Notice("An error occurred.");
|
||||
}
|
||||
}
|
||||
},
|
||||
).open();
|
||||
}).open();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -175,17 +149,14 @@ export default class CopilotPlugin extends Plugin {
|
|||
id: "apply-adhoc-prompt",
|
||||
name: "Apply ad-hoc custom prompt",
|
||||
callback: async () => {
|
||||
const modal = new AdhocPromptModal(
|
||||
this.app,
|
||||
async (adhocPrompt: string) => {
|
||||
const modal = new AdhocPromptModal(this.app, async (adhocPrompt: string) => {
|
||||
try {
|
||||
this.processCustomPrompt("applyAdhocPrompt", adhocPrompt);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
new Notice("An error occurred.");
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
modal.open();
|
||||
},
|
||||
|
|
@ -200,10 +171,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
}
|
||||
|
||||
this.fetchPromptTitles().then((promptTitles: string[]) => {
|
||||
new ListPromptModal(
|
||||
this.app,
|
||||
promptTitles,
|
||||
async (promptTitle: string) => {
|
||||
new ListPromptModal(this.app, promptTitles, async (promptTitle: string) => {
|
||||
if (!promptTitle) {
|
||||
new Notice("Please select a prompt title.");
|
||||
return;
|
||||
|
|
@ -212,27 +180,20 @@ export default class CopilotPlugin extends Plugin {
|
|||
try {
|
||||
const doc = await this.dbPrompts.get(promptTitle);
|
||||
if (doc._rev) {
|
||||
await this.dbPrompts.remove(
|
||||
doc as PouchDB.Core.RemoveDocument,
|
||||
);
|
||||
await this.dbPrompts.remove(doc as PouchDB.Core.RemoveDocument);
|
||||
new Notice(`Prompt "${promptTitle}" has been deleted.`);
|
||||
} else {
|
||||
new Notice(
|
||||
`Failed to delete prompt "${promptTitle}": No revision found.`,
|
||||
);
|
||||
new Notice(`Failed to delete prompt "${promptTitle}": No revision found.`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === "not_found") {
|
||||
new Notice(
|
||||
`No prompt found with the title "${promptTitle}".`,
|
||||
);
|
||||
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||
} else {
|
||||
console.error(err);
|
||||
new Notice("An error occurred while deleting the prompt.");
|
||||
}
|
||||
}
|
||||
},
|
||||
).open();
|
||||
}).open();
|
||||
});
|
||||
|
||||
return true;
|
||||
|
|
@ -248,19 +209,14 @@ export default class CopilotPlugin extends Plugin {
|
|||
}
|
||||
|
||||
this.fetchPromptTitles().then((promptTitles: string[]) => {
|
||||
new ListPromptModal(
|
||||
this.app,
|
||||
promptTitles,
|
||||
async (promptTitle: string) => {
|
||||
new ListPromptModal(this.app, promptTitles, async (promptTitle: string) => {
|
||||
if (!promptTitle) {
|
||||
new Notice("Please select a prompt title.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const doc = (await this.dbPrompts.get(
|
||||
promptTitle,
|
||||
)) as CustomPrompt;
|
||||
const doc = (await this.dbPrompts.get(promptTitle)) as CustomPrompt;
|
||||
if (doc.prompt) {
|
||||
new AddPromptModal(
|
||||
this.app,
|
||||
|
|
@ -273,25 +229,20 @@ export default class CopilotPlugin extends Plugin {
|
|||
},
|
||||
doc._id,
|
||||
doc.prompt,
|
||||
true,
|
||||
true
|
||||
).open();
|
||||
} else {
|
||||
new Notice(
|
||||
`No prompt found with the title "${promptTitle}".`,
|
||||
);
|
||||
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === "not_found") {
|
||||
new Notice(
|
||||
`No prompt found with the title "${promptTitle}".`,
|
||||
);
|
||||
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||
} else {
|
||||
console.error(err);
|
||||
new Notice("An error occurred.");
|
||||
}
|
||||
}
|
||||
},
|
||||
).open();
|
||||
}).open();
|
||||
});
|
||||
|
||||
return true;
|
||||
|
|
@ -307,17 +258,13 @@ export default class CopilotPlugin extends Plugin {
|
|||
await this.dbVectorStores.destroy();
|
||||
// Reinitialize the database
|
||||
this.dbVectorStores = new PouchDB<VectorStoreDocument>(
|
||||
`copilot_vector_stores_${this.getVaultIdentifier()}`,
|
||||
`copilot_vector_stores_${this.getVaultIdentifier()}`
|
||||
);
|
||||
new Notice("Local vector store cleared successfully.");
|
||||
console.log(
|
||||
"Local vector store cleared successfully, new instance created.",
|
||||
);
|
||||
console.log("Local vector store cleared successfully, new instance created.");
|
||||
} catch (err) {
|
||||
console.error("Error clearing the local vector store:", err);
|
||||
new Notice(
|
||||
"An error occurred while clearing the local vector store.",
|
||||
);
|
||||
new Notice("An error occurred while clearing the local vector store.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -329,32 +276,26 @@ export default class CopilotPlugin extends Plugin {
|
|||
try {
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
const filePaths = files.map((file) => file.path);
|
||||
const indexedFiles = await VectorDBManager.getNoteFiles(
|
||||
this.dbVectorStores,
|
||||
);
|
||||
const indexedFiles = await VectorDBManager.getNoteFiles(this.dbVectorStores);
|
||||
const indexedFilePaths = indexedFiles.map((file) => file.path);
|
||||
const filesToDelete = indexedFilePaths.filter(
|
||||
(filePath) => !filePaths.includes(filePath),
|
||||
(filePath) => !filePaths.includes(filePath)
|
||||
);
|
||||
|
||||
const deletePromises = filesToDelete.map(async (filePath) => {
|
||||
VectorDBManager.removeMemoryVectors(
|
||||
this.dbVectorStores,
|
||||
VectorDBManager.getDocumentHash(filePath),
|
||||
VectorDBManager.getDocumentHash(filePath)
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
new Notice("Local vector store garbage collected successfully.");
|
||||
console.log(
|
||||
"Local vector store garbage collected successfully, new instance created.",
|
||||
);
|
||||
console.log("Local vector store garbage collected successfully, new instance created.");
|
||||
} catch (err) {
|
||||
console.error("Error clearing the local vector store:", err);
|
||||
new Notice(
|
||||
"An error occurred while clearing the local vector store.",
|
||||
);
|
||||
new Notice("An error occurred while clearing the local vector store.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -366,12 +307,8 @@ export default class CopilotPlugin extends Plugin {
|
|||
try {
|
||||
const indexedFileCount = await this.indexVaultToVectorStore();
|
||||
|
||||
new Notice(
|
||||
`${indexedFileCount} vault files indexed to vector store.`,
|
||||
);
|
||||
console.log(
|
||||
`${indexedFileCount} vault files indexed to vector store.`,
|
||||
);
|
||||
new Notice(`${indexedFileCount} vault files indexed to vector store.`);
|
||||
console.log(`${indexedFileCount} vault files indexed to vector store.`);
|
||||
} catch (err) {
|
||||
console.error("Error indexing vault to vector store:", err);
|
||||
new Notice("An error occurred while indexing vault to vector store.");
|
||||
|
|
@ -386,17 +323,11 @@ export default class CopilotPlugin extends Plugin {
|
|||
try {
|
||||
const indexedFileCount = await this.indexVaultToVectorStore(true);
|
||||
|
||||
new Notice(
|
||||
`${indexedFileCount} vault files indexed to vector store.`,
|
||||
);
|
||||
console.log(
|
||||
`${indexedFileCount} vault files indexed to vector store.`,
|
||||
);
|
||||
new Notice(`${indexedFileCount} vault files indexed to vector store.`);
|
||||
console.log(`${indexedFileCount} vault files indexed to vector store.`);
|
||||
} catch (err) {
|
||||
console.error("Error re-indexing vault to vector store:", err);
|
||||
new Notice(
|
||||
"An error occurred while re-indexing vault to vector store.",
|
||||
);
|
||||
new Notice("An error occurred while re-indexing vault to vector store.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -405,16 +336,12 @@ export default class CopilotPlugin extends Plugin {
|
|||
id: "set-chat-note-context",
|
||||
name: "Set note context for Chat mode",
|
||||
callback: async () => {
|
||||
new ChatNoteContextModal(
|
||||
this.app,
|
||||
this.settings,
|
||||
async (path: string, tags: string[]) => {
|
||||
new ChatNoteContextModal(this.app, this.settings, async (path: string, tags: string[]) => {
|
||||
// Store the path in the plugin's settings, default to empty string
|
||||
this.settings.chatNoteContextPath = path;
|
||||
this.settings.chatNoteContextTags = tags;
|
||||
await this.saveSettings();
|
||||
},
|
||||
).open();
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -434,15 +361,12 @@ export default class CopilotPlugin extends Plugin {
|
|||
this.app.vault.on("delete", (file) => {
|
||||
const docHash = VectorDBManager.getDocumentHash(file.path);
|
||||
VectorDBManager.removeMemoryVectors(this.dbVectorStores, docHash);
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// Index vault to vector store on startup and after loading all commands
|
||||
// This can take a while, so we don't want to block the startup process
|
||||
if (
|
||||
this.settings.indexVaultToVectorStore ===
|
||||
VAULT_VECTOR_STORE_STRATEGY.ON_STARTUP
|
||||
) {
|
||||
if (this.settings.indexVaultToVectorStore === VAULT_VECTOR_STORE_STRATEGY.ON_STARTUP) {
|
||||
try {
|
||||
await this.indexVaultToVectorStore();
|
||||
} catch (err) {
|
||||
|
|
@ -451,9 +375,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("editor-menu", this.handleContextMenu),
|
||||
);
|
||||
this.registerEvent(this.app.workspace.on("editor-menu", this.handleContextMenu));
|
||||
}
|
||||
|
||||
private getVaultIdentifier(): string {
|
||||
|
|
@ -486,19 +408,12 @@ export default class CopilotPlugin extends Plugin {
|
|||
}
|
||||
|
||||
// Check if embedding model has changed
|
||||
const prevEmbeddingModel = await VectorDBManager.checkEmbeddingModel(
|
||||
this.dbVectorStores,
|
||||
);
|
||||
const prevEmbeddingModel = await VectorDBManager.checkEmbeddingModel(this.dbVectorStores);
|
||||
// TODO: Remove this when Ollama model is dynamically set
|
||||
const currEmbeddingModel =
|
||||
EmbeddingsManager.getModelName(embeddingInstance);
|
||||
const currEmbeddingModel = EmbeddingsManager.getModelName(embeddingInstance);
|
||||
|
||||
if (this.settings.debug)
|
||||
console.log(
|
||||
"Prev vs Current embedding models:",
|
||||
prevEmbeddingModel,
|
||||
currEmbeddingModel,
|
||||
);
|
||||
console.log("Prev vs Current embedding models:", prevEmbeddingModel, currEmbeddingModel);
|
||||
|
||||
if (!areEmbeddingModelsSame(prevEmbeddingModel, currEmbeddingModel)) {
|
||||
// Model has changed, clear DB and reindex from scratch
|
||||
|
|
@ -509,23 +424,17 @@ export default class CopilotPlugin extends Plugin {
|
|||
await this.dbVectorStores.destroy();
|
||||
// Reinitialize the database
|
||||
this.dbVectorStores = new PouchDB<VectorStoreDocument>(
|
||||
`copilot_vector_stores_${this.getVaultIdentifier()}`,
|
||||
);
|
||||
new Notice(
|
||||
"Detected change in embedding model. Rebuild vector store from scratch.",
|
||||
);
|
||||
console.log(
|
||||
"Detected change in embedding model. Rebuild vector store from scratch.",
|
||||
`copilot_vector_stores_${this.getVaultIdentifier()}`
|
||||
);
|
||||
new Notice("Detected change in embedding model. Rebuild vector store from scratch.");
|
||||
console.log("Detected change in embedding model. Rebuild vector store from scratch.");
|
||||
} catch (err) {
|
||||
console.error("Error clearing vector store for reindexing:", err);
|
||||
new Notice("Error clearing vector store for reindexing.");
|
||||
}
|
||||
}
|
||||
|
||||
const latestMtime = await VectorDBManager.getLatestFileMtime(
|
||||
this.dbVectorStores,
|
||||
);
|
||||
const latestMtime = await VectorDBManager.getLatestFileMtime(this.dbVectorStores);
|
||||
|
||||
const files = this.app.vault
|
||||
.getMarkdownFiles()
|
||||
|
|
@ -540,11 +449,9 @@ export default class CopilotPlugin extends Plugin {
|
|||
});
|
||||
|
||||
const fileContents: string[] = await Promise.all(
|
||||
files.map((file) => this.app.vault.cachedRead(file)),
|
||||
);
|
||||
const fileMetadatas = files.map((file) =>
|
||||
this.app.metadataCache.getFileCache(file),
|
||||
files.map((file) => this.app.vault.cachedRead(file))
|
||||
);
|
||||
const fileMetadatas = files.map((file) => this.app.metadataCache.getFileCache(file));
|
||||
|
||||
const totalFiles = files.length;
|
||||
if (totalFiles === 0) {
|
||||
|
|
@ -555,7 +462,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
let indexedCount = 0;
|
||||
const indexNotice = new Notice(
|
||||
`Copilot is indexing your vault...\n0/${totalFiles} files processed.`,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
const errors: string[] = [];
|
||||
|
|
@ -571,12 +478,12 @@ export default class CopilotPlugin extends Plugin {
|
|||
const result = await VectorDBManager.indexFile(
|
||||
this.dbVectorStores,
|
||||
embeddingInstance,
|
||||
noteFile,
|
||||
noteFile
|
||||
);
|
||||
|
||||
indexedCount++;
|
||||
indexNotice.setMessage(
|
||||
`Copilot is indexing your vault...\n${indexedCount}/${totalFiles} files processed.`,
|
||||
`Copilot is indexing your vault...\n${indexedCount}/${totalFiles} files processed.`
|
||||
);
|
||||
return result;
|
||||
} catch (err) {
|
||||
|
|
@ -591,9 +498,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
}, 2000);
|
||||
|
||||
if (errors.length > 0) {
|
||||
new Notice(
|
||||
`Indexing completed with errors. Check the console for details.`,
|
||||
);
|
||||
new Notice(`Indexing completed with errors. Check the console for details.`);
|
||||
console.log("Indexing Errors:", errors.join("\n"));
|
||||
}
|
||||
return files.length;
|
||||
|
|
@ -603,12 +508,11 @@ export default class CopilotPlugin extends Plugin {
|
|||
editor: Editor,
|
||||
eventType: string,
|
||||
eventSubtype?: string,
|
||||
checkSelectedText = true,
|
||||
checkSelectedText = true
|
||||
) {
|
||||
const selectedText = await editor.getSelection();
|
||||
|
||||
const isChatWindowActive =
|
||||
this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE).length > 0;
|
||||
const isChatWindowActive = this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE).length > 0;
|
||||
|
||||
if (!isChatWindowActive) {
|
||||
await this.activateView();
|
||||
|
|
@ -639,9 +543,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
const activeFile = this.app.workspace.getActiveFile();
|
||||
return activeFile ? this.app.vault.cachedRead(activeFile) : "";
|
||||
},
|
||||
replaceSelection:
|
||||
activeView?.editor?.replaceSelection.bind(activeView.editor) ||
|
||||
(() => {}),
|
||||
replaceSelection: activeView?.editor?.replaceSelection.bind(activeView.editor) || (() => {}),
|
||||
} as Partial<Editor> as Editor;
|
||||
}
|
||||
|
||||
|
|
@ -657,16 +559,12 @@ export default class CopilotPlugin extends Plugin {
|
|||
|
||||
async activateView() {
|
||||
this.app.workspace.detachLeavesOfType(CHAT_VIEWTYPE);
|
||||
this.activateViewPromise = this.app.workspace
|
||||
.getRightLeaf(false)
|
||||
.setViewState({
|
||||
this.activateViewPromise = this.app.workspace.getRightLeaf(false).setViewState({
|
||||
type: CHAT_VIEWTYPE,
|
||||
active: true,
|
||||
});
|
||||
await this.activateViewPromise;
|
||||
this.app.workspace.revealLeaf(
|
||||
this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0],
|
||||
);
|
||||
this.app.workspace.revealLeaf(this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0]);
|
||||
this.chatIsVisible = true;
|
||||
}
|
||||
|
||||
|
|
@ -687,9 +585,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
active: true,
|
||||
});
|
||||
await this.activateViewPromise;
|
||||
this.app.workspace.revealLeaf(
|
||||
this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0],
|
||||
);
|
||||
this.app.workspace.revealLeaf(this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0]);
|
||||
this.chatIsVisible = true;
|
||||
}
|
||||
|
||||
|
|
@ -707,16 +603,13 @@ export default class CopilotPlugin extends Plugin {
|
|||
|
||||
async fetchPromptTitles(): Promise<string[]> {
|
||||
const response = await this.dbPrompts.allDocs({ include_docs: true });
|
||||
return response.rows
|
||||
.map((row) => (row.doc as CustomPrompt)?._id)
|
||||
.filter(Boolean) as string[];
|
||||
return response.rows.map((row) => (row.doc as CustomPrompt)?._id).filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
async countTotalTokens(): Promise<number> {
|
||||
try {
|
||||
const allContent = await getAllNotesContent(this.app.vault);
|
||||
const totalTokens =
|
||||
await this.chainManager.chatModelManager.countTokens(allContent);
|
||||
const totalTokens = await this.chainManager.chatModelManager.countTokens(allContent);
|
||||
return totalTokens;
|
||||
} catch (error) {
|
||||
console.error("Error counting tokens: ", error);
|
||||
|
|
@ -802,8 +695,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
useOpenAILocalProxy: this.settings.useOpenAILocalProxy,
|
||||
openAIProxyModelName: this.settings.openAIProxyModelName,
|
||||
openAIEmbeddingProxyBaseUrl: this.settings.openAIEmbeddingProxyBaseUrl,
|
||||
openAIEmbeddingProxyModelName:
|
||||
this.settings.openAIEmbeddingProxyModelName,
|
||||
openAIEmbeddingProxyModelName: this.settings.openAIEmbeddingProxyModelName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
|||
private db: PouchDB.Database,
|
||||
private vault: Vault,
|
||||
private options: ScoreThresholdRetrieverInput<V>,
|
||||
private debug?: boolean,
|
||||
private debug?: boolean
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
|||
"\nNote Titles extracted: ",
|
||||
noteTitles,
|
||||
"\nExplicit Chunks:",
|
||||
explicitChunks,
|
||||
explicitChunks
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -42,9 +42,7 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
|||
const vectorChunks = await this.getVectorChunks(query);
|
||||
|
||||
// Combine explicit and vector chunks, removing duplicates while maintaining order
|
||||
const uniqueChunks = new Set<string>(
|
||||
explicitChunks.map((chunk) => chunk.pageContent),
|
||||
);
|
||||
const uniqueChunks = new Set<string>(explicitChunks.map((chunk) => chunk.pageContent));
|
||||
const combinedChunks: Document[] = [...explicitChunks];
|
||||
|
||||
for (const chunk of vectorChunks) {
|
||||
|
|
@ -64,17 +62,14 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
|||
for (const noteTitle of noteTitles) {
|
||||
const noteFile = await getNoteFileFromTitle(this.vault, noteTitle);
|
||||
const docHash = VectorDBManager.getDocumentHash(noteFile?.path ?? "");
|
||||
const memoryVectors = await VectorDBManager.getMemoryVectors(
|
||||
this.db,
|
||||
docHash,
|
||||
);
|
||||
const memoryVectors = await VectorDBManager.getMemoryVectors(this.db, docHash);
|
||||
if (memoryVectors) {
|
||||
const matchingChunks = memoryVectors.map(
|
||||
(memoryVector) =>
|
||||
new Document({
|
||||
pageContent: memoryVector.content,
|
||||
metadata: memoryVector.metadata,
|
||||
}),
|
||||
})
|
||||
);
|
||||
explicitChunks.push(...matchingChunks);
|
||||
}
|
||||
|
|
@ -83,14 +78,11 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
|||
}
|
||||
|
||||
private async getVectorChunks(query: string): Promise<Document[]> {
|
||||
const retriever = ScoreThresholdRetriever.fromVectorStore(
|
||||
this.options.vectorStore,
|
||||
{
|
||||
const retriever = ScoreThresholdRetriever.fromVectorStore(this.options.vectorStore, {
|
||||
minSimilarityScore: this.options.minSimilarityScore,
|
||||
maxK: this.options.maxK,
|
||||
kIncrement: this.options.kIncrement,
|
||||
},
|
||||
);
|
||||
});
|
||||
const vectorChunks = await retriever.getRelevantDocuments(query);
|
||||
return vectorChunks;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface ChatMessage {
|
||||
message: string;
|
||||
|
|
@ -24,17 +24,11 @@ class SharedState {
|
|||
|
||||
export function useSharedState(
|
||||
sharedState: SharedState
|
||||
): [
|
||||
ChatMessage[],
|
||||
(message: ChatMessage) => void,
|
||||
() => void
|
||||
] {
|
||||
): [ChatMessage[], (message: ChatMessage) => void, () => void] {
|
||||
// Initializes the local chatHistory state with the current
|
||||
// sharedState chatHistory using the useState hook
|
||||
// setChatHistory is used to update the *local* state
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessage[]>(
|
||||
sharedState.getMessages()
|
||||
);
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessage[]>(sharedState.getMessages());
|
||||
|
||||
// The useEffect hook ensures that the local state is synchronized
|
||||
// with the shared state when the component is mounted.
|
||||
|
|
@ -56,11 +50,7 @@ export function useSharedState(
|
|||
setChatHistory([]);
|
||||
};
|
||||
|
||||
return [
|
||||
chatHistory,
|
||||
addMessage,
|
||||
clearMessages,
|
||||
];
|
||||
return [chatHistory, addMessage, clearMessages];
|
||||
}
|
||||
|
||||
export default SharedState;
|
||||
|
|
|
|||
127
src/utils.ts
127
src/utils.ts
|
|
@ -13,20 +13,12 @@ import { BaseChain, RetrievalQAChain } from "langchain/chains";
|
|||
import moment from "moment";
|
||||
import { TFile, Vault, parseYaml } from "obsidian";
|
||||
|
||||
export const isFolderMatch = (
|
||||
fileFullpath: string,
|
||||
inputPath: string,
|
||||
): boolean => {
|
||||
const fileSegments = fileFullpath
|
||||
.split("/")
|
||||
.map((segment) => segment.toLowerCase());
|
||||
export const isFolderMatch = (fileFullpath: string, inputPath: string): boolean => {
|
||||
const fileSegments = fileFullpath.split("/").map((segment) => segment.toLowerCase());
|
||||
return fileSegments.includes(inputPath.toLowerCase());
|
||||
};
|
||||
|
||||
export async function getNoteFileFromTitle(
|
||||
vault: Vault,
|
||||
noteTitle: string,
|
||||
): Promise<TFile | null> {
|
||||
export async function getNoteFileFromTitle(vault: Vault, noteTitle: string): Promise<TFile | null> {
|
||||
// Get all markdown files in the vault
|
||||
const files = vault.getMarkdownFiles();
|
||||
|
||||
|
|
@ -45,10 +37,7 @@ export async function getNoteFileFromTitle(
|
|||
return null;
|
||||
}
|
||||
|
||||
export const getNotesFromPath = async (
|
||||
vault: Vault,
|
||||
path: string,
|
||||
): Promise<TFile[]> => {
|
||||
export const getNotesFromPath = async (vault: Vault, path: string): Promise<TFile[]> => {
|
||||
const files = vault.getMarkdownFiles();
|
||||
|
||||
// Special handling for the root path '/'
|
||||
|
|
@ -83,10 +72,7 @@ export const getNotesFromPath = async (
|
|||
});
|
||||
};
|
||||
|
||||
export async function getTagsFromNote(
|
||||
file: TFile,
|
||||
vault: Vault,
|
||||
): Promise<string[]> {
|
||||
export async function getTagsFromNote(file: TFile, vault: Vault): Promise<string[]> {
|
||||
const fileContent = await vault.cachedRead(file);
|
||||
// Check if the file starts with frontmatter delimiter
|
||||
if (fileContent.startsWith("---")) {
|
||||
|
|
@ -113,7 +99,7 @@ export async function getTagsFromNote(
|
|||
export async function getNotesFromTags(
|
||||
vault: Vault,
|
||||
tags: string[],
|
||||
noteFiles?: TFile[],
|
||||
noteFiles?: TFile[]
|
||||
): Promise<TFile[]> {
|
||||
if (tags.length === 0) {
|
||||
return [];
|
||||
|
|
@ -122,10 +108,7 @@ export async function getNotesFromTags(
|
|||
// Strip any '#' from the tags set from the user
|
||||
tags = tags.map((tag) => tag.replace("#", ""));
|
||||
|
||||
const files =
|
||||
noteFiles && noteFiles.length > 0
|
||||
? noteFiles
|
||||
: await getNotesFromPath(vault, "/");
|
||||
const files = noteFiles && noteFiles.length > 0 ? noteFiles : await getNotesFromPath(vault, "/");
|
||||
const filesWithTag = [];
|
||||
|
||||
for (const file of files) {
|
||||
|
|
@ -155,7 +138,7 @@ export function isPathInList(filePath: string, pathList: string): boolean {
|
|||
.trim() // Trim whitespace
|
||||
.replace(/^\[\[|\]\]$/g, "") // Remove surrounding [[ and ]]
|
||||
.replace(/^\//, "") // Remove leading slash
|
||||
.toLowerCase(), // Convert to lowercase for case-insensitive comparison
|
||||
.toLowerCase() // Convert to lowercase for case-insensitive comparison
|
||||
)
|
||||
.some((normalizedPath) => {
|
||||
// Check for exact match or proper segmentation
|
||||
|
|
@ -184,23 +167,17 @@ export const stringToChainType = (chain: string): ChainType => {
|
|||
}
|
||||
};
|
||||
|
||||
export const isLLMChain = (
|
||||
chain: RunnableSequence,
|
||||
): chain is RunnableSequence => {
|
||||
export const isLLMChain = (chain: RunnableSequence): chain is RunnableSequence => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (chain as any).last.bound.modelName || (chain as any).last.bound.model;
|
||||
};
|
||||
|
||||
export const isRetrievalQAChain = (
|
||||
chain: BaseChain,
|
||||
): chain is RetrievalQAChain => {
|
||||
export const isRetrievalQAChain = (chain: BaseChain): chain is RetrievalQAChain => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (chain as any).last.bound.retriever !== undefined;
|
||||
};
|
||||
|
||||
export const isSupportedChain = (
|
||||
chain: RunnableSequence,
|
||||
): chain is RunnableSequence => {
|
||||
export const isSupportedChain = (chain: RunnableSequence): chain is RunnableSequence => {
|
||||
return isLLMChain(chain) || isRetrievalQAChain(chain);
|
||||
};
|
||||
|
||||
|
|
@ -210,10 +187,7 @@ export const getModelName = (modelDisplayName: string): string => {
|
|||
|
||||
// Returns the last N messages from the chat history,
|
||||
// last one being the newest ai message
|
||||
export const getChatContext = (
|
||||
chatHistory: ChatMessage[],
|
||||
contextSize: number,
|
||||
) => {
|
||||
export const getChatContext = (chatHistory: ChatMessage[], contextSize: number) => {
|
||||
if (chatHistory.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -231,10 +205,7 @@ export const getChatContext = (
|
|||
return chatHistory.slice(startIndex, lastIndex + 1);
|
||||
};
|
||||
|
||||
export const formatDateTime = (
|
||||
now: Date,
|
||||
timezone: "local" | "utc" = "local",
|
||||
) => {
|
||||
export const formatDateTime = (now: Date, timezone: "local" | "utc" = "local") => {
|
||||
const formattedDateTime = moment(now);
|
||||
|
||||
if (timezone === "utc") {
|
||||
|
|
@ -244,10 +215,7 @@ export const formatDateTime = (
|
|||
return formattedDateTime.format("YYYY_MM_DD-HH_mm_ss");
|
||||
};
|
||||
|
||||
export async function getFileContent(
|
||||
file: TFile,
|
||||
vault: Vault,
|
||||
): Promise<string | null> {
|
||||
export async function getFileContent(file: TFile, vault: Vault): Promise<string | null> {
|
||||
if (file.extension != "md") return null;
|
||||
return await vault.cachedRead(file);
|
||||
}
|
||||
|
|
@ -271,7 +239,7 @@ export async function getAllNotesContent(vault: Vault): Promise<string> {
|
|||
|
||||
export function areEmbeddingModelsSame(
|
||||
model1: string | undefined,
|
||||
model2: string | undefined,
|
||||
model2: string | undefined
|
||||
): boolean {
|
||||
if (!model1 || !model2) return false;
|
||||
// TODO: Hacks to handle different embedding model names for the same model. Need better handling.
|
||||
|
|
@ -292,14 +260,10 @@ export function sanitizeSettings(settings: CopilotSettings): CopilotSettings {
|
|||
|
||||
// Stuff in settings are string even when the interface has number type!
|
||||
const temperature = Number(settings.temperature);
|
||||
sanitizedSettings.temperature = isNaN(temperature)
|
||||
? DEFAULT_SETTINGS.temperature
|
||||
: temperature;
|
||||
sanitizedSettings.temperature = isNaN(temperature) ? DEFAULT_SETTINGS.temperature : temperature;
|
||||
|
||||
const maxTokens = Number(settings.maxTokens);
|
||||
sanitizedSettings.maxTokens = isNaN(maxTokens)
|
||||
? DEFAULT_SETTINGS.maxTokens
|
||||
: maxTokens;
|
||||
sanitizedSettings.maxTokens = isNaN(maxTokens) ? DEFAULT_SETTINGS.maxTokens : maxTokens;
|
||||
|
||||
const contextTurns = Number(settings.contextTurns);
|
||||
sanitizedSettings.contextTurns = isNaN(contextTurns)
|
||||
|
|
@ -311,10 +275,7 @@ export function sanitizeSettings(settings: CopilotSettings): CopilotSettings {
|
|||
|
||||
// Basic prompts
|
||||
// Note that GPT4 is much better at following instructions than GPT3.5!
|
||||
export function sendNoteContentPrompt(
|
||||
noteName: string,
|
||||
noteContent: string | null,
|
||||
): string {
|
||||
export function sendNoteContentPrompt(noteName: string, noteContent: string | null): string {
|
||||
return (
|
||||
`Please read the note below and be ready to answer questions about it. ` +
|
||||
`If there's no information about a certain topic, just say the note ` +
|
||||
|
|
@ -326,9 +287,7 @@ export function sendNoteContentPrompt(
|
|||
);
|
||||
}
|
||||
|
||||
export function sendNotesContentPrompt(
|
||||
notes: { name: string; content: string }[],
|
||||
): string {
|
||||
export function sendNotesContentPrompt(notes: { name: string; content: string }[]): string {
|
||||
return (
|
||||
`Please read the notes below and be ready to answer questions about them. ` +
|
||||
`If there's no information about a certain topic, just say the note ` +
|
||||
|
|
@ -347,34 +306,23 @@ function getNoteTitleAndTags(noteWithTag: {
|
|||
}): string {
|
||||
return (
|
||||
`[[${noteWithTag.name}]]` +
|
||||
(noteWithTag.tags && noteWithTag.tags.length > 0
|
||||
? `\ntags: ${noteWithTag.tags.join(",")}`
|
||||
: "")
|
||||
(noteWithTag.tags && noteWithTag.tags.length > 0 ? `\ntags: ${noteWithTag.tags.join(",")}` : "")
|
||||
);
|
||||
}
|
||||
|
||||
function getChatContextStr(
|
||||
chatNoteContextPath: string,
|
||||
chatNoteContextTags: string[],
|
||||
): string {
|
||||
const pathStr = chatNoteContextPath
|
||||
? `\nChat context by path: ${chatNoteContextPath}`
|
||||
: "";
|
||||
function getChatContextStr(chatNoteContextPath: string, chatNoteContextTags: string[]): string {
|
||||
const pathStr = chatNoteContextPath ? `\nChat context by path: ${chatNoteContextPath}` : "";
|
||||
const tagsStr =
|
||||
chatNoteContextTags?.length > 0
|
||||
? `\nChat context by tags: ${chatNoteContextTags}`
|
||||
: "";
|
||||
chatNoteContextTags?.length > 0 ? `\nChat context by tags: ${chatNoteContextTags}` : "";
|
||||
return pathStr + tagsStr;
|
||||
}
|
||||
|
||||
export function getSendChatContextNotesPrompt(
|
||||
notes: { name: string; content: string }[],
|
||||
chatNoteContextPath: string,
|
||||
chatNoteContextTags: string[],
|
||||
chatNoteContextTags: string[]
|
||||
): string {
|
||||
const noteTitles = notes
|
||||
.map((note) => getNoteTitleAndTags(note))
|
||||
.join("\n\n");
|
||||
const noteTitles = notes.map((note) => getNoteTitleAndTags(note)).join("\n\n");
|
||||
return (
|
||||
`Please read the notes below and be ready to answer questions about them. ` +
|
||||
getChatContextStr(chatNoteContextPath, chatNoteContextTags) +
|
||||
|
|
@ -382,9 +330,7 @@ export function getSendChatContextNotesPrompt(
|
|||
);
|
||||
}
|
||||
|
||||
export function fixGrammarSpellingSelectionPrompt(
|
||||
selectedText: string,
|
||||
): string {
|
||||
export function fixGrammarSpellingSelectionPrompt(selectedText: string): string {
|
||||
return (
|
||||
`Please fix the grammar and spelling of the following text and return it without any other changes:\n\n` +
|
||||
`${selectedText}`
|
||||
|
|
@ -439,9 +385,7 @@ export function rewriteTweetSelectionPrompt(selectedText: string): string {
|
|||
+ ${selectedText}`;
|
||||
}
|
||||
|
||||
export function rewriteTweetThreadSelectionPrompt(
|
||||
selectedText: string,
|
||||
): string {
|
||||
export function rewriteTweetThreadSelectionPrompt(selectedText: string): string {
|
||||
return (
|
||||
`Please follow the instructions closely step by step and rewrite the content to a thread. ` +
|
||||
`1. Each paragraph must be under 240 characters. ` +
|
||||
|
|
@ -474,9 +418,7 @@ export function eli5SelectionPrompt(selectedText: string): string {
|
|||
);
|
||||
}
|
||||
|
||||
export function rewritePressReleaseSelectionPrompt(
|
||||
selectedText: string,
|
||||
): string {
|
||||
export function rewritePressReleaseSelectionPrompt(selectedText: string): string {
|
||||
return (
|
||||
`Please rewrite the following text to make it sound like a press release. Output in the same language as the source, do not output English if it is not English:\n\n` +
|
||||
`${selectedText}`
|
||||
|
|
@ -485,10 +427,7 @@ export function rewritePressReleaseSelectionPrompt(
|
|||
|
||||
export function createTranslateSelectionPrompt(language?: string) {
|
||||
return (selectedText: string): string => {
|
||||
return (
|
||||
`Please translate the following text to ${language}:\n\n` +
|
||||
`${selectedText}`
|
||||
);
|
||||
return `Please translate the following text to ${language}:\n\n` + `${selectedText}`;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -501,9 +440,7 @@ export function createChangeToneSelectionPrompt(tone?: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export function extractChatHistory(
|
||||
memoryVariables: MemoryVariables,
|
||||
): [string, string][] {
|
||||
export function extractChatHistory(memoryVariables: MemoryVariables): [string, string][] {
|
||||
const chatHistory: [string, string][] = [];
|
||||
const { history } = memoryVariables;
|
||||
|
||||
|
|
@ -520,9 +457,7 @@ export function extractNoteTitles(query: string): string[] {
|
|||
// Use a regular expression to extract note titles wrapped in [[]]
|
||||
const regex = /\[\[(.*?)\]\]/g;
|
||||
const matches = query.match(regex);
|
||||
const uniqueTitles = new Set(
|
||||
matches ? matches.map((match) => match.slice(2, -2)) : [],
|
||||
);
|
||||
const uniqueTitles = new Set(matches ? matches.map((match) => match.slice(2, -2)) : []);
|
||||
return Array.from(uniqueTitles);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,21 +41,19 @@ class VectorDBManager {
|
|||
|
||||
public static async rebuildMemoryVectorStore(
|
||||
memoryVectors: MemoryVector[],
|
||||
embeddingsAPI: Embeddings,
|
||||
embeddingsAPI: Embeddings
|
||||
) {
|
||||
if (!Array.isArray(memoryVectors)) {
|
||||
throw new TypeError("Expected memoryVectors to be an array");
|
||||
}
|
||||
// Extract the embeddings and documents from the deserialized memoryVectors
|
||||
const embeddingsArray: number[][] = memoryVectors.map(
|
||||
(memoryVector) => memoryVector.embedding,
|
||||
);
|
||||
const embeddingsArray: number[][] = memoryVectors.map((memoryVector) => memoryVector.embedding);
|
||||
const documentsArray = memoryVectors.map(
|
||||
(memoryVector) =>
|
||||
new Document({
|
||||
pageContent: memoryVector.content,
|
||||
metadata: memoryVector.metadata,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// Create a new MemoryVectorStore instance
|
||||
|
|
@ -67,7 +65,7 @@ class VectorDBManager {
|
|||
public static async getMemoryVectorStore(
|
||||
db: PouchDB.Database,
|
||||
embeddingsAPI: Embeddings,
|
||||
docHash?: string,
|
||||
docHash?: string
|
||||
): Promise<MemoryVectorStore> {
|
||||
if (!db) throw new Error("DB not initialized");
|
||||
|
||||
|
|
@ -82,23 +80,20 @@ class VectorDBManager {
|
|||
}
|
||||
|
||||
const embeddingModel = EmbeddingManager.getModelName(embeddingsAPI);
|
||||
if (!embeddingModel)
|
||||
console.error("EmbeddingManager could not determine model name!");
|
||||
if (!embeddingModel) console.error("EmbeddingManager could not determine model name!");
|
||||
const allDocs = allDocsResponse.rows
|
||||
.map((row) => row.doc as VectorStoreDocument)
|
||||
.filter((doc) => doc.embeddingModel === embeddingModel);
|
||||
const memoryVectors = allDocs
|
||||
.map((doc) => JSON.parse(doc.memory_vectors) as MemoryVector[])
|
||||
.flat();
|
||||
const embeddingsArray: number[][] = memoryVectors.map(
|
||||
(memoryVector) => memoryVector.embedding,
|
||||
);
|
||||
const embeddingsArray: number[][] = memoryVectors.map((memoryVector) => memoryVector.embedding);
|
||||
const documentsArray = memoryVectors.map(
|
||||
(memoryVector) =>
|
||||
new Document({
|
||||
pageContent: memoryVector.content,
|
||||
metadata: memoryVector.metadata,
|
||||
}),
|
||||
})
|
||||
);
|
||||
// Create a new MemoryVectorStore instance
|
||||
const memoryVectorStore = new MemoryVectorStore(embeddingsAPI);
|
||||
|
|
@ -109,7 +104,7 @@ class VectorDBManager {
|
|||
public static async setMemoryVectors(
|
||||
db: PouchDB.Database,
|
||||
memoryVectors: MemoryVector[],
|
||||
docHash: string,
|
||||
docHash: string
|
||||
): Promise<void> {
|
||||
if (!db) throw new Error("DB not initialized");
|
||||
if (!Array.isArray(memoryVectors)) {
|
||||
|
|
@ -138,33 +133,25 @@ class VectorDBManager {
|
|||
public static async indexFile(
|
||||
db: PouchDB.Database,
|
||||
embeddingsAPI: Embeddings,
|
||||
noteFile: NoteFile,
|
||||
noteFile: NoteFile
|
||||
): Promise<VectorStoreDocument | undefined> {
|
||||
if (!db) throw new Error("DB not initialized");
|
||||
|
||||
const embeddingModel = EmbeddingManager.getModelName(embeddingsAPI);
|
||||
if (!embeddingModel)
|
||||
console.error("EmbeddingManager could not determine model name!");
|
||||
if (!embeddingModel) console.error("EmbeddingManager could not determine model name!");
|
||||
|
||||
// Markdown splitter: https://js.langchain.com/docs/modules/data_connection/document_transformers/code_splitter#markdown
|
||||
const textSplitter = RecursiveCharacterTextSplitter.fromLanguage(
|
||||
"markdown",
|
||||
{
|
||||
const textSplitter = RecursiveCharacterTextSplitter.fromLanguage("markdown", {
|
||||
chunkSize: 5000,
|
||||
},
|
||||
);
|
||||
});
|
||||
// Add note title as contextual chunk headers
|
||||
// https://js.langchain.com/docs/modules/data_connection/document_transformers/contextual_chunk_headers
|
||||
const splitDocument = await textSplitter.createDocuments(
|
||||
[noteFile.content],
|
||||
[],
|
||||
{
|
||||
const splitDocument = await textSplitter.createDocuments([noteFile.content], [], {
|
||||
chunkHeader: "[[" + noteFile.basename + "]]" + "\n\n---\n\n",
|
||||
appendChunkOverlapHeader: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
const docVectors = await embeddingsAPI.embedDocuments(
|
||||
splitDocument.map((doc) => doc.pageContent),
|
||||
splitDocument.map((doc) => doc.pageContent)
|
||||
);
|
||||
const memoryVectors = docVectors.map((docVector, i) => ({
|
||||
content: splitDocument[i].pageContent,
|
||||
|
|
@ -206,9 +193,7 @@ class VectorDBManager {
|
|||
const allDocsResponse = await db.allDocs<VectorStoreDocument>({
|
||||
include_docs: true,
|
||||
});
|
||||
const allDocs = allDocsResponse.rows.map(
|
||||
(row) => row.doc as VectorStoreDocument,
|
||||
);
|
||||
const allDocs = allDocsResponse.rows.map((row) => row.doc as VectorStoreDocument);
|
||||
const memoryVectors = allDocs
|
||||
.map((doc) => JSON.parse(doc.memory_vectors) as MemoryVector[])
|
||||
.filter((memoryVectors) => memoryVectors.length > 0);
|
||||
|
|
@ -233,10 +218,7 @@ class VectorDBManager {
|
|||
}
|
||||
}
|
||||
|
||||
public static async removeMemoryVectors(
|
||||
db: PouchDB.Database,
|
||||
docHash: string,
|
||||
): Promise<void> {
|
||||
public static async removeMemoryVectors(db: PouchDB.Database, docHash: string): Promise<void> {
|
||||
if (!db) throw new Error("DB not initialized");
|
||||
try {
|
||||
const doc = await db.get(docHash);
|
||||
|
|
@ -248,21 +230,15 @@ class VectorDBManager {
|
|||
}
|
||||
}
|
||||
|
||||
public static async getLatestFileMtime(
|
||||
db: PouchDB.Database,
|
||||
): Promise<number> {
|
||||
public static async getLatestFileMtime(db: PouchDB.Database): Promise<number> {
|
||||
if (!db) throw new Error("DB not initialized");
|
||||
|
||||
try {
|
||||
const allDocsResponse = await db.allDocs<VectorStoreDocument>({
|
||||
include_docs: true,
|
||||
});
|
||||
const allDocs = allDocsResponse.rows.map(
|
||||
(row) => row.doc as VectorStoreDocument,
|
||||
);
|
||||
const newestFileMtime = allDocs
|
||||
.map((doc) => doc.file_mtime)
|
||||
.sort((a, b) => b - a)[0];
|
||||
const allDocs = allDocsResponse.rows.map((row) => row.doc as VectorStoreDocument);
|
||||
const newestFileMtime = allDocs.map((doc) => doc.file_mtime).sort((a, b) => b - a)[0];
|
||||
return newestFileMtime;
|
||||
} catch (err) {
|
||||
console.error("Error getting newest file mtime from VectorDB:", err);
|
||||
|
|
@ -270,9 +246,7 @@ class VectorDBManager {
|
|||
}
|
||||
}
|
||||
|
||||
public static async checkEmbeddingModel(
|
||||
db: PouchDB.Database,
|
||||
): Promise<string | undefined> {
|
||||
public static async checkEmbeddingModel(db: PouchDB.Database): Promise<string | undefined> {
|
||||
if (!db) throw new Error("DB not initialized");
|
||||
|
||||
try {
|
||||
|
|
@ -280,9 +254,7 @@ class VectorDBManager {
|
|||
const allDocsResponse = await db.allDocs<VectorStoreDocument>({
|
||||
include_docs: true,
|
||||
});
|
||||
const allDocs = allDocsResponse.rows.map(
|
||||
(row) => row.doc as VectorStoreDocument,
|
||||
);
|
||||
const allDocs = allDocsResponse.rows.map((row) => row.doc as VectorStoreDocument);
|
||||
|
||||
// Check if there are any documents
|
||||
if (allDocs.length === 0) {
|
||||
|
|
@ -293,9 +265,7 @@ class VectorDBManager {
|
|||
const embeddingModels = allDocs.map((doc) => doc.embeddingModel);
|
||||
|
||||
// Check if all documents have the same embeddingModel
|
||||
const allSame = embeddingModels.every(
|
||||
(model) => model === embeddingModels[0],
|
||||
);
|
||||
const allSame = embeddingModels.every((model) => model === embeddingModels[0]);
|
||||
return allSame ? embeddingModels[0] : undefined;
|
||||
} catch (err) {
|
||||
console.error("Error checking last embedding model from VectorDB:", err);
|
||||
|
|
@ -305,7 +275,7 @@ class VectorDBManager {
|
|||
|
||||
public static async getMemoryVectors(
|
||||
db: PouchDB.Database,
|
||||
docHash: string,
|
||||
docHash: string
|
||||
): Promise<MemoryVector[] | undefined> {
|
||||
if (!db) throw new Error("DB not initialized");
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { CustomPrompt, CustomPromptProcessor } from '@/customPromptProcessor';
|
||||
import { CustomPrompt, CustomPromptProcessor } from "@/customPromptProcessor";
|
||||
|
||||
// Mocking Obsidian Vault
|
||||
const mockVault = {
|
||||
read: jest.fn(),
|
||||
write: jest.fn(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
|
||||
describe('CustomPromptProcessor', () => {
|
||||
describe("CustomPromptProcessor", () => {
|
||||
let processor: CustomPromptProcessor;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -18,99 +18,98 @@ describe('CustomPromptProcessor', () => {
|
|||
processor = CustomPromptProcessor.getInstance(mockVault);
|
||||
});
|
||||
|
||||
it('should add 1 context and selectedText', async () => {
|
||||
it("should add 1 context and selectedText", async () => {
|
||||
const doc: CustomPrompt = {
|
||||
_id: 'test-prompt',
|
||||
prompt: 'This is a {variable} and {}.'
|
||||
_id: "test-prompt",
|
||||
prompt: "This is a {variable} and {}.",
|
||||
};
|
||||
const selectedText = 'here is some selected text 12345';
|
||||
const selectedText = "here is some selected text 12345";
|
||||
|
||||
// Mock the extractVariablesFromPrompt method to return predefined content
|
||||
jest.spyOn(processor, 'extractVariablesFromPrompt').mockResolvedValue(['here is the note content for note0']);
|
||||
jest
|
||||
.spyOn(processor, "extractVariablesFromPrompt")
|
||||
.mockResolvedValue(["here is the note content for note0"]);
|
||||
|
||||
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
||||
|
||||
expect(result).toContain('This is a {variable} and {selectedText}.');
|
||||
expect(result).toContain('here is some selected text 12345');
|
||||
expect(result).toContain('here is the note content for note0');
|
||||
expect(result).toContain("This is a {variable} and {selectedText}.");
|
||||
expect(result).toContain("here is some selected text 12345");
|
||||
expect(result).toContain("here is the note content for note0");
|
||||
});
|
||||
|
||||
it('should add 2 context and no selectedText', async () => {
|
||||
it("should add 2 context and no selectedText", async () => {
|
||||
const doc: CustomPrompt = {
|
||||
_id: 'test-prompt',
|
||||
prompt: 'This is a {variable} and {var2}.'
|
||||
_id: "test-prompt",
|
||||
prompt: "This is a {variable} and {var2}.",
|
||||
};
|
||||
const selectedText = 'here is some selected text 12345';
|
||||
const selectedText = "here is some selected text 12345";
|
||||
|
||||
// Mock the extractVariablesFromPrompt method to return predefined content
|
||||
jest.spyOn(processor, 'extractVariablesFromPrompt').mockResolvedValue([
|
||||
'here is the note content for note0',
|
||||
'note content for note1'
|
||||
]);
|
||||
jest
|
||||
.spyOn(processor, "extractVariablesFromPrompt")
|
||||
.mockResolvedValue(["here is the note content for note0", "note content for note1"]);
|
||||
|
||||
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
||||
|
||||
expect(result).toContain('This is a {variable} and {var2}.');
|
||||
expect(result).toContain('here is the note content for note0');
|
||||
expect(result).toContain('note content for note1');
|
||||
expect(result).toContain("This is a {variable} and {var2}.");
|
||||
expect(result).toContain("here is the note content for note0");
|
||||
expect(result).toContain("note content for note1");
|
||||
});
|
||||
|
||||
it('should add 1 selectedText and no context', async () => {
|
||||
it("should add 1 selectedText and no context", async () => {
|
||||
const doc: CustomPrompt = {
|
||||
_id: 'test-prompt',
|
||||
prompt: 'Rewrite the following text {}'
|
||||
_id: "test-prompt",
|
||||
prompt: "Rewrite the following text {}",
|
||||
};
|
||||
const selectedText = 'here is some selected text 12345';
|
||||
const selectedText = "here is some selected text 12345";
|
||||
|
||||
// Mock the extractVariablesFromPrompt method to return predefined content
|
||||
jest.spyOn(processor, 'extractVariablesFromPrompt').mockResolvedValue([
|
||||
'here is the note content for note0',
|
||||
'note content for note1'
|
||||
]);
|
||||
jest
|
||||
.spyOn(processor, "extractVariablesFromPrompt")
|
||||
.mockResolvedValue(["here is the note content for note0", "note content for note1"]);
|
||||
|
||||
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
||||
|
||||
expect(result).toContain('Rewrite the following text {selectedText}');
|
||||
expect(result).toContain('here is some selected text 12345');
|
||||
expect(result).not.toContain('here is the note content for note0');
|
||||
expect(result).not.toContain('note content for note1');
|
||||
expect(result).toContain("Rewrite the following text {selectedText}");
|
||||
expect(result).toContain("here is some selected text 12345");
|
||||
expect(result).not.toContain("here is the note content for note0");
|
||||
expect(result).not.toContain("note content for note1");
|
||||
});
|
||||
|
||||
// This is not an expected use case but it's possible
|
||||
it('should add 2 selectedText and no context', async () => {
|
||||
it("should add 2 selectedText and no context", async () => {
|
||||
const doc: CustomPrompt = {
|
||||
_id: 'test-prompt',
|
||||
prompt: 'Rewrite the following text {} and {}'
|
||||
_id: "test-prompt",
|
||||
prompt: "Rewrite the following text {} and {}",
|
||||
};
|
||||
const selectedText = 'here is some selected text 12345';
|
||||
const selectedText = "here is some selected text 12345";
|
||||
|
||||
// Mock the extractVariablesFromPrompt method to return predefined content
|
||||
jest.spyOn(processor, 'extractVariablesFromPrompt').mockResolvedValue([
|
||||
'here is the note content for note0',
|
||||
'note content for note1'
|
||||
]);
|
||||
jest
|
||||
.spyOn(processor, "extractVariablesFromPrompt")
|
||||
.mockResolvedValue(["here is the note content for note0", "note content for note1"]);
|
||||
|
||||
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
||||
|
||||
expect(result).toContain('Rewrite the following text {selectedText} and {selectedText}');
|
||||
expect(result).toContain('here is some selected text 12345');
|
||||
expect(result).not.toContain('here is the note content for note0');
|
||||
expect(result).not.toContain('note content for note1');
|
||||
expect(result).toContain("Rewrite the following text {selectedText} and {selectedText}");
|
||||
expect(result).toContain("here is some selected text 12345");
|
||||
expect(result).not.toContain("here is the note content for note0");
|
||||
expect(result).not.toContain("note content for note1");
|
||||
});
|
||||
|
||||
it('should handle prompts without variables', async () => {
|
||||
it("should handle prompts without variables", async () => {
|
||||
const doc: CustomPrompt = {
|
||||
_id: 'test-prompt',
|
||||
prompt: 'This is a test prompt with no variables.'
|
||||
_id: "test-prompt",
|
||||
prompt: "This is a test prompt with no variables.",
|
||||
};
|
||||
const selectedText = 'selected text';
|
||||
const selectedText = "selected text";
|
||||
|
||||
// Mock the extractVariablesFromPrompt method to return an empty array
|
||||
jest.spyOn(processor, 'extractVariablesFromPrompt').mockResolvedValue([]);
|
||||
jest.spyOn(processor, "extractVariablesFromPrompt").mockResolvedValue([]);
|
||||
|
||||
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
||||
|
||||
expect(result).toBe('This is a test prompt with no variables.\n\n');
|
||||
expect(result).toBe("This is a test prompt with no variables.\n\n");
|
||||
});
|
||||
|
||||
it("should process a single tag variable correctly", async () => {
|
||||
|
|
@ -120,19 +119,12 @@ describe('CustomPromptProcessor', () => {
|
|||
// Mock the extractVariablesFromPrompt method to simulate tag processing
|
||||
jest
|
||||
.spyOn(processor, "extractVariablesFromPrompt")
|
||||
.mockResolvedValue([
|
||||
'[{"name":"note","content":"Note content for #tag"}]',
|
||||
]);
|
||||
.mockResolvedValue(['[{"name":"note","content":"Note content for #tag"}]']);
|
||||
|
||||
const result = await processor.processCustomPrompt(
|
||||
customPrompt,
|
||||
selectedText
|
||||
);
|
||||
const result = await processor.processCustomPrompt(customPrompt, selectedText);
|
||||
|
||||
expect(result).toContain("Notes related to {#tag} are:");
|
||||
expect(result).toContain(
|
||||
'[{"name":"note","content":"Note content for #tag"}]'
|
||||
);
|
||||
expect(result).toContain('[{"name":"note","content":"Note content for #tag"}]');
|
||||
});
|
||||
|
||||
it("should process multiple tag variables correctly", async () => {
|
||||
|
|
@ -146,10 +138,7 @@ describe('CustomPromptProcessor', () => {
|
|||
'[{"name":"note1","content":"Note content for #tag1"},{"name":"note2","content":"Note content for #tag2"}]',
|
||||
]);
|
||||
|
||||
const result = await processor.processCustomPrompt(
|
||||
customPrompt,
|
||||
selectedText
|
||||
);
|
||||
const result = await processor.processCustomPrompt(customPrompt, selectedText);
|
||||
|
||||
expect(result).toContain("Notes related to {#tag1,#tag2,#tag3} are:");
|
||||
expect(result).toContain(
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
import EncryptionService from '@/encryptionService';
|
||||
import { CopilotSettings } from '@/settings/SettingsPage';
|
||||
import { Platform } from 'obsidian';
|
||||
import EncryptionService from "@/encryptionService";
|
||||
import { CopilotSettings } from "@/settings/SettingsPage";
|
||||
import { Platform } from "obsidian";
|
||||
|
||||
// Mocking Electron's safeStorage
|
||||
jest.mock('electron', () => {
|
||||
jest.mock("electron", () => {
|
||||
return {
|
||||
remote: {
|
||||
safeStorage: {
|
||||
encryptString: jest.fn().mockImplementation((text) => `encrypted_${text}`),
|
||||
decryptString: jest.fn().mockImplementation((buffer) => buffer.toString().replace('encrypted_', '')),
|
||||
decryptString: jest
|
||||
.fn()
|
||||
.mockImplementation((buffer) => buffer.toString().replace("encrypted_", "")),
|
||||
isEncryptionAvailable: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe('Platform-specific Tests', () => {
|
||||
it('should recognize the platform as desktop', () => {
|
||||
describe("Platform-specific Tests", () => {
|
||||
it("should recognize the platform as desktop", () => {
|
||||
expect(Platform.isDesktop).toBe(true); // Directly using the mocked value
|
||||
});
|
||||
|
||||
// Example of a conditional test based on the platform
|
||||
it('should only run certain logic on desktop', () => {
|
||||
it("should only run certain logic on desktop", () => {
|
||||
if (Platform.isDesktop) {
|
||||
// Your desktop-specific logic here
|
||||
expect(true).toBe(true); // Replace with actual assertions
|
||||
|
|
@ -36,7 +38,7 @@ interface TestSettings extends CopilotSettings {
|
|||
[key: string]: any;
|
||||
}
|
||||
|
||||
describe('EncryptionService', () => {
|
||||
describe("EncryptionService", () => {
|
||||
let service: EncryptionService;
|
||||
let settings: TestSettings;
|
||||
|
||||
|
|
@ -49,68 +51,68 @@ describe('EncryptionService', () => {
|
|||
service = new EncryptionService(settings);
|
||||
});
|
||||
|
||||
describe('getEncryptedKey', () => {
|
||||
it('should encrypt an API key', () => {
|
||||
const apiKey = 'testApiKey';
|
||||
describe("getEncryptedKey", () => {
|
||||
it("should encrypt an API key", () => {
|
||||
const apiKey = "testApiKey";
|
||||
const encryptedKey = service.getEncryptedKey(apiKey);
|
||||
expect(encryptedKey).toBe(`enc_encrypted_${apiKey}`);
|
||||
});
|
||||
|
||||
it('should return the original key if encryption is not enabled', () => {
|
||||
it("should return the original key if encryption is not enabled", () => {
|
||||
settings.enableEncryption = false;
|
||||
const apiKey = 'testApiKey';
|
||||
const apiKey = "testApiKey";
|
||||
const encryptedKey = service.getEncryptedKey(apiKey);
|
||||
expect(encryptedKey).toBe(apiKey);
|
||||
});
|
||||
|
||||
it('should return the original key if already encrypted', () => {
|
||||
const apiKey = 'enc_testApiKey';
|
||||
it("should return the original key if already encrypted", () => {
|
||||
const apiKey = "enc_testApiKey";
|
||||
const encryptedKey = service.getEncryptedKey(apiKey);
|
||||
expect(encryptedKey).toBe(apiKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDecryptedKey', () => {
|
||||
it('should decrypt an encrypted API key', () => {
|
||||
const apiKey = 'testApiKey';
|
||||
describe("getDecryptedKey", () => {
|
||||
it("should decrypt an encrypted API key", () => {
|
||||
const apiKey = "testApiKey";
|
||||
const mockEncryptedKey = `encrypted_${apiKey}`;
|
||||
const base64Encoded = Buffer.from(mockEncryptedKey).toString('base64');
|
||||
const base64Encoded = Buffer.from(mockEncryptedKey).toString("base64");
|
||||
const encryptedKey = `enc_${base64Encoded}`;
|
||||
|
||||
const decryptedKey = service.getDecryptedKey(encryptedKey);
|
||||
expect(decryptedKey).toBe(apiKey);
|
||||
});
|
||||
|
||||
it('should return the original key if it is in plain text', () => {
|
||||
const apiKey = 'testApiKey';
|
||||
it("should return the original key if it is in plain text", () => {
|
||||
const apiKey = "testApiKey";
|
||||
const decryptedKey = service.getDecryptedKey(apiKey);
|
||||
expect(decryptedKey).toBe(apiKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encryptAllKeys', () => {
|
||||
describe("encryptAllKeys", () => {
|
||||
beforeEach(() => {
|
||||
settings = {
|
||||
enableEncryption: true,
|
||||
someApiKey: 'testApiKey',
|
||||
anotherApiKey: 'anotherTestApiKey',
|
||||
nonKey: 'shouldBeIgnored',
|
||||
someApiKey: "testApiKey",
|
||||
anotherApiKey: "anotherTestApiKey",
|
||||
nonKey: "shouldBeIgnored",
|
||||
} as unknown as CopilotSettings;
|
||||
service = new EncryptionService(settings);
|
||||
});
|
||||
|
||||
it('should encrypt all keys containing "apikey"', () => {
|
||||
service.encryptAllKeys();
|
||||
expect(settings.someApiKey).toBe('enc_encrypted_testApiKey');
|
||||
expect(settings.anotherApiKey).toBe('enc_encrypted_anotherTestApiKey');
|
||||
expect(settings.someApiKey).toBe("enc_encrypted_testApiKey");
|
||||
expect(settings.anotherApiKey).toBe("enc_encrypted_anotherTestApiKey");
|
||||
expect(settings.nonApiKey).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should not encrypt keys when encryption is not enabled', () => {
|
||||
it("should not encrypt keys when encryption is not enabled", () => {
|
||||
settings.enableEncryption = false;
|
||||
service.encryptAllKeys();
|
||||
expect(settings.someApiKey).toBe('testApiKey');
|
||||
expect(settings.anotherApiKey).toBe('anotherTestApiKey');
|
||||
expect(settings.someApiKey).toBe("testApiKey");
|
||||
expect(settings.anotherApiKey).toBe("anotherTestApiKey");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -64,19 +64,13 @@ describe("getNotesFromPath", () => {
|
|||
it("should return filtered markdown files 1", async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await getNotesFromPath(vault, "test2");
|
||||
expect(files).toEqual([
|
||||
{ path: "test/test2/note1.md" },
|
||||
{ path: "test2/note3.md" },
|
||||
]);
|
||||
expect(files).toEqual([{ path: "test/test2/note1.md" }, { path: "test2/note3.md" }]);
|
||||
});
|
||||
|
||||
it("should return filtered markdown files 2", async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await getNotesFromPath(vault, "test");
|
||||
expect(files).toEqual([
|
||||
{ path: "test/test2/note1.md" },
|
||||
{ path: "test/note2.md" },
|
||||
]);
|
||||
expect(files).toEqual([{ path: "test/test2/note1.md" }, { path: "test/note2.md" }]);
|
||||
});
|
||||
|
||||
it("should return filtered markdown files 3", async () => {
|
||||
|
|
@ -88,10 +82,7 @@ describe("getNotesFromPath", () => {
|
|||
it("should return filtered markdown files 4", async () => {
|
||||
const vault = new Obsidian.Vault();
|
||||
const files = await getNotesFromPath(vault, "/test");
|
||||
expect(files).toEqual([
|
||||
{ path: "test/test2/note1.md" },
|
||||
{ path: "test/note2.md" },
|
||||
]);
|
||||
expect(files).toEqual([{ path: "test/test2/note1.md" }, { path: "test/note2.md" }]);
|
||||
});
|
||||
|
||||
it("should not return markdown files", async () => {
|
||||
|
|
@ -136,16 +127,12 @@ describe("getNotesFromPath", () => {
|
|||
});
|
||||
|
||||
it("should return the note md filename with extra spaces 2", () => {
|
||||
const variableName = processVariableNameForNotePath(
|
||||
" [[ test note ]] ",
|
||||
);
|
||||
const variableName = processVariableNameForNotePath(" [[ test note ]] ");
|
||||
expect(variableName).toEqual("test note.md");
|
||||
});
|
||||
|
||||
it("should return the note md filename with extra spaces 2", () => {
|
||||
const variableName = processVariableNameForNotePath(
|
||||
" [[ test_note note ]] ",
|
||||
);
|
||||
const variableName = processVariableNameForNotePath(" [[ test_note note ]] ");
|
||||
expect(variableName).toEqual("test_note note.md");
|
||||
});
|
||||
|
||||
|
|
@ -221,10 +208,7 @@ describe("getNotesFromTags", () => {
|
|||
it("should handle both path and tags, returning files under the specified path with the specified tags", async () => {
|
||||
const mockVault = new Obsidian.Vault();
|
||||
const tags = ["tag1"];
|
||||
const noteFiles = [
|
||||
{ path: "test/test2/note1.md" },
|
||||
{ path: "test/note2.md" },
|
||||
] as TFile[];
|
||||
const noteFiles = [{ path: "test/test2/note1.md" }, { path: "test/note2.md" }] as TFile[];
|
||||
const expectedPaths = ["test/test2/note1.md"];
|
||||
|
||||
const result = await getNotesFromTags(mockVault, tags, noteFiles);
|
||||
|
|
@ -262,10 +246,7 @@ describe("isPathInList", () => {
|
|||
});
|
||||
|
||||
it("should handle multiple excluded paths separated by commas", () => {
|
||||
const result = isPathInList(
|
||||
"test/folder/note.md",
|
||||
"another/folder,test/folder",
|
||||
);
|
||||
const result = isPathInList("test/folder/note.md", "another/folder,test/folder");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -277,16 +258,13 @@ describe("isPathInList", () => {
|
|||
it("should exclude a file path that matches any one of multiple excluded paths", () => {
|
||||
const result = isPathInList(
|
||||
"test/folder/note.md",
|
||||
"another/folder, test/folder, yet/another/folder",
|
||||
"another/folder, test/folder, yet/another/folder"
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should trim spaces around excluded paths", () => {
|
||||
const result = isPathInList(
|
||||
"test/folder/note.md",
|
||||
" another/folder , test/folder ",
|
||||
);
|
||||
const result = isPathInList("test/folder/note.md", " another/folder , test/folder ");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -300,8 +278,7 @@ describe("extractNoteTitles", () => {
|
|||
});
|
||||
|
||||
it("should extract multiple note titles", () => {
|
||||
const query =
|
||||
"Please refer to [[Note1]] and [[Note2]] for more information.";
|
||||
const query = "Please refer to [[Note1]] and [[Note2]] for more information.";
|
||||
const expected = ["Note1", "Note2"];
|
||||
const result = extractNoteTitles(query);
|
||||
expect(result).toEqual(expected);
|
||||
|
|
|
|||
|
|
@ -18,16 +18,7 @@
|
|||
"esModuleInterop": true,
|
||||
"strictNullChecks": true,
|
||||
"types": ["jest"],
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"src/**/*.ts",
|
||||
"typings/**/*.ts",
|
||||
]
|
||||
"include": ["**/*.ts", "src/**/*.ts", "typings/**/*.ts"]
|
||||
}
|
||||
|
|
|
|||
2
typings/sse.d.ts
vendored
2
typings/sse.d.ts
vendored
|
|
@ -1 +1 @@
|
|||
declare module 'sse';
|
||||
declare module "sse";
|
||||
|
|
|
|||
|
|
@ -1,38 +1,38 @@
|
|||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
|
||||
const wasmPlugin = {
|
||||
name: 'wasm',
|
||||
name: "wasm",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /\.wasm$/ }, args => {
|
||||
if (args.namespace === 'wasm-stub') {
|
||||
build.onResolve({ filter: /\.wasm$/ }, (args) => {
|
||||
if (args.namespace === "wasm-stub") {
|
||||
return {
|
||||
path: args.path,
|
||||
namespace: 'wasm-binary',
|
||||
}
|
||||
namespace: "wasm-binary",
|
||||
};
|
||||
}
|
||||
|
||||
if (args.resolveDir === '') {
|
||||
return
|
||||
if (args.resolveDir === "") {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path),
|
||||
namespace: 'wasm-stub',
|
||||
}
|
||||
})
|
||||
namespace: "wasm-stub",
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad({ filter: /.*/, namespace: 'wasm-stub' }, async (args) => ({
|
||||
build.onLoad({ filter: /.*/, namespace: "wasm-stub" }, async (args) => ({
|
||||
contents: `import wasm from ${JSON.stringify(args.path)}
|
||||
export default (imports) =>
|
||||
WebAssembly.instantiate(wasm, imports).then(
|
||||
result => result.instance.exports)`,
|
||||
}))
|
||||
}));
|
||||
|
||||
build.onLoad({ filter: /.*/, namespace: 'wasm-binary' }, async (args) => ({
|
||||
build.onLoad({ filter: /.*/, namespace: "wasm-binary" }, async (args) => ({
|
||||
contents: await fs.readFile(args.path),
|
||||
loader: 'binary',
|
||||
}))
|
||||
loader: "binary",
|
||||
}));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default wasmPlugin;
|
||||
|
|
|
|||
Loading…
Reference in a new issue