From ac07b8c97b429cab1e821b9adb995805ab9b0d94 Mon Sep 17 00:00:00 2001 From: djayatillake Date: Thu, 23 Jan 2025 22:53:43 +0000 Subject: [PATCH] initial --- .gitignore | 22 ++ esbuild.config.mjs | 47 ++++ icon.svg | 8 + main.js | 489 ++++++++++++++++++++++++++++++++++++ main.ts | 612 +++++++++++++++++++++++++++++++++++++++++++++ manifest.json | 12 + package-lock.json | 578 ++++++++++++++++++++++++++++++++++++++++++ package.json | 21 ++ styles.css | 152 +++++++++++ test.md | 11 + tsconfig.json | 26 ++ 11 files changed, 1978 insertions(+) create mode 100644 .gitignore create mode 100644 esbuild.config.mjs create mode 100644 icon.svg create mode 100644 main.js create mode 100644 main.ts create mode 100644 manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 styles.css create mode 100644 test.md create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f3e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Dependencies +node_modules/* +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build output +dist/ +build/ +*.js.map + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Environment variables +.env +.env.local +.env.*.local diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..6ae0c16 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,47 @@ +import esbuild from "esbuild"; +import process from "process"; + +const banner = +`/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = (process.argv[2] === "production"); + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ["main.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr" + ], + format: "cjs", + target: "es2018", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..17d2e85 --- /dev/null +++ b/icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..553163a --- /dev/null +++ b/main.js @@ -0,0 +1,489 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ + +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// main.ts +var main_exports = {}; +__export(main_exports, { + default: () => LLMTaggerPlugin +}); +module.exports = __toCommonJS(main_exports); +var import_obsidian = require("obsidian"); +var ICON_NAME = "llm-tagger-robot"; +var VIEW_TYPE = "llm-tagger-view"; +var DEFAULT_SETTINGS = { + selectedModel: null, + defaultTags: [], + autoAddTags: false, + taggedFiles: {} +}; +var LLMTaggerPlugin = class extends import_obsidian.Plugin { + constructor() { + super(...arguments); + this.autoTaggingEnabled = false; + } + async onload() { + console.log("Loading LLM Tagger plugin"); + await this.loadSettings(); + (0, import_obsidian.addIcon)(ICON_NAME, ` + + + + + + `); + this.registerView( + VIEW_TYPE, + (leaf) => this.view = new LLMTaggerView(leaf, this) + ); + this.addRibbonIcon(ICON_NAME, "LLM Tagger", () => { + this.activateView(); + }); + this.app.workspace.onLayoutReady(() => { + if (this.settings.autoAddTags) { + this.enableAutoTagging(); + } + }); + this.addCommand({ + id: "add-tags", + name: "Add Tags to Documents", + callback: () => this.addTagsToDocuments() + }); + this.addSettingTab(new LLMTaggerSettingTab(this.app, this)); + console.log("LLM Tagger plugin loaded"); + } + enableAutoTagging() { + if (this.autoTaggingEnabled) + return; + this.autoTaggingEnabled = true; + const debouncedAutoTag = (0, import_obsidian.debounce)(async (file) => { + await this.autoTagFile(file); + }, 2e3, true); + this.registerEvent( + this.app.vault.on("create", async (file) => { + if (this.autoTaggingEnabled && file instanceof import_obsidian.TFile && file.extension === "md") { + await debouncedAutoTag(file); + } + }) + ); + this.registerEvent( + this.app.vault.on("modify", async (file) => { + if (this.autoTaggingEnabled && file instanceof import_obsidian.TFile && file.extension === "md") { + await debouncedAutoTag(file); + } + }) + ); + } + disableAutoTagging() { + this.autoTaggingEnabled = false; + } + async autoTagFile(file) { + if (!this.settings.autoAddTags || !this.settings.selectedModel || !this.settings.defaultTags.length) { + return; + } + try { + const content = await this.app.vault.read(file); + if (!content.trim()) { + return; + } + const taggedContent = await this.processContentWithOllama( + content, + this.settings.defaultTags + ); + if (taggedContent !== content) { + await this.app.vault.modify(file, taggedContent); + new import_obsidian.Notice(`Auto-tagged: ${file.basename}`); + this.settings.taggedFiles[file.path] = Date.now(); + await this.saveSettings(); + } + } catch (error) { + console.error("Error auto-tagging file:", error); + new import_obsidian.Notice(`Failed to auto-tag ${file.basename}: ${error.message}`); + } + } + async activateView() { + const { workspace } = this.app; + let leaf = workspace.getLeavesOfType(VIEW_TYPE)[0]; + if (!leaf) { + const rightLeaf = workspace.getRightLeaf(false); + if (rightLeaf) { + await rightLeaf.setViewState({ type: VIEW_TYPE }); + leaf = rightLeaf; + } + } + if (leaf) { + workspace.revealLeaf(leaf); + } + } + async onunload() { + console.log("Unloading LLM Tagger plugin"); + this.disableAutoTagging(); + this.app.workspace.detachLeavesOfType(VIEW_TYPE); + } + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + async saveSettings() { + await this.saveData(this.settings); + if (this.settings.autoAddTags) { + this.enableAutoTagging(); + } else { + this.disableAutoTagging(); + } + } + async getOllamaModels() { + var _a; + try { + const response = await fetch("http://localhost:11434/api/tags"); + const data = await response.json(); + return ((_a = data.models) == null ? void 0 : _a.map((model) => model.name)) || []; + } catch (error) { + console.error("Failed to fetch Ollama models:", error); + return []; + } + } + async getUserDefinedTags() { + return new Promise(async (resolve) => { + const modal = new import_obsidian.Modal(this.app); + modal.titleEl.setText("Configure Tags"); + const modelContainer = modal.contentEl.createDiv(); + modelContainer.style.marginBottom = "1em"; + const modelLabel = modelContainer.createEl("label"); + modelLabel.setText("Select Ollama Model:"); + const modelSelect = modelContainer.createEl("select"); + modelSelect.style.width = "100%"; + modelSelect.style.marginTop = "0.5em"; + const placeholderOption = modelSelect.createEl("option"); + placeholderOption.value = ""; + placeholderOption.text = "Select a model..."; + placeholderOption.disabled = true; + placeholderOption.selected = !this.settings.selectedModel; + try { + const models = await this.getOllamaModels(); + models.forEach((model) => { + const option = modelSelect.createEl("option"); + option.value = model; + option.text = model; + if (model === this.settings.selectedModel) { + option.selected = true; + } + }); + } catch (error) { + console.error("Failed to load models:", error); + const option = modelSelect.createEl("option"); + option.text = "Failed to load models"; + option.disabled = true; + } + const tagsContainer = modal.contentEl.createDiv(); + tagsContainer.style.marginTop = "1em"; + const tagsLabel = tagsContainer.createEl("label"); + tagsLabel.setText("Enter tags (comma-separated):"); + const input = tagsContainer.createEl("textarea"); + input.style.width = "100%"; + input.style.height = "100px"; + input.style.marginTop = "0.5em"; + input.placeholder = "Enter tags separated by commas..."; + if (this.settings.defaultTags.length > 0) { + input.value = this.settings.defaultTags.join(", "); + } + const buttonContainer = modal.contentEl.createDiv(); + buttonContainer.style.marginTop = "1em"; + buttonContainer.style.display = "flex"; + buttonContainer.style.justifyContent = "flex-end"; + buttonContainer.style.gap = "10px"; + const cancelButton = buttonContainer.createEl("button", { text: "Cancel" }); + const okButton = buttonContainer.createEl("button", { text: "OK", cls: "mod-cta" }); + cancelButton.addEventListener("click", () => { + resolve(null); + modal.close(); + }); + okButton.addEventListener("click", () => { + if (!modelSelect.value) { + new import_obsidian.Notice("Please select a model first"); + return; + } + const tagInput = input.value.trim(); + if (!tagInput) { + new import_obsidian.Notice("Please enter at least one tag"); + return; + } + this.settings.selectedModel = modelSelect.value; + this.saveSettings(); + const tags = tagInput.split(",").map((tag) => tag.trim()).filter((tag) => tag); + resolve(tags); + modal.close(); + }); + modal.open(); + }); + } + async processContentWithOllama(content, availableTags) { + if (!this.settings.selectedModel) { + throw new Error("No Ollama model selected"); + } + if (this.isAlreadyTagged(content)) { + return content; + } + const response = await fetch("http://localhost:11434/api/generate", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: this.settings.selectedModel, + prompt: `You are an expert at analyzing and tagging markdown documents. Your task is to create a brief tagged summary of the document content. + +Available tags: ${availableTags.join(", ")} + +Instructions: +1. Create a brief 1-2 sentence summary of the content +2. Add relevant tags from the provided list +3. Only use tags from the provided list +4. Each tag MUST start with a # symbol +5. Keep the summary concise and focused + +Content to analyze: +${content} + +Provide a tagged summary:`, + stream: false + }) + }); + const data = await response.json(); + const taggedSummary = data.response.trim(); + if (taggedSummary) { + const timestamp = (/* @__PURE__ */ new Date()).toISOString(); + return `--- +LLM-tagged: ${timestamp} +--- + +${taggedSummary} + +--- + +${content}`; + } + return content; + } + shouldProcessFile(file) { + const lastTagged = this.settings.taggedFiles[file.path]; + if (!lastTagged) + return true; + return file.stat.mtime > lastTagged; + } + isAlreadyTagged(content) { + return content.includes("---\nLLM-tagged:"); + } + async addTagsToDocuments(view) { + if (!this.settings.selectedModel) { + new import_obsidian.Notice("Please select an Ollama model first"); + return; + } + const tags = await this.getUserDefinedTags(); + if (!tags) + return; + const files = this.app.vault.getMarkdownFiles(); + let processed = 0; + let modified = 0; + try { + for (const file of files) { + processed++; + if (view) { + view.updateProgress(processed, files.length, file.basename); + } + if (!this.shouldProcessFile(file)) { + console.log(`Skipping ${file.basename} - already tagged and not modified`); + continue; + } + try { + const content = await this.app.vault.read(file); + if (this.isAlreadyTagged(content)) { + console.log(`Skipping ${file.basename} - already has tag metadata`); + continue; + } + const taggedContent = await this.processContentWithOllama(content, tags); + if (taggedContent !== content) { + await this.app.vault.modify(file, taggedContent); + this.settings.taggedFiles[file.path] = Date.now(); + await this.saveSettings(); + modified++; + new import_obsidian.Notice(`Tagged: ${file.basename}`); + } + } catch (error) { + console.error(`Error processing ${file.basename}:`, error); + new import_obsidian.Notice(`Failed to process ${file.basename}: ${error.message}`); + } + } + new import_obsidian.Notice(`Completed! Tagged ${modified} of ${files.length} files`); + } finally { + if (view) { + view.resetProgress(); + } + } + } +}; +var LLMTaggerView = class extends import_obsidian.ItemView { + constructor(leaf, plugin) { + super(leaf); + this.plugin = plugin; + } + getViewType() { + return VIEW_TYPE; + } + getDisplayText() { + return "LLM Tagger"; + } + getIcon() { + return ICON_NAME; + } + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + container.createEl("h2", { text: "LLM Tagger" }); + const modelContainer = container.createDiv(); + modelContainer.createEl("h3", { text: "Select Model" }); + const modelSelect = modelContainer.createEl("select"); + modelSelect.style.width = "100%"; + modelSelect.style.marginBottom = "1em"; + const placeholderOption = modelSelect.createEl("option"); + placeholderOption.value = ""; + placeholderOption.text = "Select a model..."; + placeholderOption.disabled = true; + placeholderOption.selected = !this.plugin.settings.selectedModel; + try { + const models = await this.plugin.getOllamaModels(); + models.forEach((model) => { + const option = modelSelect.createEl("option"); + option.value = model; + option.text = model; + if (model === this.plugin.settings.selectedModel) { + option.selected = true; + } + }); + } catch (error) { + console.error("Failed to load models:", error); + const option = modelSelect.createEl("option"); + option.text = "Failed to load models"; + option.disabled = true; + } + modelSelect.addEventListener("change", async () => { + this.plugin.settings.selectedModel = modelSelect.value || null; + await this.plugin.saveSettings(); + }); + const tagsContainer = container.createDiv(); + tagsContainer.createEl("h3", { text: "Enter Tags" }); + const tagsInput = tagsContainer.createEl("textarea"); + tagsInput.placeholder = "Enter tags separated by commas..."; + tagsInput.style.width = "100%"; + tagsInput.style.height = "100px"; + tagsInput.style.marginBottom = "1em"; + if (this.plugin.settings.defaultTags.length > 0) { + tagsInput.value = this.plugin.settings.defaultTags.join(", "); + } + const progressContainer = container.createDiv(); + progressContainer.createEl("h3", { text: "Progress" }); + this.progressBar = progressContainer.createEl("progress", { + attr: { value: "0", max: "100" } + }); + this.progressBar.style.width = "100%"; + this.progressBar.style.marginBottom = "0.5em"; + this.progressText = progressContainer.createDiv("progress-text"); + this.progressText.style.fontSize = "0.9em"; + this.progressText.style.color = "var(--text-muted)"; + this.progressText.style.marginBottom = "1em"; + this.progressText.textContent = "Ready to tag documents"; + const startButton = container.createEl("button", { + text: "Start Tagging", + cls: "mod-cta" + }); + startButton.style.width = "100%"; + startButton.addEventListener("click", async () => { + if (!modelSelect.value) { + new import_obsidian.Notice("Please select a model first"); + return; + } + const tagInput = tagsInput.value.trim(); + if (!tagInput) { + new import_obsidian.Notice("Please enter at least one tag"); + return; + } + const tags = tagInput.split(",").map((tag) => tag.trim()).filter((tag) => tag); + if (tagInput !== this.plugin.settings.defaultTags.join(", ")) { + this.plugin.settings.defaultTags = tags; + await this.plugin.saveSettings(); + } + startButton.disabled = true; + try { + await this.plugin.addTagsToDocuments(this); + } finally { + startButton.disabled = false; + } + }); + } + updateProgress(current, total, filename) { + const percentage = Math.round(current / total * 100); + this.progressBar.value = percentage; + this.progressText.textContent = `Processing ${filename} (${current}/${total})`; + } + resetProgress() { + this.progressBar.value = 0; + this.progressText.textContent = "Ready to tag documents"; + } +}; +var LLMTaggerSettingTab = class extends import_obsidian.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + async display() { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl("h2", { text: "LLM Tagger Settings" }); + new import_obsidian.Setting(containerEl).setName("Default Model").setDesc("Select the default Ollama model to use").addDropdown(async (dropdown) => { + dropdown.addOption("", "Select a model..."); + try { + const models = await this.plugin.getOllamaModels(); + models.forEach((model) => { + dropdown.addOption(model, model); + }); + if (this.plugin.settings.selectedModel) { + dropdown.setValue(this.plugin.settings.selectedModel); + } + } catch (error) { + console.error("Failed to load models:", error); + dropdown.addOption("error", "Failed to load models"); + dropdown.setDisabled(true); + } + dropdown.onChange(async (value) => { + this.plugin.settings.selectedModel = value || null; + await this.plugin.saveSettings(); + }); + }); + new import_obsidian.Setting(containerEl).setName("Default Tags").setDesc("Enter default tags (comma-separated) that will be pre-filled when adding tags").addTextArea((text) => text.setPlaceholder("tag1, tag2, tag3").setValue(this.plugin.settings.defaultTags.join(", ")).onChange(async (value) => { + this.plugin.settings.defaultTags = value.split(",").map((tag) => tag.trim()).filter((tag) => tag); + await this.plugin.saveSettings(); + })); + new import_obsidian.Setting(containerEl).setName("Auto-add Tags").setDesc("Automatically add tags to new documents").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoAddTags).onChange(async (value) => { + this.plugin.settings.autoAddTags = value; + await this.plugin.saveSettings(); + })); + } +}; diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..3ff6be1 --- /dev/null +++ b/main.ts @@ -0,0 +1,612 @@ +import { + App, + Plugin, + Modal, + Notice, + TFile, + PluginSettingTab, + Setting, + WorkspaceLeaf, + ItemView, + addIcon, + debounce +} from 'obsidian'; + +const ICON_NAME = 'llm-tagger-robot'; +const VIEW_TYPE = 'llm-tagger-view'; + +interface LLMTaggerSettings { + selectedModel: string | null; + defaultTags: string[]; + autoAddTags: boolean; + taggedFiles: { [path: string]: number }; // Map of file paths to timestamp of last tagging +} + +const DEFAULT_SETTINGS: LLMTaggerSettings = { + selectedModel: null, + defaultTags: [], + autoAddTags: false, + taggedFiles: {} +} + +export default class LLMTaggerPlugin extends Plugin { + settings: LLMTaggerSettings; + view: LLMTaggerView; + private autoTaggingEnabled = false; + + async onload() { + console.log('Loading LLM Tagger plugin'); + await this.loadSettings(); + + // Add robot icon + addIcon(ICON_NAME, ` + + + + + + `); + + // Register view + this.registerView( + VIEW_TYPE, + (leaf) => (this.view = new LLMTaggerView(leaf, this)) + ); + + // Add ribbon icon + this.addRibbonIcon(ICON_NAME, 'LLM Tagger', () => { + this.activateView(); + }); + + // Wait for layout to be ready before setting up auto-tagging + this.app.workspace.onLayoutReady(() => { + if (this.settings.autoAddTags) { + this.enableAutoTagging(); + } + }); + + this.addCommand({ + id: 'add-tags', + name: 'Add Tags to Documents', + callback: () => this.addTagsToDocuments(), + }); + + this.addSettingTab(new LLMTaggerSettingTab(this.app, this)); + console.log('LLM Tagger plugin loaded'); + } + + private enableAutoTagging() { + if (this.autoTaggingEnabled) return; + this.autoTaggingEnabled = true; + + // Debounced auto-tag function to prevent multiple rapid calls + const debouncedAutoTag = debounce(async (file: TFile) => { + await this.autoTagFile(file); + }, 2000, true); + + // Handle new files + this.registerEvent( + this.app.vault.on('create', async (file) => { + if (this.autoTaggingEnabled && file instanceof TFile && file.extension === 'md') { + await debouncedAutoTag(file); + } + }) + ); + + // Handle modified files + this.registerEvent( + this.app.vault.on('modify', async (file) => { + if (this.autoTaggingEnabled && file instanceof TFile && file.extension === 'md') { + await debouncedAutoTag(file); + } + }) + ); + } + + private disableAutoTagging() { + this.autoTaggingEnabled = false; + } + + private async autoTagFile(file: TFile) { + // Don't process if auto-tagging is disabled or no model is selected + if (!this.settings.autoAddTags || !this.settings.selectedModel || !this.settings.defaultTags.length) { + return; + } + + try { + const content = await this.app.vault.read(file); + + // Skip if content is empty + if (!content.trim()) { + return; + } + + const taggedContent = await this.processContentWithOllama( + content, + this.settings.defaultTags + ); + + // Only update if tags were actually added + if (taggedContent !== content) { + await this.app.vault.modify(file, taggedContent); + new Notice(`Auto-tagged: ${file.basename}`); + this.settings.taggedFiles[file.path] = Date.now(); + await this.saveSettings(); + } + } catch (error) { + console.error('Error auto-tagging file:', error); + new Notice(`Failed to auto-tag ${file.basename}: ${error.message}`); + } + } + + async activateView() { + const { workspace } = this.app; + + let leaf = workspace.getLeavesOfType(VIEW_TYPE)[0]; + + if (!leaf) { + const rightLeaf = workspace.getRightLeaf(false); + if (rightLeaf) { + await rightLeaf.setViewState({ type: VIEW_TYPE }); + leaf = rightLeaf; + } + } + + if (leaf) { + workspace.revealLeaf(leaf); + } + } + + async onunload() { + console.log('Unloading LLM Tagger plugin'); + this.disableAutoTagging(); + this.app.workspace.detachLeavesOfType(VIEW_TYPE); + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + + // Update auto-tagging based on settings + if (this.settings.autoAddTags) { + this.enableAutoTagging(); + } else { + this.disableAutoTagging(); + } + } + + async getOllamaModels(): Promise { + try { + const response = await fetch('http://localhost:11434/api/tags'); + const data = await response.json(); + return data.models?.map((model: any) => model.name) || []; + } catch (error) { + console.error('Failed to fetch Ollama models:', error); + return []; + } + } + + async getUserDefinedTags(): Promise { + return new Promise(async (resolve) => { + const modal = new Modal(this.app); + modal.titleEl.setText("Configure Tags"); + + // Create model selection dropdown + const modelContainer = modal.contentEl.createDiv(); + modelContainer.style.marginBottom = '1em'; + const modelLabel = modelContainer.createEl('label'); + modelLabel.setText('Select Ollama Model:'); + const modelSelect = modelContainer.createEl('select'); + modelSelect.style.width = '100%'; + modelSelect.style.marginTop = '0.5em'; + + // Add a placeholder option + const placeholderOption = modelSelect.createEl('option'); + placeholderOption.value = ''; + placeholderOption.text = 'Select a model...'; + placeholderOption.disabled = true; + placeholderOption.selected = !this.settings.selectedModel; + + try { + const models = await this.getOllamaModels(); + models.forEach(model => { + const option = modelSelect.createEl('option'); + option.value = model; + option.text = model; + if (model === this.settings.selectedModel) { + option.selected = true; + } + }); + } catch (error) { + console.error('Failed to load models:', error); + const option = modelSelect.createEl('option'); + option.text = 'Failed to load models'; + option.disabled = true; + } + + // Tags input + const tagsContainer = modal.contentEl.createDiv(); + tagsContainer.style.marginTop = '1em'; + const tagsLabel = tagsContainer.createEl('label'); + tagsLabel.setText('Enter tags (comma-separated):'); + const input = tagsContainer.createEl('textarea'); + input.style.width = '100%'; + input.style.height = '100px'; + input.style.marginTop = '0.5em'; + input.placeholder = 'Enter tags separated by commas...'; + if (this.settings.defaultTags.length > 0) { + input.value = this.settings.defaultTags.join(', '); + } + + const buttonContainer = modal.contentEl.createDiv(); + buttonContainer.style.marginTop = '1em'; + buttonContainer.style.display = 'flex'; + buttonContainer.style.justifyContent = 'flex-end'; + buttonContainer.style.gap = '10px'; + + const cancelButton = buttonContainer.createEl('button', { text: 'Cancel' }); + const okButton = buttonContainer.createEl('button', { text: 'OK', cls: 'mod-cta' }); + + cancelButton.addEventListener('click', () => { + resolve(null); + modal.close(); + }); + + okButton.addEventListener('click', () => { + if (!modelSelect.value) { + new Notice('Please select a model first'); + return; + } + const tagInput = input.value.trim(); + if (!tagInput) { + new Notice('Please enter at least one tag'); + return; + } + this.settings.selectedModel = modelSelect.value; + this.saveSettings(); + const tags = tagInput.split(',').map(tag => tag.trim()).filter(tag => tag); + resolve(tags); + modal.close(); + }); + + modal.open(); + }); + } + + async processContentWithOllama(content: string, availableTags: string[]): Promise { + if (!this.settings.selectedModel) { + throw new Error('No Ollama model selected'); + } + + // Skip if already tagged + if (this.isAlreadyTagged(content)) { + return content; + } + + // Get tag suggestions and placement from Ollama + const response = await fetch('http://localhost:11434/api/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: this.settings.selectedModel, + prompt: `You are an expert at analyzing and tagging markdown documents. Your task is to create a brief tagged summary of the document content. + +Available tags: ${availableTags.join(', ')} + +Instructions: +1. Create a brief 1-2 sentence summary of the content +2. Add relevant tags from the provided list +3. Only use tags from the provided list +4. Each tag MUST start with a # symbol +5. Keep the summary concise and focused + +Content to analyze: +${content} + +Provide a tagged summary:`, + stream: false + }), + }); + const data = await response.json(); + + // Get the LLM's tagged summary + const taggedSummary = data.response.trim(); + + // If we got a valid response, combine it with metadata and original content + if (taggedSummary) { + const timestamp = new Date().toISOString(); + return `--- +LLM-tagged: ${timestamp} +--- + +${taggedSummary} + +--- + +${content}`; + } + + return content; + } + + private shouldProcessFile(file: TFile): boolean { + const lastTagged = this.settings.taggedFiles[file.path]; + if (!lastTagged) return true; + + // Check if file has been modified since last tagging + return file.stat.mtime > lastTagged; + } + + private isAlreadyTagged(content: string): boolean { + // Check if content already has our metadata section + return content.includes('---\nLLM-tagged:'); + } + + async addTagsToDocuments(view?: LLMTaggerView) { + if (!this.settings.selectedModel) { + new Notice('Please select an Ollama model first'); + return; + } + + const tags = await this.getUserDefinedTags(); + if (!tags) return; // User cancelled + + const files = this.app.vault.getMarkdownFiles(); + let processed = 0; + let modified = 0; + + try { + for (const file of files) { + processed++; + if (view) { + view.updateProgress(processed, files.length, file.basename); + } + + // Skip if file hasn't been modified since last tagging + if (!this.shouldProcessFile(file)) { + console.log(`Skipping ${file.basename} - already tagged and not modified`); + continue; + } + + try { + const content = await this.app.vault.read(file); + + // Skip if already tagged + if (this.isAlreadyTagged(content)) { + console.log(`Skipping ${file.basename} - already has tag metadata`); + continue; + } + + const taggedContent = await this.processContentWithOllama(content, tags); + + // Only update if content changed + if (taggedContent !== content) { + await this.app.vault.modify(file, taggedContent); + this.settings.taggedFiles[file.path] = Date.now(); + await this.saveSettings(); + modified++; + new Notice(`Tagged: ${file.basename}`); + } + } catch (error) { + console.error(`Error processing ${file.basename}:`, error); + new Notice(`Failed to process ${file.basename}: ${error.message}`); + } + } + + new Notice(`Completed! Tagged ${modified} of ${files.length} files`); + } finally { + if (view) { + view.resetProgress(); + } + } + } +} + +class LLMTaggerView extends ItemView { + plugin: LLMTaggerPlugin; + progressBar: HTMLProgressElement; + progressText: HTMLDivElement; + + constructor(leaf: WorkspaceLeaf, plugin: LLMTaggerPlugin) { + super(leaf); + this.plugin = plugin; + } + + getViewType() { + return VIEW_TYPE; + } + + getDisplayText() { + return 'LLM Tagger'; + } + + getIcon(): string { + return ICON_NAME; + } + + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + container.createEl('h2', { text: 'LLM Tagger' }); + + // Model selection + const modelContainer = container.createDiv(); + modelContainer.createEl('h3', { text: 'Select Model' }); + const modelSelect = modelContainer.createEl('select'); + modelSelect.style.width = '100%'; + modelSelect.style.marginBottom = '1em'; + + // Add placeholder option + const placeholderOption = modelSelect.createEl('option'); + placeholderOption.value = ''; + placeholderOption.text = 'Select a model...'; + placeholderOption.disabled = true; + placeholderOption.selected = !this.plugin.settings.selectedModel; + + try { + const models = await this.plugin.getOllamaModels(); + models.forEach(model => { + const option = modelSelect.createEl('option'); + option.value = model; + option.text = model; + if (model === this.plugin.settings.selectedModel) { + option.selected = true; + } + }); + } catch (error) { + console.error('Failed to load models:', error); + const option = modelSelect.createEl('option'); + option.text = 'Failed to load models'; + option.disabled = true; + } + + modelSelect.addEventListener('change', async () => { + this.plugin.settings.selectedModel = modelSelect.value || null; + await this.plugin.saveSettings(); + }); + + // Tags input + const tagsContainer = container.createDiv(); + tagsContainer.createEl('h3', { text: 'Enter Tags' }); + const tagsInput = tagsContainer.createEl('textarea'); + tagsInput.placeholder = 'Enter tags separated by commas...'; + tagsInput.style.width = '100%'; + tagsInput.style.height = '100px'; + tagsInput.style.marginBottom = '1em'; + if (this.plugin.settings.defaultTags.length > 0) { + tagsInput.value = this.plugin.settings.defaultTags.join(', '); + } + + // Progress section + const progressContainer = container.createDiv(); + progressContainer.createEl('h3', { text: 'Progress' }); + + // Create progress bar + this.progressBar = progressContainer.createEl('progress', { + attr: { value: '0', max: '100' } + }); + this.progressBar.style.width = '100%'; + this.progressBar.style.marginBottom = '0.5em'; + + // Progress text + this.progressText = progressContainer.createDiv('progress-text'); + this.progressText.style.fontSize = '0.9em'; + this.progressText.style.color = 'var(--text-muted)'; + this.progressText.style.marginBottom = '1em'; + this.progressText.textContent = 'Ready to tag documents'; + + // Start button + const startButton = container.createEl('button', { + text: 'Start Tagging', + cls: 'mod-cta' + }); + startButton.style.width = '100%'; + + startButton.addEventListener('click', async () => { + if (!modelSelect.value) { + new Notice('Please select a model first'); + return; + } + + const tagInput = tagsInput.value.trim(); + if (!tagInput) { + new Notice('Please enter at least one tag'); + return; + } + + const tags = tagInput.split(',').map(tag => tag.trim()).filter(tag => tag); + + // Save tags as default if they changed + if (tagInput !== this.plugin.settings.defaultTags.join(', ')) { + this.plugin.settings.defaultTags = tags; + await this.plugin.saveSettings(); + } + + startButton.disabled = true; + try { + await this.plugin.addTagsToDocuments(this); + } finally { + startButton.disabled = false; + } + }); + } + + updateProgress(current: number, total: number, filename: string) { + const percentage = Math.round((current / total) * 100); + this.progressBar.value = percentage; + this.progressText.textContent = `Processing ${filename} (${current}/${total})`; + } + + resetProgress() { + this.progressBar.value = 0; + this.progressText.textContent = 'Ready to tag documents'; + } +} + +class LLMTaggerSettingTab extends PluginSettingTab { + plugin: LLMTaggerPlugin; + + constructor(app: App, plugin: LLMTaggerPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + async display(): Promise { + const {containerEl} = this; + containerEl.empty(); + + containerEl.createEl('h2', {text: 'LLM Tagger Settings'}); + + new Setting(containerEl) + .setName('Default Model') + .setDesc('Select the default Ollama model to use') + .addDropdown(async (dropdown) => { + dropdown.addOption('', 'Select a model...'); + try { + const models = await this.plugin.getOllamaModels(); + models.forEach(model => { + dropdown.addOption(model, model); + }); + if (this.plugin.settings.selectedModel) { + dropdown.setValue(this.plugin.settings.selectedModel); + } + } catch (error) { + console.error('Failed to load models:', error); + dropdown.addOption('error', 'Failed to load models'); + dropdown.setDisabled(true); + } + dropdown.onChange(async (value) => { + this.plugin.settings.selectedModel = value || null; + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName('Default Tags') + .setDesc('Enter default tags (comma-separated) that will be pre-filled when adding tags') + .addTextArea(text => text + .setPlaceholder('tag1, tag2, tag3') + .setValue(this.plugin.settings.defaultTags.join(', ')) + .onChange(async (value) => { + this.plugin.settings.defaultTags = value.split(',') + .map(tag => tag.trim()) + .filter(tag => tag); + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Auto-add Tags') + .setDesc('Automatically add tags to new documents') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.autoAddTags) + .onChange(async (value) => { + this.plugin.settings.autoAddTags = value; + await this.plugin.saveSettings(); + })); + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..ef6c3a5 --- /dev/null +++ b/manifest.json @@ -0,0 +1,12 @@ +{ + "id": "obsidian-llm-tagger", + "name": "LLM Tagger", + "version": "1.0.0", + "minAppVersion": "0.9.7", + "description": "A plugin that uses LLMs with Ollama to add tags from a user-defined list to all documents in a vault.", + "author": "David Jayatillake", + "authorUrl": "https://github.com/david-jayatillake", + "isDesktopOnly": true, + "fundingUrl": "", + "main": "main.js" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8dd267f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,578 @@ +{ + "name": "obsidian-llm-tagger", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "obsidian-llm-tagger", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^16.11.6", + "esbuild": "^0.19.0", + "obsidian": "^1.4.11", + "tslib": "^2.6.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.1.tgz", + "integrity": "sha512-3rA9lcwciEB47ZevqvD8qgbzhM9qMb8vCcQCNmDfVRPQG4JT9mSb0Jg8H7YjKGGQcFnLN323fj9jdnG59Kx6bg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.36.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.2.tgz", + "integrity": "sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "16.18.125", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.125.tgz", + "integrity": "sha512-w7U5ojboSPfZP4zD98d+/cjcN2BDW6lKH2M0ubipt8L8vUC7qUAC6ENKGSJL4tEktH2Saw2K4y1uwSjyRGKMhw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/obsidian": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.7.2.tgz", + "integrity": "sha512-k9hN9brdknJC+afKr5FQzDRuEFGDKbDjfCazJwpgibwCAoZNYHYV8p/s3mM8I6AsnKrPKNXf8xGuMZ4enWelZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1579fa6 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "obsidian-llm-tagger", + "version": "1.0.0", + "description": "Obsidian plugin for automatic tagging using Ollama LLMs", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": ["obsidian", "plugin", "llm", "tagging"], + "author": "", + "license": "MIT", + "devDependencies": { + "@types/node": "^16.11.6", + "obsidian": "^1.4.11", + "typescript": "^5.0.0", + "esbuild": "^0.19.0", + "tslib": "^2.6.0" + } +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..f998caf --- /dev/null +++ b/styles.css @@ -0,0 +1,152 @@ +.llm-tagger-modal { + padding: 1em; +} + +.llm-tagger-modal .setting-item { + border-top: 0; + padding-top: 0.75em; +} + +.llm-tagger-modal select, +.llm-tagger-modal textarea { + width: 100%; + margin-top: 0.5em; + background-color: var(--background-modifier-form-field); + border: 1px solid var(--background-modifier-border); + color: var(--text-normal); + padding: 0.5em; + border-radius: 4px; +} + +.llm-tagger-modal textarea { + min-height: 100px; + resize: vertical; +} + +.llm-tagger-modal .button-container { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 1em; +} + +.llm-tagger-modal button { + padding: 0.5em 1em; + border-radius: 4px; + border: 1px solid var(--background-modifier-border); + background-color: var(--interactive-normal); + color: var(--text-normal); + cursor: pointer; +} + +.llm-tagger-modal button:hover { + background-color: var(--interactive-hover); +} + +.llm-tagger-modal button.mod-cta { + background-color: var(--interactive-accent); + color: var(--text-on-accent); +} + +.llm-tagger-modal button.mod-cta:hover { + background-color: var(--interactive-accent-hover); +} + +.llm-tagger-settings .setting-item { + border-top: 1px solid var(--background-modifier-border); + padding: 1em 0; +} + +.llm-tagger-settings .setting-item:first-child { + border-top: none; + padding-top: 0; +} + +.llm-tagger-settings textarea { + min-height: 80px; + width: 100%; +} + +/* View panel styles */ +.workspace-leaf-content[data-type="llm-tagger-view"] { + padding: 1em; +} + +.workspace-leaf-content[data-type="llm-tagger-view"] h2 { + margin-top: 0; + margin-bottom: 1em; + padding-bottom: 0.5em; + border-bottom: 1px solid var(--background-modifier-border); +} + +.workspace-leaf-content[data-type="llm-tagger-view"] h3 { + margin: 1em 0 0.5em; + font-size: 1.1em; + color: var(--text-muted); +} + +.workspace-leaf-content[data-type="llm-tagger-view"] select, +.workspace-leaf-content[data-type="llm-tagger-view"] textarea { + width: 100%; + background-color: var(--background-modifier-form-field); + border: 1px solid var(--background-modifier-border); + color: var(--text-normal); + padding: 0.5em; + border-radius: 4px; + margin-bottom: 1em; +} + +.workspace-leaf-content[data-type="llm-tagger-view"] textarea { + min-height: 100px; + resize: vertical; +} + +.workspace-leaf-content[data-type="llm-tagger-view"] button { + width: 100%; + padding: 0.75em; + border-radius: 4px; + border: 1px solid var(--background-modifier-border); + background-color: var(--interactive-accent); + color: var(--text-on-accent); + cursor: pointer; + font-weight: 500; + transition: background-color 0.15s ease; +} + +.workspace-leaf-content[data-type="llm-tagger-view"] button:hover { + background-color: var(--interactive-accent-hover); +} + +.workspace-leaf-content[data-type="llm-tagger-view"] .success { + color: var(--text-success); +} + +.workspace-leaf-content[data-type="llm-tagger-view"] .error { + color: var(--text-error); +} + +.workspace-leaf-content[data-type="llm-tagger-view"] progress { + -webkit-appearance: none; + appearance: none; + height: 8px; + border-radius: 4px; + background-color: var(--background-modifier-border); + border: none; +} + +.workspace-leaf-content[data-type="llm-tagger-view"] progress::-webkit-progress-bar { + background-color: var(--background-modifier-border); + border-radius: 4px; +} + +.workspace-leaf-content[data-type="llm-tagger-view"] progress::-webkit-progress-value { + background-color: var(--interactive-accent); + border-radius: 4px; + transition: width 0.2s ease; +} + +.workspace-leaf-content[data-type="llm-tagger-view"] progress::-moz-progress-bar { + background-color: var(--interactive-accent); + border-radius: 4px; + transition: width 0.2s ease; +} diff --git a/test.md b/test.md new file mode 100644 index 0000000..246e046 --- /dev/null +++ b/test.md @@ -0,0 +1,11 @@ +# Machine Learning Project Notes + +Today I worked on implementing a neural network for image classification. The model uses convolutional layers and achieved 95% accuracy on the test set. The training was done using PyTorch and the dataset consisted of 10,000 images. + +Key points: +- Used ResNet architecture +- Batch size of 32 +- Learning rate of 0.001 +- Trained for 50 epochs + +Future improvements could include data augmentation and transfer learning approaches. diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0ef73d0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "skipLibCheck": true, + "lib": [ + "DOM", + "ES5", + "ES6", + "ES7", + "ES2021" + ] + }, + "include": [ + "**/*.ts" + ] +}