import { Injectable, signal } from '@angular/core';
import { HumanRequest } from '@ask-me-mcp/askme-shared';
/**
* Options for showing notifications
*/
export interface NotificationOptions {
body?: string;
icon?: string;
tag?: string;
requireInteraction?: boolean;
timeout?: number;
onClick?: () => void;
}
/**
* Service for handling browser notifications
*
* @remarks
* This service wraps the browser's Notification API to alert users
* when new MCP requests arrive. It handles permission requests and
* provides a consistent interface for showing notifications.
*
* @example
* ```typescript
* const notificationService = inject(NotificationService);
*
* // Request permission on first visit
* if (notificationService.permission() === 'default') {
* await notificationService.requestPermission();
* }
*
* // Show notification for new request
* notificationService.notifyNewRequest(request);
* ```
*/
@Injectable({
providedIn: 'root'
})
export class NotificationService {
/**
* Current notification permission status
*/
readonly permission = signal<NotificationPermission>('default');
constructor() {
// Check initial permission state
if (this.isSupported()) {
this.permission.set(Notification.permission);
}
}
/**
* Check if notifications are supported
*/
isSupported(): boolean {
return 'Notification' in window;
}
/**
* Request notification permission from the user
*
* @returns Promise resolving to the permission result
*/
async requestPermission(): Promise<NotificationPermission> {
if (!this.isSupported()) {
this.permission.set('denied');
return 'denied';
}
try {
const permission = await Notification.requestPermission();
this.permission.set(permission);
return permission;
} catch (error) {
console.error('Failed to request notification permission:', error);
this.permission.set('denied');
return 'denied';
}
}
/**
* Show a notification
*
* @param title - Notification title
* @param options - Additional notification options
*/
showNotification(title: string, options: NotificationOptions = {}): void {
if (!this.isSupported() || this.permission() !== 'granted') {
return;
}
try {
const notification = new Notification(title, {
body: options.body,
icon: options.icon || '/assets/icons/mcp-icon.png',
tag: options.tag,
requireInteraction: options.requireInteraction || false,
});
// Handle click
notification.onclick = (event) => {
event.preventDefault();
if (options.onClick) {
options.onClick();
}
notification.close();
window.focus();
};
// Auto-close after timeout
if (options.timeout) {
setTimeout(() => notification.close(), options.timeout);
}
} catch (error) {
console.error('Failed to show notification:', error);
}
}
/**
* Show notification for a new MCP request
*
* @param request - The human request to notify about
* @param onClick - Optional click handler
*/
notifyNewRequest(request: HumanRequest, onClick?: () => void): void {
const maxLength = 100;
const body = request.question.length > maxLength
? request.question.substring(0, maxLength) + '...'
: request.question;
this.showNotification('New MCP Request', {
body,
icon: '/assets/icons/mcp-icon.png',
tag: `mcp-request-${request.id}`,
requireInteraction: true,
onClick: onClick || (() => {
// Default: focus the window
window.focus();
})
});
}
}