feat: Integrate Jina AI classification API (#40)

* feat: Add configurable multiple tag suggestions with max limit setting

* refactor: Move createLoadingNotice method before its usage in runClassifyTag

* fix: Add missing catch block and error handling in classifyTag method

* fix: Add nested try-catch for JSON parsing and improve error handling

style: Format code with consistent indentation and line breaks

* refactor: Remove max_suggestions from template and apply globally in classification logic

* refactor: Move max suggestions setting to main Advanced Settings section

* improve prompt adherence for JSON (#33)

* fix: imporove default prompt template to ensure valid JSON output.

* bump version to 1.2.1

* Fix JSON parse error when other GPT model used (#38)

* Integrate Jina AI for text classification - rebased

- Update package-lock.json
- Add JinaAI API client in src/jina-api.ts.
- Update settings (src/settings.ts) to include Jina AI engine selection, API key, base URL, and model.
- Modify main.ts to conditionally call Jina AI or ChatGPT based on selected engine.
- Adjust response parsing for Jina AI output.
- Add Jina API test button in settings.
- Hide ChatGPT-specific advanced settings when Jina AI is selected and
  vice-versa.
- Update manifest.json.
- Update .gitignore.

---------

Co-authored-by: Robin Tuszik <mail@robin.gg>
Co-authored-by: Hyeonseo Nam <auspice7g@gmail.com>
Co-authored-by: SophomoreSo <57844175+SophomoreSo@users.noreply.github.com>
This commit is contained in:
Thiago Moraes 2025-07-09 11:33:37 -03:00 committed by GitHub
parent 9415fa0f91
commit 1f89e4e38b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 990 additions and 272 deletions

5
.gitignore vendored
View file

@ -15,8 +15,11 @@ main.js
# Exclude sourcemaps
*.map
# obsidian
# obsidian data
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
# Exclude test
*.txt

View file

@ -1,10 +1,10 @@
{
"id": "auto-classifier",
"name": "Auto Classifier",
"version": "1.2.0",
"version": "1.3.0",
"minAppVersion": "1.1.1",
"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",
"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.",
"author": "Hyeonseo Nam, Thiago Moraes",
"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.0.0",
"version": "1.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "auto-classifier",
"version": "1.0.0",
"version": "1.1.2",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
@ -15,6 +15,7 @@
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"openai": "4.76.1",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
@ -212,6 +213,16 @@
"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",
@ -405,6 +416,18 @@
"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",
@ -428,6 +451,18 @@
"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",
@ -487,6 +522,12 @@
"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",
@ -529,6 +570,19 @@
"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",
@ -576,6 +630,18 @@
"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",
@ -622,6 +688,15 @@
"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",
@ -647,6 +722,65 @@
"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",
@ -909,6 +1043,15 @@
"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",
@ -1030,6 +1173,41 @@
"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",
@ -1037,12 +1215,58 @@
"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",
@ -1113,6 +1337,18 @@
"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",
@ -1130,6 +1366,54 @@
"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",
@ -1318,6 +1602,15 @@
"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",
@ -1340,6 +1633,27 @@
"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",
@ -1375,6 +1689,46 @@
"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",
@ -1399,6 +1753,41 @@
"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",
@ -1736,6 +2125,12 @@
"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",
@ -1802,6 +2197,12 @@
"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",
@ -1819,6 +2220,31 @@
"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

@ -20,6 +20,6 @@
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4",
"openai": "4.76.0"
"openai": "4.76.1"
}
}

86
src/jina-api.ts Normal file
View file

@ -0,0 +1,86 @@
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
}
const errorMessage = `Jina AI API Error: ${response.status} - ${errorDetails}`;
console.error(errorMessage);
throw new Error(errorMessage);
}
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,13 +1,21 @@
import { Plugin, Notice } from "obsidian";
import { AutoClassifierSettingTab, AutoClassifierSettings, DEFAULT_SETTINGS, OutLocation, OutType} from "src/settings";
import {
AutoClassifierSettingTab,
AutoClassifierSettings,
DEFAULT_SETTINGS,
OutLocation,
OutType,
ClassifierEngine, // Added ClassifierEngine
} from "src/settings";
import { ViewManager } from "src/view-manager";
import { ChatGPT } from 'src/api';
import { ChatGPT } from "src/api";
import { JinaAI, JinaAPIResponse } from "src/jina-api"; // Added JinaAI and JinaAPIResponse
enum InputType {
SelectedArea,
Title,
FrontMatter,
Content
Content,
}
export default class AutoClassifierPlugin extends Plugin {
@ -19,32 +27,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));
@ -57,7 +65,26 @@ export default class AutoClassifierPlugin extends Plugin {
await this.saveData(this.settings);
}
async onunload() {
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 runClassifyTag(inputType: InputType) {
@ -73,31 +100,35 @@ 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] -------
if (!this.settings.apiKey) {
new Notice(`${this.manifest.name}: You shuld input your API Key`);
return null
if (currentEngine === ClassifierEngine.ChatGPT && !this.settings.apiKey) {
new Notice(`${this.manifest.name}: ChatGPT API Key is missing.`);
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)) {
new Notice(`${this.manifest.name}: no reference tags`);
return null
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();
}
@ -107,86 +138,135 @@ export default class AutoClassifierPlugin extends Plugin {
return null;
}
// 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(','));
// Replace {{input}}, {{reference}} for ChatGPT
let user_prompt = "";
let system_role = "";
const system_role = this.settings.commandOption.prmpt_template;
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
}
// ------- [API Processing] -------
// Call API
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,
);
const jsonRegex = /reliability[\s\S]*?:\s*([\d.]+)[\s\S]*?output[\s\S]*?:\s*"([^"^}]+)/;
const match = responseRaw.match(jsonRegex);
let resOutput;
let resReliabity;
if (match && match.length > 1) {
resOutput = match[2];
resReliabity = parseFloat(match[1]);
} else {
new Notice(`${this.manifest.name}: output format error (output: ${responseRaw})`);
return null;
}
try {
let outputs: string[] = [];
// Avoid row reliability
if (resReliabity <= 0.2) {
new Notice(`${this.manifest.name}: response has row reliability (${resReliabity})`);
return null;
}
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;
// ------- [Add Tag] -------
// 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);
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) {
const jinaResponse: JinaAPIResponse = 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;
}
}
}
// 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);
}
new Notice(`${this.manifest.name}: classified to ${resOutput}`);
}
// 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');
// Limit number of suggestions
const limitedOutputs = outputs.slice(0, this.settings.commandOption.max_suggestions);
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);
if (limitedOutputs.length === 0) {
new Notice(`${this.manifest.name}: No tags were classified.`);
return null;
}
return notice;
// ------- [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,
);
}
}
new Notice(`${this.manifest.name}: classified with ${limitedOutputs.length} tags using ${ClassifierEngine[currentEngine]}.`);
} catch (error: any) {
new Notice(`${this.manifest.name} API Error: ${error.message || error}`);
console.error(`${ClassifierEngine[currentEngine]} API Error:`, error);
return null;
}
}
}

View file

@ -1,8 +1,14 @@
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,
}
export enum ReferenceType {
All,
Filter,
@ -49,6 +55,7 @@ export interface CommandOption {
prmpt_template: string;
model: string;
max_tokens: number;
max_suggestions: number;
}
@ -56,6 +63,9 @@ export class AutoClassifierSettings {
apiKey: string;
apiKeyCreatedAt: Date | null;
baseURL: string;
classifierEngine: ClassifierEngine; // Added classifierEngine
jinaApiKey: string; // Added jinaApiKey
jinaBaseURL: string; // Added jinaBaseURL
commandOption: CommandOption;
}
@ -63,6 +73,9 @@ export const DEFAULT_SETTINGS: AutoClassifierSettings = {
apiKey: '',
apiKeyCreatedAt: null,
baseURL: 'https://api.openai.com/v1',
classifierEngine: ClassifierEngine.ChatGPT, // Default to ChatGPT
jinaApiKey: 'jina_e24cf807496a48d9be7ea660bd652a37x2ZJYPjVDlgMU69KQvKSepRDbTgM', // Default Jina API Key
jinaBaseURL: 'https://api.jina.ai/v1', // Default Jina Base URL
commandOption: {
useRef: true,
refs: [],
@ -82,6 +95,7 @@ export const DEFAULT_SETTINGS: AutoClassifierSettings = {
prmpt_template: DEFAULT_PROMPT_TEMPLATE,
model: "gpt-3.5-turbo",
max_tokens: 150,
max_suggestions: 3,
},
};
@ -119,79 +133,166 @@ export class AutoClassifierSettingTab extends PluginSettingTab {
// ------- [API Setting] -------
// API Key input
containerEl.createEl('h1', { text: 'API Setting' });
new Setting(containerEl)
.setName('API Base URL')
.setDesc('Optional: Set a different base URL for API calls (e.g. for proxies)')
.addText((text) =>
text
.setPlaceholder('https://api.openai.com/v1')
.setValue(this.plugin.settings.baseURL)
.onChange((value) => {
this.plugin.settings.baseURL = value;
this.plugin.saveSettings();
})
);
// Classifier Engine Dropdown
new Setting(containerEl)
.setName('Custom Model')
.setDesc("ID of the model to use. See https://platform.openai.com/docs/models")
.addText((text) =>
text
.setPlaceholder('gpt-3.5-turbo')
.setValue(commandOption.model)
.setName('Classifier Engine')
.setDesc('Select the classification engine to use.')
.addDropdown((dropdown) => {
dropdown
.addOption(String(ClassifierEngine.ChatGPT), "ChatGPT")
.addOption(String(ClassifierEngine.JinaAI), "JinaAI")
.setValue(String(this.plugin.settings.classifierEngine))
.onChange(async (value) => {
commandOption.model = value;
this.plugin.settings.classifierEngine = parseInt(value) as ClassifierEngine;
await this.plugin.saveSettings();
})
);
this.display(); // Re-render settings to show/hide relevant fields
});
});
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.createSpan({text: 'Enter your ChatGPT API key. If you don\'t have one yet, you can create it at '});
apiKeySetting.descEl.createEl('a', {href: 'https://platform.openai.com/account/api-keys', text: 'here'})
const apiTestMessageEl = document.createElement('div');
apiKeySetting.descEl.appendChild(apiTestMessageEl);
// Conditional API Settings
if (this.plugin.settings.classifierEngine === ClassifierEngine.ChatGPT) {
new Setting(containerEl)
.setName('ChatGPT API Base URL')
.setDesc('Optional: Set a different base URL for ChatGPT API calls (e.g. for proxies)')
.addText((text) =>
text
.setPlaceholder('https://api.openai.com/v1')
.setValue(this.plugin.settings.baseURL)
.onChange((value) => {
this.plugin.settings.baseURL = value;
this.plugin.saveSettings();
})
);
//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)';
new Setting(containerEl)
.setName('ChatGPT Model')
.setDesc("ID of the ChatGPT model to use. See https://platform.openai.com/docs/models")
.addText((text) =>
text
.setPlaceholder('gpt-3.5-turbo')
.setValue(commandOption.model) // Assuming commandOption.model is for ChatGPT
.onChange(async (value) => {
commandOption.model = value;
await this.plugin.saveSettings();
})
);
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();
})
)
apiKeySetting.descEl.createSpan({ text: 'Enter your ChatGPT API key. If you don\'t have one yet, you can create it at ' });
apiKeySetting.descEl.createEl('a', { href: 'https://platform.openai.com/account/api-keys', text: 'here' })
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 (e.g., jina-embeddings-v3).")
.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.' });
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
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.'],
['positive', 'negative', 'neutral']
);
jinaApiTestMessageEl.setText('Success! Jina AI API working.');
jinaApiTestMessageEl.style.color = 'var(--success-color)';
} catch (error: any) {
jinaApiTestMessageEl.setText(`Error: Jina AI API is not working. ${error.message}`);
jinaApiTestMessageEl.style.color = 'var(--warning-color)';
console.error("Jina AI API Test Error:", error);
}
});
});
}
// 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, this.plugin.settings.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.');
apiTestMessageEl.style.color = 'var(--warning-color)';
this.plugin.settings.apiKeyCreatedAt = null;
}
});
});
// ------- [Tag Reference Setting] -------
containerEl.createEl('h1', { text: 'Tag Reference Setting' });
@ -422,112 +523,133 @@ export class AutoClassifierSettingTab extends PluginSettingTab {
// ------- [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)
.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) => {
commandOption.useCustomCommand = value;
await this.plugin.saveSettings();
this.display();
}),
const num = parseInt(value);
if (num >= 1 && num <= 10) {
commandOption.max_suggestions = num;
await this.plugin.saveSettings();
}
})
);
// 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')
.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')
.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.'});
// Conditional Advanced Settings for ChatGPT
if (this.plugin.settings.classifierEngine === ClassifierEngine.ChatGPT) {
// Toggle custom rule
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))
.setName('Use Custom Request Template (ChatGPT)')
.addToggle((toggle) =>
toggle
.setValue(commandOption.useCustomCommand)
.onChange(async (value) => {
commandOption.max_tokens = parseInt(value);
commandOption.useCustomCommand = value;
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();
})
);
}
}
// For JinaAI, these custom prompt/role/token settings are hidden as they are not applicable.
}

View file

@ -3,9 +3,10 @@ export const DEFAULT_PROMPT_TEMPLATE = `Classify this content:
"""
{{input}}
"""
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:
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:
{{reference}}
`;
@ -16,5 +17,5 @@ export const DEFAULT_PROMPT_TEMPLATE_WO_REF = `Classify this content:
"""
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.
`;