Compare commits

..

No commits in common. "main" and "1.0.0" have entirely different histories.
main ... 1.0.0

11 changed files with 413 additions and 1605 deletions

5
.gitignore vendored
View file

@ -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
View file

@ -1,158 +1,63 @@
# Auto Classifier
![GitHub release (latest by date)](https://img.shields.io/github/v/release/HyeonseoNam/auto-classifier?style=for-the-badge) ![GitHub all releases](https://img.shields.io/github/downloads/HyeonseoNam/auto-classifier/total?style=for-the-badge)
`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**
![](img/selected_to_cursor.gif)
### Use Case #2: **Content** → **FrontMatter**
![](img/content_to_frontmatter.gif)
### Use Case #3: **FrontMatter** → **Title**
![](img/frontmatter_to_totle.gif)
### Use Case #4: **Title** → **FrontMatter**
![](img/title_to_frontmatter.gif)
### 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

View file

@ -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
View file

@ -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",

View file

@ -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"
}
}

View file

@ -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;
}
}
}

View file

@ -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.');
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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.
`;
`;

View file

@ -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
}
}