├── .npmrc ├── .eslintignore ├── versions.json ├── img ├── selected_to_cursor.gif ├── content_to_frontmatter.gif ├── frontmatter_to_totle.gif └── title_to_frontmatter.gif ├── .editorconfig ├── .gitignore ├── manifest.json ├── tsconfig.json ├── version-bump.mjs ├── .eslintrc ├── src ├── template.ts ├── api.ts ├── jina-api.ts ├── view-manager.ts ├── main.ts └── settings.ts ├── package.json ├── LICENSE ├── esbuild.config.mjs ├── styles.css └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "1.1.1" 3 | } 4 | -------------------------------------------------------------------------------- /img/selected_to_cursor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyeonseoNam/auto-classifier/HEAD/img/selected_to_cursor.gif -------------------------------------------------------------------------------- /img/content_to_frontmatter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyeonseoNam/auto-classifier/HEAD/img/content_to_frontmatter.gif -------------------------------------------------------------------------------- /img/frontmatter_to_totle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyeonseoNam/auto-classifier/HEAD/img/frontmatter_to_totle.gif -------------------------------------------------------------------------------- /img/title_to_frontmatter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyeonseoNam/auto-classifier/HEAD/img/title_to_frontmatter.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian data 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | 24 | # Exclude test 25 | *.txt -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "auto-classifier", 3 | "name": "Auto Classifier", 4 | "version": "1.3.0", 5 | "minAppVersion": "1.1.1", 6 | "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.", 7 | "author": "Hyeonseo Nam", 8 | "authorUrl": "https://github.com/HyeonseoNam/auto-classifier", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /src/template.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_CHAT_ROLE = `You are a JSON answer bot. Don't answer other words.`; 2 | export const DEFAULT_PROMPT_TEMPLATE = `Classify this content: 3 | """ 4 | {{input}} 5 | """ 6 | Answer format is JSON {reliability:0~1, outputs:[tag1,tag2,...]}. 7 | Even if you are unsure, qualify the reliability and select the best matches. 8 | Respond only with valid JSON. Do not write an introduction or summary. 9 | Output tags must be from these options: 10 | 11 | {{reference}} 12 | `; 13 | 14 | export const DEFAULT_PROMPT_TEMPLATE_WO_REF = `Classify this content: 15 | """ 16 | {{input}} 17 | """ 18 | Answer format is JSON {reliability:0~1, output:selected_category}. 19 | Even if you are not sure, qualify the reliability and recommend a proper category. 20 | Respond only with valid JSON. Do not write an introduction or summary. 21 | `; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-classifier", 3 | "version": "1.3.0", 4 | "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.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": ["obsidian", "plugin", "classification", "tags", "ai", "chatgpt", "jina", "automation"], 12 | "author": "Hyeonseo Nam", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "5.29.0", 17 | "@typescript-eslint/parser": "5.29.0", 18 | "builtin-modules": "3.3.0", 19 | "esbuild": "0.17.3", 20 | "obsidian": "latest", 21 | "tslib": "2.4.0", 22 | "typescript": "4.7.4", 23 | "openai": "4.76.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hyeonseo Nam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | 3 | export class ChatGPT { 4 | static async callAPI( 5 | system_role: string, 6 | user_prompt: string, 7 | apiKey: string, 8 | model: string = 'gpt-3.5-turbo', 9 | max_tokens: number = 150, 10 | temperature: number = 0, 11 | top_p: number = 0.95, 12 | frequency_penalty: number = 0, 13 | presence_penalty: number = 0.5, 14 | baseURL?: string, 15 | ): Promise { 16 | const client = new OpenAI({ 17 | apiKey: apiKey, 18 | dangerouslyAllowBrowser: true, // Required for client-side use 19 | baseURL: baseURL || 'https://api.openai.com/v1' 20 | }); 21 | 22 | try { 23 | const completion = await client.chat.completions.create({ 24 | model: model, 25 | messages: [ 26 | { role: "system", content: system_role }, 27 | { role: "user", content: user_prompt } 28 | ], 29 | max_tokens: max_tokens, 30 | temperature: temperature, 31 | top_p: top_p, 32 | frequency_penalty: frequency_penalty, 33 | presence_penalty: presence_penalty 34 | }); 35 | 36 | return completion.choices[0].message.content || ''; 37 | } catch (error) { 38 | if (error instanceof OpenAI.APIError) { 39 | throw new Error(`OpenAI API Error: ${error.status} - ${error.message}`); 40 | } 41 | throw error; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin-top: 1.2em; 3 | } 4 | 5 | .setting-item-child { 6 | background-color: var(--background-secondary); 7 | padding-left: 1em; 8 | padding-right: 1em; 9 | border-top: 0.5px solid var(--background-primary); 10 | /* animation: child-enter 0.3s ease; */ 11 | } 12 | .setting-item-child textarea, .setting-item-child input { 13 | background-color: var(--background-primary); 14 | } 15 | 16 | @keyframes child-enter { 17 | 0% { 18 | opacity: 0; 19 | transform: translateY(-10px); 20 | } 21 | 100% { 22 | opacity: 1; 23 | transform: translateY(0); 24 | } 25 | } 26 | 27 | .wide-text-area .setting-item-info, .wide-text-area .setting-item-description { 28 | flex: 0.3 1 auto; 29 | } 30 | .block-control-item { 31 | flex-direction: column; 32 | } 33 | 34 | .block-control-item .setting-item-control, .block-control-item .setting-item-info { 35 | margin-top: 0.5em; 36 | width: 100%; 37 | margin-right: auto; 38 | } 39 | .height10-text-area textarea { 40 | width: 100%; 41 | height: 10em; 42 | } 43 | .height20-text-area textarea { 44 | width: 100%; 45 | height: 20em; 46 | } 47 | 48 | 49 | .loading-container { 50 | display: flex; 51 | align-items: center; 52 | } 53 | 54 | .loading-icon { 55 | display: inline-block; 56 | border: 2px solid rgba(0, 0, 0, 0.1); 57 | border-left-color: var(--text-muted); 58 | border-top-color: var(--text-accent); 59 | border-radius: 50%; 60 | width: 24px; 61 | height: 24px; 62 | animation: spin 1s linear infinite; 63 | margin-right: 8px; 64 | } 65 | 66 | @keyframes spin { 67 | 0% { 68 | transform: rotate(0deg); 69 | } 70 | 100% { 71 | transform: rotate(360deg); 72 | } 73 | } 74 | 75 | 76 | 77 | .checkmark-container { 78 | display: flex; 79 | align-items: center; 80 | } 81 | 82 | .checkmark { 83 | width: 24px; 84 | height: 24px; 85 | position: relative; 86 | margin-right: 8px; 87 | } 88 | 89 | .checkmark:after { 90 | content: ''; 91 | position: absolute; 92 | width: 10px; 93 | height: 3px; 94 | background-color: var(--text-accent); 95 | transform-origin: left top; 96 | transform: rotate(45deg); 97 | top: 8px; 98 | left: 6px; 99 | } 100 | 101 | .checkmark:before { 102 | content: ''; 103 | position: absolute; 104 | width: 5px; 105 | height: 3px; 106 | background-color: var(--text-accent); 107 | transform-origin: left bottom; 108 | transform: rotate(-45deg); 109 | bottom: 8px; 110 | left: 11px; 111 | } -------------------------------------------------------------------------------- /src/jina-api.ts: -------------------------------------------------------------------------------- 1 | import { requestUrl } from 'obsidian'; 2 | 3 | export interface JinaAPIRequest { 4 | model: string; 5 | input: Array<{ text: string }>; 6 | labels: string[]; 7 | } 8 | 9 | export interface JinaAPIResponsePrediction { 10 | label: string; 11 | score: number; 12 | } 13 | 14 | export interface JinaAPIResponseData { 15 | object: string; 16 | index: number; 17 | prediction: string; 18 | score: number; 19 | predictions: JinaAPIResponsePrediction[]; 20 | } 21 | 22 | export interface JinaAPIResponse { 23 | usage: { 24 | total_tokens: number; 25 | }; 26 | data: JinaAPIResponseData[]; 27 | } 28 | 29 | export class JinaAI { 30 | static async callAPI( 31 | apiKey: string, 32 | baseURL: string, 33 | model: string, 34 | inputText: string[], // Changed from string to string[] to allow multiple inputs if needed by the plugin logic later 35 | labels: string[] 36 | ): Promise { 37 | const apiUrl = `${baseURL}/classify`; // Assuming baseURL does not contain /classify 38 | const requestBody: JinaAPIRequest = { 39 | model: model, 40 | input: inputText.map(text => ({ text })), 41 | labels: labels, 42 | }; 43 | 44 | console.log('Jina AI Request:', { url: apiUrl, body: requestBody }); 45 | 46 | try { 47 | const response = await requestUrl({ 48 | url: apiUrl, 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | 'Authorization': `Bearer ${apiKey}`, 53 | }, 54 | body: JSON.stringify(requestBody), 55 | throw: false, // To handle errors manually 56 | }); 57 | 58 | console.log('Jina AI Response Status:', response.status); 59 | console.log('Jina AI Response Body:', response.text); // Log raw text for debugging 60 | 61 | if (response.status !== 200) { 62 | let errorDetails = response.text; 63 | try { 64 | const jsonError = JSON.parse(response.text); 65 | errorDetails = jsonError.detail || JSON.stringify(jsonError); 66 | } catch (e) { 67 | // Keep errorDetails as text if not JSON 68 | } 69 | 70 | // More specific error messages 71 | let userFriendlyMessage = `Jina AI API Error (${response.status})`; 72 | if (response.status === 401) { 73 | userFriendlyMessage = "Invalid Jina AI API key. Please check your API key in settings."; 74 | } else if (response.status === 403) { 75 | userFriendlyMessage = "Jina AI API access denied. Please check your API key permissions."; 76 | } else if (response.status === 429) { 77 | userFriendlyMessage = "Jina AI API rate limit exceeded. Please try again later."; 78 | } else if (response.status === 500) { 79 | userFriendlyMessage = "Jina AI server error. Please try again later."; 80 | } else { 81 | userFriendlyMessage = `Jina AI API Error: ${errorDetails}`; 82 | } 83 | 84 | console.error(`Jina AI API Error: ${response.status} - ${errorDetails}`); 85 | throw new Error(userFriendlyMessage); 86 | } 87 | 88 | const responseData: JinaAPIResponse = response.json; 89 | console.log('Jina AI Parsed Response Data:', responseData); 90 | return responseData; 91 | 92 | } catch (error) { 93 | console.error('Error calling Jina AI API:', error); 94 | if (error instanceof Error) { 95 | throw error; 96 | } 97 | throw new Error('An unknown error occurred while calling Jina AI API.'); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/view-manager.ts: -------------------------------------------------------------------------------- 1 | import { App, MarkdownView, Editor, FrontMatterCache } from "obsidian"; 2 | import { OutType } from "src/settings"; 3 | 4 | export class ViewManager { 5 | app: App; 6 | 7 | constructor(app: App) { 8 | this.app = app; 9 | } 10 | 11 | async getSelection(editor?: Editor): Promise { 12 | if (editor) { 13 | return editor.getSelection(); 14 | } 15 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 16 | if (activeView) { 17 | return activeView.editor.getSelection(); 18 | } 19 | return null; 20 | } 21 | 22 | async getTitle(): Promise { 23 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 24 | if (activeView) { 25 | return activeView.file.basename; 26 | } 27 | return null; 28 | } 29 | 30 | async getFrontMatter(): Promise { 31 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 32 | if (activeView) { 33 | const file = activeView.file; 34 | const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter as Partial; 35 | if (frontmatter?.position) { 36 | delete frontmatter.position; 37 | } 38 | return JSON.stringify(frontmatter); 39 | } 40 | return null; 41 | } 42 | 43 | async getContent(): Promise { 44 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 45 | if (activeView) { 46 | // delete frontmatter 47 | let content = activeView.getViewData(); 48 | const file = activeView.file; 49 | const frontmatter: FrontMatterCache | undefined = this.app.metadataCache.getFileCache(file)?.frontmatter; 50 | if (frontmatter) { 51 | content = content.split('---').slice(2).join('---'); 52 | } 53 | return content; 54 | } 55 | return null; 56 | } 57 | 58 | async getTags(filterRegex?: string): Promise { 59 | //@ts-ignore 60 | const tagsDict = this.app.metadataCache.getTags(); 61 | let tags = Object.keys(tagsDict); 62 | if (!tags || tags.length == 0) return null; 63 | // remove # 64 | tags = tags.map((tag) => tag.replace(/^#/, '')); 65 | // filter 66 | if (filterRegex) { 67 | return tags.filter((tag) => RegExp(filterRegex).test(tag)); 68 | } 69 | return tags; 70 | } 71 | 72 | async insertAtFrontMatter(key: string, value: string, overwrite = false, prefix = '', suffix = ''): Promise { 73 | value = `${prefix}${value}${suffix}`; 74 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 75 | 76 | if (activeView) { 77 | const file = activeView.file; 78 | await this.app.fileManager.processFrontMatter(file, (frontmatter) => { 79 | frontmatter = frontmatter || {}; 80 | 81 | if (frontmatter[key] && !overwrite) { 82 | // add value as list element if exist 83 | if (Array.isArray(frontmatter[key])) { 84 | frontmatter[key].push(value); 85 | } else { 86 | frontmatter[key] = [frontmatter[key], value]; 87 | } 88 | } else { 89 | // overwrite 90 | frontmatter[key] = value; 91 | } 92 | }); 93 | } 94 | } 95 | 96 | async insertAtTitle(value: string, overwrite = false, prefix = '', suffix = ''): Promise { 97 | value = `${prefix}${value}${suffix}`; 98 | const file = this.app.workspace.getActiveFile(); 99 | if (!file) return; 100 | let newName = file.basename; 101 | if (overwrite) { 102 | newName = `${value}`; 103 | } else { 104 | newName = `${newName} ${value}`; 105 | } 106 | newName = newName.replace(/[\"\/<>:\|?\"]/g, ''); // for window file name 107 | // @ts-ignore 108 | const newPath = file.getNewPathAfterRename(newName) 109 | await this.app.fileManager.renameFile(file, newPath); 110 | } 111 | 112 | async insertAtCursor(value: string, overwrite = false, outType: OutType, prefix = '', suffix = ''): Promise { 113 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 114 | const output = this.preprocessOutput(value, outType, prefix, suffix); 115 | 116 | if (activeView) { 117 | const editor = activeView.editor; 118 | const selection = editor.getSelection(); 119 | if (selection && !overwrite) { 120 | // replace selection 121 | editor.setSelection(editor.getCursor('to')); 122 | } 123 | // overwrite 124 | editor.replaceSelection(output); 125 | } 126 | } 127 | 128 | async insertAtContentTop(value: string, outType: OutType, prefix = '', suffix = ''): Promise { 129 | const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); 130 | const output = this.preprocessOutput(value, outType, prefix, suffix); 131 | 132 | if (activeView) { 133 | const editor = activeView.editor; 134 | const file = activeView.file; 135 | const sections = this.app.metadataCache.getFileCache(file)?.sections; 136 | 137 | // get the line after frontmatter 138 | let topLine = 0; 139 | if (sections && sections[0].type == "yaml") { 140 | topLine = sections[0].position.end.line + 1; 141 | } 142 | 143 | // replace top of the content 144 | editor.setCursor({line: topLine, ch: 0}); 145 | editor.replaceSelection(`${output}\n`); 146 | } 147 | } 148 | 149 | preprocessOutput(value: string, outType: OutType, prefix = '', suffix = ''): string { 150 | let output = ''; 151 | if (outType == OutType.Tag) { 152 | output = `${prefix}${value}${suffix}`; 153 | output = output.replace(/ /g, "_"); 154 | output = ` #${output} `; 155 | } 156 | else if (outType == OutType.Wikilink) output = `[[${prefix}${value}${suffix}]]`; 157 | return output 158 | } 159 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auto Classifier 2 | 3 | ![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) 4 | 5 | `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. 6 | 7 | ## How to use 8 | 9 | - Configure your classification engine and API settings in the settings tab: 10 | 11 | - **Choose your Classification Engine:** 12 | - `OpenAI-compatible API`: Uses OpenAI API, Local AI (Ollama, LocalAI), or other compatible APIs 13 | - `Jina AI`: Uses Jina AI Classifier (cost-effective, multilingual) 14 | 15 | - **For OpenAI-compatible API:** 16 | - Enter your API key (leave empty for local AI) 17 | - Set the base URL: 18 | - OpenAI: `https://api.openai.com/v1` (default) 19 | - Local OpenAI-compatible API (Ollama, LocalAI): 20 | - Ollama: `http://localhost:11434/v1` 21 | - LocalAI: `http://localhost:8080/v1` 22 | - Choose your model: 23 | - OpenAI: `gpt-4.1-mini` (recommended), `gpt-4.1`, `gpt-4o`, ... 24 | - Local OpenAI-compatible API: `llama3`, `mistral`, `phi3`, `qwen2`, ... (Ollama/LocalAI) 25 | - Test your configuration using the Test API call button 26 | 27 | - **For Jina AI:** 28 | - Enter your Jina AI API key (free tier available with 10M tokens) 29 | - Choose your preferred model (default: jina-embeddings-v3) 30 | - Test your configuration using the Test Jina API button 31 | - **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). 32 | 33 | - This plugin consists of **4 Input Commands** that you can run. By simply running these commands, it will automatically classify your note: 34 | 35 | - `Classify tag from Note title` 36 | - `Classify tag from Note FrontMatter` 37 | - `Classify tag from Note Content` 38 | - `Classify tag from Selected Area` 39 | 40 | - Toggle and choose from different **Tag Reference** types. The LLM will select the appropriate tag from these references: 41 | 42 | - `All tags` (default) 43 | - `Filtered Tags` with regular expression 44 | - `Manual Tags` that are defined manually 45 | 46 | - Specify the **Output Type** from the response of the LLM: 47 | 48 | - `#Tag`: at your `Current Cursor` or `Top of Content` 49 | - `[[WikiLink]]`: at your `Current Cursor` or `Top of Content` 50 | - `FrontMatter`: with `key` 51 | - `Title Alternative`: at the end of note's title 52 | 53 | - (Optional) Add a `Prefix` or `Suffix` for the output format. 54 | 55 | - (Optional) You can use your custom request for your selected API: 56 | - `Custom Prompt Template` 57 | - 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}}`. 58 | - `Custom Chat Role` 59 | - You can guide the AI's behavior by setting this system role 60 | 61 | ## Classification Engines 62 | 63 | ### OpenAI-compatible API 64 | - **Flexibility**: Supports custom prompts and chat roles 65 | - **Multiple Providers**: Works with OpenAI, Ollama, LocalAI, and other compatible APIs 66 | - **Local AI Support**: Run models locally without internet connection 67 | - **Recommended OpenAI models**: `gpt-4.1-mini`, `gpt-4.1`, `gpt-4o` 68 | - **Advanced Features**: Custom prompt templates, system roles, and token control 69 | 70 | ### Jina AI Classifier 71 | - **Cost-Effective**: Generous free tier with 10 million tokens 72 | - **Multilingual**: Supports multiple languages including non-English content 73 | - **High Performance**: Zero-shot classification with up to 8192 tokens input and 256 distinct classes 74 | - **Optimized**: Works best with semantic labels (e.g., "happy", "sad", "angry") 75 | - **Easy Setup**: Often no account creation required, can generate fresh API keys as needed 76 | 77 | ## Local AI Setup (Ollama, LocalAI) 78 | 79 | **Experimental Support:** 80 | ⚠️ 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. 81 | If you encounter issues or want to help improve compatibility, **contributions and pull requests are very welcome!** 82 | 83 | **Example setup:** 84 | 1. Install [Ollama](https://ollama.ai/) or [LocalAI](https://localai.io/) 85 | 2. Prepare your model: For Ollama, run `ollama pull llama3`. For LocalAI, configure your models as needed. 86 | 3. Set Base URL: Ollama - `http://localhost:11434/v1`, LocalAI - `http://localhost:8080/v1` 87 | 4. Set Model: e.g., `llama3`, `mistral`, etc. 88 | 5. API Key: Leave empty 89 | 90 | 91 | 92 | 93 | ## Example 94 | 95 | ### Use Case #1: **Selected area** → **Current cursor** 96 | 97 | ![](img/selected_to_cursor.gif) 98 | 99 | ### Use Case #2: **Content** → **FrontMatter** 100 | 101 | ![](img/content_to_frontmatter.gif) 102 | 103 | ### Use Case #3: **FrontMatter** → **Title** 104 | 105 | ![](img/frontmatter_to_totle.gif) 106 | 107 | ### Use Case #4: **Title** → **FrontMatter** 108 | 109 | ![](img/title_to_frontmatter.gif) 110 | 111 | ### DDC number classification 112 | 113 | If you want to use this plugin for DDC number classification, edit the `Custom Prompt Template` like this: 114 | 115 | ``` 116 | Please use Dewey Decimal Classification (DDC) to classify this content: 117 | """ 118 | {{input}} 119 | """ 120 | Answer format is JSON {reliability:0~1, output:"[ddc_number]:category"}. 121 | Even if you are not sure, qualify the reliability and select one. 122 | Convert the blank spaces to "_" in the output. 123 | ``` 124 | 125 | ### LCSH classification 126 | 127 | LCSH classification can be similar: 128 | 129 | ``` 130 | Please use Library of Congress Subject Headings (LCSH) to classify this content: 131 | """ 132 | {{input}} 133 | """ 134 | Answer format is JSON {reliability:0~1, output:"[First LCSH term]--[Second LCSH term]--[Third LCSH term]"}. 135 | Even if you are not sure, qualify the reliability and select one. 136 | Convert the blank spaces to "_" in the output. 137 | ``` 138 | 139 | ## Installation 140 | 141 | - Search for `Auto Classifier` in the Community plugin tab of the Obsidian settings. 142 | - 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. 143 | 144 | ## Support 145 | 146 | 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. 147 | 148 | ## 🙌 Contributors 149 | 150 | - @gravityfargo 151 | - @rtuszik 152 | - @tk42 153 | - @SophomoreSo 154 | - @ThiagoVsky 155 | 156 | ## License 157 | 158 | MIT License 159 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, Notice } from "obsidian"; 2 | import { 3 | AutoClassifierSettingTab, 4 | AutoClassifierSettings, 5 | DEFAULT_SETTINGS, 6 | OutLocation, 7 | OutType, 8 | ClassifierEngine, // Added ClassifierEngine 9 | } from "src/settings"; 10 | import { ViewManager } from "src/view-manager"; 11 | import { ChatGPT } from "src/api"; 12 | import { JinaAI, JinaAPIResponse } from "src/jina-api"; // Added JinaAI and JinaAPIResponse 13 | 14 | enum InputType { 15 | SelectedArea, 16 | Title, 17 | FrontMatter, 18 | Content, 19 | } 20 | 21 | export default class AutoClassifierPlugin extends Plugin { 22 | settings: AutoClassifierSettings; 23 | viewManager = new ViewManager(this.app); 24 | 25 | async onload() { 26 | await this.loadSettings(); 27 | 28 | // Commands 29 | this.addCommand({ 30 | id: "classify-tag-selected", 31 | name: "Classify tag from Selected Area", 32 | callback: async () => { 33 | await this.runClassifyTag(InputType.SelectedArea); 34 | }, 35 | }); 36 | this.addCommand({ 37 | id: "classify-tag-title", 38 | name: "Classify tag from Note Title", 39 | callback: async () => { 40 | await this.runClassifyTag(InputType.Title); 41 | }, 42 | }); 43 | this.addCommand({ 44 | id: "classify-tag-frontmatter", 45 | name: "Classify tag from FrontMatter", 46 | callback: async () => { 47 | await this.runClassifyTag(InputType.FrontMatter); 48 | }, 49 | }); 50 | this.addCommand({ 51 | id: "classify-tag-content", 52 | name: "Classify tag from Note Content", 53 | callback: async () => { 54 | await this.runClassifyTag(InputType.Content); 55 | }, 56 | }); 57 | 58 | this.addSettingTab(new AutoClassifierSettingTab(this.app, this)); 59 | } 60 | 61 | async loadSettings() { 62 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 63 | } 64 | async saveSettings() { 65 | await this.saveData(this.settings); 66 | } 67 | 68 | async onunload() {} 69 | 70 | // create loading spin in the Notice message 71 | createLoadingNotice(text: string, number = 10000): Notice { 72 | const notice = new Notice("", number); 73 | const loadingContainer = document.createElement("div"); 74 | loadingContainer.addClass("loading-container"); 75 | 76 | const loadingIcon = document.createElement("div"); 77 | loadingIcon.addClass("loading-icon"); 78 | const loadingText = document.createElement("span"); 79 | loadingText.textContent = text; 80 | //@ts-ignore 81 | notice.noticeEl.empty(); 82 | loadingContainer.appendChild(loadingIcon); 83 | loadingContainer.appendChild(loadingText); 84 | //@ts-ignore 85 | notice.noticeEl.appendChild(loadingContainer); 86 | 87 | return notice; 88 | } 89 | 90 | async runClassifyTag(inputType: InputType) { 91 | const loadingNotice = this.createLoadingNotice(`${this.manifest.name}: Processing..`); 92 | try { 93 | await this.classifyTag(inputType); 94 | loadingNotice.hide(); 95 | } catch (err) { 96 | loadingNotice.hide(); 97 | } 98 | } 99 | 100 | // Main Classification 101 | async classifyTag(inputType: InputType) { 102 | const commandOption = this.settings.commandOption; 103 | const currentEngine = this.settings.classifierEngine; 104 | 105 | // ------- [API Key check] ------- 106 | // Note: Local AI (Ollama, LocalAI) usually doesn't require API keys 107 | if (currentEngine === ClassifierEngine.ChatGPT && !this.settings.apiKey) { 108 | // Check if it's likely a local AI setup (localhost or local IP) 109 | const baseUrl = this.settings.baseURL.toLowerCase(); 110 | if (!baseUrl.includes('localhost') && !baseUrl.includes('127.0.0.1') && !baseUrl.includes('192.168.')) { 111 | new Notice(`⛔ ${this.manifest.name}: API Key is missing. Required for most cloud APIs.`); 112 | return null; 113 | } 114 | } 115 | if (currentEngine === ClassifierEngine.JinaAI && !this.settings.jinaApiKey) { 116 | new Notice(`⛔ ${this.manifest.name}: Jina AI API Key is missing.`); 117 | return null; 118 | } 119 | 120 | // ------- [Input] ------- 121 | const refs = this.settings.commandOption.refs; 122 | // reference check 123 | if (this.settings.commandOption.useRef && (!refs || refs.length == 0)) { 124 | new Notice(`⛔ ${this.manifest.name}: no reference tags`); 125 | return null; 126 | } 127 | 128 | // Jina AI has a limit of 256 classes for zero-shot classification 129 | if (currentEngine === ClassifierEngine.JinaAI && refs.length > 256) { 130 | new Notice(`⛔ ${this.manifest.name}: Jina AI supports maximum 256 reference tags, but ${refs.length} were provided. Please reduce the number of tags.`); 131 | return null; 132 | } 133 | 134 | // Set Input 135 | let input: string | null = ""; 136 | if (inputType == InputType.SelectedArea) { 137 | input = await this.viewManager.getSelection(); 138 | } else if (inputType == InputType.Title) { 139 | input = await this.viewManager.getTitle(); 140 | } else if (inputType == InputType.FrontMatter) { 141 | input = await this.viewManager.getFrontMatter(); 142 | } else if (inputType == InputType.Content) { 143 | input = await this.viewManager.getContent(); 144 | } 145 | 146 | // input error 147 | if (!input) { 148 | new Notice(`⛔ ${this.manifest.name}: no input data`); 149 | return null; 150 | } 151 | 152 | // Replace {{input}}, {{reference}} for ChatGPT 153 | let user_prompt = ""; 154 | let system_role = ""; 155 | 156 | if (currentEngine === ClassifierEngine.ChatGPT) { 157 | user_prompt = this.settings.commandOption.prmpt_template; 158 | user_prompt = user_prompt.replace("{{input}}", input); 159 | user_prompt = user_prompt.replace("{{reference}}", refs.join(",")); 160 | system_role = this.settings.commandOption.chat_role; // Corrected from prmpt_template 161 | } 162 | 163 | // ------- [API Processing] ------- 164 | try { 165 | let outputs: string[] = []; 166 | let jinaResponse: JinaAPIResponse | null = null; 167 | 168 | if (currentEngine === ClassifierEngine.ChatGPT) { 169 | const responseRaw = await ChatGPT.callAPI( 170 | system_role, 171 | user_prompt, 172 | this.settings.apiKey, 173 | this.settings.commandOption.model, 174 | this.settings.commandOption.max_tokens, 175 | undefined, 176 | undefined, 177 | undefined, 178 | undefined, 179 | this.settings.baseURL, 180 | ); 181 | // Parse ChatGPT response 182 | try { 183 | const response = JSON.parse(responseRaw.replace(/^```json\n/, "").replace(/\n```$/, "")); 184 | const resReliability = response.reliability; 185 | const resOutputs = response.outputs; 186 | 187 | if (!Array.isArray(resOutputs)) { 188 | new Notice(`⛔ ${this.manifest.name}: ChatGPT output format error (expected array)`); 189 | return null; 190 | } 191 | if (resReliability <= 0.2 && commandOption.useRef) { // Reliability check only if using references 192 | new Notice( 193 | `⛔ ${this.manifest.name}: ChatGPT response has low reliability (${resReliability})`, 194 | ); 195 | return null; 196 | } 197 | outputs = resOutputs; 198 | } catch (error) { 199 | new Notice(`⛔ ${this.manifest.name}: ChatGPT JSON parsing error - ${error}`); 200 | console.error("ChatGPT JSON parsing error:", error, "Raw response:", responseRaw); 201 | return null; 202 | } 203 | } else if (currentEngine === ClassifierEngine.JinaAI) { 204 | jinaResponse = await JinaAI.callAPI( 205 | this.settings.jinaApiKey, 206 | this.settings.jinaBaseURL, 207 | this.settings.commandOption.model || 'jina-embeddings-v3', // Ensure model is passed 208 | [input], // Jina expects an array of texts 209 | refs 210 | ); 211 | // Extract labels from Jina AI response 212 | // Assuming we take the top prediction for the first input text. 213 | // If multiple inputs were sent, this logic would need to iterate jinaResponse.data 214 | if (jinaResponse.data && jinaResponse.data.length > 0) { 215 | // Sort predictions by score and take the top ones 216 | const sortedPredictions = jinaResponse.data[0].predictions.sort((a, b) => b.score - a.score); 217 | outputs = sortedPredictions.map(p => p.label); 218 | } else { 219 | new Notice(`⛔ ${this.manifest.name}: Jina AI returned no data.`); 220 | return null; 221 | } 222 | } 223 | 224 | // Limit number of suggestions 225 | const limitedOutputs = outputs.slice(0, this.settings.commandOption.max_suggestions); 226 | 227 | if (limitedOutputs.length === 0) { 228 | new Notice(`⛔ ${this.manifest.name}: No tags were classified.`); 229 | return null; 230 | } 231 | 232 | // ------- [Add Tags] ------- 233 | for (const resOutput of limitedOutputs) { 234 | // Output Type 1. [Tag Case] + Output Type 2. [Wikilink Case] 235 | if ( 236 | commandOption.outType == OutType.Tag || 237 | commandOption.outType == OutType.Wikilink 238 | ) { 239 | if (commandOption.outLocation == OutLocation.Cursor) { 240 | this.viewManager.insertAtCursor( 241 | resOutput, 242 | commandOption.overwrite, 243 | commandOption.outType, 244 | commandOption.outPrefix, 245 | commandOption.outSuffix, 246 | ); 247 | } else if (commandOption.outLocation == OutLocation.ContentTop) { 248 | this.viewManager.insertAtContentTop( 249 | resOutput, 250 | commandOption.outType, 251 | commandOption.outPrefix, 252 | commandOption.outSuffix, 253 | ); 254 | } 255 | } 256 | // Output Type 3. [Frontmatter Case] 257 | else if (commandOption.outType == OutType.FrontMatter) { 258 | this.viewManager.insertAtFrontMatter( 259 | commandOption.key, 260 | resOutput, 261 | commandOption.overwrite, 262 | commandOption.outPrefix, 263 | commandOption.outSuffix, 264 | ); 265 | } 266 | // Output Type 4. [Title] 267 | else if (commandOption.outType == OutType.Title) { 268 | this.viewManager.insertAtTitle( 269 | resOutput, 270 | commandOption.overwrite, 271 | commandOption.outPrefix, 272 | commandOption.outSuffix, 273 | ); 274 | } 275 | } 276 | // Show token usage if available 277 | let tokenInfo = ""; 278 | if (currentEngine === ClassifierEngine.JinaAI && jinaResponse && jinaResponse.usage) { 279 | tokenInfo = ` (${jinaResponse.usage.total_tokens} tokens used)`; 280 | } 281 | const engineName = currentEngine === ClassifierEngine.ChatGPT ? "OpenAI-compatible API" : "Jina AI"; 282 | new Notice(`✅ ${this.manifest.name}: classified with ${limitedOutputs.length} tags using ${engineName}${tokenInfo}.`); 283 | 284 | } catch (error: any) { 285 | const engineName = currentEngine === ClassifierEngine.ChatGPT ? "OpenAI-compatible API" : "Jina AI"; 286 | new Notice(`⛔ ${this.manifest.name} API Error: ${error.message || error}`); 287 | console.error(`${engineName} API Error:`, error); 288 | return null; 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice, PluginSettingTab, Setting } from "obsidian"; 2 | import { ChatGPT } from 'src/api'; 3 | import { JinaAI } from 'src/jina-api'; // Added JinaAI import 4 | import type AutoClassifierPlugin from "src/main"; 5 | import { DEFAULT_CHAT_ROLE, DEFAULT_PROMPT_TEMPLATE, DEFAULT_PROMPT_TEMPLATE_WO_REF } from 'src/template' 6 | 7 | export enum ClassifierEngine { // Added ClassifierEngine enum 8 | ChatGPT, 9 | JinaAI, 10 | } 11 | 12 | export enum ReferenceType { 13 | All, 14 | Filter, 15 | Manual, 16 | } 17 | 18 | export enum OutLocation { 19 | Cursor, 20 | // Title, 21 | // FrontMatter, 22 | ContentTop, 23 | } 24 | 25 | // export enum OutLocation_link { 26 | // Cursor, 27 | // // ContentTop, 28 | // } 29 | 30 | export enum OutType { 31 | FrontMatter, 32 | Title, 33 | Tag, 34 | Wikilink, 35 | } 36 | 37 | // for tag, keyword 38 | export interface CommandOption { 39 | useRef: boolean; 40 | refs: string[]; 41 | manualRefs: string[]; 42 | refType: ReferenceType; 43 | filterRegex: string; // for ReferenceType - Filter 44 | outLocation: OutLocation; 45 | // outLocation_link: OutLocation_link; 46 | outType: OutType; 47 | key: string; // for OutLocation - FrontMatter 48 | outPrefix: string; 49 | outSuffix: string; 50 | overwrite: boolean; // for OutLocation - FrontMatter 51 | 52 | useCustomCommand: boolean; 53 | 54 | chat_role: string; 55 | prmpt_template: string; 56 | model: string; 57 | max_tokens: number; 58 | max_suggestions: number; 59 | } 60 | 61 | 62 | export class AutoClassifierSettings { 63 | apiKey: string; 64 | apiKeyCreatedAt: Date | null; 65 | baseURL: string; 66 | classifierEngine: ClassifierEngine; // Added classifierEngine 67 | jinaApiKey: string; // Added jinaApiKey 68 | jinaBaseURL: string; // Added jinaBaseURL 69 | commandOption: CommandOption; 70 | } 71 | 72 | export const DEFAULT_SETTINGS: AutoClassifierSettings = { 73 | apiKey: '', 74 | apiKeyCreatedAt: null, 75 | baseURL: 'https://api.openai.com/v1', 76 | classifierEngine: ClassifierEngine.ChatGPT, // Default to ChatGPT 77 | jinaApiKey: 'jina_***', // Default Jina API Key 78 | jinaBaseURL: 'https://api.jina.ai/v1', // Default Jina Base URL 79 | commandOption: { 80 | useRef: true, 81 | refs: [], 82 | manualRefs: [], 83 | refType: ReferenceType.All, 84 | filterRegex: '', 85 | outLocation: OutLocation.Cursor, 86 | // outLocation_link: OutLocation_link.Cursor, 87 | outType: OutType.Tag, 88 | outPrefix: '', 89 | outSuffix: '', 90 | key: 'tags', 91 | overwrite: false, 92 | useCustomCommand: false, 93 | 94 | chat_role: DEFAULT_CHAT_ROLE, 95 | prmpt_template: DEFAULT_PROMPT_TEMPLATE, 96 | model: "gpt-4.1-mini", // 기본값 및 추천 모델 97 | max_tokens: 150, 98 | max_suggestions: 3, 99 | }, 100 | }; 101 | 102 | export class AutoClassifierSettingTab extends PluginSettingTab { 103 | plugin: AutoClassifierPlugin; 104 | constructor(app: App, plugin: AutoClassifierPlugin) { 105 | super(app, plugin); 106 | this.plugin = plugin; 107 | } 108 | 109 | async display(): Promise { 110 | 111 | const { containerEl } = this; 112 | const commandOption = this.plugin.settings.commandOption; 113 | 114 | containerEl.empty(); 115 | // shortcut button 116 | const shortcutEl = new Setting(this.containerEl) 117 | .setDesc('') 118 | .addButton((cb) => { 119 | cb.setButtonText("Specify shortcuts") 120 | .setCta() 121 | .onClick(() => { 122 | // @ts-ignore 123 | app.setting.openTabById("hotkeys"); 124 | // @ts-ignore 125 | const tab = app.setting.activeTab; 126 | tab.setQuery(this.plugin.manifest.id); 127 | tab.updateHotkeyVisibility(); 128 | }); 129 | }); 130 | shortcutEl.descEl.createSpan({text: 'This plugin does not have default shortcuts to prevent shortcut conflicts.'}); 131 | shortcutEl.descEl.createEl('br'); 132 | shortcutEl.descEl.createSpan({text: 'Assign your own shortcuts to run commands for different input types.'}); 133 | 134 | 135 | // ------- [API Setting] ------- 136 | containerEl.createEl('h1', { text: 'API Setting' }); 137 | 138 | // Classifier Engine Dropdown 139 | new Setting(containerEl) 140 | .setName('Classifier Engine') 141 | .setDesc('Select the classification engine to use.') 142 | .addDropdown((dropdown) => { 143 | dropdown 144 | .addOption(String(ClassifierEngine.ChatGPT), "OpenAI-compatible API") 145 | .addOption(String(ClassifierEngine.JinaAI), "Jina AI Classifier") 146 | .setValue(String(this.plugin.settings.classifierEngine)) 147 | .onChange(async (value) => { 148 | this.plugin.settings.classifierEngine = parseInt(value) as ClassifierEngine; 149 | 150 | // Auto-set appropriate default models 151 | if (this.plugin.settings.classifierEngine === ClassifierEngine.ChatGPT) { 152 | if (!commandOption.model || commandOption.model === 'jina-embeddings-v3') { 153 | commandOption.model = 'gpt-4.1-mini'; 154 | } 155 | } else if (this.plugin.settings.classifierEngine === ClassifierEngine.JinaAI) { 156 | if (!commandOption.model || commandOption.model === 'gpt-4.1-mini' || commandOption.model === 'gpt-4o' || commandOption.model === 'gpt-4.1') { 157 | commandOption.model = 'jina-embeddings-v3'; 158 | } 159 | } 160 | 161 | await this.plugin.saveSettings(); 162 | this.display(); // Re-render settings to show/hide relevant fields 163 | }); 164 | }); 165 | 166 | // Conditional API Settings 167 | if (this.plugin.settings.classifierEngine === ClassifierEngine.ChatGPT) { 168 | new Setting(containerEl) 169 | .setName('API Base URL') 170 | .setDesc('Base URL for OpenAI-compatible API calls') 171 | .addText((text) => 172 | text 173 | .setPlaceholder('https://api.openai.com/v1') 174 | .setValue(this.plugin.settings.baseURL) 175 | .onChange((value) => { 176 | this.plugin.settings.baseURL = value; 177 | this.plugin.saveSettings(); 178 | }) 179 | ); 180 | 181 | const baseUrlSetting = containerEl.createDiv(); 182 | baseUrlSetting.createEl('div', { text: 'Common configurations:' }); 183 | baseUrlSetting.createEl('div', { text: '• OpenAI: https://api.openai.com/v1' }); 184 | baseUrlSetting.createEl('div', { text: '• Local OpenAI-compatible API (Ollama, LocalAI):' }); 185 | baseUrlSetting.createEl('div', { text: ' - Ollama: http://localhost:11434/v1' }); 186 | baseUrlSetting.createEl('div', { text: ' - LocalAI: http://localhost:8080/v1' }); 187 | baseUrlSetting.createEl('div', { text: ' ⚠️ Ollama/LocalAI are not fully compatible currently.' }); 188 | baseUrlSetting.style.marginLeft = '20px'; 189 | baseUrlSetting.style.fontSize = '0.9em'; 190 | baseUrlSetting.style.color = 'var(--text-muted)'; 191 | baseUrlSetting.style.marginBottom = '10px'; 192 | 193 | new Setting(containerEl) 194 | .setName('Model') 195 | .setDesc("Model ID to use for classification. (Recommended: gpt-4.1-mini, gpt-4.1, gpt-4o)") 196 | .addText((text) => 197 | text 198 | .setPlaceholder('gpt-4.1-mini') 199 | .setValue(commandOption.model) 200 | .onChange(async (value) => { 201 | commandOption.model = value; 202 | await this.plugin.saveSettings(); 203 | }) 204 | ); 205 | 206 | const modelSetting = containerEl.createDiv(); 207 | modelSetting.createEl('div', { text: 'Recommended for OpenAI: gpt-4.1-mini, gpt-4.1, gpt-4o' }); 208 | modelSetting.createEl('div', { text: '• OpenAI: gpt-4.1-mini, gpt-4.1, gpt-4o' }); 209 | modelSetting.createEl('div', { text: '• Local OpenAI-compatible API (Ollama, LocalAI): llama3, mistral, phi3, qwen2 등' }); 210 | modelSetting.style.marginLeft = '20px'; 211 | modelSetting.style.fontSize = '0.9em'; 212 | modelSetting.style.color = 'var(--text-muted)'; 213 | modelSetting.style.marginBottom = '10px'; 214 | 215 | const apiKeySetting = new Setting(containerEl) 216 | .setName('API Key') 217 | .setDesc('') 218 | .addText((text) => 219 | text 220 | .setPlaceholder('API key (leave empty for local AI)') 221 | .setValue(this.plugin.settings.apiKey) 222 | .onChange((value) => { 223 | this.plugin.settings.apiKey = value; 224 | this.plugin.saveSettings(); 225 | }) 226 | ) 227 | apiKeySetting.descEl.createSpan({ text: 'Enter your API key. Required for OpenAI and etc. Leave empty for local AI (Ollama, LocalAI). ' }); 228 | apiKeySetting.descEl.createEl('a', { href: 'https://platform.openai.com/account/api-keys', text: 'Get OpenAI API key' }) 229 | const apiTestMessageEl = document.createElement('div'); 230 | apiKeySetting.descEl.appendChild(apiTestMessageEl); 231 | 232 | if (this.plugin.settings.apiKey && this.plugin.settings.apiKeyCreatedAt) { 233 | apiTestMessageEl.setText(`This key was tested at ${this.plugin.settings.apiKeyCreatedAt.toString()}`); 234 | apiTestMessageEl.style.color = 'var(--success-color)'; 235 | } 236 | 237 | apiKeySetting.addButton((cb) => { 238 | cb.setButtonText('Test API call') 239 | .setCta() 240 | .onClick(async () => { 241 | apiTestMessageEl.setText('Testing API call...'); 242 | apiTestMessageEl.style.color = 'var(--text-normal)'; 243 | try { 244 | await ChatGPT.callAPI('', 'test', this.plugin.settings.apiKey, commandOption.model, undefined, undefined, undefined, undefined, undefined, this.plugin.settings.baseURL); 245 | apiTestMessageEl.setText('Success! API working.'); 246 | apiTestMessageEl.style.color = 'var(--success-color)'; 247 | this.plugin.settings.apiKeyCreatedAt = new Date(); 248 | } catch (error) { 249 | apiTestMessageEl.setText('Error: API is not working. Check console for details.'); 250 | apiTestMessageEl.style.color = 'var(--warning-color)'; 251 | this.plugin.settings.apiKeyCreatedAt = null; 252 | console.error("ChatGPT API Test Error:", error); 253 | } 254 | }); 255 | }); 256 | } else if (this.plugin.settings.classifierEngine === ClassifierEngine.JinaAI) { 257 | new Setting(containerEl) 258 | .setName('Jina AI API Base URL') 259 | .setDesc('Optional: Set a different base URL for Jina AI API calls.') 260 | .addText((text) => 261 | text 262 | .setPlaceholder('https://api.jina.ai/v1') 263 | .setValue(this.plugin.settings.jinaBaseURL) 264 | .onChange((value) => { 265 | this.plugin.settings.jinaBaseURL = value; 266 | this.plugin.saveSettings(); 267 | }) 268 | ); 269 | 270 | // Note: commandOption.model is currently shared. If Jina needs a separate model field, 271 | // it should be added to AutoClassifierSettings and handled here. 272 | // For now, we'll reuse commandOption.model but default it appropriately. 273 | new Setting(containerEl) 274 | .setName('Jina AI Model') 275 | .setDesc("ID of the Jina AI model to use. Default: jina-embeddings-v3 (supports 8192 tokens, 256 classes).") 276 | .addText((text) => 277 | text 278 | .setPlaceholder('jina-embeddings-v3') 279 | .setValue(commandOption.model) // Reuse existing model field, ensure it's set to a Jina default if empty or on switch 280 | .onChange(async (value) => { 281 | commandOption.model = value; 282 | await this.plugin.saveSettings(); 283 | }) 284 | ); 285 | 286 | const jinaApiKeySetting = new Setting(containerEl) 287 | .setName('Jina AI API Key') 288 | .setDesc('') 289 | .addText((text) => 290 | text 291 | .setPlaceholder('Jina API key') 292 | .setValue(this.plugin.settings.jinaApiKey) 293 | .onChange((value) => { 294 | this.plugin.settings.jinaApiKey = value; 295 | this.plugin.saveSettings(); 296 | }) 297 | ); 298 | jinaApiKeySetting.descEl.createSpan({ text: 'Enter your Jina AI API key. Get a free API key (10M tokens) at ' }); 299 | jinaApiKeySetting.descEl.createEl('a', { href: 'https://jina.ai/', text: 'jina.ai' }); 300 | jinaApiKeySetting.descEl.createSpan({ text: '. No account creation often required.' }); 301 | const jinaApiTestMessageEl = document.createElement('div'); 302 | jinaApiKeySetting.descEl.appendChild(jinaApiTestMessageEl); 303 | 304 | // Jina API Key test button 305 | jinaApiKeySetting.addButton((cb) => { 306 | cb.setButtonText('Test Jina API') 307 | .setCta() 308 | .onClick(async () => { 309 | jinaApiTestMessageEl.setText('Testing Jina API call...'); 310 | jinaApiTestMessageEl.style.color = 'var(--text-normal)'; 311 | try { 312 | // Use a simple test case 313 | const response = await JinaAI.callAPI( 314 | this.plugin.settings.jinaApiKey, 315 | this.plugin.settings.jinaBaseURL, 316 | commandOption.model || 'jina-embeddings-v3', // Fallback to default if model is not set 317 | ['This is a test sentence for classification.'], 318 | ['positive', 'negative', 'neutral'] 319 | ); 320 | 321 | // Show token usage if available 322 | let tokenInfo = ""; 323 | if (response.usage && response.usage.total_tokens) { 324 | tokenInfo = ` (${response.usage.total_tokens} tokens used)`; 325 | } 326 | 327 | jinaApiTestMessageEl.setText(`Success! Jina AI API working${tokenInfo}.`); 328 | jinaApiTestMessageEl.style.color = 'var(--success-color)'; 329 | } catch (error: any) { 330 | jinaApiTestMessageEl.setText(`Error: ${error.message}`); 331 | jinaApiTestMessageEl.style.color = 'var(--warning-color)'; 332 | console.error("Jina AI API Test Error:", error); 333 | } 334 | }); 335 | }); 336 | } 337 | 338 | // ------- [Tag Reference Setting] ------- 339 | containerEl.createEl('h1', { text: 'Tag Reference Setting' }); 340 | 341 | // Toggle tag reference 342 | new Setting(containerEl) 343 | .setName('Use Reference') 344 | .setDesc('If not, it will recommend new tags') 345 | .addToggle((toggle) => 346 | toggle 347 | .setValue(commandOption.useRef) 348 | .onChange(async (value) => { 349 | commandOption.useRef = value; 350 | await this.plugin.saveSettings(); 351 | this.display(); 352 | }), 353 | ); 354 | 355 | if (commandOption.useRef) { 356 | // Tag Reference Type Dropdown 357 | new Setting(containerEl) 358 | .setName('Reference type') 359 | .setDesc('Choose the type of reference tag') 360 | .setClass('setting-item-child') 361 | .addDropdown((dropdown) => { 362 | dropdown 363 | .addOption(String(ReferenceType.All), "All tags") 364 | .addOption(String(ReferenceType.Filter), "Filtered tags",) 365 | .addOption(String(ReferenceType.Manual), "Manual tags") 366 | .setValue(String(commandOption.refType)) 367 | .onChange(async (refTye) => { 368 | this.setRefType(parseInt(refTye)); 369 | this.setRefs(parseInt(refTye)); 370 | this.display(); 371 | }); 372 | }); 373 | 374 | // All tags - default setting 375 | if (commandOption.refType == ReferenceType.All) { 376 | this.setRefs(ReferenceType.All); 377 | } 378 | // Filtered tags - Regex setting 379 | if (commandOption.refType == ReferenceType.Filter) { 380 | new Setting(containerEl) 381 | .setName('Filter regex') 382 | .setDesc('Specify a regular expression to filter tags') 383 | .setClass('setting-item-child') 384 | .addText((text) => 385 | text 386 | .setPlaceholder('Regular expression') 387 | .setValue(commandOption.filterRegex) 388 | .onChange(async (value) => { 389 | this.setRefs(ReferenceType.Filter, value); 390 | }) 391 | ); 392 | } 393 | // Manual tags - manual input text area 394 | else if (commandOption.refType == ReferenceType.Manual) { 395 | new Setting(containerEl) 396 | .setName('Manual tags') 397 | .setDesc('Manually specify tags to reference.') 398 | .setClass('setting-item-child') 399 | .setClass('height10-text-area') 400 | .addTextArea((text) => { 401 | text 402 | .setPlaceholder('Tags') 403 | .setValue(commandOption.manualRefs?.join('\n')) 404 | .onChange(async (value) => { 405 | this.setRefs(ReferenceType.Manual, value); 406 | }) 407 | }) 408 | .addExtraButton(cb => { 409 | cb 410 | .setIcon('reset') 411 | .setTooltip('Bring All Tags') 412 | .onClick(async () => { 413 | const allTags = await this.plugin.viewManager.getTags() ?? []; 414 | commandOption.manualRefs = allTags; 415 | this.setRefs(ReferenceType.Manual); 416 | this.display(); 417 | }) 418 | }); 419 | } 420 | 421 | // View Reference Tags button 422 | new Setting(containerEl) 423 | .setClass('setting-item-child') 424 | .addButton((cb) => { 425 | cb.setButtonText('View Reference Tags') 426 | .onClick(async () => { 427 | const tags = commandOption.refs ?? []; 428 | let message = `${tags.join('\n')}`; 429 | if (this.plugin.settings.classifierEngine === ClassifierEngine.JinaAI && tags.length > 256) { 430 | message += `\n\n⚠️ Warning: Jina AI supports maximum 256 tags, but ${tags.length} were found. Please reduce the number of tags.`; 431 | } 432 | new Notice(message); 433 | }); 434 | }); 435 | } 436 | 437 | 438 | 439 | 440 | // ------- [Output Setting] ------- 441 | containerEl.createEl('h1', { text: 'Output Setting' }); 442 | 443 | // Output type dropdown 444 | new Setting(containerEl) 445 | .setName('Output Type') 446 | .setDesc('Specify output type') 447 | .addDropdown((cb) => { 448 | cb.addOption(String(OutType.Tag), '#Tag') 449 | .addOption(String(OutType.Wikilink), '[[Wikilink]]') 450 | .addOption(String(OutType.FrontMatter), 'FrontMatter') 451 | .addOption(String(OutType.Title), 'Title alternative') 452 | .setValue(String(commandOption.outType)) 453 | .onChange(async (value) => { 454 | commandOption.outType = parseInt(value); 455 | commandOption.outLocation = 0; // Initialize 456 | await this.plugin.saveSettings(); 457 | this.display(); 458 | }); 459 | }); 460 | 461 | // Output Type 1. [Tag Case] 462 | if (commandOption.outType == OutType.Tag) { 463 | // Tag - Location dropdown 464 | new Setting(containerEl) 465 | .setName('Output Location') 466 | .setClass('setting-item-child') 467 | .setDesc('Specify where to put the output tag') 468 | .addDropdown((cb) => { 469 | cb.addOption(String(OutLocation.Cursor), 'Current Cursor') 470 | .addOption(String(OutLocation.ContentTop), 'Top of Content') 471 | .setValue(String(commandOption.outLocation)) 472 | .onChange(async (value) => { 473 | commandOption.outLocation = parseInt(value); 474 | await this.plugin.saveSettings(); 475 | this.display(); 476 | }); 477 | }); 478 | } 479 | // Output Type 2. [Wikilink Case] 480 | else if (commandOption.outType == OutType.Wikilink) { 481 | // Wikilink - Location dropdown 482 | new Setting(containerEl) 483 | .setName('Output Location') 484 | .setClass('setting-item-child') 485 | .setDesc('Specify where to put the output wikilink') 486 | .addDropdown((cb) => { 487 | cb.addOption(String(OutLocation.Cursor), 'Current Cursor') 488 | .addOption(String(OutLocation.ContentTop), 'Top of Content') 489 | .setValue(String(commandOption.outLocation)) 490 | .onChange(async (value) => { 491 | commandOption.outLocation = parseInt(value); 492 | await this.plugin.saveSettings(); 493 | this.display(); 494 | }); 495 | }); 496 | } 497 | // Output Type 3. [Frontmatter Case] 498 | else if (commandOption.outType == OutType.FrontMatter) { 499 | // key text setting 500 | new Setting(containerEl) 501 | .setName('FrontMatter key') 502 | .setDesc('Specify FrontMatter key to put the output tag') 503 | .setClass('setting-item-child') 504 | .addText((text) => 505 | text 506 | .setPlaceholder('Key') 507 | .setValue(commandOption.key) 508 | .onChange(async (value) => { 509 | commandOption.key = value; 510 | await this.plugin.saveSettings(); 511 | }) 512 | ); 513 | } 514 | 515 | // Overwrite setting 516 | if ((commandOption.outType == OutType.Tag && commandOption.outLocation == OutLocation.Cursor) || 517 | (commandOption.outType == OutType.Wikilink && commandOption.outLocation == OutLocation.Cursor) || 518 | commandOption.outType == OutType.Title || 519 | commandOption.outType == OutType.FrontMatter) { 520 | 521 | let overwriteName = ''; 522 | if (commandOption.outLocation == OutLocation.Cursor) overwriteName = 'Overwrite if selected.'; 523 | if (commandOption.outType == OutType.Title) overwriteName = 'Overwrite whole title. If false, add to end of title.'; 524 | if (commandOption.outType == OutType.FrontMatter) overwriteName = 'Overwrite value of the key.'; 525 | 526 | new Setting(containerEl) 527 | .setName(overwriteName) 528 | .setClass('setting-item-child') 529 | .addToggle((toggle) => 530 | toggle 531 | .setValue(commandOption.overwrite) 532 | .onChange(async (value) => { 533 | commandOption.overwrite = value; 534 | await this.plugin.saveSettings(); 535 | this.display(); 536 | }) 537 | ); 538 | 539 | } 540 | 541 | // Output Prefix & Suffix 542 | new Setting(containerEl) 543 | .setName('Add Prefix & Suffix') 544 | .setDesc(`Output: {prefix} + {output} + {suffix}`); 545 | new Setting(containerEl) 546 | .setName('Prefix') 547 | .setClass('setting-item-child') 548 | .addText((text) => 549 | text 550 | .setPlaceholder('prefix') 551 | .setValue(commandOption.outPrefix) 552 | .onChange(async (value) => { 553 | commandOption.outPrefix = value; 554 | await this.plugin.saveSettings(); 555 | }) 556 | ); 557 | new Setting(containerEl) 558 | .setName('Suffix') 559 | .setClass('setting-item-child') 560 | .addText((text) => 561 | text 562 | .setPlaceholder('suffix') 563 | .setValue(commandOption.outSuffix) 564 | .onChange(async (value) => { 565 | commandOption.outSuffix = value; 566 | await this.plugin.saveSettings(); 567 | }) 568 | ); 569 | 570 | 571 | // ------- [Advanced Setting] ------- 572 | containerEl.createEl('h1', { text: 'Advanced Setting' }); 573 | 574 | new Setting(containerEl) 575 | .setName('Maximum Tag Suggestions') 576 | .setDesc("Maximum number of tags to suggest (1-10)") 577 | .addText((text) => 578 | text 579 | .setPlaceholder('3') 580 | .setValue(String(commandOption.max_suggestions)) 581 | .onChange(async (value) => { 582 | const num = parseInt(value); 583 | if (num >= 1 && num <= 10) { 584 | commandOption.max_suggestions = num; 585 | await this.plugin.saveSettings(); 586 | } 587 | }) 588 | ); 589 | 590 | // Conditional Advanced Settings for OpenAI-compatible API 591 | if (this.plugin.settings.classifierEngine === ClassifierEngine.ChatGPT) { 592 | // Toggle custom rule 593 | new Setting(containerEl) 594 | .setName('Use Custom Request Template') 595 | .setDesc('Enable advanced prompt customization for better results') 596 | .addToggle((toggle) => 597 | toggle 598 | .setValue(commandOption.useCustomCommand) 599 | .onChange(async (value) => { 600 | commandOption.useCustomCommand = value; 601 | await this.plugin.saveSettings(); 602 | this.display(); 603 | }), 604 | ); 605 | 606 | // Custom template textarea 607 | if (commandOption.useCustomCommand) { 608 | 609 | // Different default template depanding on useRef 610 | if (commandOption.useRef) { 611 | if(commandOption.prmpt_template == DEFAULT_PROMPT_TEMPLATE_WO_REF) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE; 612 | } else { 613 | if(commandOption.prmpt_template == DEFAULT_PROMPT_TEMPLATE) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE_WO_REF; 614 | } 615 | 616 | const customPromptTemplateEl = new Setting(containerEl) 617 | .setName('Custom Prompt Template (ChatGPT)') 618 | .setDesc('') 619 | .setClass('setting-item-child') 620 | .setClass('block-control-item') 621 | .setClass('height20-text-area') 622 | .addTextArea((text) => 623 | text 624 | .setPlaceholder('Write custom prompt template.') 625 | .setValue(commandOption.prmpt_template) 626 | .onChange(async (value) => { 627 | commandOption.prmpt_template = value; 628 | await this.plugin.saveSettings(); 629 | }) 630 | ) 631 | .addExtraButton(cb => { 632 | cb 633 | .setIcon('reset') 634 | .setTooltip('Restore to default') 635 | .onClick(async () => { 636 | // Different default template depanding on useRef 637 | if (commandOption.useRef) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE; 638 | else commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE_WO_REF; 639 | 640 | await this.plugin.saveSettings(); 641 | this.display(); 642 | }) 643 | }); 644 | customPromptTemplateEl.descEl.createSpan({text: 'This plugin is based on the ChatGPT answer.'}); 645 | customPromptTemplateEl.descEl.createEl('br'); 646 | customPromptTemplateEl.descEl.createSpan({text: 'You can use your own template when making a request to ChatGPT.'}); 647 | customPromptTemplateEl.descEl.createEl('br'); 648 | customPromptTemplateEl.descEl.createEl('br'); 649 | customPromptTemplateEl.descEl.createSpan({text: 'Variables:'}); 650 | customPromptTemplateEl.descEl.createEl('br'); 651 | customPromptTemplateEl.descEl.createSpan({text: '- {{input}}: The text to classify will be inserted here.'}); 652 | customPromptTemplateEl.descEl.createEl('br'); 653 | customPromptTemplateEl.descEl.createSpan({text: '- {{reference}}: The reference tags will be inserted here.'}); 654 | customPromptTemplateEl.descEl.createEl('br'); 655 | 656 | const customChatRoleEl = new Setting(containerEl) 657 | .setName('Custom Chat Role (ChatGPT)') 658 | .setDesc('') 659 | .setClass('setting-item-child') 660 | .setClass('block-control-item') 661 | .setClass('height10-text-area') 662 | .addTextArea((text) => 663 | text 664 | .setPlaceholder('Write custom chat role for gpt system.') 665 | .setValue(commandOption.chat_role) 666 | .onChange(async (value) => { 667 | commandOption.chat_role = value; 668 | await this.plugin.saveSettings(); 669 | }) 670 | ) 671 | .addExtraButton(cb => { 672 | cb 673 | .setIcon('reset') 674 | .setTooltip('Restore to default') 675 | .onClick(async () => { 676 | commandOption.chat_role = DEFAULT_CHAT_ROLE; 677 | await this.plugin.saveSettings(); 678 | this.display(); 679 | }) 680 | }); 681 | customChatRoleEl.descEl.createSpan({text: 'Define custom role to ChatGPT system.'}); 682 | 683 | 684 | new Setting(containerEl) 685 | .setName('Custom Max Tokens (ChatGPT)') 686 | .setDesc("The maximum number of tokens that can be generated in the completion.") 687 | .setClass('setting-item-child') 688 | .addText((text) => 689 | text 690 | .setPlaceholder('150') 691 | .setValue(String(commandOption.max_tokens)) 692 | .onChange(async (value) => { 693 | commandOption.max_tokens = parseInt(value); 694 | await this.plugin.saveSettings(); 695 | }) 696 | ); 697 | } 698 | // Custom template textarea 699 | if (commandOption.useCustomCommand) { 700 | 701 | // Different default template depending on useRef 702 | if (commandOption.useRef) { 703 | if(commandOption.prmpt_template == DEFAULT_PROMPT_TEMPLATE_WO_REF) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE; 704 | } else { 705 | if(commandOption.prmpt_template == DEFAULT_PROMPT_TEMPLATE) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE_WO_REF; 706 | } 707 | 708 | const customPromptTemplateEl = new Setting(containerEl) 709 | .setName('Custom Prompt Template') 710 | .setDesc('') 711 | .setClass('setting-item-child') 712 | .setClass('block-control-item') 713 | .setClass('height20-text-area') 714 | .addTextArea((text) => 715 | text 716 | .setPlaceholder('Write custom prompt template.') 717 | .setValue(commandOption.prmpt_template) 718 | .onChange(async (value) => { 719 | commandOption.prmpt_template = value; 720 | await this.plugin.saveSettings(); 721 | }) 722 | ) 723 | .addExtraButton(cb => { 724 | cb 725 | .setIcon('reset') 726 | .setTooltip('Restore to default') 727 | .onClick(async () => { 728 | // Different default template depending on useRef 729 | if (commandOption.useRef) commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE; 730 | else commandOption.prmpt_template = DEFAULT_PROMPT_TEMPLATE_WO_REF; 731 | 732 | await this.plugin.saveSettings(); 733 | this.display(); 734 | }) 735 | }); 736 | customPromptTemplateEl.descEl.createSpan({text: 'This plugin is based on the LLM response.'}); 737 | customPromptTemplateEl.descEl.createEl('br'); 738 | customPromptTemplateEl.descEl.createSpan({text: 'You can use your own template when making a request to the API.'}); 739 | customPromptTemplateEl.descEl.createEl('br'); 740 | customPromptTemplateEl.descEl.createEl('br'); 741 | customPromptTemplateEl.descEl.createSpan({text: 'Variables:'}); 742 | customPromptTemplateEl.descEl.createEl('br'); 743 | customPromptTemplateEl.descEl.createSpan({text: '- {{input}}: The text to classify will be inserted here.'}); 744 | customPromptTemplateEl.descEl.createEl('br'); 745 | customPromptTemplateEl.descEl.createSpan({text: '- {{reference}}: The reference tags will be inserted here.'}); 746 | customPromptTemplateEl.descEl.createEl('br'); 747 | 748 | const customChatRoleEl = new Setting(containerEl) 749 | .setName('Custom Chat Role') 750 | .setDesc('') 751 | .setClass('setting-item-child') 752 | .setClass('block-control-item') 753 | .setClass('height10-text-area') 754 | .addTextArea((text) => 755 | text 756 | .setPlaceholder('Write custom chat role for system.') 757 | .setValue(commandOption.chat_role) 758 | .onChange(async (value) => { 759 | commandOption.chat_role = value; 760 | await this.plugin.saveSettings(); 761 | }) 762 | ) 763 | .addExtraButton(cb => { 764 | cb 765 | .setIcon('reset') 766 | .setTooltip('Restore to default') 767 | .onClick(async () => { 768 | commandOption.chat_role = DEFAULT_CHAT_ROLE; 769 | await this.plugin.saveSettings(); 770 | this.display(); 771 | }) 772 | }); 773 | customChatRoleEl.descEl.createSpan({text: 'Define custom role for the AI system.'}); 774 | 775 | 776 | new Setting(containerEl) 777 | .setName('Custom Max Tokens') 778 | .setDesc("The maximum number of tokens that can be generated in the completion.") 779 | .setClass('setting-item-child') 780 | .addText((text) => 781 | text 782 | .setPlaceholder('150') 783 | .setValue(String(commandOption.max_tokens)) 784 | .onChange(async (value) => { 785 | commandOption.max_tokens = parseInt(value); 786 | await this.plugin.saveSettings(); 787 | }) 788 | ); 789 | } 790 | } 791 | // For JinaAI, these custom prompt/role/token settings are hidden as they are not applicable. 792 | } 793 | 794 | 795 | 796 | setRefType(refType: ReferenceType) { 797 | this.plugin.settings.commandOption.refType = refType; 798 | } 799 | 800 | async setRefs(refType: ReferenceType, value?: string) { 801 | const commandOption = this.plugin.settings.commandOption; 802 | if (refType == ReferenceType.All) { 803 | const tags = await this.plugin.viewManager.getTags() ?? []; 804 | commandOption.refs = tags 805 | } 806 | else if (refType == ReferenceType.Filter) { 807 | if (value) { 808 | commandOption.filterRegex = value; 809 | } 810 | const tags = await this.plugin.viewManager.getTags(commandOption.filterRegex) ?? []; 811 | commandOption.refs = tags 812 | } 813 | else if (refType == ReferenceType.Manual) { 814 | if (value) { 815 | commandOption.manualRefs = value?.split(/,|\n/).map((tag) => tag.trim()); 816 | } 817 | commandOption.refs = commandOption.manualRefs; 818 | } 819 | await this.plugin.saveSettings(); 820 | } 821 | } 822 | --------------------------------------------------------------------------------