Release v1.0.0

This commit is contained in:
djayatillake 2025-01-23 23:06:15 +00:00
parent 4ee9389d2c
commit 0629af074c

512
main.js Normal file
View file

@ -0,0 +1,512 @@
/*
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, `<svg width="100" height="100" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C11.1 2 10.5 2.6 10.5 3.5V4H9C7.9 4 7 4.9 7 6V7H6.5C5.7 7 5 7.7 5 8.5V11.5C5 12.3 5.7 13 6.5 13H7V14C7 15.1 7.9 16 9 16H10V16.5C10 17.3 10.7 18 11.5 18H12.5C13.3 18 14 17.3 14 16.5V16H15C16.1 16 17 15.1 17 14V13H17.5C18.3 13 19 12.3 19 11.5V8.5C19 7.7 18.3 7 17.5 7H17V6C17 4.9 16.1 4 15 4H13.5V3.5C13.5 2.6 12.9 2 12 2Z" fill="currentColor"/>
<circle cx="9.5" cy="9.5" r="1.5" fill="currentColor"/>
<circle cx="14.5" cy="9.5" r="1.5" fill="currentColor"/>
<path d="M12 12C10.6 12 9.5 13.1 9.5 14.5H14.5C14.5 13.1 13.4 12 12 12Z" fill="currentColor"/>
<path d="M6 19H18V21H6V19Z" fill="currentColor"/>
</svg>`);
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();
});
}
addDeterministicTags(content, availableTags) {
const lowerContent = content.toLowerCase();
const addedTags = /* @__PURE__ */ new Set();
for (const tag of availableTags) {
const cleanTag = tag.replace(/^#/, "").toLowerCase();
const regex = new RegExp(`\\b${cleanTag}\\b`, "i");
if (regex.test(lowerContent)) {
addedTags.add(tag.startsWith("#") ? tag : `#${tag}`);
}
}
if (addedTags.size === 0) {
return content;
}
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, availableTags) {
if (!this.settings.selectedModel) {
throw new Error("No Ollama model selected");
}
if (this.isAlreadyTagged(content)) {
return content;
}
let processedContent = this.addDeterministicTags(content, availableTags);
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 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 (with existing tags):
${processedContent}
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}
---
${processedContent}`;
}
return processedContent;
}
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();
}));
}
};