mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
235 lines
No EOL
9.1 KiB
TypeScript
235 lines
No EOL
9.1 KiB
TypeScript
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<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
|
this.workSpaceService = Resolve<WorkSpaceService>(Services.WorkSpaceService);
|
|
this.fileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
|
|
this.settingsService = Resolve<SettingsService>(Services.SettingsService);
|
|
this.eventService = Resolve<EventService>(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();
|
|
const view = this.workSpaceService.getActiveViewOfType(MarkdownView);
|
|
if (!actionsView || !view || 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>): void {
|
|
const fileSystemService = this.fileSystemService;
|
|
|
|
new (class extends FuzzySuggestModal<TFile> {
|
|
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<string | null> {
|
|
if (this.settingsService.getApiKeyForCurrentModel().trim() == "") {
|
|
openPluginSettings(this.plugin);
|
|
return null;
|
|
}
|
|
const agent = Resolve<QuickAgent>(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);
|
|
}
|
|
} |