mirror of
https://github.com/djayatillake/obsidian-llm-tagger.git
synced 2026-07-22 05:37:59 +00:00
initial
This commit is contained in:
commit
ac07b8c97b
11 changed files with 1978 additions and 0 deletions
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
47
esbuild.config.mjs
Normal file
47
esbuild.config.mjs
Normal file
|
|
@ -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();
|
||||
}
|
||||
8
icon.svg
Normal file
8
icon.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 780 B |
489
main.js
Normal file
489
main.js
Normal file
|
|
@ -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, `<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();
|
||||
});
|
||||
}
|
||||
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();
|
||||
}));
|
||||
}
|
||||
};
|
||||
612
main.ts
Normal file
612
main.ts
Normal file
|
|
@ -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, `<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>`);
|
||||
|
||||
// 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<string[]> {
|
||||
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<string[] | null> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
12
manifest.json
Normal file
12
manifest.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
578
package-lock.json
generated
Normal file
578
package-lock.json
generated
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
21
package.json
Normal file
21
package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
152
styles.css
Normal file
152
styles.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
11
test.md
Normal file
11
test.md
Normal file
|
|
@ -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.
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue