From d73a5b68599b849f92b054c295288dba20251db1 Mon Sep 17 00:00:00 2001 From: Chris Lettieri Date: Wed, 16 Apr 2025 11:40:53 -0400 Subject: [PATCH] better UX - processing status, open when done --- src/main.ts | 45 ++++++++++++- src/ui/loading-indicator.ts | 124 ++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/ui/loading-indicator.ts diff --git a/src/main.ts b/src/main.ts index 396b3a4..3d7a180 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,16 @@ -import { Plugin, Notice, TFile } from 'obsidian'; +import { Plugin, Notice, TFile, WorkspaceLeaf } from 'obsidian'; import { OpenAugiSettings, DEFAULT_SETTINGS } from './types/settings'; import { OpenAIService } from './services/openai-service'; import { FileService } from './services/file-service'; import { OpenAugiSettingTab } from './ui/settings-tab'; +import { LoadingIndicator } from './ui/loading-indicator'; +import { sanitizeFilename } from './utils/filename-utils'; export default class OpenAugiPlugin extends Plugin { settings: OpenAugiSettings; openAIService: OpenAIService; fileService: FileService; + loadingIndicator: LoadingIndicator; async onload() { // Load settings @@ -16,6 +19,14 @@ export default class OpenAugiPlugin extends Plugin { // Initialize services this.initializeServices(); + // Initialize loading indicator + this.app.workspace.onLayoutReady(() => { + const statusBar = this.addStatusBarItem(); + if (statusBar.parentElement) { + this.loadingIndicator = new LoadingIndicator(statusBar.parentElement); + } + }); + // Add command to manually parse a transcript file this.addCommand({ id: 'parse-transcript', @@ -46,16 +57,36 @@ export default class OpenAugiPlugin extends Plugin { ); } + /** + * Open a file in a new tab + * @param filePath The path to the file to open + */ + private async openFileInNewTab(filePath: string): Promise { + const file = this.app.vault.getAbstractFileByPath(filePath); + if (!file || !(file instanceof TFile)) { + console.error(`File not found: ${filePath}`); + return; + } + + const leaf = this.app.workspace.getLeaf('tab'); + await leaf.openFile(file); + } + /** * Process a transcript file * @param file The file to process */ private async processTranscriptFile(file: TFile): Promise { try { + // Show loading indicator + this.loadingIndicator?.show('Processing voice transcript'); + + // Read file content const content = await this.app.vault.read(file); // Check if API key is set if (!this.settings.apiKey) { + this.loadingIndicator?.hide(); new Notice('Please set your OpenAI API key in the plugin settings'); return; } @@ -69,8 +100,20 @@ export default class OpenAugiPlugin extends Plugin { // Write result to files await this.fileService.writeTranscriptFiles(file.basename, parsedData); + // Hide loading indicator + this.loadingIndicator?.hide(); + + // Show success message new Notice(`Successfully parsed transcript: ${file.basename}`); + + // Open the summary file in a new tab + const sanitizedFilename = sanitizeFilename(file.basename); + const summaryPath = `${this.settings.summaryFolder}/${sanitizedFilename} - summary.md`; + await this.openFileInNewTab(summaryPath); } catch (error) { + // Hide loading indicator + this.loadingIndicator?.hide(); + console.error('Failed to parse transcript:', error); new Notice('Failed to parse transcript. Check console for details.'); } diff --git a/src/ui/loading-indicator.ts b/src/ui/loading-indicator.ts new file mode 100644 index 0000000..dff45b3 --- /dev/null +++ b/src/ui/loading-indicator.ts @@ -0,0 +1,124 @@ +import { WorkspaceLeaf } from 'obsidian'; + +/** + * Modal loading indicator that shows processing status + */ +export class LoadingIndicator { + private statusBarItem: HTMLElement | null = null; + private loadingContainer: HTMLElement | null = null; + private dotElements: HTMLElement[] = []; + private animationInterval: number | null = null; + private statusBar: HTMLElement; + + constructor(statusBar: HTMLElement) { + this.statusBar = statusBar; + } + + /** + * Show the loading indicator with a message + * @param message The message to display + */ + show(message: string): void { + this.hide(); // Clear any existing indicators first + + // Create status bar item + this.statusBarItem = this.statusBar.createEl('div', { + cls: 'status-bar-item mod-clickable', + attr: { + id: 'openaugi-status', + style: 'display: flex; align-items: center; gap: 8px; color: var(--text-accent);' + } + }); + + // Create icon + const iconEl = this.statusBarItem.createEl('span', { + cls: 'status-bar-item-icon', + attr: { + style: 'display: flex; align-items: center;' + } + }); + + // Create loading spinner + const spinner = iconEl.createEl('span', { + cls: 'openaugi-spinner', + attr: { + style: ` + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid var(--text-accent); + border-radius: 50%; + border-top-color: transparent; + animation: openaugi-spin 1s linear infinite; + ` + } + }); + + // Add CSS animation + const styleEl = document.head.createEl('style'); + styleEl.textContent = ` + @keyframes openaugi-spin { + to { + transform: rotate(360deg); + } + } + `; + + // Create text container + const textEl = this.statusBarItem.createEl('span', { + text: message, + attr: { + style: 'font-size: 13px; font-weight: 500;' + } + }); + + // Create dot animation container + this.loadingContainer = this.statusBarItem.createEl('span', { + attr: { style: 'display: flex; gap: 2px;' } + }); + + // Create three dots for the animation + for (let i = 0; i < 3; i++) { + const dot = this.loadingContainer.createEl('span', { + text: '.', + attr: { + style: 'opacity: 0; transition: opacity 0.3s ease; font-weight: bold;' + } + }); + this.dotElements.push(dot); + } + + // Start dot animation + let currentDot = 0; + this.animationInterval = window.setInterval(() => { + // Reset all dots + this.dotElements.forEach(dot => dot.style.opacity = '0'); + + // Show dots up to current dot + for (let i = 0; i <= currentDot; i++) { + this.dotElements[i].style.opacity = '1'; + } + + // Increment and wrap around + currentDot = (currentDot + 1) % this.dotElements.length; + }, 500); + } + + /** + * Hide the loading indicator + */ + hide(): void { + if (this.animationInterval) { + clearInterval(this.animationInterval); + this.animationInterval = null; + } + + if (this.statusBarItem) { + this.statusBarItem.remove(); + this.statusBarItem = null; + } + + this.dotElements = []; + this.loadingContainer = null; + } +} \ No newline at end of file