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
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
otechie: # Replace with a single Otechie username
|
otechie: # Replace with a single Otechie username
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
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"]
|
||||||
|
|
|
||||||
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,10 +1,9 @@
|
||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ""
|
||||||
labels: ''
|
labels: ""
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] Screenshot of note + Copilot chat pane + dev console added **(required)**
|
- [ ] Screenshot of note + Copilot chat pane + dev console added **(required)**
|
||||||
|
|
@ -16,7 +15,7 @@ A clear and concise description of what the bug is. Clear steps to reproduce the
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
Add screenshots to help explain your problem. Please turn on debug mode in Copilot settings, turn off other plugins to leave only Copilot dev messages as necessary.
|
Add screenshots to help explain your problem. Please turn on debug mode in Copilot settings, turn off other plugins to leave only Copilot dev messages as necessary.
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
|
||||||
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,10 +1,9 @@
|
||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea for this project
|
about: Suggest an idea for this project
|
||||||
title: ''
|
title: ""
|
||||||
labels: ''
|
labels: ""
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
|
|
||||||
56
README.md
56
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# 🔍 Copilot for Obsidian
|
# 🔍 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.
|
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!
|
**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
|
## 🛠️ Features
|
||||||
|
|
||||||
- Chat with ChatGPT right inside Obsidian in the Copilot Chat window.
|
- Chat with ChatGPT right inside Obsidian in the Copilot Chat window.
|
||||||
- No repetitive login. Use your own API key (stored locally).
|
- No repetitive login. Use your own API key (stored locally).
|
||||||
- No monthly fee. Pay only for what you use.
|
- 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!
|
- 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.
|
- 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**).
|
- 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.
|
- **Local model** support for **offline chat** using LM Studio and Ollama.
|
||||||
|
|
||||||
## 🎬 Demos
|
## 🎬 Demos
|
||||||
|
|
||||||
#### 🤗 New to Copilot? Quick Guide for Beginners:
|
#### 🤗 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>
|
<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!
|
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
|
#### ⛓️ Manual Installation
|
||||||
|
|
||||||
- Go to the latest release
|
- Go to the latest release
|
||||||
- Download `main.js`, `manifest.json`, `styles.css` and put them under `.obsidian/plugins/obsidian-copilot/` in your vault
|
- 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`.
|
- Open your Obsidian settings > Community plugins, and turn on `Copilot`.
|
||||||
|
|
||||||
## 🔔 Note
|
## 🔔 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.
|
- 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.
|
- "**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.
|
- "**Use Long Note as Context**" creates a local vector index for the active long note so that you can chat with note longer than the model's context window! To start the QA, please switch from "Chat" to "QA" in the Mode Selection dropdown.
|
||||||
|
|
@ -123,54 +127,55 @@ Now you can see the chat icon in your leftside ribbon, clicking on it will open
|
||||||
<details>
|
<details>
|
||||||
<summary>"You do not have access to this model"</summary>
|
<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!
|
- 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!
|
- 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
|
- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/3#issuecomment-1544583676
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>It's not using my note as context</summary>
|
<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">
|
<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.
|
- 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
|
- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/51
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>Unresponsive QA when using Huggingface as the Embedding Provider</summary>
|
<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>
|
||||||
<details>
|
<details>
|
||||||
<summary>"insufficient_quota"</summary>
|
<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.
|
- 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
|
- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/11
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>"context_length_exceeded"</summary>
|
<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.)
|
- 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
|
- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/1#issuecomment-1542934569
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>Azure issue</summary>
|
<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\
|
curl https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/completions?api-version=VERSION\
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "api-key: YOUR_API_KEY" \
|
-H "api-key: YOUR_API_KEY" \
|
||||||
-d "{
|
-d "{
|
||||||
\"prompt\": \"Once upon a time\",
|
\"prompt\": \"Once upon a time\",
|
||||||
\"max_tokens\": 5
|
\"max_tokens\": 5
|
||||||
}"
|
}"
|
||||||
```
|
```
|
||||||
- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/98
|
- Reference issue: https://github.com/logancyang/obsidian-copilot/issues/98
|
||||||
</details>
|
</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!
|
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)
|
## 📝 Planned features (feedback welcome)
|
||||||
|
|
||||||
- New modes
|
- 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.
|
- **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.
|
- **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!
|
- 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
|
## 🙏 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!
|
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>
|
<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
|
// __mocks__/obsidian.js
|
||||||
/* eslint-disable no-undef */
|
/* eslint-disable no-undef */
|
||||||
import yaml from 'js-yaml';
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Vault: jest.fn().mockImplementation(() => {
|
Vault: jest.fn().mockImplementation(() => {
|
||||||
|
|
@ -8,19 +8,19 @@ module.exports = {
|
||||||
getMarkdownFiles: jest.fn().mockImplementation(() => {
|
getMarkdownFiles: jest.fn().mockImplementation(() => {
|
||||||
// Return an array of mock markdown file objects
|
// Return an array of mock markdown file objects
|
||||||
return [
|
return [
|
||||||
{ path: 'test/test2/note1.md' },
|
{ path: "test/test2/note1.md" },
|
||||||
{ path: 'test/note2.md' },
|
{ path: "test/note2.md" },
|
||||||
{ path: 'test2/note3.md' },
|
{ path: "test2/note3.md" },
|
||||||
{ path: 'note4.md' },
|
{ path: "note4.md" },
|
||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
cachedRead: jest.fn().mockImplementation((file) => {
|
cachedRead: jest.fn().mockImplementation((file) => {
|
||||||
// Simulate reading file contents. You can adjust the content as needed for your tests.
|
// Simulate reading file contents. You can adjust the content as needed for your tests.
|
||||||
const fileContents = {
|
const fileContents = {
|
||||||
'test/test2/note1.md': '---\ntags: [Tag1, tag2]\n---\nContent of note1',
|
"test/test2/note1.md": "---\ntags: [Tag1, tag2]\n---\nContent of note1",
|
||||||
'test/note2.md': '---\ntags: [tag2, tag3]\n---\nContent of note2',
|
"test/note2.md": "---\ntags: [tag2, tag3]\n---\nContent of note2",
|
||||||
'test2/note3.md': 'something else ---\ntags: [false_tag]\n---\nContent of note3',
|
"test2/note3.md": "something else ---\ntags: [false_tag]\n---\nContent of note3",
|
||||||
'note4.md': '---\ntags: [tag1, Tag4]\n---\nContent of note4',
|
"note4.md": "---\ntags: [tag1, Tag4]\n---\nContent of note4",
|
||||||
};
|
};
|
||||||
return Promise.resolve(fileContents[file.path]);
|
return Promise.resolve(fileContents[file.path]);
|
||||||
}),
|
}),
|
||||||
|
|
@ -32,4 +32,4 @@ module.exports = {
|
||||||
parseYaml: jest.fn().mockImplementation((content) => {
|
parseYaml: jest.fn().mockImplementation((content) => {
|
||||||
return yaml.load(content);
|
return yaml.load(content);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,13 @@ import svgPlugin from "esbuild-plugin-svg";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import wasmPlugin from "./wasmPlugin.mjs";
|
import wasmPlugin from "./wasmPlugin.mjs";
|
||||||
|
|
||||||
const banner =
|
const banner = `/*
|
||||||
`/*
|
|
||||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
if you want to view the source, please visit the github repository of this plugin
|
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({
|
const context = await esbuild.context({
|
||||||
banner: {
|
banner: {
|
||||||
|
|
@ -33,7 +32,8 @@ const context = await esbuild.context({
|
||||||
"@lezer/common",
|
"@lezer/common",
|
||||||
"@lezer/highlight",
|
"@lezer/highlight",
|
||||||
"@lezer/lr",
|
"@lezer/lr",
|
||||||
...builtins],
|
...builtins,
|
||||||
|
],
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
target: "es2018",
|
target: "es2018",
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
|
|
@ -48,4 +48,4 @@ if (prod) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
await context.watch();
|
await context.watch();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: "ts-jest",
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: "jsdom",
|
||||||
roots: ['<rootDir>/src', '<rootDir>/tests'],
|
roots: ["<rootDir>/src", "<rootDir>/tests"],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.(js|jsx|ts|tsx)$': 'ts-jest',
|
"^.+\\.(js|jsx|ts|tsx)$": "ts-jest",
|
||||||
},
|
},
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||||
'^@/(.*)$': '<rootDir>/src/$1',
|
"^@/(.*)$": "<rootDir>/src/$1",
|
||||||
'^obsidian$': '<rootDir>/__mocks__/obsidian.js'
|
"^obsidian$": "<rootDir>/__mocks__/obsidian.js",
|
||||||
},
|
},
|
||||||
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
|
testRegex: "(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||||
testPathIgnorePatterns: ['/node_modules/'],
|
testPathIgnorePatterns: ["/node_modules/"],
|
||||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
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.
|
[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:
|
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 an Apple Silicon Mac, look at your RAM
|
||||||
- If you are on a Windows PC with a GPU, look at your VRAM.
|
- 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 for Local Embeddings
|
||||||
|
|
||||||
Ollama has added support for local embeddings for RAG since v0.1.26! It's super easy to setup, just run
|
Ollama has added support for local embeddings for RAG since v0.1.26! It's super easy to setup, just run
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"id": "copilot",
|
"id": "copilot",
|
||||||
"name": "Copilot",
|
"name": "Copilot",
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "A ChatGPT Copilot in Obsidian.",
|
"description": "A ChatGPT Copilot in Obsidian.",
|
||||||
"author": "Logan Yang",
|
"author": "Logan Yang",
|
||||||
"authorUrl": "https://twitter.com/logancyang",
|
"authorUrl": "https://twitter.com/logancyang",
|
||||||
"fundingUrl": {
|
"fundingUrl": {
|
||||||
"Buy Me a Coffee": "https://www.buymeacoffee.com/logancyang",
|
"Buy Me a Coffee": "https://www.buymeacoffee.com/logancyang",
|
||||||
"GitHub Sponsor": "https://github.com/sponsors/logancyang"
|
"GitHub Sponsor": "https://github.com/sponsors/logancyang"
|
||||||
},
|
},
|
||||||
"isDesktopOnly": true
|
"isDesktopOnly": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write 'src/**/*.{js,ts,tsx,md}'",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check 'src/**/*.{js,ts,tsx,md}'",
|
||||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,7 @@ import {
|
||||||
getModelName,
|
getModelName,
|
||||||
isSupportedChain,
|
isSupportedChain,
|
||||||
} from "@/utils";
|
} from "@/utils";
|
||||||
import VectorDBManager, {
|
import VectorDBManager, { MemoryVector, NoteFile, VectorStoreDocument } from "@/vectorDBManager";
|
||||||
MemoryVector,
|
|
||||||
NoteFile,
|
|
||||||
VectorStoreDocument,
|
|
||||||
} from "@/vectorDBManager";
|
|
||||||
import { ChatOllama } from "@langchain/community/chat_models/ollama";
|
import { ChatOllama } from "@langchain/community/chat_models/ollama";
|
||||||
import { RunnableSequence } from "@langchain/core/runnables";
|
import { RunnableSequence } from "@langchain/core/runnables";
|
||||||
import { BaseChatMemory } from "langchain/memory";
|
import { BaseChatMemory } from "langchain/memory";
|
||||||
|
|
@ -64,7 +60,7 @@ export default class ChainManager {
|
||||||
encryptionService: EncryptionService,
|
encryptionService: EncryptionService,
|
||||||
settings: CopilotSettings,
|
settings: CopilotSettings,
|
||||||
// Ensure ChainManager always has the up-to-date dbVectorStores
|
// Ensure ChainManager always has the up-to-date dbVectorStores
|
||||||
getDbVectorStores: () => PouchDB.Database<VectorStoreDocument>,
|
getDbVectorStores: () => PouchDB.Database<VectorStoreDocument>
|
||||||
) {
|
) {
|
||||||
// Instantiate singletons
|
// Instantiate singletons
|
||||||
this.app = app;
|
this.app = app;
|
||||||
|
|
@ -72,10 +68,7 @@ export default class ChainManager {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.memoryManager = MemoryManager.getInstance(this.langChainParams);
|
this.memoryManager = MemoryManager.getInstance(this.langChainParams);
|
||||||
this.encryptionService = encryptionService;
|
this.encryptionService = encryptionService;
|
||||||
this.chatModelManager = ChatModelManager.getInstance(
|
this.chatModelManager = ChatModelManager.getInstance(this.langChainParams, encryptionService);
|
||||||
this.langChainParams,
|
|
||||||
encryptionService,
|
|
||||||
);
|
|
||||||
this.promptManager = PromptManager.getInstance(this.langChainParams);
|
this.promptManager = PromptManager.getInstance(this.langChainParams);
|
||||||
this.getDbVectorStores = getDbVectorStores;
|
this.getDbVectorStores = getDbVectorStores;
|
||||||
this.createChainWithNewModel(this.langChainParams.modelDisplayName);
|
this.createChainWithNewModel(this.langChainParams.modelDisplayName);
|
||||||
|
|
@ -86,8 +79,7 @@ export default class ChainManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateChainType(chainType: ChainType): void {
|
private validateChainType(chainType: ChainType): void {
|
||||||
if (chainType === undefined || chainType === null)
|
if (chainType === undefined || chainType === null) throw new Error("No chain type set");
|
||||||
throw new Error("No chain type set");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static storeRetrieverDocuments(documents: Document[]) {
|
static storeRetrieverDocuments(documents: Document[]) {
|
||||||
|
|
@ -102,8 +94,7 @@ export default class ChainManager {
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
createChainWithNewModel(newModelDisplayName: string): void {
|
createChainWithNewModel(newModelDisplayName: string): void {
|
||||||
ChainManager.isOllamaModelActive =
|
ChainManager.isOllamaModelActive = newModelDisplayName === ChatModelDisplayNames.OLLAMA;
|
||||||
newModelDisplayName === ChatModelDisplayNames.OLLAMA;
|
|
||||||
ChainManager.isOpenRouterModelActive =
|
ChainManager.isOpenRouterModelActive =
|
||||||
newModelDisplayName === ChatModelDisplayNames.OPENROUTERAI;
|
newModelDisplayName === ChatModelDisplayNames.OPENROUTERAI;
|
||||||
// model and model display name must be update at the same time!
|
// model and model display name must be update at the same time!
|
||||||
|
|
@ -150,28 +141,18 @@ export default class ChainManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setChain(
|
async setChain(chainType: ChainType, options: SetChainOptions = {}): Promise<void> {
|
||||||
chainType: ChainType,
|
if (!this.chatModelManager.validateChatModel(this.chatModelManager.getChatModel())) {
|
||||||
options: SetChainOptions = {},
|
|
||||||
): Promise<void> {
|
|
||||||
if (
|
|
||||||
!this.chatModelManager.validateChatModel(
|
|
||||||
this.chatModelManager.getChatModel(),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// No need to throw error and trigger multiple Notices to user
|
// No need to throw error and trigger multiple Notices to user
|
||||||
console.error("setChain failed: No chat model set.");
|
console.error("setChain failed: No chat model set.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.validateChainType(chainType);
|
this.validateChainType(chainType);
|
||||||
// MUST set embeddingsManager when switching to QA mode
|
// MUST set embeddingsManager when switching to QA mode
|
||||||
if (
|
if (chainType === ChainType.LONG_NOTE_QA_CHAIN || chainType === ChainType.VAULT_QA_CHAIN) {
|
||||||
chainType === ChainType.LONG_NOTE_QA_CHAIN ||
|
|
||||||
chainType === ChainType.VAULT_QA_CHAIN
|
|
||||||
) {
|
|
||||||
this.embeddingsManager = EmbeddingsManager.getInstance(
|
this.embeddingsManager = EmbeddingsManager.getInstance(
|
||||||
this.langChainParams,
|
this.langChainParams,
|
||||||
this.encryptionService,
|
this.encryptionService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,8 +169,7 @@ export default class ChainManager {
|
||||||
if (ChainManager.isOllamaModelActive) {
|
if (ChainManager.isOllamaModelActive) {
|
||||||
(chatModel as ChatOllama).model = this.langChainParams.ollamaModel;
|
(chatModel as ChatOllama).model = this.langChainParams.ollamaModel;
|
||||||
} else if (ChainManager.isOpenRouterModelActive) {
|
} else if (ChainManager.isOpenRouterModelActive) {
|
||||||
(chatModel as ProxyChatOpenAI).modelName =
|
(chatModel as ProxyChatOpenAI).modelName = this.langChainParams.openRouterModel;
|
||||||
this.langChainParams.openRouterModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChainManager.chain = ChainFactory.createNewLLMChain({
|
ChainManager.chain = ChainFactory.createNewLLMChain({
|
||||||
|
|
@ -220,36 +200,30 @@ export default class ChainManager {
|
||||||
this.setNoteFile(options.noteFile);
|
this.setNoteFile(options.noteFile);
|
||||||
const docHash = VectorDBManager.getDocumentHash(options.noteFile.path);
|
const docHash = VectorDBManager.getDocumentHash(options.noteFile.path);
|
||||||
const parsedMemoryVectors: MemoryVector[] | undefined =
|
const parsedMemoryVectors: MemoryVector[] | undefined =
|
||||||
await VectorDBManager.getMemoryVectors(
|
await VectorDBManager.getMemoryVectors(this.getDbVectorStores(), docHash);
|
||||||
this.getDbVectorStores(),
|
|
||||||
docHash,
|
|
||||||
);
|
|
||||||
const embeddingsAPI = this.embeddingsManager.getEmbeddingsAPI();
|
const embeddingsAPI = this.embeddingsManager.getEmbeddingsAPI();
|
||||||
if (!embeddingsAPI) {
|
if (!embeddingsAPI) {
|
||||||
console.error(
|
console.error("Error getting embeddings API. Please check your settings.");
|
||||||
"Error getting embeddings API. Please check your settings.",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (parsedMemoryVectors) {
|
if (parsedMemoryVectors) {
|
||||||
// Index already exists
|
// Index already exists
|
||||||
const vectorStore = await VectorDBManager.rebuildMemoryVectorStore(
|
const vectorStore = await VectorDBManager.rebuildMemoryVectorStore(
|
||||||
parsedMemoryVectors,
|
parsedMemoryVectors,
|
||||||
embeddingsAPI,
|
embeddingsAPI
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create new conversational retrieval chain
|
// Create new conversational retrieval chain
|
||||||
ChainManager.retrievalChain =
|
ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain(
|
||||||
ChainFactory.createConversationalRetrievalChain(
|
{
|
||||||
{
|
llm: chatModel,
|
||||||
llm: chatModel,
|
retriever: vectorStore.asRetriever(undefined, (doc) => {
|
||||||
retriever: vectorStore.asRetriever(undefined, (doc) => {
|
return doc.metadata.path === options.noteFile?.path;
|
||||||
return doc.metadata.path === options.noteFile?.path;
|
}),
|
||||||
}),
|
},
|
||||||
},
|
ChainManager.storeRetrieverDocuments.bind(ChainManager),
|
||||||
ChainManager.storeRetrieverDocuments.bind(ChainManager),
|
options.debug
|
||||||
options.debug,
|
);
|
||||||
);
|
|
||||||
console.log("Existing vector store for document hash: ", docHash);
|
console.log("Existing vector store for document hash: ", docHash);
|
||||||
} else {
|
} else {
|
||||||
// Index doesn't exist
|
// Index doesn't exist
|
||||||
|
|
@ -257,7 +231,7 @@ export default class ChainManager {
|
||||||
this.vectorStore = await VectorDBManager.getMemoryVectorStore(
|
this.vectorStore = await VectorDBManager.getMemoryVectorStore(
|
||||||
this.getDbVectorStores(),
|
this.getDbVectorStores(),
|
||||||
embeddingsAPI,
|
embeddingsAPI,
|
||||||
vectorStoreDoc?._id,
|
vectorStoreDoc?._id
|
||||||
);
|
);
|
||||||
if (!this.vectorStore) {
|
if (!this.vectorStore) {
|
||||||
console.error("Error creating vector store.");
|
console.error("Error creating vector store.");
|
||||||
|
|
@ -272,19 +246,17 @@ export default class ChainManager {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
ChainManager.retrievalChain =
|
ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain(
|
||||||
ChainFactory.createConversationalRetrievalChain(
|
{
|
||||||
{
|
llm: chatModel,
|
||||||
llm: chatModel,
|
retriever: retriever,
|
||||||
retriever: retriever,
|
},
|
||||||
},
|
ChainManager.storeRetrieverDocuments.bind(ChainManager),
|
||||||
ChainManager.storeRetrieverDocuments.bind(ChainManager),
|
options.debug
|
||||||
options.debug,
|
);
|
||||||
);
|
|
||||||
console.log(
|
console.log(
|
||||||
"New Long Note QA chain with multi-query retriever created for " +
|
"New Long Note QA chain with multi-query retriever created for " + "document hash: ",
|
||||||
"document hash: ",
|
docHash
|
||||||
docHash,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,14 +268,12 @@ export default class ChainManager {
|
||||||
case ChainType.VAULT_QA_CHAIN: {
|
case ChainType.VAULT_QA_CHAIN: {
|
||||||
const embeddingsAPI = this.embeddingsManager.getEmbeddingsAPI();
|
const embeddingsAPI = this.embeddingsManager.getEmbeddingsAPI();
|
||||||
if (!embeddingsAPI) {
|
if (!embeddingsAPI) {
|
||||||
console.error(
|
console.error("Error getting embeddings API. Please check your settings.");
|
||||||
"Error getting embeddings API. Please check your settings.",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const vectorStore = await VectorDBManager.getMemoryVectorStore(
|
const vectorStore = await VectorDBManager.getMemoryVectorStore(
|
||||||
this.getDbVectorStores(),
|
this.getDbVectorStores(),
|
||||||
embeddingsAPI,
|
embeddingsAPI
|
||||||
);
|
);
|
||||||
const retriever = new HybridRetriever(
|
const retriever = new HybridRetriever(
|
||||||
this.getDbVectorStores(),
|
this.getDbVectorStores(),
|
||||||
|
|
@ -314,22 +284,19 @@ export default class ChainManager {
|
||||||
maxK: this.settings.maxSourceChunks, // The maximum number of docs (chunks) to retrieve
|
maxK: this.settings.maxSourceChunks, // The maximum number of docs (chunks) to retrieve
|
||||||
kIncrement: 2,
|
kIncrement: 2,
|
||||||
},
|
},
|
||||||
options.debug,
|
options.debug
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create new conversational retrieval chain
|
// Create new conversational retrieval chain
|
||||||
ChainManager.retrievalChain =
|
ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain(
|
||||||
ChainFactory.createConversationalRetrievalChain(
|
{
|
||||||
{
|
llm: chatModel,
|
||||||
llm: chatModel,
|
retriever: retriever,
|
||||||
retriever: retriever,
|
},
|
||||||
},
|
ChainManager.storeRetrieverDocuments.bind(ChainManager),
|
||||||
ChainManager.storeRetrieverDocuments.bind(ChainManager),
|
options.debug
|
||||||
options.debug,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"New Vault QA chain with hybrid retriever created for entire vault",
|
|
||||||
);
|
);
|
||||||
|
console.log("New Vault QA chain with hybrid retriever created for entire vault");
|
||||||
|
|
||||||
this.langChainParams.chainType = ChainType.VAULT_QA_CHAIN;
|
this.langChainParams.chainType = ChainType.VAULT_QA_CHAIN;
|
||||||
console.log("Set chain:", ChainType.VAULT_QA_CHAIN);
|
console.log("Set chain:", ChainType.VAULT_QA_CHAIN);
|
||||||
|
|
@ -351,16 +318,12 @@ export default class ChainManager {
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
ignoreSystemMessage?: boolean;
|
ignoreSystemMessage?: boolean;
|
||||||
updateLoading?: (loading: boolean) => void;
|
updateLoading?: (loading: boolean) => void;
|
||||||
} = {},
|
} = {}
|
||||||
) {
|
) {
|
||||||
const { debug = false, ignoreSystemMessage = false } = options;
|
const { debug = false, ignoreSystemMessage = false } = options;
|
||||||
|
|
||||||
// Check if chat model is initialized
|
// Check if chat model is initialized
|
||||||
if (
|
if (!this.chatModelManager.validateChatModel(this.chatModelManager.getChatModel())) {
|
||||||
!this.chatModelManager.validateChatModel(
|
|
||||||
this.chatModelManager.getChatModel(),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const errorMsg =
|
const errorMsg =
|
||||||
"Chat model is not initialized properly, check your API key in Copilot setting and make sure you have API access.";
|
"Chat model is not initialized properly, check your API key in Copilot setting and make sure you have API access.";
|
||||||
new Notice(errorMsg);
|
new Notice(errorMsg);
|
||||||
|
|
@ -371,21 +334,13 @@ export default class ChainManager {
|
||||||
if (!ChainManager.chain || !isSupportedChain(ChainManager.chain)) {
|
if (!ChainManager.chain || !isSupportedChain(ChainManager.chain)) {
|
||||||
console.error(
|
console.error(
|
||||||
"Chain is not initialized properly, re-initializing chain: ",
|
"Chain is not initialized properly, re-initializing chain: ",
|
||||||
this.langChainParams.chainType,
|
this.langChainParams.chainType
|
||||||
);
|
|
||||||
this.setChain(
|
|
||||||
this.langChainParams.chainType,
|
|
||||||
this.langChainParams.options,
|
|
||||||
);
|
);
|
||||||
|
this.setChain(this.langChainParams.chainType, this.langChainParams.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { temperature, maxTokens, systemMessage, chatContextTurns, chainType } =
|
||||||
temperature,
|
this.langChainParams;
|
||||||
maxTokens,
|
|
||||||
systemMessage,
|
|
||||||
chatContextTurns,
|
|
||||||
chainType,
|
|
||||||
} = this.langChainParams;
|
|
||||||
|
|
||||||
const memory = this.memoryManager.getMemory();
|
const memory = this.memoryManager.getMemory();
|
||||||
const chatPrompt = this.promptManager.getChatPrompt();
|
const chatPrompt = this.promptManager.getChatPrompt();
|
||||||
|
|
@ -429,7 +384,7 @@ export default class ChainManager {
|
||||||
`temperature: ${temperature}\n` +
|
`temperature: ${temperature}\n` +
|
||||||
`maxTokens: ${maxTokens}\n` +
|
`maxTokens: ${maxTokens}\n` +
|
||||||
`system prompt: ${systemPrompt}\n` +
|
`system prompt: ${systemPrompt}\n` +
|
||||||
`chat context turns: ${chatContextTurns}\n`,
|
`chat context turns: ${chatContextTurns}\n`
|
||||||
);
|
);
|
||||||
console.log("chain RunnableSequence:", ChainManager.chain);
|
console.log("chain RunnableSequence:", ChainManager.chain);
|
||||||
console.log("Chat memory:", memory);
|
console.log("Chat memory:", memory);
|
||||||
|
|
@ -452,27 +407,21 @@ export default class ChainManager {
|
||||||
`temperature: ${temperature}\n` +
|
`temperature: ${temperature}\n` +
|
||||||
`maxTokens: ${maxTokens}\n` +
|
`maxTokens: ${maxTokens}\n` +
|
||||||
`system prompt: ${systemPrompt}\n` +
|
`system prompt: ${systemPrompt}\n` +
|
||||||
`chat context turns: ${chatContextTurns}\n`,
|
`chat context turns: ${chatContextTurns}\n`
|
||||||
);
|
);
|
||||||
console.log("chain RunnableSequence:", ChainManager.chain);
|
console.log("chain RunnableSequence:", ChainManager.chain);
|
||||||
console.log(
|
console.log("embedding model:", this.langChainParams.embeddingModel);
|
||||||
"embedding model:",
|
|
||||||
this.langChainParams.embeddingModel,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
fullAIResponse = await this.runRetrievalChain(
|
fullAIResponse = await this.runRetrievalChain(
|
||||||
userMessage,
|
userMessage,
|
||||||
memory,
|
memory,
|
||||||
updateCurrentAiMessage,
|
updateCurrentAiMessage,
|
||||||
abortController,
|
abortController,
|
||||||
{ debug },
|
{ debug }
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.error(
|
console.error("Chain type not supported:", this.langChainParams.chainType);
|
||||||
"Chain type not supported:",
|
|
||||||
this.langChainParams.chainType,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorData = error?.response?.data?.error || error;
|
const errorData = error?.response?.data?.error || error;
|
||||||
|
|
@ -489,10 +438,7 @@ export default class ChainManager {
|
||||||
} finally {
|
} finally {
|
||||||
if (fullAIResponse) {
|
if (fullAIResponse) {
|
||||||
// This line is a must for memory to work with RunnableSequence!
|
// This line is a must for memory to work with RunnableSequence!
|
||||||
await memory.saveContext(
|
await memory.saveContext({ input: userMessage }, { output: fullAIResponse });
|
||||||
{ input: userMessage },
|
|
||||||
{ output: fullAIResponse },
|
|
||||||
);
|
|
||||||
addMessage({
|
addMessage({
|
||||||
message: fullAIResponse,
|
message: fullAIResponse,
|
||||||
sender: AI_SENDER,
|
sender: AI_SENDER,
|
||||||
|
|
@ -511,7 +457,7 @@ export default class ChainManager {
|
||||||
abortController: AbortController,
|
abortController: AbortController,
|
||||||
options: {
|
options: {
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
} = {},
|
} = {}
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const memoryVariables = await memory.loadMemoryVariables({});
|
const memoryVariables = await memory.loadMemoryVariables({});
|
||||||
const chatHistory = extractChatHistory(memoryVariables);
|
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,
|
// to show specific chunks in the future. E.g. collapsed note title when clicked,
|
||||||
// expand and reveal the chunk
|
// expand and reveal the chunk
|
||||||
if (this.langChainParams.chainType === ChainType.VAULT_QA_CHAIN) {
|
if (this.langChainParams.chainType === ChainType.VAULT_QA_CHAIN) {
|
||||||
const docTitles = extractUniqueTitlesFromDocs(
|
const docTitles = extractUniqueTitlesFromDocs(ChainManager.retrievedDocuments);
|
||||||
ChainManager.retrievedDocuments,
|
|
||||||
);
|
|
||||||
const markdownLinks = docTitles
|
const markdownLinks = docTitles
|
||||||
.map(
|
.map(
|
||||||
(title) =>
|
(title) =>
|
||||||
`[${title}](obsidian://open?vault=${encodeURIComponent(this.app.vault.getName())}&file=${encodeURIComponent(
|
`[${title}](obsidian://open?vault=${encodeURIComponent(this.app.vault.getName())}&file=${encodeURIComponent(
|
||||||
title,
|
title
|
||||||
)})`,
|
)})`
|
||||||
)
|
)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
fullAIResponse += "\n\n**Sources**:\n" + markdownLinks;
|
fullAIResponse += "\n\n**Sources**:\n" + markdownLinks;
|
||||||
|
|
@ -556,9 +500,7 @@ export default class ChainManager {
|
||||||
return fullAIResponse;
|
return fullAIResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async indexFile(
|
async indexFile(noteFile: NoteFile): Promise<VectorStoreDocument | undefined> {
|
||||||
noteFile: NoteFile,
|
|
||||||
): Promise<VectorStoreDocument | undefined> {
|
|
||||||
const embeddingsAPI = this.embeddingsManager.getEmbeddingsAPI();
|
const embeddingsAPI = this.embeddingsManager.getEmbeddingsAPI();
|
||||||
if (!embeddingsAPI) {
|
if (!embeddingsAPI) {
|
||||||
const errorMsg =
|
const errorMsg =
|
||||||
|
|
@ -567,10 +509,6 @@ export default class ChainManager {
|
||||||
console.error(errorMsg);
|
console.error(errorMsg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return await VectorDBManager.indexFile(
|
return await VectorDBManager.indexFile(this.getDbVectorStores(), embeddingsAPI, noteFile);
|
||||||
this.getDbVectorStores(),
|
|
||||||
embeddingsAPI,
|
|
||||||
noteFile,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default class ChatModelManager {
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
private langChainParams: LangChainParams,
|
private langChainParams: LangChainParams,
|
||||||
encryptionService: EncryptionService,
|
encryptionService: EncryptionService
|
||||||
) {
|
) {
|
||||||
this.encryptionService = encryptionService;
|
this.encryptionService = encryptionService;
|
||||||
this.buildModelMap();
|
this.buildModelMap();
|
||||||
|
|
@ -45,20 +45,16 @@ export default class ChatModelManager {
|
||||||
|
|
||||||
static getInstance(
|
static getInstance(
|
||||||
langChainParams: LangChainParams,
|
langChainParams: LangChainParams,
|
||||||
encryptionService: EncryptionService,
|
encryptionService: EncryptionService
|
||||||
): ChatModelManager {
|
): ChatModelManager {
|
||||||
if (!ChatModelManager.instance) {
|
if (!ChatModelManager.instance) {
|
||||||
ChatModelManager.instance = new ChatModelManager(
|
ChatModelManager.instance = new ChatModelManager(langChainParams, encryptionService);
|
||||||
langChainParams,
|
|
||||||
encryptionService,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return ChatModelManager.instance;
|
return ChatModelManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getModelConfig(chatModelProvider: string): ModelConfig {
|
private getModelConfig(chatModelProvider: string): ModelConfig {
|
||||||
const decrypt = (key: string) =>
|
const decrypt = (key: string) => this.encryptionService.getDecryptedKey(key);
|
||||||
this.encryptionService.getDecryptedKey(key);
|
|
||||||
const params = this.langChainParams;
|
const params = this.langChainParams;
|
||||||
const baseConfig: ModelConfig = {
|
const baseConfig: ModelConfig = {
|
||||||
modelName: params.model,
|
modelName: params.model,
|
||||||
|
|
@ -70,10 +66,7 @@ export default class ChatModelManager {
|
||||||
|
|
||||||
const providerConfig = {
|
const providerConfig = {
|
||||||
[ModelProviders.OPENAI]: {
|
[ModelProviders.OPENAI]: {
|
||||||
modelName:
|
modelName: params.openAIProxyModelName || params.openAICustomModel || params.model,
|
||||||
params.openAIProxyModelName ||
|
|
||||||
params.openAICustomModel ||
|
|
||||||
params.model,
|
|
||||||
openAIApiKey: decrypt(params.openAIApiKey),
|
openAIApiKey: decrypt(params.openAIApiKey),
|
||||||
openAIOrgId: decrypt(params.openAIOrgId),
|
openAIOrgId: decrypt(params.openAIOrgId),
|
||||||
maxTokens: params.maxTokens,
|
maxTokens: params.maxTokens,
|
||||||
|
|
@ -136,9 +129,7 @@ export default class ChatModelManager {
|
||||||
ChatModelManager.modelMap = {};
|
ChatModelManager.modelMap = {};
|
||||||
const modelMap = ChatModelManager.modelMap;
|
const modelMap = ChatModelManager.modelMap;
|
||||||
|
|
||||||
const OpenAIChatModel = this.langChainParams.openAIProxyBaseUrl
|
const OpenAIChatModel = this.langChainParams.openAIProxyBaseUrl ? ProxyChatOpenAI : ChatOpenAI;
|
||||||
? ProxyChatOpenAI
|
|
||||||
: ChatOpenAI;
|
|
||||||
|
|
||||||
const modelConfigurations = [
|
const modelConfigurations = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { LangChainParams } from "@/aiParams";
|
import { LangChainParams } from "@/aiParams";
|
||||||
import {
|
import { EMBEDDING_MODEL_TO_PROVIDERS, ModelProviders, NOMIC_EMBED_TEXT } from "@/constants";
|
||||||
EMBEDDING_MODEL_TO_PROVIDERS,
|
|
||||||
ModelProviders,
|
|
||||||
NOMIC_EMBED_TEXT,
|
|
||||||
} from "@/constants";
|
|
||||||
import EncryptionService from "@/encryptionService";
|
import EncryptionService from "@/encryptionService";
|
||||||
import { ProxyOpenAIEmbeddings } from "@/langchainWrappers";
|
import { ProxyOpenAIEmbeddings } from "@/langchainWrappers";
|
||||||
import { CohereEmbeddings } from "@langchain/cohere";
|
import { CohereEmbeddings } from "@langchain/cohere";
|
||||||
|
|
@ -16,18 +12,15 @@ export default class EmbeddingManager {
|
||||||
private static instance: EmbeddingManager;
|
private static instance: EmbeddingManager;
|
||||||
private constructor(
|
private constructor(
|
||||||
private langChainParams: LangChainParams,
|
private langChainParams: LangChainParams,
|
||||||
private encryptionService: EncryptionService,
|
private encryptionService: EncryptionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static getInstance(
|
static getInstance(
|
||||||
langChainParams: LangChainParams,
|
langChainParams: LangChainParams,
|
||||||
encryptionService: EncryptionService,
|
encryptionService: EncryptionService
|
||||||
): EmbeddingManager {
|
): EmbeddingManager {
|
||||||
if (!EmbeddingManager.instance) {
|
if (!EmbeddingManager.instance) {
|
||||||
EmbeddingManager.instance = new EmbeddingManager(
|
EmbeddingManager.instance = new EmbeddingManager(langChainParams, encryptionService);
|
||||||
langChainParams,
|
|
||||||
encryptionService,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return EmbeddingManager.instance;
|
return EmbeddingManager.instance;
|
||||||
}
|
}
|
||||||
|
|
@ -40,14 +33,13 @@ export default class EmbeddingManager {
|
||||||
return emb.modelName as string;
|
return emb.modelName as string;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Embeddings instance missing model or modelName properties: ${embeddingsInstance}`,
|
`Embeddings instance missing model or modelName properties: ${embeddingsInstance}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenAIEmbeddingAPI(): OpenAIEmbeddings | undefined {
|
getOpenAIEmbeddingAPI(): OpenAIEmbeddings | undefined {
|
||||||
const decrypt = (key: string) =>
|
const decrypt = (key: string) => this.encryptionService.getDecryptedKey(key);
|
||||||
this.encryptionService.getDecryptedKey(key);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
openAIApiKey,
|
openAIApiKey,
|
||||||
|
|
@ -78,8 +70,7 @@ export default class EmbeddingManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmbeddingsAPI(): Embeddings | undefined {
|
getEmbeddingsAPI(): Embeddings | undefined {
|
||||||
const decrypt = (key: string) =>
|
const decrypt = (key: string) => this.encryptionService.getDecryptedKey(key);
|
||||||
this.encryptionService.getDecryptedKey(key);
|
|
||||||
const {
|
const {
|
||||||
azureOpenAIApiKey,
|
azureOpenAIApiKey,
|
||||||
azureOpenAIApiInstanceName,
|
azureOpenAIApiInstanceName,
|
||||||
|
|
@ -90,17 +81,14 @@ export default class EmbeddingManager {
|
||||||
|
|
||||||
const OpenAIEmbeddingsAPI = this.getOpenAIEmbeddingAPI();
|
const OpenAIEmbeddingsAPI = this.getOpenAIEmbeddingAPI();
|
||||||
|
|
||||||
const embeddingProvder =
|
const embeddingProvder = EMBEDDING_MODEL_TO_PROVIDERS[this.langChainParams.embeddingModel];
|
||||||
EMBEDDING_MODEL_TO_PROVIDERS[this.langChainParams.embeddingModel];
|
|
||||||
|
|
||||||
switch (embeddingProvder) {
|
switch (embeddingProvder) {
|
||||||
case ModelProviders.OPENAI:
|
case ModelProviders.OPENAI:
|
||||||
if (OpenAIEmbeddingsAPI) {
|
if (OpenAIEmbeddingsAPI) {
|
||||||
return OpenAIEmbeddingsAPI;
|
return OpenAIEmbeddingsAPI;
|
||||||
}
|
}
|
||||||
console.error(
|
console.error("OpenAI API key is not provided for the embedding model.");
|
||||||
"OpenAI API key is not provided for the embedding model.",
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case ModelProviders.COHEREAI:
|
case ModelProviders.COHEREAI:
|
||||||
return new CohereEmbeddings({
|
return new CohereEmbeddings({
|
||||||
|
|
@ -119,9 +107,7 @@ export default class EmbeddingManager {
|
||||||
maxConcurrency: 3,
|
maxConcurrency: 3,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.error(
|
console.error("Azure OpenAI API key is not provided for the embedding model.");
|
||||||
"Azure OpenAI API key is not provided for the embedding model.",
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case ModelProviders.OLLAMA:
|
case ModelProviders.OLLAMA:
|
||||||
return new OllamaEmbeddings({
|
return new OllamaEmbeddings({
|
||||||
|
|
@ -133,14 +119,12 @@ export default class EmbeddingManager {
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
console.error(
|
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 (
|
return (
|
||||||
OpenAIEmbeddingsAPI ||
|
OpenAIEmbeddingsAPI ||
|
||||||
new OpenAIEmbeddings({
|
new OpenAIEmbeddings({
|
||||||
modelName:
|
modelName: openAIEmbeddingProxyModelName || this.langChainParams.embeddingModel,
|
||||||
openAIEmbeddingProxyModelName ||
|
|
||||||
this.langChainParams.embeddingModel,
|
|
||||||
openAIApiKey: "default-key",
|
openAIApiKey: "default-key",
|
||||||
maxRetries: 3,
|
maxRetries: 3,
|
||||||
maxConcurrency: 3,
|
maxConcurrency: 3,
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
import { LangChainParams } from '@/aiParams';
|
import { LangChainParams } from "@/aiParams";
|
||||||
import { BaseChatMemory, BufferWindowMemory } from "langchain/memory";
|
import { BaseChatMemory, BufferWindowMemory } from "langchain/memory";
|
||||||
|
|
||||||
export default class MemoryManager {
|
export default class MemoryManager {
|
||||||
private static instance: MemoryManager;
|
private static instance: MemoryManager;
|
||||||
private memory: BaseChatMemory;
|
private memory: BaseChatMemory;
|
||||||
|
|
||||||
private constructor(
|
private constructor(private langChainParams: LangChainParams) {
|
||||||
private langChainParams: LangChainParams
|
|
||||||
) {
|
|
||||||
this.initMemory();
|
this.initMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(
|
static getInstance(langChainParams: LangChainParams): MemoryManager {
|
||||||
langChainParams: LangChainParams
|
|
||||||
): MemoryManager {
|
|
||||||
if (!MemoryManager.instance) {
|
if (!MemoryManager.instance) {
|
||||||
MemoryManager.instance = new MemoryManager(langChainParams);
|
MemoryManager.instance = new MemoryManager(langChainParams);
|
||||||
}
|
}
|
||||||
|
|
@ -23,8 +19,8 @@ export default class MemoryManager {
|
||||||
private initMemory(): void {
|
private initMemory(): void {
|
||||||
this.memory = new BufferWindowMemory({
|
this.memory = new BufferWindowMemory({
|
||||||
k: this.langChainParams.chatContextTurns * 2,
|
k: this.langChainParams.chatContextTurns * 2,
|
||||||
memoryKey: 'history',
|
memoryKey: "history",
|
||||||
inputKey: 'input',
|
inputKey: "input",
|
||||||
returnMessages: true,
|
returnMessages: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -34,8 +30,7 @@ export default class MemoryManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
clearChatMemory(): void {
|
clearChatMemory(): void {
|
||||||
console.log('clearing chat memory');
|
console.log("clearing chat memory");
|
||||||
this.memory.clear();
|
this.memory.clear();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { LangChainParams } from '@/aiParams';
|
import { LangChainParams } from "@/aiParams";
|
||||||
import {
|
import {
|
||||||
ChatPromptTemplate,
|
ChatPromptTemplate,
|
||||||
HumanMessagePromptTemplate,
|
HumanMessagePromptTemplate,
|
||||||
|
|
@ -10,15 +10,11 @@ export default class PromptManager {
|
||||||
private static instance: PromptManager;
|
private static instance: PromptManager;
|
||||||
private chatPrompt: ChatPromptTemplate;
|
private chatPrompt: ChatPromptTemplate;
|
||||||
|
|
||||||
private constructor(
|
private constructor(private langChainParams: LangChainParams) {
|
||||||
private langChainParams: LangChainParams
|
|
||||||
) {
|
|
||||||
this.initChatPrompt();
|
this.initChatPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(
|
static getInstance(langChainParams: LangChainParams): PromptManager {
|
||||||
langChainParams: LangChainParams
|
|
||||||
): PromptManager {
|
|
||||||
if (!PromptManager.instance) {
|
if (!PromptManager.instance) {
|
||||||
PromptManager.instance = new PromptManager(langChainParams);
|
PromptManager.instance = new PromptManager(langChainParams);
|
||||||
}
|
}
|
||||||
|
|
@ -27,9 +23,7 @@ export default class PromptManager {
|
||||||
|
|
||||||
private initChatPrompt(): void {
|
private initChatPrompt(): void {
|
||||||
this.chatPrompt = ChatPromptTemplate.fromMessages([
|
this.chatPrompt = ChatPromptTemplate.fromMessages([
|
||||||
SystemMessagePromptTemplate.fromTemplate(
|
SystemMessagePromptTemplate.fromTemplate(this.langChainParams.systemMessage),
|
||||||
this.langChainParams.systemMessage
|
|
||||||
),
|
|
||||||
new MessagesPlaceholder("history"),
|
new MessagesPlaceholder("history"),
|
||||||
HumanMessagePromptTemplate.fromTemplate("{input}"),
|
HumanMessagePromptTemplate.fromTemplate("{input}"),
|
||||||
]);
|
]);
|
||||||
|
|
@ -38,4 +32,4 @@ export default class PromptManager {
|
||||||
getChatPrompt(): ChatPromptTemplate {
|
getChatPrompt(): ChatPromptTemplate {
|
||||||
return this.chatPrompt;
|
return this.chatPrompt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import ChainManager from '@/LLMProviders/chainManager';
|
import ChainManager from "@/LLMProviders/chainManager";
|
||||||
import { SetChainOptions } from '@/aiParams';
|
import { SetChainOptions } from "@/aiParams";
|
||||||
import { ChainType } from '@/chainFactory';
|
import { ChainType } from "@/chainFactory";
|
||||||
import { BaseChatMemory } from "langchain/memory";
|
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.
|
* React hook to manage state related to model, chain and memory in Chat component.
|
||||||
*/
|
*/
|
||||||
export function useAIState(
|
export function useAIState(
|
||||||
chainManager: ChainManager,
|
chainManager: ChainManager
|
||||||
): [
|
): [
|
||||||
string,
|
string,
|
||||||
(model: string) => void,
|
(model: string) => void,
|
||||||
|
|
@ -36,11 +36,5 @@ export function useAIState(
|
||||||
setCurrentChain(newChain);
|
setCurrentChain(newChain);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [currentModel, setModel, currentChain, setChain, clearChatMemory];
|
||||||
currentModel,
|
|
||||||
setModel,
|
|
||||||
currentChain,
|
|
||||||
setChain,
|
|
||||||
clearChatMemory,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import { StringOutputParser } from "@langchain/core/output_parsers";
|
import { StringOutputParser } from "@langchain/core/output_parsers";
|
||||||
import { BaseRetriever } from "@langchain/core/retrievers";
|
import { BaseRetriever } from "@langchain/core/retrievers";
|
||||||
import {
|
import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
|
||||||
RunnablePassthrough,
|
|
||||||
RunnableSequence,
|
|
||||||
} from "@langchain/core/runnables";
|
|
||||||
import { BaseLanguageModel } from "langchain/base_language";
|
import { BaseLanguageModel } from "langchain/base_language";
|
||||||
import { BaseChatMemory } from "langchain/memory";
|
import { BaseChatMemory } from "langchain/memory";
|
||||||
import { ChatPromptTemplate, PromptTemplate } from "langchain/prompts";
|
import { ChatPromptTemplate, PromptTemplate } from "langchain/prompts";
|
||||||
|
|
@ -129,7 +126,7 @@ class ChainFactory {
|
||||||
public static createConversationalRetrievalChain(
|
public static createConversationalRetrievalChain(
|
||||||
args: ConversationalRetrievalChainParams,
|
args: ConversationalRetrievalChainParams,
|
||||||
onDocumentsRetrieved: (documents: Document[]) => void,
|
onDocumentsRetrieved: (documents: Document[]) => void,
|
||||||
debug?: boolean,
|
debug?: boolean
|
||||||
): RunnableSequence {
|
): RunnableSequence {
|
||||||
const { llm, retriever } = args;
|
const { llm, retriever } = args;
|
||||||
|
|
||||||
|
|
@ -144,9 +141,7 @@ class ChainFactory {
|
||||||
{chat_history}
|
{chat_history}
|
||||||
Follow Up Input: {question}
|
Follow Up Input: {question}
|
||||||
Standalone question:`;
|
Standalone question:`;
|
||||||
const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate(
|
const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate(condenseQuestionTemplate);
|
||||||
condenseQuestionTemplate,
|
|
||||||
);
|
|
||||||
|
|
||||||
const answerTemplate = `Answer the question with as detailed as possible based only on the following context:
|
const answerTemplate = `Answer the question with as detailed as possible based only on the following context:
|
||||||
{context}
|
{context}
|
||||||
|
|
@ -157,8 +152,7 @@ class ChainFactory {
|
||||||
|
|
||||||
const formatChatHistory = (chatHistory: [string, string][]) => {
|
const formatChatHistory = (chatHistory: [string, string][]) => {
|
||||||
const formattedDialogueTurns = chatHistory.map(
|
const formattedDialogueTurns = chatHistory.map(
|
||||||
(dialogueTurn) =>
|
(dialogueTurn) => `Human: ${dialogueTurn[0]}\nAssistant: ${dialogueTurn[1]}`
|
||||||
`Human: ${dialogueTurn[0]}\nAssistant: ${dialogueTurn[1]}`,
|
|
||||||
);
|
);
|
||||||
return formattedDialogueTurns.join("\n");
|
return formattedDialogueTurns.join("\n");
|
||||||
};
|
};
|
||||||
|
|
@ -171,8 +165,7 @@ class ChainFactory {
|
||||||
},
|
},
|
||||||
chat_history: (input: ConversationalRetrievalQAChainInput) => {
|
chat_history: (input: ConversationalRetrievalQAChainInput) => {
|
||||||
const formattedChatHistory = formatChatHistory(input.chat_history);
|
const formattedChatHistory = formatChatHistory(input.chat_history);
|
||||||
if (debug)
|
if (debug) console.log("Formatted Chat History: ", formattedChatHistory);
|
||||||
console.log("Formatted Chat History: ", formattedChatHistory);
|
|
||||||
return formattedChatHistory;
|
return formattedChatHistory;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -196,8 +189,7 @@ class ChainFactory {
|
||||||
llm,
|
llm,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const conversationalRetrievalQAChain =
|
const conversationalRetrievalQAChain = standaloneQuestionChain.pipe(answerChain);
|
||||||
standaloneQuestionChain.pipe(answerChain);
|
|
||||||
return conversationalRetrievalQAChain as RunnableSequence;
|
return conversationalRetrievalQAChain as RunnableSequence;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,7 @@ import { Editor, Notice } from "obsidian";
|
||||||
import { COMMAND_IDS } from "./constants";
|
import { COMMAND_IDS } from "./constants";
|
||||||
|
|
||||||
export function registerBuiltInCommands(plugin: CopilotPlugin) {
|
export function registerBuiltInCommands(plugin: CopilotPlugin) {
|
||||||
const addCommandIfEnabled = (
|
const addCommandIfEnabled = (id: string, callback: (editor: Editor) => void) => {
|
||||||
id: string,
|
|
||||||
callback: (editor: Editor) => void,
|
|
||||||
) => {
|
|
||||||
const commandSettings = plugin.settings.enabledCommands[id];
|
const commandSettings = plugin.settings.enabledCommands[id];
|
||||||
if (commandSettings && commandSettings.enabled) {
|
if (commandSettings && commandSettings.enabled) {
|
||||||
plugin.addCommand({
|
plugin.addCommand({
|
||||||
|
|
|
||||||
|
|
@ -1,121 +1,101 @@
|
||||||
import { App, Modal, Notice } from "obsidian";
|
import { App, Modal, Notice } from "obsidian";
|
||||||
|
|
||||||
|
|
||||||
export class AddPromptModal extends Modal {
|
export class AddPromptModal extends Modal {
|
||||||
constructor(
|
constructor(
|
||||||
app: App,
|
app: App,
|
||||||
onSave: (title: string, prompt: string) => void,
|
onSave: (title: string, prompt: string) => void,
|
||||||
initialTitle = '',
|
initialTitle = "",
|
||||||
initialPrompt = '',
|
initialPrompt = "",
|
||||||
disabledTitle?: boolean,
|
disabledTitle?: boolean
|
||||||
) {
|
) {
|
||||||
super(app);
|
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(
|
const titleContainer = formContainer.createEl("div", {
|
||||||
'div',
|
cls: "copilot-command-input-container",
|
||||||
{ cls: 'copilot-command-input-container' }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
titleContainer.createEl(
|
titleContainer.createEl("h3", { text: "Title", cls: "copilot-command-header" });
|
||||||
'h3', { text: 'Title', cls: 'copilot-command-header' }
|
titleContainer.createEl("p", {
|
||||||
);
|
text: "The title of the prompt, must be unique.",
|
||||||
titleContainer.createEl(
|
cls: "copilot-command-input-description",
|
||||||
'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) {
|
if (disabledTitle) {
|
||||||
titleField.setAttribute('disabled', 'true');
|
titleField.setAttribute("disabled", "true");
|
||||||
}
|
}
|
||||||
if (initialTitle) {
|
if (initialTitle) {
|
||||||
titleField.value = initialTitle;
|
titleField.value = initialTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptContainer = formContainer.createEl(
|
const promptContainer = formContainer.createEl("div", {
|
||||||
'div',
|
cls: "copilot-command-input-container",
|
||||||
{ cls: 'copilot-command-input-container' }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
promptContainer.createEl(
|
promptContainer.createEl("h3", { text: "Prompt", cls: "copilot-command-header" });
|
||||||
'h3', { text: 'Prompt', cls: 'copilot-command-header' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const promptDescFragment = createFragment((frag) => {
|
const promptDescFragment = createFragment((frag) => {
|
||||||
frag.createEl(
|
frag.createEl("strong", { text: "- {} represents the selected text (not required). " });
|
||||||
'strong',
|
frag.createEl("br");
|
||||||
{ text: '- {} represents the selected text (not required). ' }
|
frag.createEl("strong", { text: "- {[[Note Title]]} represents a note. " });
|
||||||
);
|
frag.createEl("br");
|
||||||
frag.createEl('br');
|
frag.createEl("strong", { text: "- {FolderPath} represents a folder of notes. " });
|
||||||
frag.createEl(
|
frag.createEl("br");
|
||||||
'strong',
|
frag.createEl("strong", {
|
||||||
{ text: '- {[[Note Title]]} represents a note. ' }
|
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.createEl(
|
frag.createEl("br");
|
||||||
'strong',
|
frag.appendText("Tip: turn on debug mode to show the processed prompt in the chat window.");
|
||||||
{ text: '- {FolderPath} represents a folder of notes. ' }
|
frag.createEl("br");
|
||||||
);
|
frag.createEl("br");
|
||||||
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);
|
promptContainer.appendChild(promptDescFragment);
|
||||||
|
|
||||||
const promptField = promptContainer.createEl('textarea');
|
const promptField = promptContainer.createEl("textarea");
|
||||||
if (initialPrompt) {
|
if (initialPrompt) {
|
||||||
promptField.value = initialPrompt;
|
promptField.value = initialPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const descFragment = createFragment((frag) => {
|
const descFragment = createFragment((frag) => {
|
||||||
frag.appendText('Save the prompt to the local prompt library. You can then use it with the Copilot command: ');
|
frag.appendText(
|
||||||
frag.createEl(
|
"Save the prompt to the local prompt library. You can then use it with the Copilot command: "
|
||||||
'strong',
|
|
||||||
{ text: 'Apply custom prompt to selection.' }
|
|
||||||
);
|
);
|
||||||
frag.createEl('br');
|
frag.createEl("strong", { text: "Apply custom prompt to selection." });
|
||||||
frag.appendText('Check out the ');
|
frag.createEl("br");
|
||||||
frag.createEl('a', {
|
frag.appendText("Check out the ");
|
||||||
href: 'https://github.com/f/awesome-chatgpt-prompts',
|
frag
|
||||||
text: 'awesome chatGPT prompts',
|
.createEl("a", {
|
||||||
}).setAttr('target', '_blank');
|
href: "https://github.com/f/awesome-chatgpt-prompts",
|
||||||
frag.appendText(' for inspiration.');
|
text: "awesome chatGPT prompts",
|
||||||
|
})
|
||||||
|
.setAttr("target", "_blank");
|
||||||
|
frag.appendText(" for inspiration.");
|
||||||
});
|
});
|
||||||
|
|
||||||
const descContainer = promptContainer.createEl('p', {
|
const descContainer = promptContainer.createEl("p", {
|
||||||
cls: 'copilot-command-input-description',
|
cls: "copilot-command-input-description",
|
||||||
});
|
});
|
||||||
|
|
||||||
descContainer.appendChild(descFragment);
|
descContainer.appendChild(descFragment);
|
||||||
|
|
||||||
const saveButtonContainer = formContainer.createEl(
|
const saveButtonContainer = formContainer.createEl("div", {
|
||||||
'div',
|
cls: "copilot-command-save-btn-container",
|
||||||
{ cls: 'copilot-command-save-btn-container' }
|
});
|
||||||
);
|
const saveButton = saveButtonContainer.createEl("button", {
|
||||||
const saveButton = saveButtonContainer.createEl(
|
text: "Save",
|
||||||
'button',
|
cls: "copilot-command-save-btn",
|
||||||
{ text: 'Save', cls: 'copilot-command-save-btn' }
|
});
|
||||||
);
|
saveButton.addEventListener("click", () => {
|
||||||
saveButton.addEventListener('click', () => {
|
|
||||||
if (titleField.value && promptField.value) {
|
if (titleField.value && promptField.value) {
|
||||||
onSave(titleField.value, promptField.value);
|
onSave(titleField.value, promptField.value);
|
||||||
this.close();
|
this.close();
|
||||||
} else {
|
} else {
|
||||||
new Notice('Please fill in both fields: Title and Prompt.');
|
new Notice("Please fill in both fields: Title and Prompt.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,60 @@
|
||||||
import { App, Modal } from 'obsidian';
|
import { App, Modal } from "obsidian";
|
||||||
|
|
||||||
export class AdhocPromptModal extends Modal {
|
export class AdhocPromptModal extends Modal {
|
||||||
result: string;
|
result: string;
|
||||||
onSubmit: (result: string) => void;
|
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) {
|
constructor(app: App, onSubmit: (result: string) => void) {
|
||||||
super(app);
|
super(app);
|
||||||
this.onSubmit = onSubmit;
|
this.onSubmit = onSubmit;
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
|
||||||
const promptDescFragment = createFragment((frag) => {
|
const promptDescFragment = createFragment((frag) => {
|
||||||
frag.createEl(
|
frag.createEl("strong", { text: "- {} represents the selected text (not required). " });
|
||||||
'strong',
|
frag.createEl("br");
|
||||||
{ text: '- {} represents the selected text (not required). ' }
|
frag.createEl("strong", { text: "- {[[Note Title]]} represents a note. " });
|
||||||
);
|
frag.createEl("br");
|
||||||
frag.createEl('br');
|
frag.createEl("strong", { text: "- {FolderPath} represents a folder of notes. " });
|
||||||
frag.createEl(
|
frag.createEl("br");
|
||||||
'strong',
|
frag.createEl("strong", {
|
||||||
{ text: '- {[[Note Title]]} represents a note. ' }
|
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.createEl(
|
frag.createEl("br");
|
||||||
'strong',
|
frag.appendText("Tip: turn on debug mode to show the processed prompt in the chat window.");
|
||||||
{ text: '- {FolderPath} represents a folder of notes. ' }
|
frag.createEl("br");
|
||||||
);
|
frag.createEl("br");
|
||||||
frag.createEl('br');
|
});
|
||||||
frag.createEl(
|
contentEl.appendChild(promptDescFragment);
|
||||||
'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 } });
|
const textareaEl = contentEl.createEl("textarea", {
|
||||||
textareaEl.style.width = '100%';
|
attr: { placeholder: this.placeholderText },
|
||||||
textareaEl.style.height = '100px'; // Set the desired height
|
});
|
||||||
textareaEl.style.padding = '10px';
|
textareaEl.style.width = "100%";
|
||||||
textareaEl.style.resize = 'vertical'; // Allow vertical resizing
|
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;
|
this.result = (evt.target as HTMLTextAreaElement).value;
|
||||||
});
|
});
|
||||||
|
|
||||||
textareaEl.addEventListener('keydown', (evt) => {
|
textareaEl.addEventListener("keydown", (evt) => {
|
||||||
if (evt.key === 'Enter' && !evt.shiftKey) {
|
if (evt.key === "Enter" && !evt.shiftKey) {
|
||||||
evt.preventDefault(); // Prevent line break unless Shift key is pressed
|
evt.preventDefault(); // Prevent line break unless Shift key is pressed
|
||||||
this.close();
|
this.close();
|
||||||
this.onSubmit(this.result);
|
this.onSubmit(this.result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
import ChainManager from '@/LLMProviders/chainManager';
|
import ChainManager from "@/LLMProviders/chainManager";
|
||||||
import { useAIState } from '@/aiState';
|
import { useAIState } from "@/aiState";
|
||||||
import { ChainType } from '@/chainFactory';
|
import { ChainType } from "@/chainFactory";
|
||||||
import ChatIcons from '@/components/ChatComponents/ChatIcons';
|
import ChatIcons from "@/components/ChatComponents/ChatIcons";
|
||||||
import ChatInput from '@/components/ChatComponents/ChatInput';
|
import ChatInput from "@/components/ChatComponents/ChatInput";
|
||||||
import ChatMessages from '@/components/ChatComponents/ChatMessages';
|
import ChatMessages from "@/components/ChatComponents/ChatMessages";
|
||||||
import { AI_SENDER, USER_SENDER } from '@/constants';
|
import { AI_SENDER, USER_SENDER } from "@/constants";
|
||||||
import { AppContext } from '@/context';
|
import { AppContext } from "@/context";
|
||||||
import { CustomPromptProcessor } from '@/customPromptProcessor';
|
import { CustomPromptProcessor } from "@/customPromptProcessor";
|
||||||
import { getAIResponse } from '@/langchainStream';
|
import { getAIResponse } from "@/langchainStream";
|
||||||
import CopilotPlugin from '@/main';
|
import CopilotPlugin from "@/main";
|
||||||
import { CopilotSettings } from '@/settings/SettingsPage';
|
import { CopilotSettings } from "@/settings/SettingsPage";
|
||||||
import SharedState, {
|
import SharedState, { ChatMessage, useSharedState } from "@/sharedState";
|
||||||
ChatMessage, useSharedState,
|
|
||||||
} from '@/sharedState';
|
|
||||||
import {
|
import {
|
||||||
createChangeToneSelectionPrompt,
|
createChangeToneSelectionPrompt,
|
||||||
createTranslateSelectionPrompt,
|
createTranslateSelectionPrompt,
|
||||||
|
|
@ -39,14 +37,10 @@ import {
|
||||||
simplifyPrompt,
|
simplifyPrompt,
|
||||||
summarizePrompt,
|
summarizePrompt,
|
||||||
tocPrompt,
|
tocPrompt,
|
||||||
} from '@/utils';
|
} from "@/utils";
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from "events";
|
||||||
import { Notice, TFile } from 'obsidian';
|
import { Notice, TFile } from "obsidian";
|
||||||
import React, {
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
interface CreateEffectOptions {
|
interface CreateEffectOptions {
|
||||||
custom_temperature?: number;
|
custom_temperature?: number;
|
||||||
|
|
@ -73,16 +67,13 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
getChatVisibility,
|
getChatVisibility,
|
||||||
defaultSaveFolder,
|
defaultSaveFolder,
|
||||||
plugin,
|
plugin,
|
||||||
debug
|
debug,
|
||||||
}) => {
|
}) => {
|
||||||
const [
|
const [chatHistory, addMessage, clearMessages] = useSharedState(sharedState);
|
||||||
chatHistory, addMessage, clearMessages,
|
const [currentModel, setModel, currentChain, setChain, clearChatMemory] =
|
||||||
] = useSharedState(sharedState);
|
useAIState(chainManager);
|
||||||
const [
|
const [currentAiMessage, setCurrentAiMessage] = useState("");
|
||||||
currentModel, setModel, currentChain, setChain, clearChatMemory,
|
const [inputMessage, setInputMessage] = useState("");
|
||||||
] = useAIState(chainManager);
|
|
||||||
const [currentAiMessage, setCurrentAiMessage] = useState('');
|
|
||||||
const [inputMessage, setInputMessage] = useState('');
|
|
||||||
const [abortController, setAbortController] = useState<AbortController | null>(null);
|
const [abortController, setAbortController] = useState<AbortController | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
|
@ -121,7 +112,7 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
addMessage(userMessage);
|
addMessage(userMessage);
|
||||||
addMessage(promptMessageHidden);
|
addMessage(promptMessageHidden);
|
||||||
// Clear input
|
// Clear input
|
||||||
setInputMessage('');
|
setInputMessage("");
|
||||||
|
|
||||||
// Display running dots to indicate loading
|
// Display running dots to indicate loading
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -131,14 +122,14 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
addMessage,
|
addMessage,
|
||||||
setCurrentAiMessage,
|
setCurrentAiMessage,
|
||||||
setAbortController,
|
setAbortController,
|
||||||
{ debug },
|
{ debug }
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (event.nativeEvent.isComposing) return;
|
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
|
event.preventDefault(); // Prevents adding a newline to the textarea
|
||||||
handleSendMessage();
|
handleSendMessage();
|
||||||
}
|
}
|
||||||
|
|
@ -146,16 +137,16 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
|
|
||||||
const handleSaveAsNote = async () => {
|
const handleSaveAsNote = async () => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
console.error('App instance is not available.');
|
console.error("App instance is not available.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the chat history as a new note in the vault
|
// Save the chat history as a new note in the vault
|
||||||
// Only visible messages are included
|
// Only visible messages are included
|
||||||
const chatContent = chatHistory
|
const chatContent = chatHistory
|
||||||
.filter(message => message.isVisible)
|
.filter((message) => message.isVisible)
|
||||||
.map((message) => `**${message.sender}**: ${message.message}`)
|
.map((message) => `**${message.sender}**: ${message.message}`)
|
||||||
.join('\n\n');
|
.join("\n\n");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if the default folder exists or create it
|
// Check if the default folder exists or create it
|
||||||
|
|
@ -170,20 +161,20 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
const leaf = app.workspace.getLeaf();
|
const leaf = app.workspace.getLeaf();
|
||||||
leaf.openFile(newNote);
|
leaf.openFile(newNote);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving chat as note:', error);
|
console.error("Error saving chat as note:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendActiveNoteToPrompt = async () => {
|
const handleSendActiveNoteToPrompt = async () => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
console.error('App instance is not available.');
|
console.error("App instance is not available.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let noteFiles: TFile[] = [];
|
let noteFiles: TFile[] = [];
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log('Chat note context path:', settings.chatNoteContextPath);
|
console.log("Chat note context path:", settings.chatNoteContextPath);
|
||||||
console.log('Chat note context tags:', settings.chatNoteContextTags);
|
console.log("Chat note context tags:", settings.chatNoteContextTags);
|
||||||
}
|
}
|
||||||
if (settings.chatNoteContextPath) {
|
if (settings.chatNoteContextPath) {
|
||||||
// Recursively get all note TFiles in the path
|
// 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 no note context provided, default to the active note
|
||||||
if (noteFiles.length === 0) {
|
if (noteFiles.length === 0) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
new Notice('No active note found.');
|
new Notice("No active note found.");
|
||||||
console.error('No active note found.');
|
console.error("No active note found.");
|
||||||
return;
|
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];
|
noteFiles = [file];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,7 +220,7 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
const sendNoteContentUserMessage = getSendChatContextNotesPrompt(
|
const sendNoteContentUserMessage = getSendChatContextNotesPrompt(
|
||||||
notes,
|
notes,
|
||||||
settings.chatNoteContextPath,
|
settings.chatNoteContextPath,
|
||||||
settings.chatNoteContextTags,
|
settings.chatNoteContextTags
|
||||||
);
|
);
|
||||||
const promptMessageVisible: ChatMessage = {
|
const promptMessageVisible: ChatMessage = {
|
||||||
message: sendNoteContentUserMessage,
|
message: sendNoteContentUserMessage,
|
||||||
|
|
@ -247,32 +238,32 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
addMessage,
|
addMessage,
|
||||||
setCurrentAiMessage,
|
setCurrentAiMessage,
|
||||||
setAbortController,
|
setAbortController,
|
||||||
{ debug },
|
{ debug }
|
||||||
);
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const forceRebuildActiveNoteContext = async () => {
|
const forceRebuildActiveNoteContext = async () => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
console.error('App instance is not available.');
|
console.error("App instance is not available.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = app.workspace.getActiveFile();
|
const file = app.workspace.getActiveFile();
|
||||||
if (!file) {
|
if (!file) {
|
||||||
new Notice('No active note found.');
|
new Notice("No active note found.");
|
||||||
console.error('No active note found.');
|
console.error("No active note found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const noteContent = await getFileContent(file, app.vault);
|
const noteContent = await getFileContent(file, app.vault);
|
||||||
const noteName = getFileName(file);
|
const noteName = getFileName(file);
|
||||||
if (!noteContent) {
|
if (!noteContent) {
|
||||||
new Notice('No note content found.');
|
new Notice("No note content found.");
|
||||||
console.error('No note content found.');
|
console.error("No note content found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileMetadata = app.metadataCache.getFileCache(file)
|
const fileMetadata = app.metadataCache.getFileCache(file);
|
||||||
const noteFile = {
|
const noteFile = {
|
||||||
path: file.path,
|
path: file.path,
|
||||||
basename: file.basename,
|
basename: file.basename,
|
||||||
|
|
@ -296,16 +287,16 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
|
|
||||||
const refreshVaultContext = async () => {
|
const refreshVaultContext = async () => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
console.error('App instance is not available.');
|
console.error("App instance is not available.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await plugin.indexVaultToVectorStore();
|
await plugin.indexVaultToVectorStore();
|
||||||
new Notice('Vault index refreshed.');
|
new Notice("Vault index refreshed.");
|
||||||
}
|
};
|
||||||
|
|
||||||
const clearCurrentAiMessage = () => {
|
const clearCurrentAiMessage = () => {
|
||||||
setCurrentAiMessage('');
|
setCurrentAiMessage("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStopGenerating = () => {
|
const handleStopGenerating = () => {
|
||||||
|
|
@ -317,7 +308,7 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function handleSelection(selectedText: string) {
|
async function handleSelection(selectedText: string) {
|
||||||
const wordCount = selectedText.split(' ').length;
|
const wordCount = selectedText.split(" ").length;
|
||||||
const tokenCount = await chainManager.chatModelManager.countTokens(selectedText);
|
const tokenCount = await chainManager.chatModelManager.countTokens(selectedText);
|
||||||
const tokenCountMessage: ChatMessage = {
|
const tokenCountMessage: ChatMessage = {
|
||||||
sender: AI_SENDER,
|
sender: AI_SENDER,
|
||||||
|
|
@ -327,11 +318,11 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
addMessage(tokenCountMessage);
|
addMessage(tokenCountMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.on('countTokensSelection', handleSelection);
|
emitter.on("countTokensSelection", handleSelection);
|
||||||
|
|
||||||
// Cleanup function to remove the event listener when the component unmounts
|
// Cleanup function to remove the event listener when the component unmounts
|
||||||
return () => {
|
return () => {
|
||||||
emitter.removeListener('countTokensSelection', handleSelection);
|
emitter.removeListener("countTokensSelection", handleSelection);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -339,13 +330,13 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
const createEffect = (
|
const createEffect = (
|
||||||
eventType: string,
|
eventType: string,
|
||||||
promptFn: (selectedText: string, eventSubtype?: string) => string | Promise<string>,
|
promptFn: (selectedText: string, eventSubtype?: string) => string | Promise<string>,
|
||||||
options: CreateEffectOptions = {},
|
options: CreateEffectOptions = {}
|
||||||
) => {
|
) => {
|
||||||
return () => {
|
return () => {
|
||||||
const {
|
const {
|
||||||
custom_temperature,
|
custom_temperature,
|
||||||
isVisible = false,
|
isVisible = false,
|
||||||
ignoreSystemMessage = true, // Ignore system message by default for commands
|
ignoreSystemMessage = true, // Ignore system message by default for commands
|
||||||
} = options;
|
} = options;
|
||||||
const handleSelection = async (selectedText: string, eventSubtype?: string) => {
|
const handleSelection = async (selectedText: string, eventSubtype?: string) => {
|
||||||
const messageWithPrompt = await promptFn(selectedText, eventSubtype);
|
const messageWithPrompt = await promptFn(selectedText, eventSubtype);
|
||||||
|
|
@ -390,37 +381,35 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(createEffect('fixGrammarSpellingSelection', fixGrammarSpellingSelectionPrompt), []);
|
useEffect(createEffect("fixGrammarSpellingSelection", fixGrammarSpellingSelectionPrompt), []);
|
||||||
useEffect(createEffect('summarizeSelection', summarizePrompt), []);
|
useEffect(createEffect("summarizeSelection", summarizePrompt), []);
|
||||||
useEffect(createEffect('tocSelection', tocPrompt), []);
|
useEffect(createEffect("tocSelection", tocPrompt), []);
|
||||||
useEffect(createEffect('glossarySelection', glossaryPrompt), []);
|
useEffect(createEffect("glossarySelection", glossaryPrompt), []);
|
||||||
useEffect(createEffect('simplifySelection', simplifyPrompt), []);
|
useEffect(createEffect("simplifySelection", simplifyPrompt), []);
|
||||||
useEffect(createEffect('emojifySelection', emojifyPrompt), []);
|
useEffect(createEffect("emojifySelection", emojifyPrompt), []);
|
||||||
useEffect(createEffect('removeUrlsFromSelection', removeUrlsFromSelectionPrompt), []);
|
useEffect(createEffect("removeUrlsFromSelection", removeUrlsFromSelectionPrompt), []);
|
||||||
useEffect(
|
useEffect(
|
||||||
createEffect(
|
createEffect("rewriteTweetSelection", rewriteTweetSelectionPrompt, { custom_temperature: 0.2 }),
|
||||||
'rewriteTweetSelection', rewriteTweetSelectionPrompt, { custom_temperature: 0.2 },
|
|
||||||
),
|
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
useEffect(
|
useEffect(
|
||||||
createEffect(
|
createEffect("rewriteTweetThreadSelection", rewriteTweetThreadSelectionPrompt, {
|
||||||
'rewriteTweetThreadSelection', rewriteTweetThreadSelectionPrompt, { custom_temperature: 0.2 },
|
custom_temperature: 0.2,
|
||||||
),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
useEffect(createEffect('rewriteShorterSelection', rewriteShorterSelectionPrompt), []);
|
useEffect(createEffect("rewriteShorterSelection", rewriteShorterSelectionPrompt), []);
|
||||||
useEffect(createEffect('rewriteLongerSelection', rewriteLongerSelectionPrompt), []);
|
useEffect(createEffect("rewriteLongerSelection", rewriteLongerSelectionPrompt), []);
|
||||||
useEffect(createEffect('eli5Selection', eli5SelectionPrompt), []);
|
useEffect(createEffect("eli5Selection", eli5SelectionPrompt), []);
|
||||||
useEffect(createEffect('rewritePressReleaseSelection', rewritePressReleaseSelectionPrompt), []);
|
useEffect(createEffect("rewritePressReleaseSelection", rewritePressReleaseSelectionPrompt), []);
|
||||||
useEffect(
|
useEffect(
|
||||||
createEffect('translateSelection', (selectedText, language) =>
|
createEffect("translateSelection", (selectedText, language) =>
|
||||||
createTranslateSelectionPrompt(language)(selectedText)
|
createTranslateSelectionPrompt(language)(selectedText)
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
useEffect(
|
useEffect(
|
||||||
createEffect('changeToneSelection', (selectedText, tone) =>
|
createEffect("changeToneSelection", (selectedText, tone) =>
|
||||||
createChangeToneSelectionPrompt(tone)(selectedText)
|
createChangeToneSelectionPrompt(tone)(selectedText)
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
|
|
@ -429,33 +418,32 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
const customPromptProcessor = CustomPromptProcessor.getInstance(app.vault);
|
const customPromptProcessor = CustomPromptProcessor.getInstance(app.vault);
|
||||||
useEffect(
|
useEffect(
|
||||||
createEffect(
|
createEffect(
|
||||||
'applyCustomPrompt',
|
"applyCustomPrompt",
|
||||||
async (selectedText, customPrompt) => {
|
async (selectedText, customPrompt) => {
|
||||||
if (!customPrompt) {
|
if (!customPrompt) {
|
||||||
return selectedText;
|
return selectedText;
|
||||||
}
|
}
|
||||||
return await customPromptProcessor.processCustomPrompt(customPrompt, selectedText);
|
return await customPromptProcessor.processCustomPrompt(customPrompt, selectedText);
|
||||||
},
|
},
|
||||||
{ isVisible: debug, ignoreSystemMessage: true, custom_temperature: 0.1 },
|
{ isVisible: debug, ignoreSystemMessage: true, custom_temperature: 0.1 }
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
createEffect(
|
createEffect(
|
||||||
'applyAdhocPrompt',
|
"applyAdhocPrompt",
|
||||||
async (selectedText, customPrompt) => {
|
async (selectedText, customPrompt) => {
|
||||||
if (!customPrompt) {
|
if (!customPrompt) {
|
||||||
return selectedText;
|
return selectedText;
|
||||||
}
|
}
|
||||||
return await customPromptProcessor.processCustomPrompt(customPrompt, selectedText);
|
return await customPromptProcessor.processCustomPrompt(customPrompt, selectedText);
|
||||||
},
|
},
|
||||||
{ isVisible: debug, ignoreSystemMessage: true, custom_temperature: 0.1 },
|
{ isVisible: debug, ignoreSystemMessage: true, custom_temperature: 0.1 }
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-container">
|
<div className="chat-container">
|
||||||
<ChatMessages
|
<ChatMessages
|
||||||
|
|
@ -463,20 +451,18 @@ const Chat: React.FC<ChatProps> = ({
|
||||||
currentAiMessage={currentAiMessage}
|
currentAiMessage={currentAiMessage}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
<div className='bottom-container'>
|
<div className="bottom-container">
|
||||||
<ChatIcons
|
<ChatIcons
|
||||||
currentModel={currentModel}
|
currentModel={currentModel}
|
||||||
setCurrentModel={setModel}
|
setCurrentModel={setModel}
|
||||||
currentChain={currentChain}
|
currentChain={currentChain}
|
||||||
setCurrentChain={setChain}
|
setCurrentChain={setChain}
|
||||||
onStopGenerating={handleStopGenerating}
|
onStopGenerating={handleStopGenerating}
|
||||||
onNewChat={
|
onNewChat={() => {
|
||||||
() => {
|
clearMessages();
|
||||||
clearMessages();
|
clearChatMemory();
|
||||||
clearChatMemory();
|
clearCurrentAiMessage();
|
||||||
clearCurrentAiMessage();
|
}}
|
||||||
}
|
|
||||||
}
|
|
||||||
onSaveAsNote={handleSaveAsNote}
|
onSaveAsNote={handleSaveAsNote}
|
||||||
onSendActiveNoteToPrompt={handleSendActiveNoteToPrompt}
|
onSendActiveNoteToPrompt={handleSendActiveNoteToPrompt}
|
||||||
onForceRebuildActiveNoteContext={forceRebuildActiveNoteContext}
|
onForceRebuildActiveNoteContext={forceRebuildActiveNoteContext}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { SetChainOptions } from "@/aiParams";
|
import { SetChainOptions } from "@/aiParams";
|
||||||
import {
|
import { AI_SENDER, ChatModelDisplayNames, VAULT_VECTOR_STORE_STRATEGY } from "@/constants";
|
||||||
AI_SENDER,
|
|
||||||
ChatModelDisplayNames,
|
|
||||||
VAULT_VECTOR_STORE_STRATEGY,
|
|
||||||
} from "@/constants";
|
|
||||||
import { ProxyServer } from "@/proxyServer";
|
import { ProxyServer } from "@/proxyServer";
|
||||||
import { ChatMessage } from "@/sharedState";
|
import { ChatMessage } from "@/sharedState";
|
||||||
import { getFileContent, getFileName } from "@/utils";
|
import { getFileContent, getFileName } from "@/utils";
|
||||||
|
|
@ -64,7 +60,10 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
||||||
// Start proxy server based on the selected model & settings
|
// Start proxy server based on the selected model & settings
|
||||||
const proxyServerURL = proxyServer.getProxyURL(selectedModel);
|
const proxyServerURL = proxyServer.getProxyURL(selectedModel);
|
||||||
if (proxyServerURL) {
|
if (proxyServerURL) {
|
||||||
await proxyServer.startProxyServer(proxyServerURL, selectedModel !== ChatModelDisplayNames.CLAUDE);
|
await proxyServer.startProxyServer(
|
||||||
|
proxyServerURL,
|
||||||
|
selectedModel !== ChatModelDisplayNames.CLAUDE
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await proxyServer.stopProxyServer();
|
await proxyServer.stopProxyServer();
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +71,10 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const startProxyServerForClaude = async (proxyServerURL: string) => {
|
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
|
// Call the function on component mount
|
||||||
|
|
@ -87,9 +89,7 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChainChange = async (
|
const handleChainChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
event: React.ChangeEvent<HTMLSelectElement>
|
|
||||||
) => {
|
|
||||||
setSelectedChain(stringToChainType(event.target.value));
|
setSelectedChain(stringToChainType(event.target.value));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -158,12 +158,8 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
||||||
value={currentModel}
|
value={currentModel}
|
||||||
onChange={handleModelChange}
|
onChange={handleModelChange}
|
||||||
>
|
>
|
||||||
<option value={ChatModelDisplayNames.GPT_4}>
|
<option value={ChatModelDisplayNames.GPT_4}>{ChatModelDisplayNames.GPT_4}</option>
|
||||||
{ChatModelDisplayNames.GPT_4}
|
<option value={ChatModelDisplayNames.GPT_4o}>{ChatModelDisplayNames.GPT_4o}</option>
|
||||||
</option>
|
|
||||||
<option value={ChatModelDisplayNames.GPT_4o}>
|
|
||||||
{ChatModelDisplayNames.GPT_4o}
|
|
||||||
</option>
|
|
||||||
<option value={ChatModelDisplayNames.GPT_4o_mini}>
|
<option value={ChatModelDisplayNames.GPT_4o_mini}>
|
||||||
{ChatModelDisplayNames.GPT_4o_mini}
|
{ChatModelDisplayNames.GPT_4o_mini}
|
||||||
</option>
|
</option>
|
||||||
|
|
@ -176,9 +172,7 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
||||||
<option value={ChatModelDisplayNames.AZURE_OPENAI}>
|
<option value={ChatModelDisplayNames.AZURE_OPENAI}>
|
||||||
{ChatModelDisplayNames.AZURE_OPENAI}
|
{ChatModelDisplayNames.AZURE_OPENAI}
|
||||||
</option>
|
</option>
|
||||||
<option value={ChatModelDisplayNames.CLAUDE}>
|
<option value={ChatModelDisplayNames.CLAUDE}>{ChatModelDisplayNames.CLAUDE}</option>
|
||||||
{ChatModelDisplayNames.CLAUDE}
|
|
||||||
</option>
|
|
||||||
<option value={ChatModelDisplayNames.GEMINI_PRO}>
|
<option value={ChatModelDisplayNames.GEMINI_PRO}>
|
||||||
{ChatModelDisplayNames.GEMINI_PRO}
|
{ChatModelDisplayNames.GEMINI_PRO}
|
||||||
</option>
|
</option>
|
||||||
|
|
@ -188,15 +182,11 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
||||||
<option value={ChatModelDisplayNames.OPENROUTERAI}>
|
<option value={ChatModelDisplayNames.OPENROUTERAI}>
|
||||||
{ChatModelDisplayNames.OPENROUTERAI}
|
{ChatModelDisplayNames.OPENROUTERAI}
|
||||||
</option>
|
</option>
|
||||||
<option value={ChatModelDisplayNames.GROQ}>
|
<option value={ChatModelDisplayNames.GROQ}>{ChatModelDisplayNames.GROQ}</option>
|
||||||
{ChatModelDisplayNames.GROQ}
|
|
||||||
</option>
|
|
||||||
<option value={ChatModelDisplayNames.LM_STUDIO}>
|
<option value={ChatModelDisplayNames.LM_STUDIO}>
|
||||||
{ChatModelDisplayNames.LM_STUDIO}
|
{ChatModelDisplayNames.LM_STUDIO}
|
||||||
</option>
|
</option>
|
||||||
<option value={ChatModelDisplayNames.OLLAMA}>
|
<option value={ChatModelDisplayNames.OLLAMA}>{ChatModelDisplayNames.OLLAMA}</option>
|
||||||
{ChatModelDisplayNames.OLLAMA}
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
<span className="tooltip-text">Model Selection</span>
|
<span className="tooltip-text">Model Selection</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -247,10 +237,7 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{selectedChain === "long_note_qa" && (
|
{selectedChain === "long_note_qa" && (
|
||||||
<button
|
<button className="chat-icon-button" onClick={onForceRebuildActiveNoteContext}>
|
||||||
className="chat-icon-button"
|
|
||||||
onClick={onForceRebuildActiveNoteContext}
|
|
||||||
>
|
|
||||||
<UseActiveNoteAsContextIcon className="icon-scaler" />
|
<UseActiveNoteAsContextIcon className="icon-scaler" />
|
||||||
<span className="tooltip-text">
|
<span className="tooltip-text">
|
||||||
Refresh Index
|
Refresh Index
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { NoteTitleModal } from '@/components/ NoteTitleModal';
|
import { NoteTitleModal } from "@/components/NoteTitleModal";
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
inputMessage: string;
|
inputMessage: string;
|
||||||
|
|
@ -10,7 +10,11 @@ interface ChatInputProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatInput: React.FC<ChatInputProps> = ({
|
const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
inputMessage, setInputMessage, handleKeyDown, handleSendMessage, getChatVisibility,
|
inputMessage,
|
||||||
|
setInputMessage,
|
||||||
|
handleKeyDown,
|
||||||
|
handleSendMessage,
|
||||||
|
getChatVisibility,
|
||||||
}) => {
|
}) => {
|
||||||
const [rows, setRows] = useState(1);
|
const [rows, setRows] = useState(1);
|
||||||
const [shouldFocus, setShouldFocus] = useState(false);
|
const [shouldFocus, setShouldFocus] = useState(false);
|
||||||
|
|
@ -22,22 +26,18 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
updateRows(inputValue);
|
updateRows(inputValue);
|
||||||
|
|
||||||
// Check if the user typed `[[`
|
// Check if the user typed `[[`
|
||||||
if (inputValue.slice(-2) === '[[') {
|
if (inputValue.slice(-2) === "[[") {
|
||||||
showNoteTitleModal();
|
showNoteTitleModal();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showNoteTitleModal = () => {
|
const showNoteTitleModal = () => {
|
||||||
const fetchNoteTitles = async () => {
|
const fetchNoteTitles = async () => {
|
||||||
const noteTitles = app.vault.getMarkdownFiles().map(file => file.basename);
|
const noteTitles = app.vault.getMarkdownFiles().map((file) => file.basename);
|
||||||
|
|
||||||
new NoteTitleModal(
|
new NoteTitleModal(app, noteTitles, (noteTitle: string) => {
|
||||||
app,
|
setInputMessage(inputMessage.slice(0, -2) + ` [[${noteTitle}]]`);
|
||||||
noteTitles,
|
}).open();
|
||||||
(noteTitle: string) => {
|
|
||||||
setInputMessage(inputMessage.slice(0, -2) + ` [[${noteTitle}]]`);
|
|
||||||
}
|
|
||||||
).open();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchNoteTitles();
|
fetchNoteTitles();
|
||||||
|
|
@ -49,7 +49,8 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
const minRows = 1;
|
const minRows = 1;
|
||||||
|
|
||||||
const rowsNeeded = Math.min(
|
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);
|
setRows(rowsNeeded);
|
||||||
};
|
};
|
||||||
|
|
@ -87,4 +88,3 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChatInput;
|
export default ChatInput;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import ChatSingleMessage from '@/components/ChatComponents/ChatSingleMessage';
|
import ChatSingleMessage from "@/components/ChatComponents/ChatSingleMessage";
|
||||||
import { BotIcon } from '@/components/Icons';
|
import { BotIcon } from "@/components/Icons";
|
||||||
import ReactMarkdown from '@/components/Markdown/MemoizedReactMarkdown';
|
import ReactMarkdown from "@/components/Markdown/MemoizedReactMarkdown";
|
||||||
import { ChatMessage } from '@/sharedState';
|
import { ChatMessage } from "@/sharedState";
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface ChatMessagesProps {
|
interface ChatMessagesProps {
|
||||||
chatHistory: ChatMessage[];
|
chatHistory: ChatMessage[];
|
||||||
|
|
@ -10,37 +10,37 @@ interface ChatMessagesProps {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatMessages: React.FC<ChatMessagesProps> = ({
|
const ChatMessages: React.FC<ChatMessagesProps> = ({ chatHistory, currentAiMessage, loading }) => {
|
||||||
chatHistory, currentAiMessage, loading
|
const [loadingDots, setLoadingDots] = useState("");
|
||||||
}) => {
|
|
||||||
const [loadingDots, setLoadingDots] = useState('');
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
const chatMessagesContainer = document.querySelector('.chat-messages');
|
const chatMessagesContainer = document.querySelector(".chat-messages");
|
||||||
if (chatMessagesContainer) {
|
if (chatMessagesContainer) {
|
||||||
chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight;
|
chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {scrollToBottom()}, [chatHistory]);
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [chatHistory]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let intervalId: NodeJS.Timeout;
|
let intervalId: NodeJS.Timeout;
|
||||||
if (loading) {
|
if (loading) {
|
||||||
intervalId = setInterval(() => {
|
intervalId = setInterval(() => {
|
||||||
setLoadingDots(dots => dots.length < 6 ? dots + '.' : '');
|
setLoadingDots((dots) => (dots.length < 6 ? dots + "." : ""));
|
||||||
}, 200);
|
}, 200);
|
||||||
} else {
|
} else {
|
||||||
setLoadingDots('');
|
setLoadingDots("");
|
||||||
}
|
}
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [loading]);
|
}, [loading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-messages">
|
<div className="chat-messages">
|
||||||
{chatHistory.map((message, index) => (
|
{chatHistory.map(
|
||||||
message.isVisible && <ChatSingleMessage key={index} message={message} />
|
(message, index) => message.isVisible && <ChatSingleMessage key={index} message={message} />
|
||||||
))}
|
)}
|
||||||
{currentAiMessage ? (
|
{currentAiMessage ? (
|
||||||
<div className="message bot-message" key={`ai_message_${currentAiMessage}`}>
|
<div className="message bot-message" key={`ai_message_${currentAiMessage}`}>
|
||||||
<div className="message-icon">
|
<div className="message-icon">
|
||||||
|
|
@ -51,14 +51,16 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
loading && <div className="message bot-message" key={`ai_message_${currentAiMessage}`}>
|
loading && (
|
||||||
<div className="message-icon">
|
<div className="message bot-message" key={`ai_message_${currentAiMessage}`}>
|
||||||
<BotIcon />
|
<div className="message-icon">
|
||||||
|
<BotIcon />
|
||||||
|
</div>
|
||||||
|
<div className="message-content">
|
||||||
|
<ReactMarkdown>{loadingDots}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="message-content">
|
)
|
||||||
<ReactMarkdown>{loadingDots}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,86 +5,83 @@ export class ChatNoteContextModal extends Modal {
|
||||||
private settings: CopilotSettings;
|
private settings: CopilotSettings;
|
||||||
private onSubmit: (path: string, tags: string[]) => void;
|
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);
|
super(app);
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.onSubmit = onSubmit;
|
this.onSubmit = onSubmit;
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const formContainer = this.contentEl.createEl('div', { cls: 'copilot-command-modal' });
|
const formContainer = this.contentEl.createEl("div", { cls: "copilot-command-modal" });
|
||||||
const pathContainer = formContainer.createEl('div', { cls: 'copilot-command-input-container' });
|
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) => {
|
const descFragment = createFragment((frag) => {
|
||||||
frag.appendText('All notes under the path will be sent to the prompt when the ');
|
frag.appendText("All notes under the path will be sent to the prompt when the ");
|
||||||
frag.createEl(
|
frag.createEl("strong", { text: "Send Note(s) to Prompt" });
|
||||||
'strong',
|
frag.appendText(" button is clicked in Chat mode. ");
|
||||||
{ text: 'Send Note(s) to Prompt' }
|
frag.appendText("If none provided, ");
|
||||||
);
|
frag.createEl("strong", { text: "default context is the active note" });
|
||||||
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);
|
pathContainer.appendChild(descFragment);
|
||||||
|
|
||||||
const pathField = pathContainer.createEl(
|
const pathField = pathContainer.createEl("input", {
|
||||||
'input',
|
type: "text",
|
||||||
{
|
cls: "copilot-command-input",
|
||||||
type: 'text',
|
value: this.settings.chatNoteContextPath,
|
||||||
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) => {
|
const descTagsFragment = createFragment((frag) => {
|
||||||
frag.createEl(
|
frag.createEl("strong", {
|
||||||
'strong',
|
text: "Only tags in note property are used, tags in note content are not used.",
|
||||||
{ text: 'Only tags in note property are used, tags in note content are not used.' }
|
});
|
||||||
)
|
frag.createEl("p", {
|
||||||
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. ' });
|
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',
|
frag.createEl("strong", { text: "Tags function as an OR filter, " });
|
||||||
{ 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);
|
pathContainer.appendChild(descTagsFragment);
|
||||||
|
|
||||||
const tagsField = pathContainer.createEl(
|
const tagsField = pathContainer.createEl("input", {
|
||||||
'input',
|
type: "text",
|
||||||
{
|
cls: "copilot-command-input",
|
||||||
type: 'text',
|
value: this.settings.chatNoteContextTags.join(","),
|
||||||
cls: 'copilot-command-input',
|
});
|
||||||
value: this.settings.chatNoteContextTags.join(','),
|
tagsField.setAttribute("name", "tags");
|
||||||
}
|
|
||||||
);
|
|
||||||
tagsField.setAttribute('name', 'tags');
|
|
||||||
|
|
||||||
const submitButtonContainer = formContainer.createEl('div', { cls: 'copilot-command-save-btn-container' });
|
const submitButtonContainer = formContainer.createEl("div", {
|
||||||
const submitButton = submitButtonContainer.createEl('button', { text: 'Submit', cls: 'copilot-command-save-btn' });
|
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
|
// Remove the leading slash if it exists
|
||||||
let pathValue = pathField.value;
|
let pathValue = pathField.value;
|
||||||
if (pathValue.startsWith('/') && pathValue.length > 1) {
|
if (pathValue.startsWith("/") && pathValue.length > 1) {
|
||||||
pathValue = pathValue.slice(1);
|
pathValue = pathValue.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsValue = tagsField.value
|
const tagsValue = tagsField.value
|
||||||
.split(',')
|
.split(",")
|
||||||
.map(tag => tag.trim())
|
.map((tag) => tag.trim())
|
||||||
.map(tag => tag.toLowerCase())
|
.map((tag) => tag.toLowerCase())
|
||||||
.map(tag => tag.replace('#', ''))
|
.map((tag) => tag.replace("#", ""))
|
||||||
.filter(tag => tag !== '');
|
.filter((tag) => tag !== "");
|
||||||
|
|
||||||
this.onSubmit(pathValue, tagsValue);
|
this.onSubmit(pathValue, tagsValue);
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import ChainManager from '@/LLMProviders/chainManager';
|
import ChainManager from "@/LLMProviders/chainManager";
|
||||||
import Chat from '@/components/Chat';
|
import Chat from "@/components/Chat";
|
||||||
import { CHAT_VIEWTYPE } from '@/constants';
|
import { CHAT_VIEWTYPE } from "@/constants";
|
||||||
import { AppContext } from '@/context';
|
import { AppContext } from "@/context";
|
||||||
import CopilotPlugin from '@/main';
|
import CopilotPlugin from "@/main";
|
||||||
import { CopilotSettings } from '@/settings/SettingsPage';
|
import { CopilotSettings } from "@/settings/SettingsPage";
|
||||||
import SharedState from '@/sharedState';
|
import SharedState from "@/sharedState";
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from "events";
|
||||||
import { ItemView, WorkspaceLeaf } from 'obsidian';
|
import { ItemView, WorkspaceLeaf } from "obsidian";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { Root, createRoot } from 'react-dom/client';
|
import { Root, createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
|
||||||
export default class CopilotView extends ItemView {
|
export default class CopilotView extends ItemView {
|
||||||
private sharedState: SharedState;
|
private sharedState: SharedState;
|
||||||
|
|
@ -19,9 +18,12 @@ export default class CopilotView extends ItemView {
|
||||||
private defaultSaveFolder: string;
|
private defaultSaveFolder: string;
|
||||||
private debug = false;
|
private debug = false;
|
||||||
emitter: EventEmitter;
|
emitter: EventEmitter;
|
||||||
userSystemPrompt = '';
|
userSystemPrompt = "";
|
||||||
|
|
||||||
constructor(leaf: WorkspaceLeaf, private plugin: CopilotPlugin) {
|
constructor(
|
||||||
|
leaf: WorkspaceLeaf,
|
||||||
|
private plugin: CopilotPlugin
|
||||||
|
) {
|
||||||
super(leaf);
|
super(leaf);
|
||||||
this.sharedState = plugin.sharedState;
|
this.sharedState = plugin.sharedState;
|
||||||
this.settings = plugin.settings;
|
this.settings = plugin.settings;
|
||||||
|
|
@ -41,19 +43,19 @@ export default class CopilotView extends ItemView {
|
||||||
|
|
||||||
// Return an icon for this view
|
// Return an icon for this view
|
||||||
getIcon(): string {
|
getIcon(): string {
|
||||||
return 'message-square';
|
return "message-square";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a title for this view
|
// Return a title for this view
|
||||||
getTitle(): string {
|
getTitle(): string {
|
||||||
return 'Copilot Chat';
|
return "Copilot Chat";
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayText(): string {
|
getDisplayText(): string {
|
||||||
return 'Copilot';
|
return "Copilot";
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChatVisibility(){
|
async getChatVisibility() {
|
||||||
if (this.plugin.activateViewPromise) {
|
if (this.plugin.activateViewPromise) {
|
||||||
await this.plugin.activateViewPromise;
|
await this.plugin.activateViewPromise;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,38 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
type IconProps = {
|
type IconProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserIcon = () => (
|
export const UserIcon = () => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
<svg
|
||||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
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>
|
<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>
|
<circle cx="12" cy="7" r="4"></circle>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const BotIcon = () => (
|
export const BotIcon = () => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
<svg
|
||||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
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>
|
<rect x="3" y="11" width="18" height="10" rx="2"></rect>
|
||||||
<circle cx="12" cy="5" r="2"></circle>
|
<circle cx="12" cy="5" r="2"></circle>
|
||||||
<path d="M12 7v4"></path>
|
<path d="M12 7v4"></path>
|
||||||
|
|
@ -24,67 +42,145 @@ export const BotIcon = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CopyClipboardIcon = () => (
|
export const CopyClipboardIcon = () => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
<svg
|
||||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
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>
|
<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>
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CheckIcon = () => (
|
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"
|
stroke="currentColor"
|
||||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RefreshIcon: React.FC<IconProps> = ({ className }) => (
|
export const RefreshIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
<svg
|
||||||
viewBox="0 0 24 24" fill="none"
|
className={className}
|
||||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path d="M3 2v6h6"></path><path d="M21 12A9 9 0 0 0 6 5.3L3 8"></path>
|
width="24"
|
||||||
<path d="M21 22v-6h-6"></path><path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"></path>
|
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>
|
</svg>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const StopIcon: React.FC<IconProps> = ({ className }) => (
|
export const StopIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
<svg
|
||||||
viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
className={className}
|
||||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<circle cx="12" cy="12" r="10"></circle><rect x="9" y="9" width="6" height="6"></rect></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 }) => (
|
export const NewChatIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
<svg
|
||||||
viewBox="0 0 24 24" fill="none"
|
className={className}
|
||||||
stroke="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SaveAsNoteIcon: React.FC<IconProps> = ({ className }) => (
|
export const SaveAsNoteIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
<svg
|
||||||
viewBox="0 0 24 24" fill="none"
|
className={className}
|
||||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path d="M12 3v12"></path><path d="m8 11 4 4 4-4"></path>
|
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>
|
<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>
|
</svg>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const UseActiveNoteAsContextIcon: React.FC<IconProps> = ({ className }) => (
|
export const UseActiveNoteAsContextIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
<svg
|
||||||
viewBox="0 0 24 24" fill="none"
|
className={className}
|
||||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<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>
|
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>
|
</svg>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const SendActiveNoteToPromptIcon: React.FC<IconProps> = ({ className }) => (
|
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} />
|
<circle cx={12} cy={12} r={10} />
|
||||||
<path d="m16 12-4-4-4 4M12 16V8" />
|
<path d="m16 12-4-4-4 4M12 16V8" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { App, FuzzySuggestModal } from "obsidian";
|
import { App, FuzzySuggestModal } from "obsidian";
|
||||||
|
|
||||||
|
|
||||||
export class ListPromptModal extends FuzzySuggestModal<string> {
|
export class ListPromptModal extends FuzzySuggestModal<string> {
|
||||||
private onChoosePromptTitle: (promptTitle: string) => void;
|
private onChoosePromptTitle: (promptTitle: string) => void;
|
||||||
private promptTitles: string[];
|
private promptTitles: string[];
|
||||||
|
|
@ -27,5 +26,3 @@ export class ListPromptModal extends FuzzySuggestModal<string> {
|
||||||
this.onChoosePromptTitle(promptTitle);
|
this.onChoosePromptTitle(promptTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@ export class NoteTitleModal extends FuzzySuggestModal<string> {
|
||||||
private onChooseNoteTitle: (noteTitle: string) => void;
|
private onChooseNoteTitle: (noteTitle: string) => void;
|
||||||
private noteTitles: string[];
|
private noteTitles: string[];
|
||||||
|
|
||||||
constructor(
|
constructor(app: App, noteTitles: string[], onChooseNoteTitle: (noteTitle: string) => void) {
|
||||||
app: App,
|
|
||||||
noteTitles: string[],
|
|
||||||
onChooseNoteTitle: (noteTitle: string) => void
|
|
||||||
) {
|
|
||||||
super(app);
|
super(app);
|
||||||
this.noteTitles = noteTitles;
|
this.noteTitles = noteTitles;
|
||||||
this.onChooseNoteTitle = onChooseNoteTitle;
|
this.onChooseNoteTitle = onChooseNoteTitle;
|
||||||
|
|
@ -25,4 +21,4 @@ export class NoteTitleModal extends FuzzySuggestModal<string> {
|
||||||
onChooseItem(noteTitle: string, evt: MouseEvent | KeyboardEvent) {
|
onChooseItem(noteTitle: string, evt: MouseEvent | KeyboardEvent) {
|
||||||
this.onChooseNoteTitle(noteTitle);
|
this.onChooseNoteTitle(noteTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,39 +12,44 @@ export class QAExclusionModal extends Modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const formContainer = this.contentEl.createEl('div', { cls: 'copilot-command-modal' });
|
const formContainer = this.contentEl.createEl("div", { cls: "copilot-command-modal" });
|
||||||
const pathContainer = formContainer.createEl('div', { cls: 'copilot-command-input-container' });
|
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) => {
|
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);
|
pathContainer.appendChild(descFragment);
|
||||||
|
|
||||||
const pathField = pathContainer.createEl(
|
const pathField = pathContainer.createEl("input", {
|
||||||
'input',
|
type: "text",
|
||||||
{
|
cls: "copilot-command-input",
|
||||||
type: 'text',
|
value: this.settings.qaExclusionPaths,
|
||||||
cls: 'copilot-command-input',
|
placeholder: "Enter /folderPath, [[note title]] separated by commas",
|
||||||
value: this.settings.qaExclusionPaths,
|
});
|
||||||
placeholder: 'Enter /folderPath, [[note title]] separated by commas',
|
pathField.setAttribute("name", "folderPath");
|
||||||
}
|
|
||||||
);
|
|
||||||
pathField.setAttribute('name', 'folderPath');
|
|
||||||
|
|
||||||
const submitButtonContainer = formContainer.createEl('div', { cls: 'copilot-command-save-btn-container' });
|
const submitButtonContainer = formContainer.createEl("div", {
|
||||||
const submitButton = submitButtonContainer.createEl('button', { text: 'Submit', cls: 'copilot-command-save-btn' });
|
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
|
// Parse the input list
|
||||||
const pathsValue = pathField.value
|
const pathsValue = pathField.value
|
||||||
.split(',')
|
.split(",")
|
||||||
.map(pathValue => pathValue.trim())
|
.map((pathValue) => pathValue.trim())
|
||||||
.filter(pathValue => pathValue !== '')
|
.filter((pathValue) => pathValue !== "")
|
||||||
.join(',');
|
.join(",");
|
||||||
|
|
||||||
this.onSubmit(pathsValue);
|
this.onSubmit(pathsValue);
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
import { App, FuzzySuggestModal } from "obsidian";
|
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> {
|
export class ToneModal extends FuzzySuggestModal<string> {
|
||||||
private onChooseTone: (tone: string) => void;
|
private onChooseTone: (tone: string) => void;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { App } from 'obsidian';
|
import { App } from "obsidian";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
// App context
|
// App context
|
||||||
export const AppContext = React.createContext<App | undefined>(undefined);
|
export const AppContext = React.createContext<App | undefined>(undefined);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,12 +56,8 @@ export class CustomPromptProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const processedVariableName =
|
const processedVariableName = processVariableNameForNotePath(variableName);
|
||||||
processVariableNameForNotePath(variableName);
|
const noteFiles = await getNotesFromPath(this.vault, processedVariableName);
|
||||||
const noteFiles = await getNotesFromPath(
|
|
||||||
this.vault,
|
|
||||||
processedVariableName
|
|
||||||
);
|
|
||||||
for (const file of noteFiles) {
|
for (const file of noteFiles) {
|
||||||
const content = await getFileContent(file, this.vault);
|
const content = await getFileContent(file, this.vault);
|
||||||
if (content) {
|
if (content) {
|
||||||
|
|
@ -73,22 +69,15 @@ export class CustomPromptProcessor {
|
||||||
if (notes.length > 0) {
|
if (notes.length > 0) {
|
||||||
variablesWithContent.push(JSON.stringify(notes));
|
variablesWithContent.push(JSON.stringify(notes));
|
||||||
} else {
|
} else {
|
||||||
new Notice(
|
new Notice(`Warning: No valid notes found for the provided path '${variableName}'.`);
|
||||||
`Warning: No valid notes found for the provided path '${variableName}'.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return variablesWithContent;
|
return variablesWithContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
async processCustomPrompt(
|
async processCustomPrompt(customPrompt: string, selectedText: string): Promise<string> {
|
||||||
customPrompt: string,
|
const variablesWithContent = await this.extractVariablesFromPrompt(customPrompt);
|
||||||
selectedText: string
|
|
||||||
): Promise<string> {
|
|
||||||
const variablesWithContent = await this.extractVariablesFromPrompt(
|
|
||||||
customPrompt
|
|
||||||
);
|
|
||||||
let processedPrompt = customPrompt;
|
let processedPrompt = customPrompt;
|
||||||
const matches = [...processedPrompt.matchAll(/\{([^}]+)\}/g)];
|
const matches = [...processedPrompt.matchAll(/\{([^}]+)\}/g)];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,7 @@ export default class EncryptionService {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
private isDecrypted(keyBuffer: any): boolean {
|
private isDecrypted(keyBuffer: any): boolean {
|
||||||
return (
|
return keyBuffer.startsWith(EncryptionService.DECRYPTION_PREFIX);
|
||||||
keyBuffer.startsWith(EncryptionService.DECRYPTION_PREFIX)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public encryptAllKeys(): void {
|
public encryptAllKeys(): void {
|
||||||
|
|
@ -65,9 +63,7 @@ export default class EncryptionService {
|
||||||
}
|
}
|
||||||
const encryptedBuffer = safeStorage.encryptString(apiKey) as Buffer;
|
const encryptedBuffer = safeStorage.encryptString(apiKey) as Buffer;
|
||||||
// Convert the encrypted buffer to a Base64 string and prepend the prefix
|
// Convert the encrypted buffer to a Base64 string and prepend the prefix
|
||||||
return (
|
return EncryptionService.ENCRYPTION_PREFIX + encryptedBuffer.toString("base64");
|
||||||
EncryptionService.ENCRYPTION_PREFIX + encryptedBuffer.toString("base64")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the actual key for use in the LLM chain
|
// 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
|
// Remove the prefix and convert from Base64 to a buffer before decryption
|
||||||
const base64Data = apiKey.replace(
|
const base64Data = apiKey.replace(EncryptionService.ENCRYPTION_PREFIX, "");
|
||||||
EncryptionService.ENCRYPTION_PREFIX,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
const buffer = Buffer.from(base64Data, "base64");
|
const buffer = Buffer.from(base64Data, "base64");
|
||||||
return safeStorage.decryptString(buffer) as string;
|
return safeStorage.decryptString(buffer) as string;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import ChainManager from '@/LLMProviders/chainManager';
|
import ChainManager from "@/LLMProviders/chainManager";
|
||||||
import { ChatMessage } from '@/sharedState';
|
import { ChatMessage } from "@/sharedState";
|
||||||
import { Notice } from 'obsidian';
|
import { Notice } from "obsidian";
|
||||||
|
|
||||||
export type Role = 'assistant' | 'user' | 'system';
|
export type Role = "assistant" | "user" | "system";
|
||||||
|
|
||||||
export const getAIResponse = async (
|
export const getAIResponse = async (
|
||||||
userMessage: ChatMessage,
|
userMessage: ChatMessage,
|
||||||
|
|
@ -11,10 +11,10 @@ export const getAIResponse = async (
|
||||||
updateCurrentAiMessage: (message: string) => void,
|
updateCurrentAiMessage: (message: string) => void,
|
||||||
updateShouldAbort: (abortController: AbortController | null) => void,
|
updateShouldAbort: (abortController: AbortController | null) => void,
|
||||||
options: {
|
options: {
|
||||||
debug?: boolean,
|
debug?: boolean;
|
||||||
ignoreSystemMessage?: boolean,
|
ignoreSystemMessage?: boolean;
|
||||||
updateLoading?: (loading: boolean) => void
|
updateLoading?: (loading: boolean) => void;
|
||||||
} = {},
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
updateShouldAbort(abortController);
|
updateShouldAbort(abortController);
|
||||||
|
|
@ -24,10 +24,10 @@ export const getAIResponse = async (
|
||||||
abortController,
|
abortController,
|
||||||
updateCurrentAiMessage,
|
updateCurrentAiMessage,
|
||||||
addMessage,
|
addMessage,
|
||||||
options,
|
options
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Model request failed:', error);
|
console.error("Model request failed:", error);
|
||||||
new Notice('Model request failed:', error);
|
new Notice("Model request failed:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
372
src/main.ts
372
src/main.ts
|
|
@ -29,15 +29,7 @@ import {
|
||||||
} from "@/utils";
|
} from "@/utils";
|
||||||
import VectorDBManager, { VectorStoreDocument } from "@/vectorDBManager";
|
import VectorDBManager, { VectorStoreDocument } from "@/vectorDBManager";
|
||||||
import { MD5 } from "crypto-js";
|
import { MD5 } from "crypto-js";
|
||||||
import {
|
import { Editor, MarkdownView, Menu, Notice, Plugin, TFile, WorkspaceLeaf } from "obsidian";
|
||||||
Editor,
|
|
||||||
MarkdownView,
|
|
||||||
Menu,
|
|
||||||
Notice,
|
|
||||||
Plugin,
|
|
||||||
TFile,
|
|
||||||
WorkspaceLeaf,
|
|
||||||
} from "obsidian";
|
|
||||||
import PouchDB from "pouchdb";
|
import PouchDB from "pouchdb";
|
||||||
|
|
||||||
export default class CopilotPlugin extends Plugin {
|
export default class CopilotPlugin extends Plugin {
|
||||||
|
|
@ -65,30 +57,24 @@ export default class CopilotPlugin extends Plugin {
|
||||||
const langChainParams = this.getChainManagerParams();
|
const langChainParams = this.getChainManagerParams();
|
||||||
this.encryptionService = new EncryptionService(this.settings);
|
this.encryptionService = new EncryptionService(this.settings);
|
||||||
this.dbVectorStores = new PouchDB<VectorStoreDocument>(
|
this.dbVectorStores = new PouchDB<VectorStoreDocument>(
|
||||||
`copilot_vector_stores_${this.getVaultIdentifier()}`,
|
`copilot_vector_stores_${this.getVaultIdentifier()}`
|
||||||
);
|
);
|
||||||
this.chainManager = new ChainManager(
|
this.chainManager = new ChainManager(
|
||||||
this.app,
|
this.app,
|
||||||
langChainParams,
|
langChainParams,
|
||||||
this.encryptionService,
|
this.encryptionService,
|
||||||
this.settings,
|
this.settings,
|
||||||
() => this.dbVectorStores,
|
() => this.dbVectorStores
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.settings.enableEncryption) {
|
if (this.settings.enableEncryption) {
|
||||||
await this.saveSettings();
|
await this.saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.embeddingsManager = EmbeddingsManager.getInstance(
|
this.embeddingsManager = EmbeddingsManager.getInstance(langChainParams, this.encryptionService);
|
||||||
langChainParams,
|
|
||||||
this.encryptionService,
|
|
||||||
);
|
|
||||||
this.dbPrompts = new PouchDB<CustomPrompt>("copilot_custom_prompts");
|
this.dbPrompts = new PouchDB<CustomPrompt>("copilot_custom_prompts");
|
||||||
|
|
||||||
this.registerView(
|
this.registerView(CHAT_VIEWTYPE, (leaf: WorkspaceLeaf) => new CopilotView(leaf, this));
|
||||||
CHAT_VIEWTYPE,
|
|
||||||
(leaf: WorkspaceLeaf) => new CopilotView(leaf, this),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "chat-toggle-window",
|
id: "chat-toggle-window",
|
||||||
|
|
@ -122,9 +108,7 @@ export default class CopilotPlugin extends Plugin {
|
||||||
await this.dbPrompts.put({ _id: title, prompt: prompt });
|
await this.dbPrompts.put({ _id: title, prompt: prompt });
|
||||||
new Notice("Custom prompt saved successfully.");
|
new Notice("Custom prompt saved successfully.");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(
|
new Notice("Error saving custom prompt. Please check if the title already exists.");
|
||||||
"Error saving custom prompt. Please check if the title already exists.",
|
|
||||||
);
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}).open();
|
}).open();
|
||||||
|
|
@ -136,37 +120,27 @@ export default class CopilotPlugin extends Plugin {
|
||||||
name: "Apply custom prompt",
|
name: "Apply custom prompt",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
this.fetchPromptTitles().then((promptTitles: string[]) => {
|
this.fetchPromptTitles().then((promptTitles: string[]) => {
|
||||||
new ListPromptModal(
|
new ListPromptModal(this.app, promptTitles, async (promptTitle: string) => {
|
||||||
this.app,
|
if (!promptTitle) {
|
||||||
promptTitles,
|
new Notice("Please select a prompt title.");
|
||||||
async (promptTitle: string) => {
|
return;
|
||||||
if (!promptTitle) {
|
}
|
||||||
new Notice("Please select a prompt title.");
|
try {
|
||||||
|
const doc = (await this.dbPrompts.get(promptTitle)) as CustomPrompt;
|
||||||
|
if (!doc.prompt) {
|
||||||
|
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
this.processCustomPrompt("applyCustomPrompt", doc.prompt);
|
||||||
const doc = (await this.dbPrompts.get(
|
} catch (err) {
|
||||||
promptTitle,
|
if (err.name === "not_found") {
|
||||||
)) as CustomPrompt;
|
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||||
if (!doc.prompt) {
|
} else {
|
||||||
new Notice(
|
console.error(err);
|
||||||
`No prompt found with the title "${promptTitle}".`,
|
new Notice("An error occurred.");
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.processCustomPrompt("applyCustomPrompt", doc.prompt);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name === "not_found") {
|
|
||||||
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",
|
id: "apply-adhoc-prompt",
|
||||||
name: "Apply ad-hoc custom prompt",
|
name: "Apply ad-hoc custom prompt",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
const modal = new AdhocPromptModal(
|
const modal = new AdhocPromptModal(this.app, async (adhocPrompt: string) => {
|
||||||
this.app,
|
try {
|
||||||
async (adhocPrompt: string) => {
|
this.processCustomPrompt("applyAdhocPrompt", adhocPrompt);
|
||||||
try {
|
} catch (err) {
|
||||||
this.processCustomPrompt("applyAdhocPrompt", adhocPrompt);
|
console.error(err);
|
||||||
} catch (err) {
|
new Notice("An error occurred.");
|
||||||
console.error(err);
|
}
|
||||||
new Notice("An error occurred.");
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
modal.open();
|
modal.open();
|
||||||
},
|
},
|
||||||
|
|
@ -200,39 +171,29 @@ export default class CopilotPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchPromptTitles().then((promptTitles: string[]) => {
|
this.fetchPromptTitles().then((promptTitles: string[]) => {
|
||||||
new ListPromptModal(
|
new ListPromptModal(this.app, promptTitles, async (promptTitle: string) => {
|
||||||
this.app,
|
if (!promptTitle) {
|
||||||
promptTitles,
|
new Notice("Please select a prompt title.");
|
||||||
async (promptTitle: string) => {
|
return;
|
||||||
if (!promptTitle) {
|
}
|
||||||
new Notice("Please select a prompt title.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const doc = await this.dbPrompts.get(promptTitle);
|
const doc = await this.dbPrompts.get(promptTitle);
|
||||||
if (doc._rev) {
|
if (doc._rev) {
|
||||||
await this.dbPrompts.remove(
|
await this.dbPrompts.remove(doc as PouchDB.Core.RemoveDocument);
|
||||||
doc as PouchDB.Core.RemoveDocument,
|
new Notice(`Prompt "${promptTitle}" has been deleted.`);
|
||||||
);
|
} else {
|
||||||
new Notice(`Prompt "${promptTitle}" has been deleted.`);
|
new Notice(`Failed to delete prompt "${promptTitle}": No revision found.`);
|
||||||
} else {
|
|
||||||
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}".`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error(err);
|
|
||||||
new Notice("An error occurred while deleting the prompt.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
} catch (err) {
|
||||||
).open();
|
if (err.name === "not_found") {
|
||||||
|
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
new Notice("An error occurred while deleting the prompt.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).open();
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -248,50 +209,40 @@ export default class CopilotPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchPromptTitles().then((promptTitles: string[]) => {
|
this.fetchPromptTitles().then((promptTitles: string[]) => {
|
||||||
new ListPromptModal(
|
new ListPromptModal(this.app, promptTitles, async (promptTitle: string) => {
|
||||||
this.app,
|
if (!promptTitle) {
|
||||||
promptTitles,
|
new Notice("Please select a prompt title.");
|
||||||
async (promptTitle: string) => {
|
return;
|
||||||
if (!promptTitle) {
|
}
|
||||||
new Notice("Please select a prompt title.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const doc = (await this.dbPrompts.get(
|
const doc = (await this.dbPrompts.get(promptTitle)) as CustomPrompt;
|
||||||
promptTitle,
|
if (doc.prompt) {
|
||||||
)) as CustomPrompt;
|
new AddPromptModal(
|
||||||
if (doc.prompt) {
|
this.app,
|
||||||
new AddPromptModal(
|
(title: string, newPrompt: string) => {
|
||||||
this.app,
|
this.dbPrompts.put({
|
||||||
(title: string, newPrompt: string) => {
|
...doc,
|
||||||
this.dbPrompts.put({
|
prompt: newPrompt,
|
||||||
...doc,
|
});
|
||||||
prompt: newPrompt,
|
new Notice(`Prompt "${title}" has been updated.`);
|
||||||
});
|
},
|
||||||
new Notice(`Prompt "${title}" has been updated.`);
|
doc._id,
|
||||||
},
|
doc.prompt,
|
||||||
doc._id,
|
true
|
||||||
doc.prompt,
|
).open();
|
||||||
true,
|
} else {
|
||||||
).open();
|
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||||
} else {
|
|
||||||
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}".`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error(err);
|
|
||||||
new Notice("An error occurred.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
} catch (err) {
|
||||||
).open();
|
if (err.name === "not_found") {
|
||||||
|
new Notice(`No prompt found with the title "${promptTitle}".`);
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
new Notice("An error occurred.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).open();
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -307,17 +258,13 @@ export default class CopilotPlugin extends Plugin {
|
||||||
await this.dbVectorStores.destroy();
|
await this.dbVectorStores.destroy();
|
||||||
// Reinitialize the database
|
// Reinitialize the database
|
||||||
this.dbVectorStores = new PouchDB<VectorStoreDocument>(
|
this.dbVectorStores = new PouchDB<VectorStoreDocument>(
|
||||||
`copilot_vector_stores_${this.getVaultIdentifier()}`,
|
`copilot_vector_stores_${this.getVaultIdentifier()}`
|
||||||
);
|
);
|
||||||
new Notice("Local vector store cleared successfully.");
|
new Notice("Local vector store cleared successfully.");
|
||||||
console.log(
|
console.log("Local vector store cleared successfully, new instance created.");
|
||||||
"Local vector store cleared successfully, new instance created.",
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error clearing the local vector store:", err);
|
console.error("Error clearing the local vector store:", err);
|
||||||
new Notice(
|
new Notice("An error occurred while clearing the local vector store.");
|
||||||
"An error occurred while clearing the local vector store.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -329,32 +276,26 @@ export default class CopilotPlugin extends Plugin {
|
||||||
try {
|
try {
|
||||||
const files = this.app.vault.getMarkdownFiles();
|
const files = this.app.vault.getMarkdownFiles();
|
||||||
const filePaths = files.map((file) => file.path);
|
const filePaths = files.map((file) => file.path);
|
||||||
const indexedFiles = await VectorDBManager.getNoteFiles(
|
const indexedFiles = await VectorDBManager.getNoteFiles(this.dbVectorStores);
|
||||||
this.dbVectorStores,
|
|
||||||
);
|
|
||||||
const indexedFilePaths = indexedFiles.map((file) => file.path);
|
const indexedFilePaths = indexedFiles.map((file) => file.path);
|
||||||
const filesToDelete = indexedFilePaths.filter(
|
const filesToDelete = indexedFilePaths.filter(
|
||||||
(filePath) => !filePaths.includes(filePath),
|
(filePath) => !filePaths.includes(filePath)
|
||||||
);
|
);
|
||||||
|
|
||||||
const deletePromises = filesToDelete.map(async (filePath) => {
|
const deletePromises = filesToDelete.map(async (filePath) => {
|
||||||
VectorDBManager.removeMemoryVectors(
|
VectorDBManager.removeMemoryVectors(
|
||||||
this.dbVectorStores,
|
this.dbVectorStores,
|
||||||
VectorDBManager.getDocumentHash(filePath),
|
VectorDBManager.getDocumentHash(filePath)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(deletePromises);
|
await Promise.all(deletePromises);
|
||||||
|
|
||||||
new Notice("Local vector store garbage collected successfully.");
|
new Notice("Local vector store garbage collected successfully.");
|
||||||
console.log(
|
console.log("Local vector store garbage collected successfully, new instance created.");
|
||||||
"Local vector store garbage collected successfully, new instance created.",
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error clearing the local vector store:", err);
|
console.error("Error clearing the local vector store:", err);
|
||||||
new Notice(
|
new Notice("An error occurred while clearing the local vector store.");
|
||||||
"An error occurred while clearing the local vector store.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -366,12 +307,8 @@ export default class CopilotPlugin extends Plugin {
|
||||||
try {
|
try {
|
||||||
const indexedFileCount = await this.indexVaultToVectorStore();
|
const indexedFileCount = await this.indexVaultToVectorStore();
|
||||||
|
|
||||||
new Notice(
|
new Notice(`${indexedFileCount} vault files indexed to vector store.`);
|
||||||
`${indexedFileCount} vault files indexed to vector store.`,
|
console.log(`${indexedFileCount} vault files indexed to vector store.`);
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${indexedFileCount} vault files indexed to vector store.`,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error indexing vault to vector store:", err);
|
console.error("Error indexing vault to vector store:", err);
|
||||||
new Notice("An error occurred while indexing vault to vector store.");
|
new Notice("An error occurred while indexing vault to vector store.");
|
||||||
|
|
@ -386,17 +323,11 @@ export default class CopilotPlugin extends Plugin {
|
||||||
try {
|
try {
|
||||||
const indexedFileCount = await this.indexVaultToVectorStore(true);
|
const indexedFileCount = await this.indexVaultToVectorStore(true);
|
||||||
|
|
||||||
new Notice(
|
new Notice(`${indexedFileCount} vault files indexed to vector store.`);
|
||||||
`${indexedFileCount} vault files indexed to vector store.`,
|
console.log(`${indexedFileCount} vault files indexed to vector store.`);
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${indexedFileCount} vault files indexed to vector store.`,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error re-indexing vault to vector store:", err);
|
console.error("Error re-indexing vault to vector store:", err);
|
||||||
new Notice(
|
new Notice("An error occurred while re-indexing vault to vector store.");
|
||||||
"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",
|
id: "set-chat-note-context",
|
||||||
name: "Set note context for Chat mode",
|
name: "Set note context for Chat mode",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
new ChatNoteContextModal(
|
new ChatNoteContextModal(this.app, this.settings, async (path: string, tags: string[]) => {
|
||||||
this.app,
|
// Store the path in the plugin's settings, default to empty string
|
||||||
this.settings,
|
this.settings.chatNoteContextPath = path;
|
||||||
async (path: string, tags: string[]) => {
|
this.settings.chatNoteContextTags = tags;
|
||||||
// Store the path in the plugin's settings, default to empty string
|
await this.saveSettings();
|
||||||
this.settings.chatNoteContextPath = path;
|
}).open();
|
||||||
this.settings.chatNoteContextTags = tags;
|
|
||||||
await this.saveSettings();
|
|
||||||
},
|
|
||||||
).open();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -434,15 +361,12 @@ export default class CopilotPlugin extends Plugin {
|
||||||
this.app.vault.on("delete", (file) => {
|
this.app.vault.on("delete", (file) => {
|
||||||
const docHash = VectorDBManager.getDocumentHash(file.path);
|
const docHash = VectorDBManager.getDocumentHash(file.path);
|
||||||
VectorDBManager.removeMemoryVectors(this.dbVectorStores, docHash);
|
VectorDBManager.removeMemoryVectors(this.dbVectorStores, docHash);
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Index vault to vector store on startup and after loading all commands
|
// 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
|
// This can take a while, so we don't want to block the startup process
|
||||||
if (
|
if (this.settings.indexVaultToVectorStore === VAULT_VECTOR_STORE_STRATEGY.ON_STARTUP) {
|
||||||
this.settings.indexVaultToVectorStore ===
|
|
||||||
VAULT_VECTOR_STORE_STRATEGY.ON_STARTUP
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
await this.indexVaultToVectorStore();
|
await this.indexVaultToVectorStore();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -451,9 +375,7 @@ export default class CopilotPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.registerEvent(
|
this.registerEvent(this.app.workspace.on("editor-menu", this.handleContextMenu));
|
||||||
this.app.workspace.on("editor-menu", this.handleContextMenu),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getVaultIdentifier(): string {
|
private getVaultIdentifier(): string {
|
||||||
|
|
@ -486,19 +408,12 @@ export default class CopilotPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if embedding model has changed
|
// Check if embedding model has changed
|
||||||
const prevEmbeddingModel = await VectorDBManager.checkEmbeddingModel(
|
const prevEmbeddingModel = await VectorDBManager.checkEmbeddingModel(this.dbVectorStores);
|
||||||
this.dbVectorStores,
|
|
||||||
);
|
|
||||||
// TODO: Remove this when Ollama model is dynamically set
|
// TODO: Remove this when Ollama model is dynamically set
|
||||||
const currEmbeddingModel =
|
const currEmbeddingModel = EmbeddingsManager.getModelName(embeddingInstance);
|
||||||
EmbeddingsManager.getModelName(embeddingInstance);
|
|
||||||
|
|
||||||
if (this.settings.debug)
|
if (this.settings.debug)
|
||||||
console.log(
|
console.log("Prev vs Current embedding models:", prevEmbeddingModel, currEmbeddingModel);
|
||||||
"Prev vs Current embedding models:",
|
|
||||||
prevEmbeddingModel,
|
|
||||||
currEmbeddingModel,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!areEmbeddingModelsSame(prevEmbeddingModel, currEmbeddingModel)) {
|
if (!areEmbeddingModelsSame(prevEmbeddingModel, currEmbeddingModel)) {
|
||||||
// Model has changed, clear DB and reindex from scratch
|
// Model has changed, clear DB and reindex from scratch
|
||||||
|
|
@ -509,23 +424,17 @@ export default class CopilotPlugin extends Plugin {
|
||||||
await this.dbVectorStores.destroy();
|
await this.dbVectorStores.destroy();
|
||||||
// Reinitialize the database
|
// Reinitialize the database
|
||||||
this.dbVectorStores = new PouchDB<VectorStoreDocument>(
|
this.dbVectorStores = new PouchDB<VectorStoreDocument>(
|
||||||
`copilot_vector_stores_${this.getVaultIdentifier()}`,
|
`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.",
|
|
||||||
);
|
);
|
||||||
|
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) {
|
} catch (err) {
|
||||||
console.error("Error clearing vector store for reindexing:", err);
|
console.error("Error clearing vector store for reindexing:", err);
|
||||||
new Notice("Error clearing vector store for reindexing.");
|
new Notice("Error clearing vector store for reindexing.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestMtime = await VectorDBManager.getLatestFileMtime(
|
const latestMtime = await VectorDBManager.getLatestFileMtime(this.dbVectorStores);
|
||||||
this.dbVectorStores,
|
|
||||||
);
|
|
||||||
|
|
||||||
const files = this.app.vault
|
const files = this.app.vault
|
||||||
.getMarkdownFiles()
|
.getMarkdownFiles()
|
||||||
|
|
@ -540,11 +449,9 @@ export default class CopilotPlugin extends Plugin {
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileContents: string[] = await Promise.all(
|
const fileContents: string[] = await Promise.all(
|
||||||
files.map((file) => this.app.vault.cachedRead(file)),
|
files.map((file) => this.app.vault.cachedRead(file))
|
||||||
);
|
|
||||||
const fileMetadatas = files.map((file) =>
|
|
||||||
this.app.metadataCache.getFileCache(file),
|
|
||||||
);
|
);
|
||||||
|
const fileMetadatas = files.map((file) => this.app.metadataCache.getFileCache(file));
|
||||||
|
|
||||||
const totalFiles = files.length;
|
const totalFiles = files.length;
|
||||||
if (totalFiles === 0) {
|
if (totalFiles === 0) {
|
||||||
|
|
@ -555,7 +462,7 @@ export default class CopilotPlugin extends Plugin {
|
||||||
let indexedCount = 0;
|
let indexedCount = 0;
|
||||||
const indexNotice = new Notice(
|
const indexNotice = new Notice(
|
||||||
`Copilot is indexing your vault...\n0/${totalFiles} files processed.`,
|
`Copilot is indexing your vault...\n0/${totalFiles} files processed.`,
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
@ -571,12 +478,12 @@ export default class CopilotPlugin extends Plugin {
|
||||||
const result = await VectorDBManager.indexFile(
|
const result = await VectorDBManager.indexFile(
|
||||||
this.dbVectorStores,
|
this.dbVectorStores,
|
||||||
embeddingInstance,
|
embeddingInstance,
|
||||||
noteFile,
|
noteFile
|
||||||
);
|
);
|
||||||
|
|
||||||
indexedCount++;
|
indexedCount++;
|
||||||
indexNotice.setMessage(
|
indexNotice.setMessage(
|
||||||
`Copilot is indexing your vault...\n${indexedCount}/${totalFiles} files processed.`,
|
`Copilot is indexing your vault...\n${indexedCount}/${totalFiles} files processed.`
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -591,9 +498,7 @@ export default class CopilotPlugin extends Plugin {
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
new Notice(
|
new Notice(`Indexing completed with errors. Check the console for details.`);
|
||||||
`Indexing completed with errors. Check the console for details.`,
|
|
||||||
);
|
|
||||||
console.log("Indexing Errors:", errors.join("\n"));
|
console.log("Indexing Errors:", errors.join("\n"));
|
||||||
}
|
}
|
||||||
return files.length;
|
return files.length;
|
||||||
|
|
@ -603,12 +508,11 @@ export default class CopilotPlugin extends Plugin {
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
eventType: string,
|
eventType: string,
|
||||||
eventSubtype?: string,
|
eventSubtype?: string,
|
||||||
checkSelectedText = true,
|
checkSelectedText = true
|
||||||
) {
|
) {
|
||||||
const selectedText = await editor.getSelection();
|
const selectedText = await editor.getSelection();
|
||||||
|
|
||||||
const isChatWindowActive =
|
const isChatWindowActive = this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE).length > 0;
|
||||||
this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE).length > 0;
|
|
||||||
|
|
||||||
if (!isChatWindowActive) {
|
if (!isChatWindowActive) {
|
||||||
await this.activateView();
|
await this.activateView();
|
||||||
|
|
@ -639,9 +543,7 @@ export default class CopilotPlugin extends Plugin {
|
||||||
const activeFile = this.app.workspace.getActiveFile();
|
const activeFile = this.app.workspace.getActiveFile();
|
||||||
return activeFile ? this.app.vault.cachedRead(activeFile) : "";
|
return activeFile ? this.app.vault.cachedRead(activeFile) : "";
|
||||||
},
|
},
|
||||||
replaceSelection:
|
replaceSelection: activeView?.editor?.replaceSelection.bind(activeView.editor) || (() => {}),
|
||||||
activeView?.editor?.replaceSelection.bind(activeView.editor) ||
|
|
||||||
(() => {}),
|
|
||||||
} as Partial<Editor> as Editor;
|
} as Partial<Editor> as Editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -657,16 +559,12 @@ export default class CopilotPlugin extends Plugin {
|
||||||
|
|
||||||
async activateView() {
|
async activateView() {
|
||||||
this.app.workspace.detachLeavesOfType(CHAT_VIEWTYPE);
|
this.app.workspace.detachLeavesOfType(CHAT_VIEWTYPE);
|
||||||
this.activateViewPromise = this.app.workspace
|
this.activateViewPromise = this.app.workspace.getRightLeaf(false).setViewState({
|
||||||
.getRightLeaf(false)
|
type: CHAT_VIEWTYPE,
|
||||||
.setViewState({
|
active: true,
|
||||||
type: CHAT_VIEWTYPE,
|
});
|
||||||
active: true,
|
|
||||||
});
|
|
||||||
await this.activateViewPromise;
|
await this.activateViewPromise;
|
||||||
this.app.workspace.revealLeaf(
|
this.app.workspace.revealLeaf(this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0]);
|
||||||
this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0],
|
|
||||||
);
|
|
||||||
this.chatIsVisible = true;
|
this.chatIsVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -687,9 +585,7 @@ export default class CopilotPlugin extends Plugin {
|
||||||
active: true,
|
active: true,
|
||||||
});
|
});
|
||||||
await this.activateViewPromise;
|
await this.activateViewPromise;
|
||||||
this.app.workspace.revealLeaf(
|
this.app.workspace.revealLeaf(this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0]);
|
||||||
this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0],
|
|
||||||
);
|
|
||||||
this.chatIsVisible = true;
|
this.chatIsVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -707,16 +603,13 @@ export default class CopilotPlugin extends Plugin {
|
||||||
|
|
||||||
async fetchPromptTitles(): Promise<string[]> {
|
async fetchPromptTitles(): Promise<string[]> {
|
||||||
const response = await this.dbPrompts.allDocs({ include_docs: true });
|
const response = await this.dbPrompts.allDocs({ include_docs: true });
|
||||||
return response.rows
|
return response.rows.map((row) => (row.doc as CustomPrompt)?._id).filter(Boolean) as string[];
|
||||||
.map((row) => (row.doc as CustomPrompt)?._id)
|
|
||||||
.filter(Boolean) as string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async countTotalTokens(): Promise<number> {
|
async countTotalTokens(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const allContent = await getAllNotesContent(this.app.vault);
|
const allContent = await getAllNotesContent(this.app.vault);
|
||||||
const totalTokens =
|
const totalTokens = await this.chainManager.chatModelManager.countTokens(allContent);
|
||||||
await this.chainManager.chatModelManager.countTokens(allContent);
|
|
||||||
return totalTokens;
|
return totalTokens;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error counting tokens: ", error);
|
console.error("Error counting tokens: ", error);
|
||||||
|
|
@ -802,8 +695,7 @@ export default class CopilotPlugin extends Plugin {
|
||||||
useOpenAILocalProxy: this.settings.useOpenAILocalProxy,
|
useOpenAILocalProxy: this.settings.useOpenAILocalProxy,
|
||||||
openAIProxyModelName: this.settings.openAIProxyModelName,
|
openAIProxyModelName: this.settings.openAIProxyModelName,
|
||||||
openAIEmbeddingProxyBaseUrl: this.settings.openAIEmbeddingProxyBaseUrl,
|
openAIEmbeddingProxyBaseUrl: this.settings.openAIEmbeddingProxyBaseUrl,
|
||||||
openAIEmbeddingProxyModelName:
|
openAIEmbeddingProxyModelName: this.settings.openAIEmbeddingProxyModelName,
|
||||||
this.settings.openAIEmbeddingProxyModelName,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
||||||
private db: PouchDB.Database,
|
private db: PouchDB.Database,
|
||||||
private vault: Vault,
|
private vault: Vault,
|
||||||
private options: ScoreThresholdRetrieverInput<V>,
|
private options: ScoreThresholdRetrieverInput<V>,
|
||||||
private debug?: boolean,
|
private debug?: boolean
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
||||||
"\nNote Titles extracted: ",
|
"\nNote Titles extracted: ",
|
||||||
noteTitles,
|
noteTitles,
|
||||||
"\nExplicit Chunks:",
|
"\nExplicit Chunks:",
|
||||||
explicitChunks,
|
explicitChunks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,9 +42,7 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
||||||
const vectorChunks = await this.getVectorChunks(query);
|
const vectorChunks = await this.getVectorChunks(query);
|
||||||
|
|
||||||
// Combine explicit and vector chunks, removing duplicates while maintaining order
|
// Combine explicit and vector chunks, removing duplicates while maintaining order
|
||||||
const uniqueChunks = new Set<string>(
|
const uniqueChunks = new Set<string>(explicitChunks.map((chunk) => chunk.pageContent));
|
||||||
explicitChunks.map((chunk) => chunk.pageContent),
|
|
||||||
);
|
|
||||||
const combinedChunks: Document[] = [...explicitChunks];
|
const combinedChunks: Document[] = [...explicitChunks];
|
||||||
|
|
||||||
for (const chunk of vectorChunks) {
|
for (const chunk of vectorChunks) {
|
||||||
|
|
@ -64,17 +62,14 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
||||||
for (const noteTitle of noteTitles) {
|
for (const noteTitle of noteTitles) {
|
||||||
const noteFile = await getNoteFileFromTitle(this.vault, noteTitle);
|
const noteFile = await getNoteFileFromTitle(this.vault, noteTitle);
|
||||||
const docHash = VectorDBManager.getDocumentHash(noteFile?.path ?? "");
|
const docHash = VectorDBManager.getDocumentHash(noteFile?.path ?? "");
|
||||||
const memoryVectors = await VectorDBManager.getMemoryVectors(
|
const memoryVectors = await VectorDBManager.getMemoryVectors(this.db, docHash);
|
||||||
this.db,
|
|
||||||
docHash,
|
|
||||||
);
|
|
||||||
if (memoryVectors) {
|
if (memoryVectors) {
|
||||||
const matchingChunks = memoryVectors.map(
|
const matchingChunks = memoryVectors.map(
|
||||||
(memoryVector) =>
|
(memoryVector) =>
|
||||||
new Document({
|
new Document({
|
||||||
pageContent: memoryVector.content,
|
pageContent: memoryVector.content,
|
||||||
metadata: memoryVector.metadata,
|
metadata: memoryVector.metadata,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
explicitChunks.push(...matchingChunks);
|
explicitChunks.push(...matchingChunks);
|
||||||
}
|
}
|
||||||
|
|
@ -83,14 +78,11 @@ export class HybridRetriever<V extends VectorStore> extends BaseRetriever {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getVectorChunks(query: string): Promise<Document[]> {
|
private async getVectorChunks(query: string): Promise<Document[]> {
|
||||||
const retriever = ScoreThresholdRetriever.fromVectorStore(
|
const retriever = ScoreThresholdRetriever.fromVectorStore(this.options.vectorStore, {
|
||||||
this.options.vectorStore,
|
minSimilarityScore: this.options.minSimilarityScore,
|
||||||
{
|
maxK: this.options.maxK,
|
||||||
minSimilarityScore: this.options.minSimilarityScore,
|
kIncrement: this.options.kIncrement,
|
||||||
maxK: this.options.maxK,
|
});
|
||||||
kIncrement: this.options.kIncrement,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const vectorChunks = await retriever.getRelevantDocuments(query);
|
const vectorChunks = await retriever.getRelevantDocuments(query);
|
||||||
return vectorChunks;
|
return vectorChunks;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
message: string;
|
message: string;
|
||||||
|
|
@ -24,17 +24,11 @@ class SharedState {
|
||||||
|
|
||||||
export function useSharedState(
|
export function useSharedState(
|
||||||
sharedState: SharedState
|
sharedState: SharedState
|
||||||
): [
|
): [ChatMessage[], (message: ChatMessage) => void, () => void] {
|
||||||
ChatMessage[],
|
|
||||||
(message: ChatMessage) => void,
|
|
||||||
() => void
|
|
||||||
] {
|
|
||||||
// Initializes the local chatHistory state with the current
|
// Initializes the local chatHistory state with the current
|
||||||
// sharedState chatHistory using the useState hook
|
// sharedState chatHistory using the useState hook
|
||||||
// setChatHistory is used to update the *local* state
|
// setChatHistory is used to update the *local* state
|
||||||
const [chatHistory, setChatHistory] = useState<ChatMessage[]>(
|
const [chatHistory, setChatHistory] = useState<ChatMessage[]>(sharedState.getMessages());
|
||||||
sharedState.getMessages()
|
|
||||||
);
|
|
||||||
|
|
||||||
// The useEffect hook ensures that the local state is synchronized
|
// The useEffect hook ensures that the local state is synchronized
|
||||||
// with the shared state when the component is mounted.
|
// with the shared state when the component is mounted.
|
||||||
|
|
@ -56,11 +50,7 @@ export function useSharedState(
|
||||||
setChatHistory([]);
|
setChatHistory([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [chatHistory, addMessage, clearMessages];
|
||||||
chatHistory,
|
|
||||||
addMessage,
|
|
||||||
clearMessages,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SharedState;
|
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 moment from "moment";
|
||||||
import { TFile, Vault, parseYaml } from "obsidian";
|
import { TFile, Vault, parseYaml } from "obsidian";
|
||||||
|
|
||||||
export const isFolderMatch = (
|
export const isFolderMatch = (fileFullpath: string, inputPath: string): boolean => {
|
||||||
fileFullpath: string,
|
const fileSegments = fileFullpath.split("/").map((segment) => segment.toLowerCase());
|
||||||
inputPath: string,
|
|
||||||
): boolean => {
|
|
||||||
const fileSegments = fileFullpath
|
|
||||||
.split("/")
|
|
||||||
.map((segment) => segment.toLowerCase());
|
|
||||||
return fileSegments.includes(inputPath.toLowerCase());
|
return fileSegments.includes(inputPath.toLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getNoteFileFromTitle(
|
export async function getNoteFileFromTitle(vault: Vault, noteTitle: string): Promise<TFile | null> {
|
||||||
vault: Vault,
|
|
||||||
noteTitle: string,
|
|
||||||
): Promise<TFile | null> {
|
|
||||||
// Get all markdown files in the vault
|
// Get all markdown files in the vault
|
||||||
const files = vault.getMarkdownFiles();
|
const files = vault.getMarkdownFiles();
|
||||||
|
|
||||||
|
|
@ -45,10 +37,7 @@ export async function getNoteFileFromTitle(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNotesFromPath = async (
|
export const getNotesFromPath = async (vault: Vault, path: string): Promise<TFile[]> => {
|
||||||
vault: Vault,
|
|
||||||
path: string,
|
|
||||||
): Promise<TFile[]> => {
|
|
||||||
const files = vault.getMarkdownFiles();
|
const files = vault.getMarkdownFiles();
|
||||||
|
|
||||||
// Special handling for the root path '/'
|
// Special handling for the root path '/'
|
||||||
|
|
@ -83,10 +72,7 @@ export const getNotesFromPath = async (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getTagsFromNote(
|
export async function getTagsFromNote(file: TFile, vault: Vault): Promise<string[]> {
|
||||||
file: TFile,
|
|
||||||
vault: Vault,
|
|
||||||
): Promise<string[]> {
|
|
||||||
const fileContent = await vault.cachedRead(file);
|
const fileContent = await vault.cachedRead(file);
|
||||||
// Check if the file starts with frontmatter delimiter
|
// Check if the file starts with frontmatter delimiter
|
||||||
if (fileContent.startsWith("---")) {
|
if (fileContent.startsWith("---")) {
|
||||||
|
|
@ -113,7 +99,7 @@ export async function getTagsFromNote(
|
||||||
export async function getNotesFromTags(
|
export async function getNotesFromTags(
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
tags: string[],
|
tags: string[],
|
||||||
noteFiles?: TFile[],
|
noteFiles?: TFile[]
|
||||||
): Promise<TFile[]> {
|
): Promise<TFile[]> {
|
||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -122,10 +108,7 @@ export async function getNotesFromTags(
|
||||||
// Strip any '#' from the tags set from the user
|
// Strip any '#' from the tags set from the user
|
||||||
tags = tags.map((tag) => tag.replace("#", ""));
|
tags = tags.map((tag) => tag.replace("#", ""));
|
||||||
|
|
||||||
const files =
|
const files = noteFiles && noteFiles.length > 0 ? noteFiles : await getNotesFromPath(vault, "/");
|
||||||
noteFiles && noteFiles.length > 0
|
|
||||||
? noteFiles
|
|
||||||
: await getNotesFromPath(vault, "/");
|
|
||||||
const filesWithTag = [];
|
const filesWithTag = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
|
@ -155,7 +138,7 @@ export function isPathInList(filePath: string, pathList: string): boolean {
|
||||||
.trim() // Trim whitespace
|
.trim() // Trim whitespace
|
||||||
.replace(/^\[\[|\]\]$/g, "") // Remove surrounding [[ and ]]
|
.replace(/^\[\[|\]\]$/g, "") // Remove surrounding [[ and ]]
|
||||||
.replace(/^\//, "") // Remove leading slash
|
.replace(/^\//, "") // Remove leading slash
|
||||||
.toLowerCase(), // Convert to lowercase for case-insensitive comparison
|
.toLowerCase() // Convert to lowercase for case-insensitive comparison
|
||||||
)
|
)
|
||||||
.some((normalizedPath) => {
|
.some((normalizedPath) => {
|
||||||
// Check for exact match or proper segmentation
|
// Check for exact match or proper segmentation
|
||||||
|
|
@ -184,23 +167,17 @@ export const stringToChainType = (chain: string): ChainType => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isLLMChain = (
|
export const isLLMChain = (chain: RunnableSequence): chain is RunnableSequence => {
|
||||||
chain: RunnableSequence,
|
|
||||||
): chain is RunnableSequence => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return (chain as any).last.bound.modelName || (chain as any).last.bound.model;
|
return (chain as any).last.bound.modelName || (chain as any).last.bound.model;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isRetrievalQAChain = (
|
export const isRetrievalQAChain = (chain: BaseChain): chain is RetrievalQAChain => {
|
||||||
chain: BaseChain,
|
|
||||||
): chain is RetrievalQAChain => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return (chain as any).last.bound.retriever !== undefined;
|
return (chain as any).last.bound.retriever !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isSupportedChain = (
|
export const isSupportedChain = (chain: RunnableSequence): chain is RunnableSequence => {
|
||||||
chain: RunnableSequence,
|
|
||||||
): chain is RunnableSequence => {
|
|
||||||
return isLLMChain(chain) || isRetrievalQAChain(chain);
|
return isLLMChain(chain) || isRetrievalQAChain(chain);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -210,10 +187,7 @@ export const getModelName = (modelDisplayName: string): string => {
|
||||||
|
|
||||||
// Returns the last N messages from the chat history,
|
// Returns the last N messages from the chat history,
|
||||||
// last one being the newest ai message
|
// last one being the newest ai message
|
||||||
export const getChatContext = (
|
export const getChatContext = (chatHistory: ChatMessage[], contextSize: number) => {
|
||||||
chatHistory: ChatMessage[],
|
|
||||||
contextSize: number,
|
|
||||||
) => {
|
|
||||||
if (chatHistory.length === 0) {
|
if (chatHistory.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -231,10 +205,7 @@ export const getChatContext = (
|
||||||
return chatHistory.slice(startIndex, lastIndex + 1);
|
return chatHistory.slice(startIndex, lastIndex + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatDateTime = (
|
export const formatDateTime = (now: Date, timezone: "local" | "utc" = "local") => {
|
||||||
now: Date,
|
|
||||||
timezone: "local" | "utc" = "local",
|
|
||||||
) => {
|
|
||||||
const formattedDateTime = moment(now);
|
const formattedDateTime = moment(now);
|
||||||
|
|
||||||
if (timezone === "utc") {
|
if (timezone === "utc") {
|
||||||
|
|
@ -244,10 +215,7 @@ export const formatDateTime = (
|
||||||
return formattedDateTime.format("YYYY_MM_DD-HH_mm_ss");
|
return formattedDateTime.format("YYYY_MM_DD-HH_mm_ss");
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getFileContent(
|
export async function getFileContent(file: TFile, vault: Vault): Promise<string | null> {
|
||||||
file: TFile,
|
|
||||||
vault: Vault,
|
|
||||||
): Promise<string | null> {
|
|
||||||
if (file.extension != "md") return null;
|
if (file.extension != "md") return null;
|
||||||
return await vault.cachedRead(file);
|
return await vault.cachedRead(file);
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +239,7 @@ export async function getAllNotesContent(vault: Vault): Promise<string> {
|
||||||
|
|
||||||
export function areEmbeddingModelsSame(
|
export function areEmbeddingModelsSame(
|
||||||
model1: string | undefined,
|
model1: string | undefined,
|
||||||
model2: string | undefined,
|
model2: string | undefined
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!model1 || !model2) return false;
|
if (!model1 || !model2) return false;
|
||||||
// TODO: Hacks to handle different embedding model names for the same model. Need better handling.
|
// 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!
|
// Stuff in settings are string even when the interface has number type!
|
||||||
const temperature = Number(settings.temperature);
|
const temperature = Number(settings.temperature);
|
||||||
sanitizedSettings.temperature = isNaN(temperature)
|
sanitizedSettings.temperature = isNaN(temperature) ? DEFAULT_SETTINGS.temperature : temperature;
|
||||||
? DEFAULT_SETTINGS.temperature
|
|
||||||
: temperature;
|
|
||||||
|
|
||||||
const maxTokens = Number(settings.maxTokens);
|
const maxTokens = Number(settings.maxTokens);
|
||||||
sanitizedSettings.maxTokens = isNaN(maxTokens)
|
sanitizedSettings.maxTokens = isNaN(maxTokens) ? DEFAULT_SETTINGS.maxTokens : maxTokens;
|
||||||
? DEFAULT_SETTINGS.maxTokens
|
|
||||||
: maxTokens;
|
|
||||||
|
|
||||||
const contextTurns = Number(settings.contextTurns);
|
const contextTurns = Number(settings.contextTurns);
|
||||||
sanitizedSettings.contextTurns = isNaN(contextTurns)
|
sanitizedSettings.contextTurns = isNaN(contextTurns)
|
||||||
|
|
@ -311,10 +275,7 @@ export function sanitizeSettings(settings: CopilotSettings): CopilotSettings {
|
||||||
|
|
||||||
// Basic prompts
|
// Basic prompts
|
||||||
// Note that GPT4 is much better at following instructions than GPT3.5!
|
// Note that GPT4 is much better at following instructions than GPT3.5!
|
||||||
export function sendNoteContentPrompt(
|
export function sendNoteContentPrompt(noteName: string, noteContent: string | null): string {
|
||||||
noteName: string,
|
|
||||||
noteContent: string | null,
|
|
||||||
): string {
|
|
||||||
return (
|
return (
|
||||||
`Please read the note below and be ready to answer questions about it. ` +
|
`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 ` +
|
`If there's no information about a certain topic, just say the note ` +
|
||||||
|
|
@ -326,9 +287,7 @@ export function sendNoteContentPrompt(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendNotesContentPrompt(
|
export function sendNotesContentPrompt(notes: { name: string; content: string }[]): string {
|
||||||
notes: { name: string; content: string }[],
|
|
||||||
): string {
|
|
||||||
return (
|
return (
|
||||||
`Please read the notes below and be ready to answer questions about them. ` +
|
`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 ` +
|
`If there's no information about a certain topic, just say the note ` +
|
||||||
|
|
@ -347,34 +306,23 @@ function getNoteTitleAndTags(noteWithTag: {
|
||||||
}): string {
|
}): string {
|
||||||
return (
|
return (
|
||||||
`[[${noteWithTag.name}]]` +
|
`[[${noteWithTag.name}]]` +
|
||||||
(noteWithTag.tags && noteWithTag.tags.length > 0
|
(noteWithTag.tags && noteWithTag.tags.length > 0 ? `\ntags: ${noteWithTag.tags.join(",")}` : "")
|
||||||
? `\ntags: ${noteWithTag.tags.join(",")}`
|
|
||||||
: "")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChatContextStr(
|
function getChatContextStr(chatNoteContextPath: string, chatNoteContextTags: string[]): string {
|
||||||
chatNoteContextPath: string,
|
const pathStr = chatNoteContextPath ? `\nChat context by path: ${chatNoteContextPath}` : "";
|
||||||
chatNoteContextTags: string[],
|
|
||||||
): string {
|
|
||||||
const pathStr = chatNoteContextPath
|
|
||||||
? `\nChat context by path: ${chatNoteContextPath}`
|
|
||||||
: "";
|
|
||||||
const tagsStr =
|
const tagsStr =
|
||||||
chatNoteContextTags?.length > 0
|
chatNoteContextTags?.length > 0 ? `\nChat context by tags: ${chatNoteContextTags}` : "";
|
||||||
? `\nChat context by tags: ${chatNoteContextTags}`
|
|
||||||
: "";
|
|
||||||
return pathStr + tagsStr;
|
return pathStr + tagsStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSendChatContextNotesPrompt(
|
export function getSendChatContextNotesPrompt(
|
||||||
notes: { name: string; content: string }[],
|
notes: { name: string; content: string }[],
|
||||||
chatNoteContextPath: string,
|
chatNoteContextPath: string,
|
||||||
chatNoteContextTags: string[],
|
chatNoteContextTags: string[]
|
||||||
): string {
|
): string {
|
||||||
const noteTitles = notes
|
const noteTitles = notes.map((note) => getNoteTitleAndTags(note)).join("\n\n");
|
||||||
.map((note) => getNoteTitleAndTags(note))
|
|
||||||
.join("\n\n");
|
|
||||||
return (
|
return (
|
||||||
`Please read the notes below and be ready to answer questions about them. ` +
|
`Please read the notes below and be ready to answer questions about them. ` +
|
||||||
getChatContextStr(chatNoteContextPath, chatNoteContextTags) +
|
getChatContextStr(chatNoteContextPath, chatNoteContextTags) +
|
||||||
|
|
@ -382,9 +330,7 @@ export function getSendChatContextNotesPrompt(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fixGrammarSpellingSelectionPrompt(
|
export function fixGrammarSpellingSelectionPrompt(selectedText: string): string {
|
||||||
selectedText: string,
|
|
||||||
): string {
|
|
||||||
return (
|
return (
|
||||||
`Please fix the grammar and spelling of the following text and return it without any other changes:\n\n` +
|
`Please fix the grammar and spelling of the following text and return it without any other changes:\n\n` +
|
||||||
`${selectedText}`
|
`${selectedText}`
|
||||||
|
|
@ -439,9 +385,7 @@ export function rewriteTweetSelectionPrompt(selectedText: string): string {
|
||||||
+ ${selectedText}`;
|
+ ${selectedText}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rewriteTweetThreadSelectionPrompt(
|
export function rewriteTweetThreadSelectionPrompt(selectedText: string): string {
|
||||||
selectedText: string,
|
|
||||||
): string {
|
|
||||||
return (
|
return (
|
||||||
`Please follow the instructions closely step by step and rewrite the content to a thread. ` +
|
`Please follow the instructions closely step by step and rewrite the content to a thread. ` +
|
||||||
`1. Each paragraph must be under 240 characters. ` +
|
`1. Each paragraph must be under 240 characters. ` +
|
||||||
|
|
@ -474,9 +418,7 @@ export function eli5SelectionPrompt(selectedText: string): string {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rewritePressReleaseSelectionPrompt(
|
export function rewritePressReleaseSelectionPrompt(selectedText: string): string {
|
||||||
selectedText: string,
|
|
||||||
): string {
|
|
||||||
return (
|
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` +
|
`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}`
|
`${selectedText}`
|
||||||
|
|
@ -485,10 +427,7 @@ export function rewritePressReleaseSelectionPrompt(
|
||||||
|
|
||||||
export function createTranslateSelectionPrompt(language?: string) {
|
export function createTranslateSelectionPrompt(language?: string) {
|
||||||
return (selectedText: string): string => {
|
return (selectedText: string): string => {
|
||||||
return (
|
return `Please translate the following text to ${language}:\n\n` + `${selectedText}`;
|
||||||
`Please translate the following text to ${language}:\n\n` +
|
|
||||||
`${selectedText}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -501,9 +440,7 @@ export function createChangeToneSelectionPrompt(tone?: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractChatHistory(
|
export function extractChatHistory(memoryVariables: MemoryVariables): [string, string][] {
|
||||||
memoryVariables: MemoryVariables,
|
|
||||||
): [string, string][] {
|
|
||||||
const chatHistory: [string, string][] = [];
|
const chatHistory: [string, string][] = [];
|
||||||
const { history } = memoryVariables;
|
const { history } = memoryVariables;
|
||||||
|
|
||||||
|
|
@ -520,9 +457,7 @@ export function extractNoteTitles(query: string): string[] {
|
||||||
// Use a regular expression to extract note titles wrapped in [[]]
|
// Use a regular expression to extract note titles wrapped in [[]]
|
||||||
const regex = /\[\[(.*?)\]\]/g;
|
const regex = /\[\[(.*?)\]\]/g;
|
||||||
const matches = query.match(regex);
|
const matches = query.match(regex);
|
||||||
const uniqueTitles = new Set(
|
const uniqueTitles = new Set(matches ? matches.map((match) => match.slice(2, -2)) : []);
|
||||||
matches ? matches.map((match) => match.slice(2, -2)) : [],
|
|
||||||
);
|
|
||||||
return Array.from(uniqueTitles);
|
return Array.from(uniqueTitles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,21 +41,19 @@ class VectorDBManager {
|
||||||
|
|
||||||
public static async rebuildMemoryVectorStore(
|
public static async rebuildMemoryVectorStore(
|
||||||
memoryVectors: MemoryVector[],
|
memoryVectors: MemoryVector[],
|
||||||
embeddingsAPI: Embeddings,
|
embeddingsAPI: Embeddings
|
||||||
) {
|
) {
|
||||||
if (!Array.isArray(memoryVectors)) {
|
if (!Array.isArray(memoryVectors)) {
|
||||||
throw new TypeError("Expected memoryVectors to be an array");
|
throw new TypeError("Expected memoryVectors to be an array");
|
||||||
}
|
}
|
||||||
// Extract the embeddings and documents from the deserialized memoryVectors
|
// Extract the embeddings and documents from the deserialized memoryVectors
|
||||||
const embeddingsArray: number[][] = memoryVectors.map(
|
const embeddingsArray: number[][] = memoryVectors.map((memoryVector) => memoryVector.embedding);
|
||||||
(memoryVector) => memoryVector.embedding,
|
|
||||||
);
|
|
||||||
const documentsArray = memoryVectors.map(
|
const documentsArray = memoryVectors.map(
|
||||||
(memoryVector) =>
|
(memoryVector) =>
|
||||||
new Document({
|
new Document({
|
||||||
pageContent: memoryVector.content,
|
pageContent: memoryVector.content,
|
||||||
metadata: memoryVector.metadata,
|
metadata: memoryVector.metadata,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a new MemoryVectorStore instance
|
// Create a new MemoryVectorStore instance
|
||||||
|
|
@ -67,7 +65,7 @@ class VectorDBManager {
|
||||||
public static async getMemoryVectorStore(
|
public static async getMemoryVectorStore(
|
||||||
db: PouchDB.Database,
|
db: PouchDB.Database,
|
||||||
embeddingsAPI: Embeddings,
|
embeddingsAPI: Embeddings,
|
||||||
docHash?: string,
|
docHash?: string
|
||||||
): Promise<MemoryVectorStore> {
|
): Promise<MemoryVectorStore> {
|
||||||
if (!db) throw new Error("DB not initialized");
|
if (!db) throw new Error("DB not initialized");
|
||||||
|
|
||||||
|
|
@ -82,23 +80,20 @@ class VectorDBManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const embeddingModel = EmbeddingManager.getModelName(embeddingsAPI);
|
const embeddingModel = EmbeddingManager.getModelName(embeddingsAPI);
|
||||||
if (!embeddingModel)
|
if (!embeddingModel) console.error("EmbeddingManager could not determine model name!");
|
||||||
console.error("EmbeddingManager could not determine model name!");
|
|
||||||
const allDocs = allDocsResponse.rows
|
const allDocs = allDocsResponse.rows
|
||||||
.map((row) => row.doc as VectorStoreDocument)
|
.map((row) => row.doc as VectorStoreDocument)
|
||||||
.filter((doc) => doc.embeddingModel === embeddingModel);
|
.filter((doc) => doc.embeddingModel === embeddingModel);
|
||||||
const memoryVectors = allDocs
|
const memoryVectors = allDocs
|
||||||
.map((doc) => JSON.parse(doc.memory_vectors) as MemoryVector[])
|
.map((doc) => JSON.parse(doc.memory_vectors) as MemoryVector[])
|
||||||
.flat();
|
.flat();
|
||||||
const embeddingsArray: number[][] = memoryVectors.map(
|
const embeddingsArray: number[][] = memoryVectors.map((memoryVector) => memoryVector.embedding);
|
||||||
(memoryVector) => memoryVector.embedding,
|
|
||||||
);
|
|
||||||
const documentsArray = memoryVectors.map(
|
const documentsArray = memoryVectors.map(
|
||||||
(memoryVector) =>
|
(memoryVector) =>
|
||||||
new Document({
|
new Document({
|
||||||
pageContent: memoryVector.content,
|
pageContent: memoryVector.content,
|
||||||
metadata: memoryVector.metadata,
|
metadata: memoryVector.metadata,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
// Create a new MemoryVectorStore instance
|
// Create a new MemoryVectorStore instance
|
||||||
const memoryVectorStore = new MemoryVectorStore(embeddingsAPI);
|
const memoryVectorStore = new MemoryVectorStore(embeddingsAPI);
|
||||||
|
|
@ -109,7 +104,7 @@ class VectorDBManager {
|
||||||
public static async setMemoryVectors(
|
public static async setMemoryVectors(
|
||||||
db: PouchDB.Database,
|
db: PouchDB.Database,
|
||||||
memoryVectors: MemoryVector[],
|
memoryVectors: MemoryVector[],
|
||||||
docHash: string,
|
docHash: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!db) throw new Error("DB not initialized");
|
if (!db) throw new Error("DB not initialized");
|
||||||
if (!Array.isArray(memoryVectors)) {
|
if (!Array.isArray(memoryVectors)) {
|
||||||
|
|
@ -138,33 +133,25 @@ class VectorDBManager {
|
||||||
public static async indexFile(
|
public static async indexFile(
|
||||||
db: PouchDB.Database,
|
db: PouchDB.Database,
|
||||||
embeddingsAPI: Embeddings,
|
embeddingsAPI: Embeddings,
|
||||||
noteFile: NoteFile,
|
noteFile: NoteFile
|
||||||
): Promise<VectorStoreDocument | undefined> {
|
): Promise<VectorStoreDocument | undefined> {
|
||||||
if (!db) throw new Error("DB not initialized");
|
if (!db) throw new Error("DB not initialized");
|
||||||
|
|
||||||
const embeddingModel = EmbeddingManager.getModelName(embeddingsAPI);
|
const embeddingModel = EmbeddingManager.getModelName(embeddingsAPI);
|
||||||
if (!embeddingModel)
|
if (!embeddingModel) console.error("EmbeddingManager could not determine model name!");
|
||||||
console.error("EmbeddingManager could not determine model name!");
|
|
||||||
|
|
||||||
// Markdown splitter: https://js.langchain.com/docs/modules/data_connection/document_transformers/code_splitter#markdown
|
// Markdown splitter: https://js.langchain.com/docs/modules/data_connection/document_transformers/code_splitter#markdown
|
||||||
const textSplitter = RecursiveCharacterTextSplitter.fromLanguage(
|
const textSplitter = RecursiveCharacterTextSplitter.fromLanguage("markdown", {
|
||||||
"markdown",
|
chunkSize: 5000,
|
||||||
{
|
});
|
||||||
chunkSize: 5000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// Add note title as contextual chunk headers
|
// Add note title as contextual chunk headers
|
||||||
// https://js.langchain.com/docs/modules/data_connection/document_transformers/contextual_chunk_headers
|
// https://js.langchain.com/docs/modules/data_connection/document_transformers/contextual_chunk_headers
|
||||||
const splitDocument = await textSplitter.createDocuments(
|
const splitDocument = await textSplitter.createDocuments([noteFile.content], [], {
|
||||||
[noteFile.content],
|
chunkHeader: "[[" + noteFile.basename + "]]" + "\n\n---\n\n",
|
||||||
[],
|
appendChunkOverlapHeader: true,
|
||||||
{
|
});
|
||||||
chunkHeader: "[[" + noteFile.basename + "]]" + "\n\n---\n\n",
|
|
||||||
appendChunkOverlapHeader: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const docVectors = await embeddingsAPI.embedDocuments(
|
const docVectors = await embeddingsAPI.embedDocuments(
|
||||||
splitDocument.map((doc) => doc.pageContent),
|
splitDocument.map((doc) => doc.pageContent)
|
||||||
);
|
);
|
||||||
const memoryVectors = docVectors.map((docVector, i) => ({
|
const memoryVectors = docVectors.map((docVector, i) => ({
|
||||||
content: splitDocument[i].pageContent,
|
content: splitDocument[i].pageContent,
|
||||||
|
|
@ -206,9 +193,7 @@ class VectorDBManager {
|
||||||
const allDocsResponse = await db.allDocs<VectorStoreDocument>({
|
const allDocsResponse = await db.allDocs<VectorStoreDocument>({
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
});
|
});
|
||||||
const allDocs = allDocsResponse.rows.map(
|
const allDocs = allDocsResponse.rows.map((row) => row.doc as VectorStoreDocument);
|
||||||
(row) => row.doc as VectorStoreDocument,
|
|
||||||
);
|
|
||||||
const memoryVectors = allDocs
|
const memoryVectors = allDocs
|
||||||
.map((doc) => JSON.parse(doc.memory_vectors) as MemoryVector[])
|
.map((doc) => JSON.parse(doc.memory_vectors) as MemoryVector[])
|
||||||
.filter((memoryVectors) => memoryVectors.length > 0);
|
.filter((memoryVectors) => memoryVectors.length > 0);
|
||||||
|
|
@ -233,10 +218,7 @@ class VectorDBManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async removeMemoryVectors(
|
public static async removeMemoryVectors(db: PouchDB.Database, docHash: string): Promise<void> {
|
||||||
db: PouchDB.Database,
|
|
||||||
docHash: string,
|
|
||||||
): Promise<void> {
|
|
||||||
if (!db) throw new Error("DB not initialized");
|
if (!db) throw new Error("DB not initialized");
|
||||||
try {
|
try {
|
||||||
const doc = await db.get(docHash);
|
const doc = await db.get(docHash);
|
||||||
|
|
@ -248,21 +230,15 @@ class VectorDBManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getLatestFileMtime(
|
public static async getLatestFileMtime(db: PouchDB.Database): Promise<number> {
|
||||||
db: PouchDB.Database,
|
|
||||||
): Promise<number> {
|
|
||||||
if (!db) throw new Error("DB not initialized");
|
if (!db) throw new Error("DB not initialized");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const allDocsResponse = await db.allDocs<VectorStoreDocument>({
|
const allDocsResponse = await db.allDocs<VectorStoreDocument>({
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
});
|
});
|
||||||
const allDocs = allDocsResponse.rows.map(
|
const allDocs = allDocsResponse.rows.map((row) => row.doc as VectorStoreDocument);
|
||||||
(row) => row.doc as VectorStoreDocument,
|
const newestFileMtime = allDocs.map((doc) => doc.file_mtime).sort((a, b) => b - a)[0];
|
||||||
);
|
|
||||||
const newestFileMtime = allDocs
|
|
||||||
.map((doc) => doc.file_mtime)
|
|
||||||
.sort((a, b) => b - a)[0];
|
|
||||||
return newestFileMtime;
|
return newestFileMtime;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error getting newest file mtime from VectorDB:", err);
|
console.error("Error getting newest file mtime from VectorDB:", err);
|
||||||
|
|
@ -270,9 +246,7 @@ class VectorDBManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async checkEmbeddingModel(
|
public static async checkEmbeddingModel(db: PouchDB.Database): Promise<string | undefined> {
|
||||||
db: PouchDB.Database,
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
if (!db) throw new Error("DB not initialized");
|
if (!db) throw new Error("DB not initialized");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -280,9 +254,7 @@ class VectorDBManager {
|
||||||
const allDocsResponse = await db.allDocs<VectorStoreDocument>({
|
const allDocsResponse = await db.allDocs<VectorStoreDocument>({
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
});
|
});
|
||||||
const allDocs = allDocsResponse.rows.map(
|
const allDocs = allDocsResponse.rows.map((row) => row.doc as VectorStoreDocument);
|
||||||
(row) => row.doc as VectorStoreDocument,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if there are any documents
|
// Check if there are any documents
|
||||||
if (allDocs.length === 0) {
|
if (allDocs.length === 0) {
|
||||||
|
|
@ -293,9 +265,7 @@ class VectorDBManager {
|
||||||
const embeddingModels = allDocs.map((doc) => doc.embeddingModel);
|
const embeddingModels = allDocs.map((doc) => doc.embeddingModel);
|
||||||
|
|
||||||
// Check if all documents have the same embeddingModel
|
// Check if all documents have the same embeddingModel
|
||||||
const allSame = embeddingModels.every(
|
const allSame = embeddingModels.every((model) => model === embeddingModels[0]);
|
||||||
(model) => model === embeddingModels[0],
|
|
||||||
);
|
|
||||||
return allSame ? embeddingModels[0] : undefined;
|
return allSame ? embeddingModels[0] : undefined;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error checking last embedding model from VectorDB:", err);
|
console.error("Error checking last embedding model from VectorDB:", err);
|
||||||
|
|
@ -305,7 +275,7 @@ class VectorDBManager {
|
||||||
|
|
||||||
public static async getMemoryVectors(
|
public static async getMemoryVectors(
|
||||||
db: PouchDB.Database,
|
db: PouchDB.Database,
|
||||||
docHash: string,
|
docHash: string
|
||||||
): Promise<MemoryVector[] | undefined> {
|
): Promise<MemoryVector[] | undefined> {
|
||||||
if (!db) throw new Error("DB not initialized");
|
if (!db) throw new Error("DB not initialized");
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { CustomPrompt, CustomPromptProcessor } from '@/customPromptProcessor';
|
import { CustomPrompt, CustomPromptProcessor } from "@/customPromptProcessor";
|
||||||
|
|
||||||
// Mocking Obsidian Vault
|
// Mocking Obsidian Vault
|
||||||
const mockVault = {
|
const mockVault = {
|
||||||
read: jest.fn(),
|
read: jest.fn(),
|
||||||
write: 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;
|
} as any;
|
||||||
|
|
||||||
describe('CustomPromptProcessor', () => {
|
describe("CustomPromptProcessor", () => {
|
||||||
let processor: CustomPromptProcessor;
|
let processor: CustomPromptProcessor;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -18,99 +18,98 @@ describe('CustomPromptProcessor', () => {
|
||||||
processor = CustomPromptProcessor.getInstance(mockVault);
|
processor = CustomPromptProcessor.getInstance(mockVault);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add 1 context and selectedText', async () => {
|
it("should add 1 context and selectedText", async () => {
|
||||||
const doc: CustomPrompt = {
|
const doc: CustomPrompt = {
|
||||||
_id: 'test-prompt',
|
_id: "test-prompt",
|
||||||
prompt: 'This is a {variable} and {}.'
|
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
|
// 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);
|
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
||||||
|
|
||||||
expect(result).toContain('This is a {variable} and {selectedText}.');
|
expect(result).toContain("This is a {variable} and {selectedText}.");
|
||||||
expect(result).toContain('here is some selected text 12345');
|
expect(result).toContain("here is some selected text 12345");
|
||||||
expect(result).toContain('here is the note content for note0');
|
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 = {
|
const doc: CustomPrompt = {
|
||||||
_id: 'test-prompt',
|
_id: "test-prompt",
|
||||||
prompt: 'This is a {variable} and {var2}.'
|
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
|
// Mock the extractVariablesFromPrompt method to return predefined content
|
||||||
jest.spyOn(processor, 'extractVariablesFromPrompt').mockResolvedValue([
|
jest
|
||||||
'here is the note content for note0',
|
.spyOn(processor, "extractVariablesFromPrompt")
|
||||||
'note content for note1'
|
.mockResolvedValue(["here is the note content for note0", "note content for note1"]);
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
||||||
|
|
||||||
expect(result).toContain('This is a {variable} and {var2}.');
|
expect(result).toContain("This is a {variable} and {var2}.");
|
||||||
expect(result).toContain('here is the note content for note0');
|
expect(result).toContain("here is the note content for note0");
|
||||||
expect(result).toContain('note content for note1');
|
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 = {
|
const doc: CustomPrompt = {
|
||||||
_id: 'test-prompt',
|
_id: "test-prompt",
|
||||||
prompt: 'Rewrite the following text {}'
|
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
|
// Mock the extractVariablesFromPrompt method to return predefined content
|
||||||
jest.spyOn(processor, 'extractVariablesFromPrompt').mockResolvedValue([
|
jest
|
||||||
'here is the note content for note0',
|
.spyOn(processor, "extractVariablesFromPrompt")
|
||||||
'note content for note1'
|
.mockResolvedValue(["here is the note content for note0", "note content for note1"]);
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
||||||
|
|
||||||
expect(result).toContain('Rewrite the following text {selectedText}');
|
expect(result).toContain("Rewrite the following text {selectedText}");
|
||||||
expect(result).toContain('here is some selected text 12345');
|
expect(result).toContain("here is some selected text 12345");
|
||||||
expect(result).not.toContain('here is the note content for note0');
|
expect(result).not.toContain("here is the note content for note0");
|
||||||
expect(result).not.toContain('note content for note1');
|
expect(result).not.toContain("note content for note1");
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is not an expected use case but it's possible
|
// 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 = {
|
const doc: CustomPrompt = {
|
||||||
_id: 'test-prompt',
|
_id: "test-prompt",
|
||||||
prompt: 'Rewrite the following text {} and {}'
|
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
|
// Mock the extractVariablesFromPrompt method to return predefined content
|
||||||
jest.spyOn(processor, 'extractVariablesFromPrompt').mockResolvedValue([
|
jest
|
||||||
'here is the note content for note0',
|
.spyOn(processor, "extractVariablesFromPrompt")
|
||||||
'note content for note1'
|
.mockResolvedValue(["here is the note content for note0", "note content for note1"]);
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
const result = await processor.processCustomPrompt(doc.prompt, selectedText);
|
||||||
|
|
||||||
expect(result).toContain('Rewrite the following text {selectedText} and {selectedText}');
|
expect(result).toContain("Rewrite the following text {selectedText} and {selectedText}");
|
||||||
expect(result).toContain('here is some selected text 12345');
|
expect(result).toContain("here is some selected text 12345");
|
||||||
expect(result).not.toContain('here is the note content for note0');
|
expect(result).not.toContain("here is the note content for note0");
|
||||||
expect(result).not.toContain('note content for note1');
|
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 = {
|
const doc: CustomPrompt = {
|
||||||
_id: 'test-prompt',
|
_id: "test-prompt",
|
||||||
prompt: 'This is a test prompt with no variables.'
|
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
|
// 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);
|
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 () => {
|
it("should process a single tag variable correctly", async () => {
|
||||||
|
|
@ -120,19 +119,12 @@ describe('CustomPromptProcessor', () => {
|
||||||
// Mock the extractVariablesFromPrompt method to simulate tag processing
|
// Mock the extractVariablesFromPrompt method to simulate tag processing
|
||||||
jest
|
jest
|
||||||
.spyOn(processor, "extractVariablesFromPrompt")
|
.spyOn(processor, "extractVariablesFromPrompt")
|
||||||
.mockResolvedValue([
|
.mockResolvedValue(['[{"name":"note","content":"Note content for #tag"}]']);
|
||||||
'[{"name":"note","content":"Note content for #tag"}]',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await processor.processCustomPrompt(
|
const result = await processor.processCustomPrompt(customPrompt, selectedText);
|
||||||
customPrompt,
|
|
||||||
selectedText
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toContain("Notes related to {#tag} are:");
|
expect(result).toContain("Notes related to {#tag} are:");
|
||||||
expect(result).toContain(
|
expect(result).toContain('[{"name":"note","content":"Note content for #tag"}]');
|
||||||
'[{"name":"note","content":"Note content for #tag"}]'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should process multiple tag variables correctly", async () => {
|
it("should process multiple tag variables correctly", async () => {
|
||||||
|
|
@ -146,14 +138,11 @@ describe('CustomPromptProcessor', () => {
|
||||||
'[{"name":"note1","content":"Note content for #tag1"},{"name":"note2","content":"Note content for #tag2"}]',
|
'[{"name":"note1","content":"Note content for #tag1"},{"name":"note2","content":"Note content for #tag2"}]',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result = await processor.processCustomPrompt(
|
const result = await processor.processCustomPrompt(customPrompt, selectedText);
|
||||||
customPrompt,
|
|
||||||
selectedText
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toContain("Notes related to {#tag1,#tag2,#tag3} are:");
|
expect(result).toContain("Notes related to {#tag1,#tag2,#tag3} are:");
|
||||||
expect(result).toContain(
|
expect(result).toContain(
|
||||||
'[{"name":"note1","content":"Note content for #tag1"},{"name":"note2","content":"Note content for #tag2"}]'
|
'[{"name":"note1","content":"Note content for #tag1"},{"name":"note2","content":"Note content for #tag2"}]'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,29 @@
|
||||||
import EncryptionService from '@/encryptionService';
|
import EncryptionService from "@/encryptionService";
|
||||||
import { CopilotSettings } from '@/settings/SettingsPage';
|
import { CopilotSettings } from "@/settings/SettingsPage";
|
||||||
import { Platform } from 'obsidian';
|
import { Platform } from "obsidian";
|
||||||
|
|
||||||
// Mocking Electron's safeStorage
|
// Mocking Electron's safeStorage
|
||||||
jest.mock('electron', () => {
|
jest.mock("electron", () => {
|
||||||
return {
|
return {
|
||||||
remote: {
|
remote: {
|
||||||
safeStorage: {
|
safeStorage: {
|
||||||
encryptString: jest.fn().mockImplementation((text) => `encrypted_${text}`),
|
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),
|
isEncryptionAvailable: jest.fn().mockReturnValue(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Platform-specific Tests', () => {
|
describe("Platform-specific Tests", () => {
|
||||||
it('should recognize the platform as desktop', () => {
|
it("should recognize the platform as desktop", () => {
|
||||||
expect(Platform.isDesktop).toBe(true); // Directly using the mocked value
|
expect(Platform.isDesktop).toBe(true); // Directly using the mocked value
|
||||||
});
|
});
|
||||||
|
|
||||||
// Example of a conditional test based on the platform
|
// 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) {
|
if (Platform.isDesktop) {
|
||||||
// Your desktop-specific logic here
|
// Your desktop-specific logic here
|
||||||
expect(true).toBe(true); // Replace with actual assertions
|
expect(true).toBe(true); // Replace with actual assertions
|
||||||
|
|
@ -36,7 +38,7 @@ interface TestSettings extends CopilotSettings {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('EncryptionService', () => {
|
describe("EncryptionService", () => {
|
||||||
let service: EncryptionService;
|
let service: EncryptionService;
|
||||||
let settings: TestSettings;
|
let settings: TestSettings;
|
||||||
|
|
||||||
|
|
@ -49,68 +51,68 @@ describe('EncryptionService', () => {
|
||||||
service = new EncryptionService(settings);
|
service = new EncryptionService(settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getEncryptedKey', () => {
|
describe("getEncryptedKey", () => {
|
||||||
it('should encrypt an API key', () => {
|
it("should encrypt an API key", () => {
|
||||||
const apiKey = 'testApiKey';
|
const apiKey = "testApiKey";
|
||||||
const encryptedKey = service.getEncryptedKey(apiKey);
|
const encryptedKey = service.getEncryptedKey(apiKey);
|
||||||
expect(encryptedKey).toBe(`enc_encrypted_${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;
|
settings.enableEncryption = false;
|
||||||
const apiKey = 'testApiKey';
|
const apiKey = "testApiKey";
|
||||||
const encryptedKey = service.getEncryptedKey(apiKey);
|
const encryptedKey = service.getEncryptedKey(apiKey);
|
||||||
expect(encryptedKey).toBe(apiKey);
|
expect(encryptedKey).toBe(apiKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the original key if already encrypted', () => {
|
it("should return the original key if already encrypted", () => {
|
||||||
const apiKey = 'enc_testApiKey';
|
const apiKey = "enc_testApiKey";
|
||||||
const encryptedKey = service.getEncryptedKey(apiKey);
|
const encryptedKey = service.getEncryptedKey(apiKey);
|
||||||
expect(encryptedKey).toBe(apiKey);
|
expect(encryptedKey).toBe(apiKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getDecryptedKey', () => {
|
describe("getDecryptedKey", () => {
|
||||||
it('should decrypt an encrypted API key', () => {
|
it("should decrypt an encrypted API key", () => {
|
||||||
const apiKey = 'testApiKey';
|
const apiKey = "testApiKey";
|
||||||
const mockEncryptedKey = `encrypted_${apiKey}`;
|
const mockEncryptedKey = `encrypted_${apiKey}`;
|
||||||
const base64Encoded = Buffer.from(mockEncryptedKey).toString('base64');
|
const base64Encoded = Buffer.from(mockEncryptedKey).toString("base64");
|
||||||
const encryptedKey = `enc_${base64Encoded}`;
|
const encryptedKey = `enc_${base64Encoded}`;
|
||||||
|
|
||||||
const decryptedKey = service.getDecryptedKey(encryptedKey);
|
const decryptedKey = service.getDecryptedKey(encryptedKey);
|
||||||
expect(decryptedKey).toBe(apiKey);
|
expect(decryptedKey).toBe(apiKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the original key if it is in plain text', () => {
|
it("should return the original key if it is in plain text", () => {
|
||||||
const apiKey = 'testApiKey';
|
const apiKey = "testApiKey";
|
||||||
const decryptedKey = service.getDecryptedKey(apiKey);
|
const decryptedKey = service.getDecryptedKey(apiKey);
|
||||||
expect(decryptedKey).toBe(apiKey);
|
expect(decryptedKey).toBe(apiKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('encryptAllKeys', () => {
|
describe("encryptAllKeys", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
settings = {
|
settings = {
|
||||||
enableEncryption: true,
|
enableEncryption: true,
|
||||||
someApiKey: 'testApiKey',
|
someApiKey: "testApiKey",
|
||||||
anotherApiKey: 'anotherTestApiKey',
|
anotherApiKey: "anotherTestApiKey",
|
||||||
nonKey: 'shouldBeIgnored',
|
nonKey: "shouldBeIgnored",
|
||||||
} as unknown as CopilotSettings;
|
} as unknown as CopilotSettings;
|
||||||
service = new EncryptionService(settings);
|
service = new EncryptionService(settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encrypt all keys containing "apikey"', () => {
|
it('should encrypt all keys containing "apikey"', () => {
|
||||||
service.encryptAllKeys();
|
service.encryptAllKeys();
|
||||||
expect(settings.someApiKey).toBe('enc_encrypted_testApiKey');
|
expect(settings.someApiKey).toBe("enc_encrypted_testApiKey");
|
||||||
expect(settings.anotherApiKey).toBe('enc_encrypted_anotherTestApiKey');
|
expect(settings.anotherApiKey).toBe("enc_encrypted_anotherTestApiKey");
|
||||||
expect(settings.nonApiKey).toBe(undefined);
|
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;
|
settings.enableEncryption = false;
|
||||||
service.encryptAllKeys();
|
service.encryptAllKeys();
|
||||||
expect(settings.someApiKey).toBe('testApiKey');
|
expect(settings.someApiKey).toBe("testApiKey");
|
||||||
expect(settings.anotherApiKey).toBe('anotherTestApiKey');
|
expect(settings.anotherApiKey).toBe("anotherTestApiKey");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -64,19 +64,13 @@ describe("getNotesFromPath", () => {
|
||||||
it("should return filtered markdown files 1", async () => {
|
it("should return filtered markdown files 1", async () => {
|
||||||
const vault = new Obsidian.Vault();
|
const vault = new Obsidian.Vault();
|
||||||
const files = await getNotesFromPath(vault, "test2");
|
const files = await getNotesFromPath(vault, "test2");
|
||||||
expect(files).toEqual([
|
expect(files).toEqual([{ path: "test/test2/note1.md" }, { path: "test2/note3.md" }]);
|
||||||
{ path: "test/test2/note1.md" },
|
|
||||||
{ path: "test2/note3.md" },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return filtered markdown files 2", async () => {
|
it("should return filtered markdown files 2", async () => {
|
||||||
const vault = new Obsidian.Vault();
|
const vault = new Obsidian.Vault();
|
||||||
const files = await getNotesFromPath(vault, "test");
|
const files = await getNotesFromPath(vault, "test");
|
||||||
expect(files).toEqual([
|
expect(files).toEqual([{ path: "test/test2/note1.md" }, { path: "test/note2.md" }]);
|
||||||
{ path: "test/test2/note1.md" },
|
|
||||||
{ path: "test/note2.md" },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return filtered markdown files 3", async () => {
|
it("should return filtered markdown files 3", async () => {
|
||||||
|
|
@ -88,10 +82,7 @@ describe("getNotesFromPath", () => {
|
||||||
it("should return filtered markdown files 4", async () => {
|
it("should return filtered markdown files 4", async () => {
|
||||||
const vault = new Obsidian.Vault();
|
const vault = new Obsidian.Vault();
|
||||||
const files = await getNotesFromPath(vault, "/test");
|
const files = await getNotesFromPath(vault, "/test");
|
||||||
expect(files).toEqual([
|
expect(files).toEqual([{ path: "test/test2/note1.md" }, { path: "test/note2.md" }]);
|
||||||
{ path: "test/test2/note1.md" },
|
|
||||||
{ path: "test/note2.md" },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not return markdown files", async () => {
|
it("should not return markdown files", async () => {
|
||||||
|
|
@ -136,16 +127,12 @@ describe("getNotesFromPath", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the note md filename with extra spaces 2", () => {
|
it("should return the note md filename with extra spaces 2", () => {
|
||||||
const variableName = processVariableNameForNotePath(
|
const variableName = processVariableNameForNotePath(" [[ test note ]] ");
|
||||||
" [[ test note ]] ",
|
|
||||||
);
|
|
||||||
expect(variableName).toEqual("test note.md");
|
expect(variableName).toEqual("test note.md");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the note md filename with extra spaces 2", () => {
|
it("should return the note md filename with extra spaces 2", () => {
|
||||||
const variableName = processVariableNameForNotePath(
|
const variableName = processVariableNameForNotePath(" [[ test_note note ]] ");
|
||||||
" [[ test_note note ]] ",
|
|
||||||
);
|
|
||||||
expect(variableName).toEqual("test_note note.md");
|
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 () => {
|
it("should handle both path and tags, returning files under the specified path with the specified tags", async () => {
|
||||||
const mockVault = new Obsidian.Vault();
|
const mockVault = new Obsidian.Vault();
|
||||||
const tags = ["tag1"];
|
const tags = ["tag1"];
|
||||||
const noteFiles = [
|
const noteFiles = [{ path: "test/test2/note1.md" }, { path: "test/note2.md" }] as TFile[];
|
||||||
{ path: "test/test2/note1.md" },
|
|
||||||
{ path: "test/note2.md" },
|
|
||||||
] as TFile[];
|
|
||||||
const expectedPaths = ["test/test2/note1.md"];
|
const expectedPaths = ["test/test2/note1.md"];
|
||||||
|
|
||||||
const result = await getNotesFromTags(mockVault, tags, noteFiles);
|
const result = await getNotesFromTags(mockVault, tags, noteFiles);
|
||||||
|
|
@ -262,10 +246,7 @@ describe("isPathInList", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle multiple excluded paths separated by commas", () => {
|
it("should handle multiple excluded paths separated by commas", () => {
|
||||||
const result = isPathInList(
|
const result = isPathInList("test/folder/note.md", "another/folder,test/folder");
|
||||||
"test/folder/note.md",
|
|
||||||
"another/folder,test/folder",
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -277,16 +258,13 @@ describe("isPathInList", () => {
|
||||||
it("should exclude a file path that matches any one of multiple excluded paths", () => {
|
it("should exclude a file path that matches any one of multiple excluded paths", () => {
|
||||||
const result = isPathInList(
|
const result = isPathInList(
|
||||||
"test/folder/note.md",
|
"test/folder/note.md",
|
||||||
"another/folder, test/folder, yet/another/folder",
|
"another/folder, test/folder, yet/another/folder"
|
||||||
);
|
);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should trim spaces around excluded paths", () => {
|
it("should trim spaces around excluded paths", () => {
|
||||||
const result = isPathInList(
|
const result = isPathInList("test/folder/note.md", " another/folder , test/folder ");
|
||||||
"test/folder/note.md",
|
|
||||||
" another/folder , test/folder ",
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -300,8 +278,7 @@ describe("extractNoteTitles", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should extract multiple note titles", () => {
|
it("should extract multiple note titles", () => {
|
||||||
const query =
|
const query = "Please refer to [[Note1]] and [[Note2]] for more information.";
|
||||||
"Please refer to [[Note1]] and [[Note2]] for more information.";
|
|
||||||
const expected = ["Note1", "Note2"];
|
const expected = ["Note1", "Note2"];
|
||||||
const result = extractNoteTitles(query);
|
const result = extractNoteTitles(query);
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,7 @@
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"types": ["jest"],
|
"types": ["jest"],
|
||||||
"lib": [
|
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||||
"DOM",
|
|
||||||
"ES5",
|
|
||||||
"ES6",
|
|
||||||
"ES7"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["**/*.ts", "src/**/*.ts", "typings/**/*.ts"]
|
||||||
"**/*.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,48 +1,48 @@
|
||||||
{
|
{
|
||||||
"1.0.0": "0.15.0",
|
"1.0.0": "0.15.0",
|
||||||
"1.1.0": "0.15.0",
|
"1.1.0": "0.15.0",
|
||||||
"1.1.1": "0.15.0",
|
"1.1.1": "0.15.0",
|
||||||
"1.2.0": "0.15.0",
|
"1.2.0": "0.15.0",
|
||||||
"1.2.1": "0.15.0",
|
"1.2.1": "0.15.0",
|
||||||
"1.2.2": "0.15.0",
|
"1.2.2": "0.15.0",
|
||||||
"1.2.3": "0.15.0",
|
"1.2.3": "0.15.0",
|
||||||
"1.2.4": "0.15.0",
|
"1.2.4": "0.15.0",
|
||||||
"2.0.0": "0.15.0",
|
"2.0.0": "0.15.0",
|
||||||
"2.1.0": "0.15.0",
|
"2.1.0": "0.15.0",
|
||||||
"2.2.0": "0.15.0",
|
"2.2.0": "0.15.0",
|
||||||
"2.2.1": "0.15.0",
|
"2.2.1": "0.15.0",
|
||||||
"2.2.2": "0.15.0",
|
"2.2.2": "0.15.0",
|
||||||
"2.2.3": "0.15.0",
|
"2.2.3": "0.15.0",
|
||||||
"2.2.4": "0.15.0",
|
"2.2.4": "0.15.0",
|
||||||
"2.3.0": "0.15.0",
|
"2.3.0": "0.15.0",
|
||||||
"2.3.1": "0.15.0",
|
"2.3.1": "0.15.0",
|
||||||
"2.3.2": "0.15.0",
|
"2.3.2": "0.15.0",
|
||||||
"2.3.3": "0.15.0",
|
"2.3.3": "0.15.0",
|
||||||
"2.3.4": "0.15.0",
|
"2.3.4": "0.15.0",
|
||||||
"2.3.5": "0.15.0",
|
"2.3.5": "0.15.0",
|
||||||
"2.3.6": "0.15.0",
|
"2.3.6": "0.15.0",
|
||||||
"2.4.0": "0.15.0",
|
"2.4.0": "0.15.0",
|
||||||
"2.4.1": "0.15.0",
|
"2.4.1": "0.15.0",
|
||||||
"2.4.2": "0.15.0",
|
"2.4.2": "0.15.0",
|
||||||
"2.4.3": "0.15.0",
|
"2.4.3": "0.15.0",
|
||||||
"2.4.4": "0.15.0",
|
"2.4.4": "0.15.0",
|
||||||
"2.4.5": "0.15.0",
|
"2.4.5": "0.15.0",
|
||||||
"2.4.6": "0.15.0",
|
"2.4.6": "0.15.0",
|
||||||
"2.4.7": "0.15.0",
|
"2.4.7": "0.15.0",
|
||||||
"2.4.8": "0.15.0",
|
"2.4.8": "0.15.0",
|
||||||
"2.4.9": "0.15.0",
|
"2.4.9": "0.15.0",
|
||||||
"2.4.10": "0.15.0",
|
"2.4.10": "0.15.0",
|
||||||
"2.4.11": "0.15.0",
|
"2.4.11": "0.15.0",
|
||||||
"2.4.12": "0.15.0",
|
"2.4.12": "0.15.0",
|
||||||
"2.4.13": "0.15.0",
|
"2.4.13": "0.15.0",
|
||||||
"2.4.14": "0.15.0",
|
"2.4.14": "0.15.0",
|
||||||
"2.4.15": "0.15.0",
|
"2.4.15": "0.15.0",
|
||||||
"2.4.16": "0.15.0",
|
"2.4.16": "0.15.0",
|
||||||
"2.4.17": "0.15.0",
|
"2.4.17": "0.15.0",
|
||||||
"2.4.18": "0.15.0",
|
"2.4.18": "0.15.0",
|
||||||
"2.5.0": "0.15.0",
|
"2.5.0": "0.15.0",
|
||||||
"2.5.1": "0.15.0",
|
"2.5.1": "0.15.0",
|
||||||
"2.5.2": "0.15.0",
|
"2.5.2": "0.15.0",
|
||||||
"2.5.3": "0.15.0",
|
"2.5.3": "0.15.0",
|
||||||
"2.5.4": "0.15.0"
|
"2.5.4": "0.15.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,38 @@
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from "fs/promises";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
|
|
||||||
const wasmPlugin = {
|
const wasmPlugin = {
|
||||||
name: 'wasm',
|
name: "wasm",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
build.onResolve({ filter: /\.wasm$/ }, args => {
|
build.onResolve({ filter: /\.wasm$/ }, (args) => {
|
||||||
if (args.namespace === 'wasm-stub') {
|
if (args.namespace === "wasm-stub") {
|
||||||
return {
|
return {
|
||||||
path: args.path,
|
path: args.path,
|
||||||
namespace: 'wasm-binary',
|
namespace: "wasm-binary",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.resolveDir === '') {
|
if (args.resolveDir === "") {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path),
|
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)}
|
contents: `import wasm from ${JSON.stringify(args.path)}
|
||||||
export default (imports) =>
|
export default (imports) =>
|
||||||
WebAssembly.instantiate(wasm, imports).then(
|
WebAssembly.instantiate(wasm, imports).then(
|
||||||
result => result.instance.exports)`,
|
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),
|
contents: await fs.readFile(args.path),
|
||||||
loader: 'binary',
|
loader: "binary",
|
||||||
}))
|
}));
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default wasmPlugin;
|
export default wasmPlugin;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue