import { Resolve } from "./DependencyService"; import { Services } from "./Services"; import type { QuickAgent } from "./AIServices/QuickAgent"; import type VaultkeeperAIPlugin from "main"; import { FuzzySuggestModal, MarkdownView, Menu, Notice, setIcon, TFile, type Editor, type EventRef, type MarkdownFileInfo } from "obsidian"; import { FileSystemService } from "./FileSystemService"; import { BeautifyPrompt } from "AIPrompts/QuickActionPrompts/Beautify"; import Spinner from "Components/Spinner.svelte"; import { mount } from "svelte"; import { ApplyTemplatePrompt } from "AIPrompts/QuickActionPrompts/ApplyTemplatePrompt"; import { Copy } from "Enums/Copy"; import type { WorkSpaceService } from "./WorkSpaceService"; import type { SettingsService } from "./SettingsService"; import type { EventService } from "./EventService"; import { Event } from "Enums/Event"; import { openPluginSettings } from "Helpers/Helpers"; export class QuickActionsService { private plugin: VaultkeeperAIPlugin; private workSpaceService: WorkSpaceService; private fileSystemService: FileSystemService; private settingsService: SettingsService; private eventService: EventService; private editorMenuEventRef: EventRef | null = null; private layoutChangeEventRef: EventRef | null = null; public constructor() { this.plugin = Resolve(Services.VaultkeeperAIPlugin); this.workSpaceService = Resolve(Services.WorkSpaceService); this.fileSystemService = Resolve(Services.FileSystemService); this.settingsService = Resolve(Services.SettingsService); this.eventService = Resolve(Services.EventService); this.plugin.registerEvent( this.eventService.on(Event.QuickActionsSettingsChanged, () => { this.updateRegistrations(); }) ); this.registerEditorMenuActions(); this.registerViewActions(); } /* Action Definitions */ private async beautify(menu: Menu, editor: Editor, view: MarkdownView | MarkdownFileInfo) { const file = view.file; if (!file) { return; } const selection = editor.getSelection(); const content = await this.fileSystemService.readFile(file); if (content instanceof Error || (selection.trim() === "" && content.trim() === "")) { return; // Either an excluded file or nothing to beautify } const notice = this.showNotice("Beautifying content..."); try { if (selection.length > 0) { const result = await this.newAction(BeautifyPrompt, selection); if (result) { await this.fileSystemService.patchFile(file, [selection], [result], false, false); } } else { const result = await this.newAction(BeautifyPrompt, content); if (result) { await this.fileSystemService.writeToFile(file, result, false, false); } } } finally { notice.hide(); } } private async applyTemplate(menu: Menu, editor: Editor, view: MarkdownView | MarkdownFileInfo) { const file = view.file; if (!file) { return; } const content = await this.fileSystemService.readFile(file); if (content instanceof Error || content.trim() === "") { return; // Either an excluded file or nothing to apply a template to } this.userSelectFile(this.plugin, async (templateFile) => { const templateContent = await this.fileSystemService.readFile(templateFile); if (templateContent instanceof Error || templateContent.trim() === "") { return; // Either an excluded file or the template is empty } const notice = this.showNotice("Applying template..."); try { const context = `${Copy.ApplyTemplateTemplateSeparator}\n${templateContent}\n${Copy.ApplyTemplateContentSeparator}\n${content}`; const result = await this.newAction(ApplyTemplatePrompt, context); if (result && result.trim() !== Copy.ApplyTemplateCancelled.toString()) { await this.fileSystemService.writeToFile(file, result, false, false); } } finally { notice.hide(); } }); } /* Action Registration */ private registerEditorMenuActions() { if (!this.settingsService.settings.enableContextMenuActions) { if (this.editorMenuEventRef) { this.plugin.app.workspace.offref(this.editorMenuEventRef); this.editorMenuEventRef = null; } return; } if (this.editorMenuEventRef) { return; } this.editorMenuEventRef = this.plugin.app.workspace.on("editor-menu", (menu, editor, view) => { menu.addItem((item) => { item.setTitle("Beautify") .setIcon("palette") .onClick(async () => this.beautify(menu, editor, view)); }); menu.addItem((item) => { item.setTitle("Apply template") .setIcon("notepad-text-dashed") .onClick(async () => this.applyTemplate(menu, editor, view)); }); }); this.plugin.registerEvent(this.editorMenuEventRef); } private registerViewActions() { if (!this.settingsService.settings.enableToolbarActions) { if (this.layoutChangeEventRef) { this.plugin.app.workspace.offref(this.layoutChangeEventRef); this.layoutChangeEventRef = null; // Remove any existing toolbar buttons this.plugin.app.workspace.containerEl .querySelectorAll(".vault-keeper-ai-actions") .forEach(el => el.remove()); } return; } if (this.layoutChangeEventRef) { return; } this.layoutChangeEventRef = this.plugin.app.workspace.on("layout-change", () => { this.injectToolbarButton(); }); this.plugin.registerEvent(this.layoutChangeEventRef); this.injectToolbarButton(); } private injectToolbarButton() { const actionsView = this.workSpaceService.getViewActionsView(); if (!actionsView || actionsView.querySelector(".vault-keeper-ai-actions")) { return; } const button = createEl("button", { cls: "clickable-icon view-action vault-keeper-ai-actions" }); button.setAttribute('aria-label', 'AI quick actions'); button.addEventListener('click', (evt) => { const view = this.workSpaceService.getActiveViewOfType(MarkdownView); if (view) { const { editor } = view; const menu = new Menu(); menu.addItem((item) => item.setTitle("Beautify") .setIcon("palette") .onClick(async () => this.beautify(menu, editor, view)) ); menu.addItem((item) => item.setTitle("Apply template") .setIcon("notepad-text-dashed") .onClick(async () => this.applyTemplate(menu, editor, view)) ); menu.showAtMouseEvent(evt); } }); setIcon(button, "sparkles"); actionsView.prepend(button); } private updateRegistrations() { this.registerEditorMenuActions(); this.registerViewActions(); } /* Helpers */ private userSelectFile(plugin: VaultkeeperAIPlugin, onSelected: (file: TFile) => Promise): void { const fileSystemService = this.fileSystemService; new (class extends FuzzySuggestModal { getItems() { return fileSystemService.getMarkdownFiles(); } getItemText(f: TFile) { return f.path; } onChooseItem(f: TFile) { void onSelected(f); } })(plugin.app).open(); } private async newAction(action: string, context: string): Promise { if (this.settingsService.getApiKeyForCurrentModel().trim() == "") { openPluginSettings(this.plugin); return null; } const agent = Resolve(Services.QuickAgent); agent.resolveAIProvider(); return agent.quickAction(action, context); } private showNotice(message: string): Notice { const fragment = activeDocument.createDocumentFragment(); const container = activeDocument.createElement("div"); container.addClass("quick-action-notice"); mount(Spinner, { target: container, props: { width: "var(--size-4-4)", height: "var(--size-4-4)", background: "var(--background-modifier-message)" }}); container.createSpan({ text: message }); fragment.appendChild(container); return new Notice(fragment, 0); } }