mirror of
https://github.com/hyeonseonam/auto-classifier.git
synced 2026-07-22 07:40:29 +00:00
Compare commits
No commits in common. "main" and "1.0.0" have entirely different histories.
11 changed files with 413 additions and 1605 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -15,11 +15,8 @@ main.js
|
|||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian data
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
# Exclude test
|
||||
*.txt
|
||||
153
README.md
153
README.md
|
|
@ -1,158 +1,63 @@
|
|||
# Auto Classifier
|
||||
|
||||
 
|
||||
|
||||
`Auto Classifier` is an [Obsidian](https://obsidian.md/) plugin that helps you automatically classify tags in your notes using either OpenAI-compatible APIs (ChatGPT, Ollama, LocalAI, etc.) or Jina AI Classifier. The plugin can analyze your note (including its title, frontmatter, content, or selected area) and suggest relevant tags based on the input with tags in your vault. This can be used for various specific purposes, for example, DDC classification for books, keyword recommendation, research paper categorization, and so on. Save time and improve your note organization.
|
||||
`Auto Classifier` is an [Obsidian](https://obsidian.md/) plugin that helps you automatically classify tag from your notes using the ChatGPT API. The plugin can analyze your note (It can be title, frontmatter, content or selected area) and suggest relevant tag based on the input with tags in your valut. This can be used for various and specific purposes, for example, DDC classification for books, keyword recommendation, research paper categorization and so on. Save you time and improving your note organization.
|
||||
|
||||
|
||||
## How to use
|
||||
|
||||
- Configure your classification engine and API settings in the settings tab:
|
||||
- Enter your ChatGPT API key in setting tab. You can create it at [OpenAI](https://platform.openai.com/account/api-keys). Check api setting by the `Test API call` button.
|
||||
|
||||
- **Choose your Classification Engine:**
|
||||
- `OpenAI-compatible API`: Uses OpenAI API, Local AI (Ollama, LocalAI), or other compatible APIs
|
||||
- `Jina AI`: Uses Jina AI Classifier (cost-effective, multilingual)
|
||||
|
||||
- **For OpenAI-compatible API:**
|
||||
- Enter your API key (leave empty for local AI)
|
||||
- Set the base URL:
|
||||
- OpenAI: `https://api.openai.com/v1` (default)
|
||||
- Local OpenAI-compatible API (Ollama, LocalAI):
|
||||
- Ollama: `http://localhost:11434/v1`
|
||||
- LocalAI: `http://localhost:8080/v1`
|
||||
- Choose your model:
|
||||
- OpenAI: `gpt-4.1-mini` (recommended), `gpt-4.1`, `gpt-4o`, ...
|
||||
- Local OpenAI-compatible API: `llama3`, `mistral`, `phi3`, `qwen2`, ... (Ollama/LocalAI)
|
||||
- Test your configuration using the Test API call button
|
||||
|
||||
- **For Jina AI:**
|
||||
- Enter your Jina AI API key (free tier available with 10M tokens)
|
||||
- Choose your preferred model (default: jina-embeddings-v3)
|
||||
- Test your configuration using the Test Jina API button
|
||||
- **Getting a Jina AI API Key**: Visit the [Jina AI website](https://jina.ai/) to obtain a free temporary API key (you can get the key without creating an account).
|
||||
|
||||
- This plugin consists of **4 Input Commands** that you can run. By simply running these commands, it will automatically classify your note:
|
||||
|
||||
- `Classify tag from Note title`
|
||||
- `Classify tag from Note FrontMatter`
|
||||
- `Classify tag from Note Content`
|
||||
- `Classify tag from Selected Area`
|
||||
|
||||
- Toggle and choose from different **Tag Reference** types. The LLM will select the appropriate tag from these references:
|
||||
|
||||
- `All tags` (default)
|
||||
- `Filtered Tags` with regular expression
|
||||
- `Manual Tags` that are defined manually
|
||||
|
||||
- Specify the **Output Type** from the response of the LLM:
|
||||
|
||||
- `#Tag`: at your `Current Cursor` or `Top of Content`
|
||||
- `[[WikiLink]]`: at your `Current Cursor` or `Top of Content`
|
||||
- `FrontMatter`: with `key`
|
||||
- `Title Alternative`: at the end of note's title
|
||||
|
||||
- (Optional) Add a `Prefix` or `Suffix` for the output format.
|
||||
|
||||
- (Optional) You can use your custom request for your selected API:
|
||||
- `Custom Prompt Template`
|
||||
- The LLM will respond based on this prompt. The input coming from your Command will be replaced by `{{input}}`, and the reference tags you set will be placed in `{{reference}}`.
|
||||
- `Custom Chat Role`
|
||||
- You can guide the AI's behavior by setting this system role
|
||||
|
||||
## Classification Engines
|
||||
|
||||
### OpenAI-compatible API
|
||||
- **Flexibility**: Supports custom prompts and chat roles
|
||||
- **Multiple Providers**: Works with OpenAI, Ollama, LocalAI, and other compatible APIs
|
||||
- **Local AI Support**: Run models locally without internet connection
|
||||
- **Recommended OpenAI models**: `gpt-4.1-mini`, `gpt-4.1`, `gpt-4o`
|
||||
- **Advanced Features**: Custom prompt templates, system roles, and token control
|
||||
|
||||
### Jina AI Classifier
|
||||
- **Cost-Effective**: Generous free tier with 10 million tokens
|
||||
- **Multilingual**: Supports multiple languages including non-English content
|
||||
- **High Performance**: Zero-shot classification with up to 8192 tokens input and 256 distinct classes
|
||||
- **Optimized**: Works best with semantic labels (e.g., "happy", "sad", "angry")
|
||||
- **Easy Setup**: Often no account creation required, can generate fresh API keys as needed
|
||||
|
||||
## Local AI Setup (Ollama, LocalAI)
|
||||
|
||||
**Experimental Support:**
|
||||
⚠️ You may use local OpenAI-compatible APIs such as Ollama or LocalAI. However, support for these engines is experimental and full compatibility or stability is **not guaranteed**. Some features may not work as expected. Please test thoroughly before relying on these engines for important workflows.
|
||||
If you encounter issues or want to help improve compatibility, **contributions and pull requests are very welcome!**
|
||||
|
||||
**Example setup:**
|
||||
1. Install [Ollama](https://ollama.ai/) or [LocalAI](https://localai.io/)
|
||||
2. Prepare your model: For Ollama, run `ollama pull llama3`. For LocalAI, configure your models as needed.
|
||||
3. Set Base URL: Ollama - `http://localhost:11434/v1`, LocalAI - `http://localhost:8080/v1`
|
||||
4. Set Model: e.g., `llama3`, `mistral`, etc.
|
||||
5. API Key: Leave empty
|
||||
- This plugin consists of **4 Input Commands** that you can run. By simply running these commands, it will automatically classify your note.
|
||||
- `Classify tag from Note title`
|
||||
- `Classify tag from Note FrontMatter`
|
||||
- `Classify tag from Note Content`
|
||||
- `Classify tag from Selected Area`
|
||||
|
||||
- Choose from different **Tag Reference** types. ChatGPT will select the appropriate tag from these references.
|
||||
- `All tags` (default)
|
||||
- `Filtered Tags` with regular expression
|
||||
- `Manual Tags` that defined manually
|
||||
|
||||
- Specify the **Output Location** for the generated tag and decide whether it **overwrites**:
|
||||
- At `FrontMatter` (with specified key)
|
||||
- At `Title`
|
||||
- At `Current Cursor`
|
||||
|
||||
- (Optional) Your can use your custom request for the ChatGPT API.
|
||||
- `Custom Prompt Template`
|
||||
- ChatGPT will respond based on this prompt. The input coming from your Command will be replaced by `{{input}}`, and the reference tags you set will be placed in `{{reference}}`.
|
||||
- `Custom Chat Role`
|
||||
- You can guide ChatGPT's behabior by setting this system role
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
### Use Case #1: **Selected area** → **Current cursor**
|
||||
|
||||

|
||||
|
||||
### Use Case #2: **Content** → **FrontMatter**
|
||||
|
||||

|
||||
|
||||
### Use Case #3: **FrontMatter** → **Title**
|
||||
|
||||

|
||||
|
||||
### Use Case #4: **Title** → **FrontMatter**
|
||||
|
||||

|
||||
|
||||
### DDC number classification
|
||||
|
||||
If you want to use this plugin for DDC number classification, edit the `Custom Prompt Template` like this:
|
||||
|
||||
```
|
||||
Please use Dewey Decimal Classification (DDC) to classify this content:
|
||||
"""
|
||||
{{input}}
|
||||
"""
|
||||
Answer format is JSON {reliability:0~1, output:"[ddc_number]:category"}.
|
||||
Even if you are not sure, qualify the reliability and select one.
|
||||
Convert the blank spaces to "_" in the output.
|
||||
```
|
||||
|
||||
### LCSH classification
|
||||
|
||||
LCSH classification can be similar:
|
||||
|
||||
```
|
||||
Please use Library of Congress Subject Headings (LCSH) to classify this content:
|
||||
"""
|
||||
{{input}}
|
||||
"""
|
||||
Answer format is JSON {reliability:0~1, output:"[First LCSH term]--[Second LCSH term]--[Third LCSH term]"}.
|
||||
Even if you are not sure, qualify the reliability and select one.
|
||||
Convert the blank spaces to "_" in the output.
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
- Search for `Auto Classifier` in the Community plugin tab of the Obsidian settings.
|
||||
- Alternatively, you can manually download the latest release from this repository's [GitHub releases](https://github.com/hyeonseonam/auto-tagger/releases) and extract the ZIP file to your Obsidian plugins folder.
|
||||
- Search `Auto Classifier` in the Community plugin tab of the Obsidian settings.
|
||||
- Also, you can manually download the latest release from this repository [GitHub releases](https://github.com/hyeonseonam/auto-tagger/releases) and extrat the ZIP file to your Obsidian plugins folder.
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues while using this plugin or have suggestions for improvement, please feel free to submit an issue on the GitHub repository. Pull requests are also welcome.
|
||||
If you encounter any issues while using this plugin or have any suggestions for improvement, please feel free to submit an issue on the GitHub repository. Pull requests are also welcome.
|
||||
|
||||
## 🙌 Contributors
|
||||
|
||||
- @gravityfargo
|
||||
- @rtuszik
|
||||
- @tk42
|
||||
- @SophomoreSo
|
||||
- @ThiagoVsky
|
||||
## Author
|
||||
|
||||
Hyeonseo Nam
|
||||
## License
|
||||
|
||||
MIT License
|
||||
MIT License
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"id": "auto-classifier",
|
||||
"name": "Auto Classifier",
|
||||
"version": "1.3.0",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "1.1.1",
|
||||
"description": "This plugin automatically classify tag from your notes using ChatGPT API or Jina Classifier. It analyze your note (It can be title, frontmatter, content or selected area) and automatically insert tag where you set.",
|
||||
"description": "This plugin automatically classify tag from your notes using ChatGPT API. It analyze your note (It can be title, frontmatter, content or selected area) and automatically insert tag where you set.",
|
||||
"author": "Hyeonseo Nam",
|
||||
"authorUrl": "https://github.com/HyeonseoNam/auto-classifier",
|
||||
"isDesktopOnly": false
|
||||
|
|
|
|||
430
package-lock.json
generated
430
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "auto-classifier",
|
||||
"version": "1.1.2",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "auto-classifier",
|
||||
"version": "1.1.2",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"openai": "4.76.1",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
|
|
@ -213,16 +212,6 @@
|
|||
"integrity": "sha512-wvzClDGQXOCVNU4APPopC2KtMYukaF1MN/W3xAmslx22Z4/IF1/izDMekuyoUlwfnDHYCIZGaj7jMwnJKBTxKw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz",
|
||||
|
|
@ -416,18 +405,6 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.8.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
|
||||
|
|
@ -451,18 +428,6 @@
|
|||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agentkeepalive": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
||||
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
|
|
@ -522,12 +487,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
|
@ -570,19 +529,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
|
|
@ -630,18 +576,6 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
|
@ -688,15 +622,6 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
|
|
@ -722,65 +647,6 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.17.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz",
|
||||
|
|
@ -1043,15 +909,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
|
@ -1173,41 +1030,6 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"node-domexception": "1.0.0",
|
||||
"web-streams-polyfill": "4.0.0-beta.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
|
|
@ -1215,58 +1037,12 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/functional-red-black-tree": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
|
||||
"integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
|
|
@ -1337,18 +1113,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/grapheme-splitter": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
|
||||
|
|
@ -1366,54 +1130,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
|
|
@ -1602,15 +1318,6 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
|
@ -1633,27 +1340,6 @@
|
|||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
|
|
@ -1689,46 +1375,6 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.1.1.tgz",
|
||||
|
|
@ -1753,41 +1399,6 @@
|
|||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.76.1",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.76.1.tgz",
|
||||
"integrity": "sha512-ci63/WFEMd6QjjEVeH0pV7hnFS6CCqhgJydSti4Aak/8uo2SpgzKjteUDaY+OkwziVj11mi6j+0mRUIiGKUzWw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/@types/node": {
|
||||
"version": "18.19.115",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.115.tgz",
|
||||
"integrity": "sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
|
||||
|
|
@ -2125,12 +1736,6 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
|
|
@ -2197,12 +1802,6 @@
|
|||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
|
|
@ -2220,31 +1819,6 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"name": "auto-classifier",
|
||||
"version": "1.3.0",
|
||||
"description": "This plugin automatically classify tag from your notes using OpenAI-compatible APIs (ChatGPT, Ollama, LocalAI) or Jina AI Classifier. It analyze your note (It can be title, frontmatter, content or selected area) and automatically insert tag where you set.",
|
||||
"version": "1.0.0",
|
||||
"description": "This plugin automatically classify tag from your notes using ChatGPT API. It analyze your note (It can be title, frontmatter, content or selected area) and automatically insert tag where you set.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": ["obsidian", "plugin", "classification", "tags", "ai", "chatgpt", "jina", "automation"],
|
||||
"author": "Hyeonseo Nam",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
|
|
@ -19,7 +19,6 @@
|
|||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4",
|
||||
"openai": "4.76.1"
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
74
src/api.ts
74
src/api.ts
|
|
@ -1,44 +1,50 @@
|
|||
import OpenAI from 'openai';
|
||||
|
||||
export class ChatGPT {
|
||||
private static baseUrl = 'https://api.openai.com/v1/chat/completions';
|
||||
|
||||
static async callAPI(
|
||||
system_role: string,
|
||||
user_prompt: string,
|
||||
apiKey: string,
|
||||
model: string = 'gpt-3.5-turbo',
|
||||
max_tokens: number = 150,
|
||||
temperature: number = 0,
|
||||
top_p: number = 0.95,
|
||||
frequency_penalty: number = 0,
|
||||
presence_penalty: number = 0.5,
|
||||
baseURL?: string,
|
||||
): Promise<string> {
|
||||
const client = new OpenAI({
|
||||
apiKey: apiKey,
|
||||
dangerouslyAllowBrowser: true, // Required for client-side use
|
||||
baseURL: baseURL || 'https://api.openai.com/v1'
|
||||
model = "gpt-3.5-turbo",
|
||||
temperature = 0,
|
||||
max_tokens = 150,
|
||||
top_p = 0.95,
|
||||
frequency_penalty = 0,
|
||||
presence_penalty = 0.5): Promise<string> {
|
||||
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
});
|
||||
|
||||
try {
|
||||
const completion = await client.chat.completions.create({
|
||||
model: model,
|
||||
messages: [
|
||||
{ role: "system", content: system_role },
|
||||
{ role: "user", content: user_prompt }
|
||||
],
|
||||
max_tokens: max_tokens,
|
||||
temperature: temperature,
|
||||
top_p: top_p,
|
||||
frequency_penalty: frequency_penalty,
|
||||
presence_penalty: presence_penalty
|
||||
});
|
||||
const body = JSON.stringify({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{ "role": "system", "content": system_role },
|
||||
{ "role": "user", "content": user_prompt },
|
||||
],
|
||||
max_tokens: max_tokens,
|
||||
n: 1,
|
||||
// stop: '\n',
|
||||
stop: null,
|
||||
temperature: temperature,
|
||||
top_p: top_p,
|
||||
frequency_penalty: frequency_penalty,
|
||||
presence_penalty: presence_penalty
|
||||
});
|
||||
|
||||
return completion.choices[0].message.content || '';
|
||||
} catch (error) {
|
||||
if (error instanceof OpenAI.APIError) {
|
||||
throw new Error(`OpenAI API Error: ${error.status} - ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
const response = await fetch(`${this.baseUrl}`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API call error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// return data.choices[0].text.trim();
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/jina-api.ts
100
src/jina-api.ts
|
|
@ -1,100 +0,0 @@
|
|||
import { requestUrl } from 'obsidian';
|
||||
|
||||
export interface JinaAPIRequest {
|
||||
model: string;
|
||||
input: Array<{ text: string }>;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export interface JinaAPIResponsePrediction {
|
||||
label: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface JinaAPIResponseData {
|
||||
object: string;
|
||||
index: number;
|
||||
prediction: string;
|
||||
score: number;
|
||||
predictions: JinaAPIResponsePrediction[];
|
||||
}
|
||||
|
||||
export interface JinaAPIResponse {
|
||||
usage: {
|
||||
total_tokens: number;
|
||||
};
|
||||
data: JinaAPIResponseData[];
|
||||
}
|
||||
|
||||
export class JinaAI {
|
||||
static async callAPI(
|
||||
apiKey: string,
|
||||
baseURL: string,
|
||||
model: string,
|
||||
inputText: string[], // Changed from string to string[] to allow multiple inputs if needed by the plugin logic later
|
||||
labels: string[]
|
||||
): Promise<JinaAPIResponse> {
|
||||
const apiUrl = `${baseURL}/classify`; // Assuming baseURL does not contain /classify
|
||||
const requestBody: JinaAPIRequest = {
|
||||
model: model,
|
||||
input: inputText.map(text => ({ text })),
|
||||
labels: labels,
|
||||
};
|
||||
|
||||
console.log('Jina AI Request:', { url: apiUrl, body: requestBody });
|
||||
|
||||
try {
|
||||
const response = await requestUrl({
|
||||
url: apiUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
throw: false, // To handle errors manually
|
||||
});
|
||||
|
||||
console.log('Jina AI Response Status:', response.status);
|
||||
console.log('Jina AI Response Body:', response.text); // Log raw text for debugging
|
||||
|
||||
if (response.status !== 200) {
|
||||
let errorDetails = response.text;
|
||||
try {
|
||||
const jsonError = JSON.parse(response.text);
|
||||
errorDetails = jsonError.detail || JSON.stringify(jsonError);
|
||||
} catch (e) {
|
||||
// Keep errorDetails as text if not JSON
|
||||
}
|
||||
|
||||
// More specific error messages
|
||||
let userFriendlyMessage = `Jina AI API Error (${response.status})`;
|
||||
if (response.status === 401) {
|
||||
userFriendlyMessage = "Invalid Jina AI API key. Please check your API key in settings.";
|
||||
} else if (response.status === 403) {
|
||||
userFriendlyMessage = "Jina AI API access denied. Please check your API key permissions.";
|
||||
} else if (response.status === 429) {
|
||||
userFriendlyMessage = "Jina AI API rate limit exceeded. Please try again later.";
|
||||
} else if (response.status === 500) {
|
||||
userFriendlyMessage = "Jina AI server error. Please try again later.";
|
||||
} else {
|
||||
userFriendlyMessage = `Jina AI API Error: ${errorDetails}`;
|
||||
}
|
||||
|
||||
console.error(`Jina AI API Error: ${response.status} - ${errorDetails}`);
|
||||
throw new Error(userFriendlyMessage);
|
||||
}
|
||||
|
||||
const responseData: JinaAPIResponse = response.json;
|
||||
console.log('Jina AI Parsed Response Data:', responseData);
|
||||
return responseData;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error calling Jina AI API:', error);
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error('An unknown error occurred while calling Jina AI API.');
|
||||
}
|
||||
}
|
||||
}
|
||||
300
src/main.ts
300
src/main.ts
|
|
@ -1,21 +1,13 @@
|
|||
import { Plugin, Notice } from "obsidian";
|
||||
import {
|
||||
AutoClassifierSettingTab,
|
||||
AutoClassifierSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
OutLocation,
|
||||
OutType,
|
||||
ClassifierEngine, // Added ClassifierEngine
|
||||
} from "src/settings";
|
||||
import { AutoClassifierSettingTab, AutoClassifierSettings, DEFAULT_SETTINGS, OutLocation } from "src/settings";
|
||||
import { ViewManager } from "src/view-manager";
|
||||
import { ChatGPT } from "src/api";
|
||||
import { JinaAI, JinaAPIResponse } from "src/jina-api"; // Added JinaAI and JinaAPIResponse
|
||||
import { ChatGPT } from 'src/api';
|
||||
|
||||
enum InputType {
|
||||
SelectedArea,
|
||||
Title,
|
||||
FrontMatter,
|
||||
Content,
|
||||
Content
|
||||
}
|
||||
|
||||
export default class AutoClassifierPlugin extends Plugin {
|
||||
|
|
@ -27,32 +19,32 @@ export default class AutoClassifierPlugin extends Plugin {
|
|||
|
||||
// Commands
|
||||
this.addCommand({
|
||||
id: "classify-tag-selected",
|
||||
name: "Classify tag from Selected Area",
|
||||
id: 'classify-tag-selected',
|
||||
name: 'Classify tag from Selected Area',
|
||||
callback: async () => {
|
||||
await this.runClassifyTag(InputType.SelectedArea);
|
||||
},
|
||||
}
|
||||
});
|
||||
this.addCommand({
|
||||
id: "classify-tag-title",
|
||||
name: "Classify tag from Note Title",
|
||||
id: 'classify-tag-title',
|
||||
name: 'Classify tag from Note Title',
|
||||
callback: async () => {
|
||||
await this.runClassifyTag(InputType.Title);
|
||||
},
|
||||
}
|
||||
});
|
||||
this.addCommand({
|
||||
id: "classify-tag-frontmatter",
|
||||
name: "Classify tag from FrontMatter",
|
||||
id: 'classify-tag-frontmatter',
|
||||
name: 'Classify tag from FrontMatter',
|
||||
callback: async () => {
|
||||
await this.runClassifyTag(InputType.FrontMatter);
|
||||
},
|
||||
}
|
||||
});
|
||||
this.addCommand({
|
||||
id: "classify-tag-content",
|
||||
name: "Classify tag from Note Content",
|
||||
id: 'classify-tag-content',
|
||||
name: 'Classify tag from Note Content',
|
||||
callback: async () => {
|
||||
await this.runClassifyTag(InputType.Content);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
this.addSettingTab(new AutoClassifierSettingTab(this.app, this));
|
||||
|
|
@ -65,26 +57,7 @@ export default class AutoClassifierPlugin extends Plugin {
|
|||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
async onunload() {}
|
||||
|
||||
// create loading spin in the Notice message
|
||||
createLoadingNotice(text: string, number = 10000): Notice {
|
||||
const notice = new Notice("", number);
|
||||
const loadingContainer = document.createElement("div");
|
||||
loadingContainer.addClass("loading-container");
|
||||
|
||||
const loadingIcon = document.createElement("div");
|
||||
loadingIcon.addClass("loading-icon");
|
||||
const loadingText = document.createElement("span");
|
||||
loadingText.textContent = text;
|
||||
//@ts-ignore
|
||||
notice.noticeEl.empty();
|
||||
loadingContainer.appendChild(loadingIcon);
|
||||
loadingContainer.appendChild(loadingText);
|
||||
//@ts-ignore
|
||||
notice.noticeEl.appendChild(loadingContainer);
|
||||
|
||||
return notice;
|
||||
async onunload() {
|
||||
}
|
||||
|
||||
async runClassifyTag(inputType: InputType) {
|
||||
|
|
@ -100,46 +73,31 @@ export default class AutoClassifierPlugin extends Plugin {
|
|||
// Main Classification
|
||||
async classifyTag(inputType: InputType) {
|
||||
const commandOption = this.settings.commandOption;
|
||||
const currentEngine = this.settings.classifierEngine;
|
||||
|
||||
// ------- [API Key check] -------
|
||||
// Note: Local AI (Ollama, LocalAI) usually doesn't require API keys
|
||||
if (currentEngine === ClassifierEngine.ChatGPT && !this.settings.apiKey) {
|
||||
// Check if it's likely a local AI setup (localhost or local IP)
|
||||
const baseUrl = this.settings.baseURL.toLowerCase();
|
||||
if (!baseUrl.includes('localhost') && !baseUrl.includes('127.0.0.1') && !baseUrl.includes('192.168.')) {
|
||||
new Notice(`⛔ ${this.manifest.name}: API Key is missing. Required for most cloud APIs.`);
|
||||
return null;
|
||||
}
|
||||
if (!this.settings.apiKey) {
|
||||
new Notice(`⛔ ${this.manifest.name}: You shuld input your API Key`);
|
||||
return null
|
||||
}
|
||||
if (currentEngine === ClassifierEngine.JinaAI && !this.settings.jinaApiKey) {
|
||||
new Notice(`⛔ ${this.manifest.name}: Jina AI API Key is missing.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------- [Input] -------
|
||||
const refs = this.settings.commandOption.refs;
|
||||
// reference check
|
||||
if (this.settings.commandOption.useRef && (!refs || refs.length == 0)) {
|
||||
if (!refs || refs.length == 0) {
|
||||
new Notice(`⛔ ${this.manifest.name}: no reference tags`);
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Jina AI has a limit of 256 classes for zero-shot classification
|
||||
if (currentEngine === ClassifierEngine.JinaAI && refs.length > 256) {
|
||||
new Notice(`⛔ ${this.manifest.name}: Jina AI supports maximum 256 reference tags, but ${refs.length} were provided. Please reduce the number of tags.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set Input
|
||||
let input: string | null = "";
|
||||
// Set Input
|
||||
let input: string | null = '';
|
||||
if (inputType == InputType.SelectedArea) {
|
||||
input = await this.viewManager.getSelection();
|
||||
} else if (inputType == InputType.Title) {
|
||||
}
|
||||
else if (inputType == InputType.Title) {
|
||||
input = await this.viewManager.getTitle();
|
||||
} else if (inputType == InputType.FrontMatter) {
|
||||
}
|
||||
else if (inputType == InputType.FrontMatter) {
|
||||
input = await this.viewManager.getFrontMatter();
|
||||
} else if (inputType == InputType.Content) {
|
||||
}
|
||||
else if (inputType == InputType.Content) {
|
||||
input = await this.viewManager.getContent();
|
||||
}
|
||||
|
||||
|
|
@ -149,143 +107,89 @@ export default class AutoClassifierPlugin extends Plugin {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Replace {{input}}, {{reference}} for ChatGPT
|
||||
let user_prompt = "";
|
||||
let system_role = "";
|
||||
// Replace {{input}}, {{reference}}
|
||||
let user_prompt = this.settings.commandOption.prmpt_template;
|
||||
user_prompt = user_prompt.replace('{{input}}', input);
|
||||
user_prompt = user_prompt.replace('{{reference}}', refs.join(','));
|
||||
|
||||
if (currentEngine === ClassifierEngine.ChatGPT) {
|
||||
user_prompt = this.settings.commandOption.prmpt_template;
|
||||
user_prompt = user_prompt.replace("{{input}}", input);
|
||||
user_prompt = user_prompt.replace("{{reference}}", refs.join(","));
|
||||
system_role = this.settings.commandOption.chat_role; // Corrected from prmpt_template
|
||||
}
|
||||
const system_role = this.settings.commandOption.prmpt_template;
|
||||
|
||||
// ------- [API Processing] -------
|
||||
try {
|
||||
let outputs: string[] = [];
|
||||
let jinaResponse: JinaAPIResponse | null = null;
|
||||
// Call API
|
||||
const responseRaw = await ChatGPT.callAPI(system_role, user_prompt, this.settings.apiKey);
|
||||
|
||||
if (currentEngine === ClassifierEngine.ChatGPT) {
|
||||
const responseRaw = await ChatGPT.callAPI(
|
||||
system_role,
|
||||
user_prompt,
|
||||
this.settings.apiKey,
|
||||
this.settings.commandOption.model,
|
||||
this.settings.commandOption.max_tokens,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
this.settings.baseURL,
|
||||
);
|
||||
// Parse ChatGPT response
|
||||
try {
|
||||
const response = JSON.parse(responseRaw.replace(/^```json\n/, "").replace(/\n```$/, ""));
|
||||
const resReliability = response.reliability;
|
||||
const resOutputs = response.outputs;
|
||||
|
||||
if (!Array.isArray(resOutputs)) {
|
||||
new Notice(`⛔ ${this.manifest.name}: ChatGPT output format error (expected array)`);
|
||||
return null;
|
||||
}
|
||||
if (resReliability <= 0.2 && commandOption.useRef) { // Reliability check only if using references
|
||||
new Notice(
|
||||
`⛔ ${this.manifest.name}: ChatGPT response has low reliability (${resReliability})`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
outputs = resOutputs;
|
||||
} catch (error) {
|
||||
new Notice(`⛔ ${this.manifest.name}: ChatGPT JSON parsing error - ${error}`);
|
||||
console.error("ChatGPT JSON parsing error:", error, "Raw response:", responseRaw);
|
||||
return null;
|
||||
}
|
||||
} else if (currentEngine === ClassifierEngine.JinaAI) {
|
||||
jinaResponse = await JinaAI.callAPI(
|
||||
this.settings.jinaApiKey,
|
||||
this.settings.jinaBaseURL,
|
||||
this.settings.commandOption.model || 'jina-embeddings-v3', // Ensure model is passed
|
||||
[input], // Jina expects an array of texts
|
||||
refs
|
||||
);
|
||||
// Extract labels from Jina AI response
|
||||
// Assuming we take the top prediction for the first input text.
|
||||
// If multiple inputs were sent, this logic would need to iterate jinaResponse.data
|
||||
if (jinaResponse.data && jinaResponse.data.length > 0) {
|
||||
// Sort predictions by score and take the top ones
|
||||
const sortedPredictions = jinaResponse.data[0].predictions.sort((a, b) => b.score - a.score);
|
||||
outputs = sortedPredictions.map(p => p.label);
|
||||
} else {
|
||||
new Notice(`⛔ ${this.manifest.name}: Jina AI returned no data.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit number of suggestions
|
||||
const limitedOutputs = outputs.slice(0, this.settings.commandOption.max_suggestions);
|
||||
|
||||
if (limitedOutputs.length === 0) {
|
||||
new Notice(`⛔ ${this.manifest.name}: No tags were classified.`);
|
||||
// String type to JSON type
|
||||
const regexToJson = /\{([^}]+)\}/g;
|
||||
const match = responseRaw.match(regexToJson);
|
||||
let resOutput;
|
||||
let resReliabity;
|
||||
if (match) {
|
||||
const resJson = JSON.parse(match[0]);
|
||||
// Property check
|
||||
if (!resJson.hasOwnProperty('output') || !resJson.hasOwnProperty('reliability')) {
|
||||
new Notice(`⛔ ${this.manifest.name}: output format error (No 'output' and 'reliability' key)`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------- [Add Tags] -------
|
||||
for (const resOutput of limitedOutputs) {
|
||||
// Output Type 1. [Tag Case] + Output Type 2. [Wikilink Case]
|
||||
if (
|
||||
commandOption.outType == OutType.Tag ||
|
||||
commandOption.outType == OutType.Wikilink
|
||||
) {
|
||||
if (commandOption.outLocation == OutLocation.Cursor) {
|
||||
this.viewManager.insertAtCursor(
|
||||
resOutput,
|
||||
commandOption.overwrite,
|
||||
commandOption.outType,
|
||||
commandOption.outPrefix,
|
||||
commandOption.outSuffix,
|
||||
);
|
||||
} else if (commandOption.outLocation == OutLocation.ContentTop) {
|
||||
this.viewManager.insertAtContentTop(
|
||||
resOutput,
|
||||
commandOption.outType,
|
||||
commandOption.outPrefix,
|
||||
commandOption.outSuffix,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Output Type 3. [Frontmatter Case]
|
||||
else if (commandOption.outType == OutType.FrontMatter) {
|
||||
this.viewManager.insertAtFrontMatter(
|
||||
commandOption.key,
|
||||
resOutput,
|
||||
commandOption.overwrite,
|
||||
commandOption.outPrefix,
|
||||
commandOption.outSuffix,
|
||||
);
|
||||
}
|
||||
// Output Type 4. [Title]
|
||||
else if (commandOption.outType == OutType.Title) {
|
||||
this.viewManager.insertAtTitle(
|
||||
resOutput,
|
||||
commandOption.overwrite,
|
||||
commandOption.outPrefix,
|
||||
commandOption.outSuffix,
|
||||
);
|
||||
resOutput = resJson.output;
|
||||
resReliabity = resJson.reliability;
|
||||
} else
|
||||
if (!match) {
|
||||
// Property check
|
||||
const resOutputRegex = /utput:\s*([^\n\r]+)/;
|
||||
const resReliabityRegex = /eliability:\s*([^\n\r]+)/;
|
||||
try {
|
||||
resOutput = String(responseRaw.match(resOutputRegex)?.[1]);
|
||||
resReliabity = parseFloat(String(responseRaw.match(resReliabityRegex)?.[1]));
|
||||
} catch (err) {
|
||||
new Notice(`⛔ ${this.manifest.name}: output format error`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Show token usage if available
|
||||
let tokenInfo = "";
|
||||
if (currentEngine === ClassifierEngine.JinaAI && jinaResponse && jinaResponse.usage) {
|
||||
tokenInfo = ` (${jinaResponse.usage.total_tokens} tokens used)`;
|
||||
}
|
||||
const engineName = currentEngine === ClassifierEngine.ChatGPT ? "OpenAI-compatible API" : "Jina AI";
|
||||
new Notice(`✅ ${this.manifest.name}: classified with ${limitedOutputs.length} tags using ${engineName}${tokenInfo}.`);
|
||||
|
||||
} catch (error: any) {
|
||||
const engineName = currentEngine === ClassifierEngine.ChatGPT ? "OpenAI-compatible API" : "Jina AI";
|
||||
new Notice(`⛔ ${this.manifest.name} API Error: ${error.message || error}`);
|
||||
console.error(`${engineName} API Error:`, error);
|
||||
// Avoid row reliability
|
||||
if (resReliabity <= 0.2) {
|
||||
new Notice(`⛔ ${this.manifest.name}: response has row reliability (${resReliabity})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------- [Add Tag] -------
|
||||
// FrontMatter
|
||||
if (commandOption.outLocation == OutLocation.FrontMatter) {
|
||||
this.viewManager.insertAtFrontMatter(commandOption.key, resOutput, commandOption.overwrite);
|
||||
new Notice(`✅ ${this.manifest.name}: classified to ${resOutput} at FrontMatter[${commandOption.key}]`);
|
||||
}
|
||||
// Title
|
||||
else if (commandOption.outLocation == OutLocation.Title) {
|
||||
this.viewManager.insertAtTitle(resOutput, commandOption.overwrite);
|
||||
new Notice(`✅ ${this.manifest.name}: classified to ${resOutput} at Title`);
|
||||
}
|
||||
// Cursor
|
||||
else if (commandOption.outLocation == OutLocation.Cursor) {
|
||||
this.viewManager.insertAtCursor(resOutput, commandOption.overwrite);
|
||||
new Notice(`✅ ${this.manifest.name}: classified to ${resOutput} at Current Cursor`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// create loading spin in the Notice message
|
||||
createLoadingNotice(text: string, number = 10000): Notice {
|
||||
const notice = new Notice('', number);
|
||||
const loadingContainer = document.createElement('div');
|
||||
loadingContainer.addClass('loading-container');
|
||||
|
||||
const loadingIcon = document.createElement('div');
|
||||
loadingIcon.addClass('loading-icon');
|
||||
const loadingText = document.createElement('span');
|
||||
loadingText.textContent = text;
|
||||
//@ts-ignore
|
||||
notice.noticeEl.empty();
|
||||
loadingContainer.appendChild(loadingIcon);
|
||||
loadingContainer.appendChild(loadingText);
|
||||
//@ts-ignore
|
||||
notice.noticeEl.appendChild(loadingContainer);
|
||||
|
||||
return notice;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
876
src/settings.ts
876
src/settings.ts
|
|
@ -1,13 +1,7 @@
|
|||
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import { ChatGPT } from 'src/api';
|
||||
import { JinaAI } from 'src/jina-api'; // Added JinaAI import
|
||||
import type AutoClassifierPlugin from "src/main";
|
||||
import { DEFAULT_CHAT_ROLE, DEFAULT_PROMPT_TEMPLATE, DEFAULT_PROMPT_TEMPLATE_WO_REF } from 'src/template'
|
||||
|
||||
export enum ClassifierEngine { // Added ClassifierEngine enum
|
||||
ChatGPT,
|
||||
JinaAI,
|
||||
}
|
||||
import { DEFAULT_CHAT_ROLE, DEFAULT_PROMPT_TEMPLATE } from 'src/template'
|
||||
|
||||
export enum ReferenceType {
|
||||
All,
|
||||
|
|
@ -16,22 +10,9 @@ export enum ReferenceType {
|
|||
}
|
||||
|
||||
export enum OutLocation {
|
||||
Cursor,
|
||||
// Title,
|
||||
// FrontMatter,
|
||||
ContentTop,
|
||||
}
|
||||
|
||||
// export enum OutLocation_link {
|
||||
// Cursor,
|
||||
// // ContentTop,
|
||||
// }
|
||||
|
||||
export enum OutType {
|
||||
FrontMatter,
|
||||
Title,
|
||||
Tag,
|
||||
Wikilink,
|
||||
Cursor,
|
||||
}
|
||||
|
||||
// for tag, keyword
|
||||
|
|
@ -42,60 +23,38 @@ export interface CommandOption {
|
|||
refType: ReferenceType;
|
||||
filterRegex: string; // for ReferenceType - Filter
|
||||
outLocation: OutLocation;
|
||||
// outLocation_link: OutLocation_link;
|
||||
outType: OutType;
|
||||
key: string; // for OutLocation - FrontMatter
|
||||
outPrefix: string;
|
||||
outSuffix: string;
|
||||
overwrite: boolean; // for OutLocation - FrontMatter
|
||||
|
||||
useCustomCommand: boolean;
|
||||
|
||||
chat_role: string;
|
||||
prmpt_template: string;
|
||||
model: string;
|
||||
max_tokens: number;
|
||||
max_suggestions: number;
|
||||
}
|
||||
|
||||
|
||||
export class AutoClassifierSettings {
|
||||
apiKey: string;
|
||||
apiKeyCreatedAt: Date | null;
|
||||
baseURL: string;
|
||||
classifierEngine: ClassifierEngine; // Added classifierEngine
|
||||
jinaApiKey: string; // Added jinaApiKey
|
||||
jinaBaseURL: string; // Added jinaBaseURL
|
||||
commandOption: CommandOption;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AutoClassifierSettings = {
|
||||
apiKey: '',
|
||||
apiKeyCreatedAt: null,
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
classifierEngine: ClassifierEngine.ChatGPT, // Default to ChatGPT
|
||||
jinaApiKey: 'jina_***', // Default Jina API Key
|
||||
jinaBaseURL: 'https://api.jina.ai/v1', // Default Jina Base URL
|
||||
commandOption: {
|
||||
useRef: true,
|
||||
refs: [],
|
||||
manualRefs: [],
|
||||
refType: ReferenceType.All,
|
||||
filterRegex: '',
|
||||
outLocation: OutLocation.Cursor,
|
||||
// outLocation_link: OutLocation_link.Cursor,
|
||||
outType: OutType.Tag,
|
||||
outPrefix: '',
|
||||
outSuffix: '',
|
||||
outLocation: OutLocation.FrontMatter,
|
||||
key: 'tags',
|
||||
overwrite: false,
|
||||
useCustomCommand: false,
|
||||
|
||||
chat_role: DEFAULT_CHAT_ROLE,
|
||||
prmpt_template: DEFAULT_PROMPT_TEMPLATE,
|
||||
model: "gpt-4.1-mini", // 기본값 및 추천 모델
|
||||
max_tokens: 150,
|
||||
max_suggestions: 3,
|
||||
prmpt_template: DEFAULT_PROMPT_TEMPLATE
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -123,380 +82,164 @@ export class AutoClassifierSettingTab extends PluginSettingTab {
|
|||
app.setting.openTabById("hotkeys");
|
||||
// @ts-ignore
|
||||
const tab = app.setting.activeTab;
|
||||
tab.setQuery(this.plugin.manifest.id);
|
||||
tab.searchInputEl.value = `${this.plugin.manifest.name}:`;
|
||||
tab.updateHotkeyVisibility();
|
||||
});
|
||||
});
|
||||
shortcutEl.descEl.createSpan({text: 'This plugin does not have default shortcuts to prevent shortcut conflicts.'});
|
||||
shortcutEl.descEl.createEl('br');
|
||||
shortcutEl.descEl.createSpan({text: 'Assign your own shortcuts to run commands for different input types.'});
|
||||
shortcutEl.descEl.innerHTML += `
|
||||
This plugin does not have default shortcuts to prevent shortcut conflicts. <br>
|
||||
Assign your own shortcuts to run commands for different input types.`
|
||||
|
||||
|
||||
// ------- [API Setting] -------
|
||||
// API Key input
|
||||
containerEl.createEl('h1', { text: 'API Setting' });
|
||||
const apiKeySetting = new Setting(containerEl)
|
||||
.setName('ChatGPT API Key')
|
||||
.setDesc('')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('API key')
|
||||
.setValue(this.plugin.settings.apiKey)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.apiKey = value;
|
||||
this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
// API Key Description & Message
|
||||
apiKeySetting.descEl.innerHTML += 'Enter your ChatGPT API key. If you don\'t have one yet, you can create it at <a href="https://platform.openai.com/account/api-keys">here</a>';
|
||||
const apiTestMessageEl = document.createElement('div');
|
||||
apiKeySetting.descEl.appendChild(apiTestMessageEl);
|
||||
|
||||
// Classifier Engine Dropdown
|
||||
new Setting(containerEl)
|
||||
.setName('Classifier Engine')
|
||||
.setDesc('Select the classification engine to use.')
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption(String(ClassifierEngine.ChatGPT), "OpenAI-compatible API")
|
||||
.addOption(String(ClassifierEngine.JinaAI), "Jina AI Classifier")
|
||||
.setValue(String(this.plugin.settings.classifierEngine))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.classifierEngine = parseInt(value) as ClassifierEngine;
|
||||
|
||||
// Auto-set appropriate default models
|
||||
if (this.plugin.settings.classifierEngine === ClassifierEngine.ChatGPT) {
|
||||
if (!commandOption.model || commandOption.model === 'jina-embeddings-v3') {
|
||||
commandOption.model = 'gpt-4.1-mini';
|
||||
}
|
||||
} else if (this.plugin.settings.classifierEngine === ClassifierEngine.JinaAI) {
|
||||
if (!commandOption.model || commandOption.model === 'gpt-4.1-mini' || commandOption.model === 'gpt-4o' || commandOption.model === 'gpt-4.1') {
|
||||
commandOption.model = 'jina-embeddings-v3';
|
||||
}
|
||||
}
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
this.display(); // Re-render settings to show/hide relevant fields
|
||||
});
|
||||
});
|
||||
|
||||
// Conditional API Settings
|
||||
if (this.plugin.settings.classifierEngine === ClassifierEngine.ChatGPT) {
|
||||
new Setting(containerEl)
|
||||
.setName('API Base URL')
|
||||
.setDesc('Base URL for OpenAI-compatible API calls')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('https://api.openai.com/v1')
|
||||
.setValue(this.plugin.settings.baseURL)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.baseURL = value;
|
||||
this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
const baseUrlSetting = containerEl.createDiv();
|
||||
baseUrlSetting.createEl('div', { text: 'Common configurations:' });
|
||||
baseUrlSetting.createEl('div', { text: '• OpenAI: https://api.openai.com/v1' });
|
||||
baseUrlSetting.createEl('div', { text: '• Local OpenAI-compatible API (Ollama, LocalAI):' });
|
||||
baseUrlSetting.createEl('div', { text: ' - Ollama: http://localhost:11434/v1' });
|
||||
baseUrlSetting.createEl('div', { text: ' - LocalAI: http://localhost:8080/v1' });
|
||||
baseUrlSetting.createEl('div', { text: ' ⚠️ Ollama/LocalAI are not fully compatible currently.' });
|
||||
baseUrlSetting.style.marginLeft = '20px';
|
||||
baseUrlSetting.style.fontSize = '0.9em';
|
||||
baseUrlSetting.style.color = 'var(--text-muted)';
|
||||
baseUrlSetting.style.marginBottom = '10px';
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Model')
|
||||
.setDesc("Model ID to use for classification. (Recommended: gpt-4.1-mini, gpt-4.1, gpt-4o)")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('gpt-4.1-mini')
|
||||
.setValue(commandOption.model)
|
||||
.onChange(async (value) => {
|
||||
commandOption.model = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
const modelSetting = containerEl.createDiv();
|
||||
modelSetting.createEl('div', { text: 'Recommended for OpenAI: gpt-4.1-mini, gpt-4.1, gpt-4o' });
|
||||
modelSetting.createEl('div', { text: '• OpenAI: gpt-4.1-mini, gpt-4.1, gpt-4o' });
|
||||
modelSetting.createEl('div', { text: '• Local OpenAI-compatible API (Ollama, LocalAI): llama3, mistral, phi3, qwen2 등' });
|
||||
modelSetting.style.marginLeft = '20px';
|
||||
modelSetting.style.fontSize = '0.9em';
|
||||
modelSetting.style.color = 'var(--text-muted)';
|
||||
modelSetting.style.marginBottom = '10px';
|
||||
|
||||
const apiKeySetting = new Setting(containerEl)
|
||||
.setName('API Key')
|
||||
.setDesc('')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('API key (leave empty for local AI)')
|
||||
.setValue(this.plugin.settings.apiKey)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.apiKey = value;
|
||||
this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
apiKeySetting.descEl.createSpan({ text: 'Enter your API key. Required for OpenAI and etc. Leave empty for local AI (Ollama, LocalAI). ' });
|
||||
apiKeySetting.descEl.createEl('a', { href: 'https://platform.openai.com/account/api-keys', text: 'Get OpenAI API key' })
|
||||
const apiTestMessageEl = document.createElement('div');
|
||||
apiKeySetting.descEl.appendChild(apiTestMessageEl);
|
||||
|
||||
if (this.plugin.settings.apiKey && this.plugin.settings.apiKeyCreatedAt) {
|
||||
apiTestMessageEl.setText(`This key was tested at ${this.plugin.settings.apiKeyCreatedAt.toString()}`);
|
||||
apiTestMessageEl.style.color = 'var(--success-color)';
|
||||
}
|
||||
|
||||
apiKeySetting.addButton((cb) => {
|
||||
cb.setButtonText('Test API call')
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
apiTestMessageEl.setText('Testing API call...');
|
||||
apiTestMessageEl.style.color = 'var(--text-normal)';
|
||||
try {
|
||||
await ChatGPT.callAPI('', 'test', this.plugin.settings.apiKey, commandOption.model, undefined, undefined, undefined, undefined, undefined, this.plugin.settings.baseURL);
|
||||
apiTestMessageEl.setText('Success! API working.');
|
||||
apiTestMessageEl.style.color = 'var(--success-color)';
|
||||
this.plugin.settings.apiKeyCreatedAt = new Date();
|
||||
} catch (error) {
|
||||
apiTestMessageEl.setText('Error: API is not working. Check console for details.');
|
||||
apiTestMessageEl.style.color = 'var(--warning-color)';
|
||||
this.plugin.settings.apiKeyCreatedAt = null;
|
||||
console.error("ChatGPT API Test Error:", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (this.plugin.settings.classifierEngine === ClassifierEngine.JinaAI) {
|
||||
new Setting(containerEl)
|
||||
.setName('Jina AI API Base URL')
|
||||
.setDesc('Optional: Set a different base URL for Jina AI API calls.')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('https://api.jina.ai/v1')
|
||||
.setValue(this.plugin.settings.jinaBaseURL)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.jinaBaseURL = value;
|
||||
this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
// Note: commandOption.model is currently shared. If Jina needs a separate model field,
|
||||
// it should be added to AutoClassifierSettings and handled here.
|
||||
// For now, we'll reuse commandOption.model but default it appropriately.
|
||||
new Setting(containerEl)
|
||||
.setName('Jina AI Model')
|
||||
.setDesc("ID of the Jina AI model to use. Default: jina-embeddings-v3 (supports 8192 tokens, 256 classes).")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('jina-embeddings-v3')
|
||||
.setValue(commandOption.model) // Reuse existing model field, ensure it's set to a Jina default if empty or on switch
|
||||
.onChange(async (value) => {
|
||||
commandOption.model = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
const jinaApiKeySetting = new Setting(containerEl)
|
||||
.setName('Jina AI API Key')
|
||||
.setDesc('')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('Jina API key')
|
||||
.setValue(this.plugin.settings.jinaApiKey)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.jinaApiKey = value;
|
||||
this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
jinaApiKeySetting.descEl.createSpan({ text: 'Enter your Jina AI API key. Get a free API key (10M tokens) at ' });
|
||||
jinaApiKeySetting.descEl.createEl('a', { href: 'https://jina.ai/', text: 'jina.ai' });
|
||||
jinaApiKeySetting.descEl.createSpan({ text: '. No account creation often required.' });
|
||||
const jinaApiTestMessageEl = document.createElement('div');
|
||||
jinaApiKeySetting.descEl.appendChild(jinaApiTestMessageEl);
|
||||
|
||||
// Jina API Key test button
|
||||
jinaApiKeySetting.addButton((cb) => {
|
||||
cb.setButtonText('Test Jina API')
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
jinaApiTestMessageEl.setText('Testing Jina API call...');
|
||||
jinaApiTestMessageEl.style.color = 'var(--text-normal)';
|
||||
try {
|
||||
// Use a simple test case
|
||||
const response = await JinaAI.callAPI(
|
||||
this.plugin.settings.jinaApiKey,
|
||||
this.plugin.settings.jinaBaseURL,
|
||||
commandOption.model || 'jina-embeddings-v3', // Fallback to default if model is not set
|
||||
['This is a test sentence for classification.'],
|
||||
['positive', 'negative', 'neutral']
|
||||
);
|
||||
|
||||
// Show token usage if available
|
||||
let tokenInfo = "";
|
||||
if (response.usage && response.usage.total_tokens) {
|
||||
tokenInfo = ` (${response.usage.total_tokens} tokens used)`;
|
||||
}
|
||||
|
||||
jinaApiTestMessageEl.setText(`Success! Jina AI API working${tokenInfo}.`);
|
||||
jinaApiTestMessageEl.style.color = 'var(--success-color)';
|
||||
} catch (error: any) {
|
||||
jinaApiTestMessageEl.setText(`Error: ${error.message}`);
|
||||
jinaApiTestMessageEl.style.color = 'var(--warning-color)';
|
||||
console.error("Jina AI API Test Error:", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
//API Key default message
|
||||
if (this.plugin.settings.apiKey && this.plugin.settings.apiKeyCreatedAt) {
|
||||
apiTestMessageEl.setText(`This key was tested at ${this.plugin.settings.apiKeyCreatedAt.toString()}`);
|
||||
apiTestMessageEl.style.color = 'var(--success-color)';
|
||||
}
|
||||
|
||||
// API Key test button
|
||||
apiKeySetting.addButton((cb) => {
|
||||
cb.setButtonText('Test API call')
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.apiKeyCreatedAt
|
||||
apiTestMessageEl.setText('Testing api call...');
|
||||
apiTestMessageEl.style.color = 'var(--text-normal)';
|
||||
try {
|
||||
await ChatGPT.callAPI('', 'test', this.plugin.settings.apiKey);
|
||||
apiTestMessageEl.setText('Success! API working.');
|
||||
apiTestMessageEl.style.color = 'var(--success-color)';
|
||||
this.plugin.settings.apiKeyCreatedAt = new Date();
|
||||
} catch (error) {
|
||||
apiTestMessageEl.setText('Error: API is not working.');
|
||||
apiTestMessageEl.style.color = 'var(--warning-color)';
|
||||
this.plugin.settings.apiKeyCreatedAt = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ------- [Tag Reference Setting] -------
|
||||
containerEl.createEl('h1', { text: 'Tag Reference Setting' });
|
||||
|
||||
// Toggle tag reference
|
||||
// Tag Reference Type Dropdown
|
||||
new Setting(containerEl)
|
||||
.setName('Use Reference')
|
||||
.setDesc('If not, it will recommend new tags')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(commandOption.useRef)
|
||||
.onChange(async (value) => {
|
||||
commandOption.useRef = value;
|
||||
await this.plugin.saveSettings();
|
||||
.setName('Reference type')
|
||||
.setDesc('Choose the type of reference tag')
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption(String(ReferenceType.All), "All tags")
|
||||
.addOption(String(ReferenceType.Filter), "Filtered tags",)
|
||||
.addOption(String(ReferenceType.Manual), "Manual tags")
|
||||
.setValue(String(commandOption.refType))
|
||||
.onChange(async (refTye) => {
|
||||
this.setRefType(parseInt(refTye));
|
||||
this.setRefs(parseInt(refTye));
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
if (commandOption.useRef) {
|
||||
// Tag Reference Type Dropdown
|
||||
new Setting(containerEl)
|
||||
.setName('Reference type')
|
||||
.setDesc('Choose the type of reference tag')
|
||||
.setClass('setting-item-child')
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption(String(ReferenceType.All), "All tags")
|
||||
.addOption(String(ReferenceType.Filter), "Filtered tags",)
|
||||
.addOption(String(ReferenceType.Manual), "Manual tags")
|
||||
.setValue(String(commandOption.refType))
|
||||
.onChange(async (refTye) => {
|
||||
this.setRefType(parseInt(refTye));
|
||||
this.setRefs(parseInt(refTye));
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
// All tags - default setting
|
||||
if (commandOption.refType == ReferenceType.All) {
|
||||
this.setRefs(ReferenceType.All);
|
||||
}
|
||||
// Filtered tags - Regex setting
|
||||
if (commandOption.refType == ReferenceType.Filter) {
|
||||
new Setting(containerEl)
|
||||
.setName('Filter regex')
|
||||
.setDesc('Specify a regular expression to filter tags')
|
||||
.setClass('setting-item-child')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('Regular expression')
|
||||
.setValue(commandOption.filterRegex)
|
||||
.onChange(async (value) => {
|
||||
this.setRefs(ReferenceType.Filter, value);
|
||||
})
|
||||
);
|
||||
}
|
||||
// Manual tags - manual input text area
|
||||
else if (commandOption.refType == ReferenceType.Manual) {
|
||||
new Setting(containerEl)
|
||||
.setName('Manual tags')
|
||||
.setDesc('Manually specify tags to reference.')
|
||||
.setClass('setting-item-child')
|
||||
.setClass('height10-text-area')
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setPlaceholder('Tags')
|
||||
.setValue(commandOption.manualRefs?.join('\n'))
|
||||
.onChange(async (value) => {
|
||||
this.setRefs(ReferenceType.Manual, value);
|
||||
})
|
||||
})
|
||||
.addExtraButton(cb => {
|
||||
cb
|
||||
.setIcon('reset')
|
||||
.setTooltip('Bring All Tags')
|
||||
.onClick(async () => {
|
||||
const allTags = await this.plugin.viewManager.getTags() ?? [];
|
||||
commandOption.manualRefs = allTags;
|
||||
this.setRefs(ReferenceType.Manual);
|
||||
this.display();
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// View Reference Tags button
|
||||
// All tags - default setting
|
||||
if (commandOption.refType == ReferenceType.All) {
|
||||
this.setRefs(ReferenceType.All);
|
||||
}
|
||||
// Filtered tags - Regex setting
|
||||
if (commandOption.refType == ReferenceType.Filter) {
|
||||
new Setting(containerEl)
|
||||
.setName('Filter regex')
|
||||
.setDesc('Specify a regular expression to filter tags')
|
||||
.setClass('setting-item-child')
|
||||
.addButton((cb) => {
|
||||
cb.setButtonText('View Reference Tags')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('Regular expression')
|
||||
.setValue(commandOption.filterRegex)
|
||||
.onChange(async (value) => {
|
||||
this.setRefs(ReferenceType.Filter, value);
|
||||
})
|
||||
);
|
||||
}
|
||||
// Manual tags - manual input text area
|
||||
else if (commandOption.refType == ReferenceType.Manual) {
|
||||
new Setting(containerEl)
|
||||
.setName('Manual tags')
|
||||
.setDesc('Manually specify tags to reference.')
|
||||
.setClass('setting-item-child')
|
||||
.setClass('height10-text-area')
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setPlaceholder('Tags')
|
||||
.setValue(commandOption.manualRefs?.join('\n'))
|
||||
.onChange(async (value) => {
|
||||
this.setRefs(ReferenceType.Manual, value);
|
||||
})
|
||||
})
|
||||
.addExtraButton(cb => {
|
||||
cb
|
||||
.setIcon('reset')
|
||||
.setTooltip('Bring All Tags')
|
||||
.onClick(async () => {
|
||||
const tags = commandOption.refs ?? [];
|
||||
let message = `${tags.join('\n')}`;
|
||||
if (this.plugin.settings.classifierEngine === ClassifierEngine.JinaAI && tags.length > 256) {
|
||||
message += `\n\n⚠️ Warning: Jina AI supports maximum 256 tags, but ${tags.length} were found. Please reduce the number of tags.`;
|
||||
}
|
||||
new Notice(message);
|
||||
});
|
||||
const allTags = await this.plugin.viewManager.getTags() ?? [];
|
||||
commandOption.manualRefs = allTags;
|
||||
this.setRefs(ReferenceType.Manual);
|
||||
this.display();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ------- [Output Setting] -------
|
||||
containerEl.createEl('h1', { text: 'Output Setting' });
|
||||
|
||||
// Output type dropdown
|
||||
// View Reference Tags button
|
||||
new Setting(containerEl)
|
||||
.setName('Output Type')
|
||||
.setDesc('Specify output type')
|
||||
.setClass('setting-item-child')
|
||||
.addButton((cb) => {
|
||||
cb.setButtonText('View Reference Tags')
|
||||
.onClick(async () => {
|
||||
const tags = commandOption.refs ?? [];
|
||||
new Notice(`${tags.join('\n')}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ------- [Output Tag Setting] -------
|
||||
// Tag Location dropdown
|
||||
containerEl.createEl('h1', { text: 'Output Tag Setting' });
|
||||
new Setting(containerEl)
|
||||
.setName('Output Tag Location')
|
||||
.setDesc('Specify where to put the output tag')
|
||||
.addDropdown((cb) => {
|
||||
cb.addOption(String(OutType.Tag), '#Tag')
|
||||
.addOption(String(OutType.Wikilink), '[[Wikilink]]')
|
||||
.addOption(String(OutType.FrontMatter), 'FrontMatter')
|
||||
.addOption(String(OutType.Title), 'Title alternative')
|
||||
.setValue(String(commandOption.outType))
|
||||
cb.addOption(String(OutLocation.FrontMatter), 'FrontMatter')
|
||||
.addOption(String(OutLocation.Title), 'Title alternative')
|
||||
.addOption(String(OutLocation.Cursor), 'Current cursor')
|
||||
.setValue(String(commandOption.outLocation))
|
||||
.onChange(async (value) => {
|
||||
commandOption.outType = parseInt(value);
|
||||
commandOption.outLocation = 0; // Initialize
|
||||
commandOption.outLocation = parseInt(value);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
// Output Type 1. [Tag Case]
|
||||
if (commandOption.outType == OutType.Tag) {
|
||||
// Tag - Location dropdown
|
||||
new Setting(containerEl)
|
||||
.setName('Output Location')
|
||||
.setClass('setting-item-child')
|
||||
.setDesc('Specify where to put the output tag')
|
||||
.addDropdown((cb) => {
|
||||
cb.addOption(String(OutLocation.Cursor), 'Current Cursor')
|
||||
.addOption(String(OutLocation.ContentTop), 'Top of Content')
|
||||
.setValue(String(commandOption.outLocation))
|
||||
.onChange(async (value) => {
|
||||
commandOption.outLocation = parseInt(value);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Output Type 2. [Wikilink Case]
|
||||
else if (commandOption.outType == OutType.Wikilink) {
|
||||
// Wikilink - Location dropdown
|
||||
new Setting(containerEl)
|
||||
.setName('Output Location')
|
||||
.setClass('setting-item-child')
|
||||
.setDesc('Specify where to put the output wikilink')
|
||||
.addDropdown((cb) => {
|
||||
cb.addOption(String(OutLocation.Cursor), 'Current Cursor')
|
||||
.addOption(String(OutLocation.ContentTop), 'Top of Content')
|
||||
.setValue(String(commandOption.outLocation))
|
||||
.onChange(async (value) => {
|
||||
commandOption.outLocation = parseInt(value);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Output Type 3. [Frontmatter Case]
|
||||
else if (commandOption.outType == OutType.FrontMatter) {
|
||||
// key text setting
|
||||
|
||||
// Frontmatter - key text setting
|
||||
let overwriteName = '';
|
||||
if (commandOption.outLocation == OutLocation.FrontMatter) {
|
||||
overwriteName = 'Overwrite value of the key';
|
||||
new Setting(containerEl)
|
||||
.setName('FrontMatter key')
|
||||
.setDesc('Specify FrontMatter key to put the output tag')
|
||||
|
|
@ -511,284 +254,109 @@ export class AutoClassifierSettingTab extends PluginSettingTab {
|
|||
})
|
||||
);
|
||||
}
|
||||
else if (commandOption.outLocation == OutLocation.Title) {
|
||||
overwriteName = 'Overwrite whole title';
|
||||
}
|
||||
else if (commandOption.outLocation == OutLocation.Cursor) {
|
||||
overwriteName = 'Overwrite selected area';
|
||||
}
|
||||
|
||||
// Overwrite setting
|
||||
if ((commandOption.outType == OutType.Tag && commandOption.outLocation == OutLocation.Cursor) ||
|
||||
(commandOption.outType == OutType.Wikilink && commandOption.outLocation == OutLocation.Cursor) ||
|
||||
commandOption.outType == OutType.Title ||
|
||||
commandOption.outType == OutType.FrontMatter) {
|
||||
// Overwrite toggle
|
||||
new Setting(containerEl)
|
||||
.setName(overwriteName)
|
||||
.setClass('setting-item-child')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(commandOption.overwrite)
|
||||
.onChange(async (value) => {
|
||||
commandOption.overwrite = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
let overwriteName = '';
|
||||
if (commandOption.outLocation == OutLocation.Cursor) overwriteName = 'Overwrite if selected.';
|
||||
if (commandOption.outType == OutType.Title) overwriteName = 'Overwrite whole title. If false, add to end of title.';
|
||||
if (commandOption.outType == OutType.FrontMatter) overwriteName = 'Overwrite value of the key.';
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(overwriteName)
|
||||
// ------- [Advanced Setting] -------
|
||||
// Toggle custom rule
|
||||
containerEl.createEl('h1', { text: 'Advanced Setting' });
|
||||
new Setting(containerEl)
|
||||
.setName('Use Custom Request Template')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(commandOption.useCustomCommand)
|
||||
.onChange(async (value) => {
|
||||
commandOption.useCustomCommand = value;
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
// Custom template textarea
|
||||
if (commandOption.useCustomCommand) {
|
||||
const customPromptTemplateEl = new Setting(containerEl)
|
||||
.setName('Custom Prompt Template')
|
||||
.setDesc('')
|
||||
.setClass('setting-item-child')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(commandOption.overwrite)
|
||||
.setClass('block-control-item')
|
||||
.setClass('height20-text-area')
|
||||
.addTextArea((text) =>
|
||||
text
|
||||
.setPlaceholder('Write custom prompt template.')
|
||||
.setValue(commandOption.prmpt_template)
|
||||
.onChange(async (value) => {
|
||||
commandOption.overwrite = value;
|
||||
commandOption.prmpt_template = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addExtraButton(cb => {
|
||||
cb
|
||||
.setIcon('reset')
|
||||
.setTooltip('Restore to default')
|
||||
.onClick(async () => {
|
||||
commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
}
|
||||
customPromptTemplateEl.descEl.innerHTML += `
|
||||
This plugin is based on the ChatGPT answer.<br>
|
||||
You can use your own template when making a request to ChatGPT.<br><br>
|
||||
Variables:<br>
|
||||
- {{input}}: The text to classify will be inserted here.<br>
|
||||
- {{reference}}: The reference tags will be inserted here.<br>`;
|
||||
|
||||
// Output Prefix & Suffix
|
||||
new Setting(containerEl)
|
||||
.setName('Add Prefix & Suffix')
|
||||
.setDesc(`Output: {prefix} + {output} + {suffix}`);
|
||||
new Setting(containerEl)
|
||||
.setName('Prefix')
|
||||
.setClass('setting-item-child')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('prefix')
|
||||
.setValue(commandOption.outPrefix)
|
||||
.onChange(async (value) => {
|
||||
commandOption.outPrefix = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName('Suffix')
|
||||
.setClass('setting-item-child')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('suffix')
|
||||
.setValue(commandOption.outSuffix)
|
||||
.onChange(async (value) => {
|
||||
commandOption.outSuffix = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
// ------- [Advanced Setting] -------
|
||||
containerEl.createEl('h1', { text: 'Advanced Setting' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Maximum Tag Suggestions')
|
||||
.setDesc("Maximum number of tags to suggest (1-10)")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('3')
|
||||
.setValue(String(commandOption.max_suggestions))
|
||||
.onChange(async (value) => {
|
||||
const num = parseInt(value);
|
||||
if (num >= 1 && num <= 10) {
|
||||
commandOption.max_suggestions = num;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Conditional Advanced Settings for OpenAI-compatible API
|
||||
if (this.plugin.settings.classifierEngine === ClassifierEngine.ChatGPT) {
|
||||
// Toggle custom rule
|
||||
new Setting(containerEl)
|
||||
.setName('Use Custom Request Template')
|
||||
.setDesc('Enable advanced prompt customization for better results')
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(commandOption.useCustomCommand)
|
||||
const customChatRoleEl = new Setting(containerEl)
|
||||
.setName('Custom Chat Role')
|
||||
.setDesc('')
|
||||
.setClass('setting-item-child')
|
||||
.setClass('block-control-item')
|
||||
.setClass('height10-text-area')
|
||||
.addTextArea((text) =>
|
||||
text
|
||||
.setPlaceholder('Write custom chat role for gpt system.')
|
||||
.setValue(commandOption.chat_role)
|
||||
.onChange(async (value) => {
|
||||
commandOption.useCustomCommand = value;
|
||||
commandOption.chat_role = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addExtraButton(cb => {
|
||||
cb
|
||||
.setIcon('reset')
|
||||
.setTooltip('Restore to default')
|
||||
.onClick(async () => {
|
||||
commandOption.chat_role = DEFAULT_CHAT_ROLE;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
// Custom template textarea
|
||||
if (commandOption.useCustomCommand) {
|
||||
|
||||
// Different default template depanding on useRef
|
||||
if (commandOption.useRef) {
|
||||
if(commandOption.prmpt_template == DEFAULT_PROMPT_TEMPLATE_WO_REF) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE;
|
||||
} else {
|
||||
if(commandOption.prmpt_template == DEFAULT_PROMPT_TEMPLATE) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE_WO_REF;
|
||||
}
|
||||
|
||||
const customPromptTemplateEl = new Setting(containerEl)
|
||||
.setName('Custom Prompt Template (ChatGPT)')
|
||||
.setDesc('')
|
||||
.setClass('setting-item-child')
|
||||
.setClass('block-control-item')
|
||||
.setClass('height20-text-area')
|
||||
.addTextArea((text) =>
|
||||
text
|
||||
.setPlaceholder('Write custom prompt template.')
|
||||
.setValue(commandOption.prmpt_template)
|
||||
.onChange(async (value) => {
|
||||
commandOption.prmpt_template = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addExtraButton(cb => {
|
||||
cb
|
||||
.setIcon('reset')
|
||||
.setTooltip('Restore to default')
|
||||
.onClick(async () => {
|
||||
// Different default template depanding on useRef
|
||||
if (commandOption.useRef) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE;
|
||||
else commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE_WO_REF;
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
});
|
||||
customPromptTemplateEl.descEl.createSpan({text: 'This plugin is based on the ChatGPT answer.'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createSpan({text: 'You can use your own template when making a request to ChatGPT.'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createSpan({text: 'Variables:'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createSpan({text: '- {{input}}: The text to classify will be inserted here.'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createSpan({text: '- {{reference}}: The reference tags will be inserted here.'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
|
||||
const customChatRoleEl = new Setting(containerEl)
|
||||
.setName('Custom Chat Role (ChatGPT)')
|
||||
.setDesc('')
|
||||
.setClass('setting-item-child')
|
||||
.setClass('block-control-item')
|
||||
.setClass('height10-text-area')
|
||||
.addTextArea((text) =>
|
||||
text
|
||||
.setPlaceholder('Write custom chat role for gpt system.')
|
||||
.setValue(commandOption.chat_role)
|
||||
.onChange(async (value) => {
|
||||
commandOption.chat_role = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addExtraButton(cb => {
|
||||
cb
|
||||
.setIcon('reset')
|
||||
.setTooltip('Restore to default')
|
||||
.onClick(async () => {
|
||||
commandOption.chat_role = DEFAULT_CHAT_ROLE;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
});
|
||||
customChatRoleEl.descEl.createSpan({text: 'Define custom role to ChatGPT system.'});
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Custom Max Tokens (ChatGPT)')
|
||||
.setDesc("The maximum number of tokens that can be generated in the completion.")
|
||||
.setClass('setting-item-child')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('150')
|
||||
.setValue(String(commandOption.max_tokens))
|
||||
.onChange(async (value) => {
|
||||
commandOption.max_tokens = parseInt(value);
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
// Custom template textarea
|
||||
if (commandOption.useCustomCommand) {
|
||||
|
||||
// Different default template depending on useRef
|
||||
if (commandOption.useRef) {
|
||||
if(commandOption.prmpt_template == DEFAULT_PROMPT_TEMPLATE_WO_REF) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE;
|
||||
} else {
|
||||
if(commandOption.prmpt_template == DEFAULT_PROMPT_TEMPLATE) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE_WO_REF;
|
||||
}
|
||||
|
||||
const customPromptTemplateEl = new Setting(containerEl)
|
||||
.setName('Custom Prompt Template')
|
||||
.setDesc('')
|
||||
.setClass('setting-item-child')
|
||||
.setClass('block-control-item')
|
||||
.setClass('height20-text-area')
|
||||
.addTextArea((text) =>
|
||||
text
|
||||
.setPlaceholder('Write custom prompt template.')
|
||||
.setValue(commandOption.prmpt_template)
|
||||
.onChange(async (value) => {
|
||||
commandOption.prmpt_template = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addExtraButton(cb => {
|
||||
cb
|
||||
.setIcon('reset')
|
||||
.setTooltip('Restore to default')
|
||||
.onClick(async () => {
|
||||
// Different default template depending on useRef
|
||||
if (commandOption.useRef) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE;
|
||||
else commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE_WO_REF;
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
});
|
||||
customPromptTemplateEl.descEl.createSpan({text: 'This plugin is based on the LLM response.'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createSpan({text: 'You can use your own template when making a request to the API.'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createSpan({text: 'Variables:'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createSpan({text: '- {{input}}: The text to classify will be inserted here.'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
customPromptTemplateEl.descEl.createSpan({text: '- {{reference}}: The reference tags will be inserted here.'});
|
||||
customPromptTemplateEl.descEl.createEl('br');
|
||||
|
||||
const customChatRoleEl = new Setting(containerEl)
|
||||
.setName('Custom Chat Role')
|
||||
.setDesc('')
|
||||
.setClass('setting-item-child')
|
||||
.setClass('block-control-item')
|
||||
.setClass('height10-text-area')
|
||||
.addTextArea((text) =>
|
||||
text
|
||||
.setPlaceholder('Write custom chat role for system.')
|
||||
.setValue(commandOption.chat_role)
|
||||
.onChange(async (value) => {
|
||||
commandOption.chat_role = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addExtraButton(cb => {
|
||||
cb
|
||||
.setIcon('reset')
|
||||
.setTooltip('Restore to default')
|
||||
.onClick(async () => {
|
||||
commandOption.chat_role = DEFAULT_CHAT_ROLE;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
});
|
||||
customChatRoleEl.descEl.createSpan({text: 'Define custom role for the AI system.'});
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Custom Max Tokens')
|
||||
.setDesc("The maximum number of tokens that can be generated in the completion.")
|
||||
.setClass('setting-item-child')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('150')
|
||||
.setValue(String(commandOption.max_tokens))
|
||||
.onChange(async (value) => {
|
||||
commandOption.max_tokens = parseInt(value);
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
customChatRoleEl.descEl.innerHTML += `
|
||||
Define custom role to ChatGPT system.`
|
||||
}
|
||||
// For JinaAI, these custom prompt/role/token settings are hidden as they are not applicable.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -818,4 +386,4 @@ export class AutoClassifierSettingTab extends PluginSettingTab {
|
|||
}
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,19 +3,9 @@ export const DEFAULT_PROMPT_TEMPLATE = `Classify this content:
|
|||
"""
|
||||
{{input}}
|
||||
"""
|
||||
Answer format is JSON {reliability:0~1, outputs:[tag1,tag2,...]}.
|
||||
Even if you are unsure, qualify the reliability and select the best matches.
|
||||
Respond only with valid JSON. Do not write an introduction or summary.
|
||||
Output tags must be from these options:
|
||||
Answer format is JSON {reliability:0~1, output:selected_category}.
|
||||
Even if you are not sure, qualify the reliability and select one.
|
||||
Output must be one of these:
|
||||
|
||||
{{reference}}
|
||||
`;
|
||||
|
||||
export const DEFAULT_PROMPT_TEMPLATE_WO_REF = `Classify this content:
|
||||
"""
|
||||
{{input}}
|
||||
"""
|
||||
Answer format is JSON {reliability:0~1, output:selected_category}.
|
||||
Even if you are not sure, qualify the reliability and recommend a proper category.
|
||||
Respond only with valid JSON. Do not write an introduction or summary.
|
||||
`;
|
||||
`;
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { App, MarkdownView, Editor, FrontMatterCache } from "obsidian";
|
||||
import { OutType } from "src/settings";
|
||||
|
||||
export class ViewManager {
|
||||
app: App;
|
||||
|
|
@ -69,10 +68,9 @@ export class ViewManager {
|
|||
return tags;
|
||||
}
|
||||
|
||||
async insertAtFrontMatter(key: string, value: string, overwrite = false, prefix = '', suffix = ''): Promise<void> {
|
||||
value = `${prefix}${value}${suffix}`;
|
||||
async insertAtFrontMatter(key: string, value: string, overwrite = false): Promise<void> {
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
|
||||
|
||||
if (activeView) {
|
||||
const file = activeView.file;
|
||||
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
|
|
@ -93,8 +91,7 @@ export class ViewManager {
|
|||
}
|
||||
}
|
||||
|
||||
async insertAtTitle(value: string, overwrite = false, prefix = '', suffix = ''): Promise<void> {
|
||||
value = `${prefix}${value}${suffix}`;
|
||||
async insertAtTitle(value: string, overwrite = false): Promise<void> {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return;
|
||||
let newName = file.basename;
|
||||
|
|
@ -109,10 +106,9 @@ export class ViewManager {
|
|||
await this.app.fileManager.renameFile(file, newPath);
|
||||
}
|
||||
|
||||
async insertAtCursor(value: string, overwrite = false, outType: OutType, prefix = '', suffix = ''): Promise<void> {
|
||||
async insertAtCursor(value: string, overwrite = false): Promise<void> {
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const output = this.preprocessOutput(value, outType, prefix, suffix);
|
||||
|
||||
let tag = ` #${value} `;
|
||||
if (activeView) {
|
||||
const editor = activeView.editor;
|
||||
const selection = editor.getSelection();
|
||||
|
|
@ -121,39 +117,8 @@ export class ViewManager {
|
|||
editor.setSelection(editor.getCursor('to'));
|
||||
}
|
||||
// overwrite
|
||||
editor.replaceSelection(output);
|
||||
editor.replaceSelection(tag);
|
||||
}
|
||||
}
|
||||
|
||||
async insertAtContentTop(value: string, outType: OutType, prefix = '', suffix = ''): Promise<void> {
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const output = this.preprocessOutput(value, outType, prefix, suffix);
|
||||
|
||||
if (activeView) {
|
||||
const editor = activeView.editor;
|
||||
const file = activeView.file;
|
||||
const sections = this.app.metadataCache.getFileCache(file)?.sections;
|
||||
|
||||
// get the line after frontmatter
|
||||
let topLine = 0;
|
||||
if (sections && sections[0].type == "yaml") {
|
||||
topLine = sections[0].position.end.line + 1;
|
||||
}
|
||||
|
||||
// replace top of the content
|
||||
editor.setCursor({line: topLine, ch: 0});
|
||||
editor.replaceSelection(`${output}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
preprocessOutput(value: string, outType: OutType, prefix = '', suffix = ''): string {
|
||||
let output = '';
|
||||
if (outType == OutType.Tag) {
|
||||
output = `${prefix}${value}${suffix}`;
|
||||
output = output.replace(/ /g, "_");
|
||||
output = ` #${output} `;
|
||||
}
|
||||
else if (outType == OutType.Wikilink) output = `[[${prefix}${value}${suffix}]]`;
|
||||
return output
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue