Auto tagging obsidian notes w/ AI
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: address UI improvements and code optimization

- Update ribbon icon to perform useful action instead of showing notice
- Use FileManager.processFrontMatter for atomic frontmatter updates
- Implement persistent notices with auto-updates
- Apply sentence case to UI text for consistency
- Fix plugin ID in settings
- Remove unnecessary heading in settings tab

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>

+60 -66
+60 -66
main.ts
··· 1 - import * as yaml from "js-yaml"; 2 1 import { 3 2 App, 4 3 MarkdownView, ··· 40 39 if (!this.settings.apiKey) { 41 40 new ConfirmModal( 42 41 this.app, 43 - "AI Tagging API Key Missing", 42 + "AI tagging API key missing", 44 43 () => {}, 45 44 true, 46 45 "API key not configured. Please add your API key in the plugin settings." 47 46 ).open(); 48 47 return; 49 48 } 50 - new Notice( 51 - "Select a command to auto-tag the current note or all notes" 52 - ); 49 + // Tag the current note directly when clicking the ribbon icon 50 + this.tagCurrentNote(); 53 51 } 54 52 ); 55 53 ribbonIconEl.addClass("ai-tagger-ribbon-class"); ··· 66 64 if (!this.settings.apiKey) { 67 65 new ConfirmModal( 68 66 this.app, 69 - "AI Tagging API Key Missing", 67 + "AI tagging API key missing", 70 68 () => {}, 71 69 true, 72 70 "API key not configured. Please add your API key in the plugin settings." ··· 89 87 if (!this.settings.apiKey) { 90 88 new ConfirmModal( 91 89 this.app, 92 - "AI Tagging API Key Missing", 90 + "AI tagging API key missing", 93 91 () => {}, 94 92 true, 95 93 "API key not configured. Please add your API key in the plugin settings." ··· 138 136 const file = activeView.file; 139 137 const content = await this.app.vault.read(file); 140 138 139 + // Create a persistent notice 140 + const notice = new Notice("Analyzing note content and generating tags...", 0); 141 + 141 142 try { 142 - new Notice("Analyzing note content and generating tags..."); 143 143 const tags = await this.generateTags(content); 144 144 await this.updateNoteFrontmatter(file, tags); 145 - new Notice(`Successfully added tags: ${tags.join(", ")}`); 145 + 146 + // Update the notice with success message 147 + notice.setMessage(`Successfully added tags: ${tags.join(", ")}`); 148 + 149 + // Hide after 3 seconds 150 + setTimeout(() => { 151 + notice.hide(); 152 + }, 3000); 146 153 } catch (error) { 147 154 console.error("Error tagging note:", error); 148 155 const errorMessage = error instanceof Error ? error.message : String(error); 149 - new Notice(`Error tagging note: ${errorMessage}`); 156 + 157 + // Update the notice with error message 158 + notice.setMessage(`Error tagging note: ${errorMessage}`); 159 + 160 + // Hide after 5 seconds for error messages (giving more time to read) 161 + setTimeout(() => { 162 + notice.hide(); 163 + }, 5000); 150 164 } 151 165 } 152 166 ··· 161 175 const files = this.app.vault.getMarkdownFiles(); 162 176 let processed = 0; 163 177 let successful = 0; 164 - 165 - new Notice(`Starting to tag ${files.length} notes...`); 178 + 179 + // Create a persistent notice that we'll update 180 + const notice = new Notice(`Starting to tag ${files.length} notes...`, 0); 166 181 167 182 for (const file of files) { 168 183 try { 184 + // Update the notice with current file 185 + notice.setMessage(`Processing: ${file.path}\nProgress: ${processed}/${files.length} (${successful} successful)`); 186 + 169 187 const content = await this.app.vault.read(file); 170 188 const tags = await this.generateTags(content); 171 189 await this.updateNoteFrontmatter(file, tags); ··· 175 193 } 176 194 177 195 processed++; 178 - if (processed % 10 === 0) { 179 - new Notice( 180 - `Processed ${processed}/${files.length} notes (${successful} successful)` 181 - ); 182 - } 183 196 } 184 197 185 - new Notice( 186 - `Completed tagging ${successful}/${files.length} notes successfully` 187 - ); 198 + // Final notice with completion message 199 + notice.setMessage(`Completed tagging ${successful}/${files.length} notes successfully`); 200 + 201 + // Hide the notice after 3 seconds 202 + setTimeout(() => { 203 + notice.hide(); 204 + }, 3000); 188 205 } 189 206 190 207 async generateTags(content: string): Promise<string[]> { ··· 244 261 } 245 262 246 263 async updateNoteFrontmatter(file: TFile, newTags: string[]): Promise<void> { 247 - // Read the file content 248 - const content = await this.app.vault.read(file); 249 - 250 - let frontmatter: Record<string, unknown> = {}; 251 - let fileContent = content; 252 - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/); 264 + // Use FileManager.processFrontMatter to atomically update the frontmatter 265 + await this.app.fileManager.processFrontMatter(file, (frontmatter) => { 266 + // Add new tags to existing tags or create new tags field 267 + if (!frontmatter.tags) { 268 + frontmatter.tags = newTags; 269 + } else { 270 + // Handle case where tags is a string 271 + if (typeof frontmatter.tags === "string") { 272 + frontmatter.tags = [frontmatter.tags, ...newTags]; 273 + } 274 + // Handle case where tags is already an array 275 + else if (Array.isArray(frontmatter.tags)) { 276 + frontmatter.tags = [...frontmatter.tags, ...newTags]; 277 + } 253 278 254 - // If frontmatter exists, parse it 255 - if (frontmatterMatch) { 256 - try { 257 - frontmatter = yaml.load(frontmatterMatch[1]); 258 - fileContent = content.replace(/^---\n[\s\S]*?\n---\n/, ""); 259 - } catch (e) { 260 - console.error("Error parsing frontmatter:", e); 261 - throw new Error("Could not parse existing frontmatter"); 279 + // Remove duplicates 280 + frontmatter.tags = [...new Set(frontmatter.tags)]; 262 281 } 263 - } 264 - 265 - // Add new tags to existing tags or create new tags field 266 - if (!frontmatter.tags) { 267 - frontmatter.tags = newTags; 268 - } else { 269 - // Handle case where tags is a string 270 - if (typeof frontmatter.tags === "string") { 271 - frontmatter.tags = [frontmatter.tags, ...newTags]; 272 - } 273 - // Handle case where tags is already an array 274 - else if (Array.isArray(frontmatter.tags)) { 275 - frontmatter.tags = [...frontmatter.tags, ...newTags]; 276 - } 277 - 278 - // Remove duplicates 279 - frontmatter.tags = [...new Set(frontmatter.tags)]; 280 - } 281 - 282 - // Convert frontmatter back to YAML 283 - const newFrontmatter = yaml.dump(frontmatter); 284 - const newContent = `---\n${newFrontmatter}---\n${fileContent}`; 285 - 286 - // Write the updated content back to the file 287 - await this.app.vault.modify(file, newContent); 282 + }); 288 283 } 289 284 } 290 285 ··· 321 316 buttonContainer.addClass("ai-tagger-modal-buttons"); 322 317 323 318 const settingsButton = buttonContainer.createEl("button", { 324 - text: "Open Settings", 319 + text: "Open settings", 325 320 }); 326 321 settingsButton.addEventListener("click", () => { 327 322 this.close(); ··· 331 326 if ('setting' in this.app) { 332 327 const appWithSetting = this.app as unknown as { setting: { open: () => void; openTabById: (id: string) => void } }; 333 328 appWithSetting.setting.open(); 334 - appWithSetting.setting.openTabById("obsidian-sample-plugin"); 329 + appWithSetting.setting.openTabById("ai-tagger"); 335 330 } 336 331 }); 337 332 ··· 380 375 const { containerEl } = this; 381 376 382 377 containerEl.empty(); 383 - containerEl.createEl("h2", { text: "AI Tagging Settings" }); 384 378 385 379 new Setting(containerEl) 386 - .setName("API Key") 380 + .setName("API key") 387 381 .setDesc( 388 382 "Your Anthropic API key. Required to use the AI service. Get it from https://console.anthropic.com/ if you don't have one already. We recommend using a dedicated key for this plugin." 389 383 ) ··· 398 392 ); 399 393 400 394 new Setting(containerEl) 401 - .setName("AI Model") 395 + .setName("AI model") 402 396 .setDesc("Choose which AI model to use for tag generation.") 403 397 .addDropdown((dropdown) => 404 398 dropdown ··· 414 408 ); 415 409 416 410 new Setting(containerEl) 417 - .setName("Maximum Number of Tags") 411 + .setName("Maximum number of tags") 418 412 .setDesc("Set the maximum number of tags to generate per note.") 419 413 .addSlider((slider) => 420 414 slider ··· 428 422 ); 429 423 430 424 new Setting(containerEl) 431 - .setName("Prompt Template") 425 + .setName("Prompt template") 432 426 .setDesc( 433 427 "Customize the prompt sent to the AI. Use {maxTags} and {content} as placeholders." 434 428 )