diff --git a/.gitignore b/.gitignore index 6f3e00a..a765044 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ yarn-error.log* dist/ build/ *.js.map +main.js +data.json # IDE and editor files .idea/ diff --git a/main.js b/main.js deleted file mode 100644 index 553163a..0000000 --- a/main.js +++ /dev/null @@ -1,489 +0,0 @@ -/* -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 index 3ff6be1..4f099aa 100644 --- a/main.ts +++ b/main.ts @@ -276,6 +276,40 @@ export default class LLMTaggerPlugin extends Plugin { }); } + private addDeterministicTags(content: string, availableTags: string[]): string { + // Convert content to lowercase for case-insensitive matching + const lowerContent = content.toLowerCase(); + + // Create a set to track which tags we've added to avoid duplicates + const addedTags = new Set(); + + // Find matching tags + for (const tag of availableTags) { + // Remove # if present and convert to lowercase + const cleanTag = tag.replace(/^#/, '').toLowerCase(); + + // Check if the tag exists as a whole word in the content + const regex = new RegExp(`\\b${cleanTag}\\b`, 'i'); + if (regex.test(lowerContent)) { + addedTags.add(tag.startsWith('#') ? tag : `#${tag}`); + } + } + + if (addedTags.size === 0) { + return content; + } + + // Add tags at the start of the content, after any existing frontmatter + const frontMatterMatch = content.match(/^---\n[\s\S]*?\n---\n/); + if (frontMatterMatch) { + const frontMatter = frontMatterMatch[0]; + const restContent = content.slice(frontMatter.length); + return `${frontMatter}${Array.from(addedTags).join(' ')} ${restContent}`; + } else { + return `${Array.from(addedTags).join(' ')} ${content}`; + } + } + async processContentWithOllama(content: string, availableTags: string[]): Promise { if (!this.settings.selectedModel) { throw new Error('No Ollama model selected'); @@ -286,7 +320,10 @@ export default class LLMTaggerPlugin extends Plugin { return content; } - // Get tag suggestions and placement from Ollama + // First, add deterministic tags based on word matches + let processedContent = this.addDeterministicTags(content, availableTags); + + // Get tag suggestions and placement from Ollama for additional semantic tagging const response = await fetch('http://localhost:11434/api/generate', { method: 'POST', headers: { @@ -300,13 +337,13 @@ Available tags: ${availableTags.join(', ')} Instructions: 1. Create a brief 1-2 sentence summary of the content -2. Add relevant tags from the provided list +2. Add relevant tags from the provided list that WEREN'T already matched by word (don't repeat tags) 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} +Content to analyze (with existing tags): +${processedContent} Provide a tagged summary:`, stream: false @@ -328,10 +365,10 @@ ${taggedSummary} --- -${content}`; +${processedContent}`; } - return content; + return processedContent; } private shouldProcessFile(file: TFile): boolean {