interface UserInteractionRequest { id: string; type: 'confirmation' | 'choice' | 'input' | 'tool_confirmation'; title: string; message: string; options?: Array<{ id: string; label: string; description?: string; style?: 'primary' | 'secondary' | 'warning' | 'danger'; action?: string; }>; defaultValue?: string; timeout?: number; // milliseconds tool?: { name: string; description: string; arguments: Record; riskLevel?: 'low' | 'medium' | 'high'; }; } interface UserInteractionResponse { id: string; response: string; value?: any; timestamp: number; } /** * User Interaction Manager for LLM Chat * Handles confirmations, choices, and input prompts during LLM operations */ export class InteractionManager { private activeInteractions: Map = new Map(); private responseCallbacks: Map void> = new Map(); private modalContainer: HTMLElement; private overlay: HTMLElement; constructor(parentElement: HTMLElement) { this.createModalContainer(parentElement); } /** * Create modal container and overlay */ private createModalContainer(parentElement: HTMLElement): void { // Create overlay this.overlay = document.createElement('div'); this.overlay.className = 'llm-interaction-overlay'; this.overlay.style.display = 'none'; // Create modal container this.modalContainer = document.createElement('div'); this.modalContainer.className = 'llm-interaction-modal-container'; this.overlay.appendChild(this.modalContainer); parentElement.appendChild(this.overlay); // Close on overlay click this.overlay.addEventListener('click', (e) => { if (e.target === this.overlay) { this.cancelAllInteractions(); } }); // Handle escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.hasActiveInteractions()) { this.cancelAllInteractions(); } }); } /** * Request user interaction */ public async requestUserInteraction(request: UserInteractionRequest): Promise { this.activeInteractions.set(request.id, request); return new Promise((resolve, reject) => { // Set up response callback this.responseCallbacks.set(request.id, resolve); // Create and show modal const modal = this.createInteractionModal(request); this.showModal(modal); // Set up timeout if specified if (request.timeout && request.timeout > 0) { setTimeout(() => { if (this.activeInteractions.has(request.id)) { this.handleTimeout(request.id); } }, request.timeout); } }); } /** * Create interaction modal based on request type */ private createInteractionModal(request: UserInteractionRequest): HTMLElement { const modal = document.createElement('div'); modal.className = `llm-interaction-modal llm-interaction-${request.type}`; modal.setAttribute('data-interaction-id', request.id); switch (request.type) { case 'tool_confirmation': return this.createToolConfirmationModal(modal, request); case 'confirmation': return this.createConfirmationModal(modal, request); case 'choice': return this.createChoiceModal(modal, request); case 'input': return this.createInputModal(modal, request); default: return this.createGenericModal(modal, request); } } /** * Create tool confirmation modal */ private createToolConfirmationModal(modal: HTMLElement, request: UserInteractionRequest): HTMLElement { const tool = request.tool!; const riskClass = tool.riskLevel ? `risk-${tool.riskLevel}` : ''; modal.innerHTML = ` `; this.attachButtonEvents(modal, request); return modal; } /** * Create confirmation modal */ private createConfirmationModal(modal: HTMLElement, request: UserInteractionRequest): HTMLElement { modal.innerHTML = ` `; this.attachButtonEvents(modal, request); return modal; } /** * Create choice modal */ private createChoiceModal(modal: HTMLElement, request: UserInteractionRequest): HTMLElement { modal.innerHTML = ` `; this.attachChoiceEvents(modal, request); return modal; } /** * Create input modal */ private createInputModal(modal: HTMLElement, request: UserInteractionRequest): HTMLElement { modal.innerHTML = ` `; this.attachInputEvents(modal, request); return modal; } /** * Create generic modal */ private createGenericModal(modal: HTMLElement, request: UserInteractionRequest): HTMLElement { modal.innerHTML = ` `; this.attachButtonEvents(modal, request); return modal; } /** * Format tool arguments for display */ private formatToolArguments(args: Record): string { const formatted = Object.entries(args).map(([key, value]) => { let displayValue: string; if (typeof value === 'string') { displayValue = value.length > 100 ? value.substring(0, 100) + '...' : value; displayValue = `"${displayValue}"`; } else if (typeof value === 'object') { displayValue = JSON.stringify(value, null, 2); } else { displayValue = String(value); } return `
${key}: ${displayValue}
`; }).join(''); return formatted || '
No parameters
'; } /** * Create action buttons based on request options */ private createActionButtons(request: UserInteractionRequest): string { if (request.options && request.options.length > 0) { return request.options.map(option => ` `).join(''); } else { // Default confirmation buttons return ` `; } } /** * Create timeout indicator */ private createTimeoutIndicator(timeout?: number): string { if (!timeout || timeout <= 0) return ''; return `
Auto-cancel in:
${Math.ceil(timeout / 1000)}s
`; } /** * Show modal */ private showModal(modal: HTMLElement): void { this.modalContainer.innerHTML = ''; this.modalContainer.appendChild(modal); this.overlay.style.display = 'flex'; // Trigger animation setTimeout(() => { this.overlay.classList.add('show'); modal.classList.add('show'); }, 10); // Start timeout countdown if present this.startTimeoutCountdown(modal); // Focus first input if present const firstInput = modal.querySelector('input, button') as HTMLElement; if (firstInput) { firstInput.focus(); } } /** * Hide modal */ private hideModal(): void { this.overlay.classList.remove('show'); const modal = this.modalContainer.querySelector('.llm-interaction-modal') as HTMLElement; if (modal) { modal.classList.remove('show'); } setTimeout(() => { this.overlay.style.display = 'none'; this.modalContainer.innerHTML = ''; }, 300); } /** * Attach button events */ private attachButtonEvents(modal: HTMLElement, request: UserInteractionRequest): void { const buttons = modal.querySelectorAll('.action-btn, .confirm-btn, .cancel-btn'); buttons.forEach(button => { button.addEventListener('click', (e) => { const target = e.target as HTMLElement; const response = target.getAttribute('data-response') || 'cancel'; this.respondToInteraction(request.id, response); }); }); } /** * Attach choice events */ private attachChoiceEvents(modal: HTMLElement, request: UserInteractionRequest): void { const options = modal.querySelectorAll('.choice-option'); options.forEach(option => { option.addEventListener('click', (e) => { const target = e.currentTarget as HTMLElement; const optionId = target.getAttribute('data-option-id'); if (optionId) { this.respondToInteraction(request.id, optionId); } }); }); // Cancel button const cancelBtn = modal.querySelector('.cancel-btn'); if (cancelBtn) { cancelBtn.addEventListener('click', () => { this.respondToInteraction(request.id, 'cancel'); }); } } /** * Attach input events */ private attachInputEvents(modal: HTMLElement, request: UserInteractionRequest): void { const input = modal.querySelector('input') as HTMLInputElement; const submitBtn = modal.querySelector('.submit-btn') as HTMLElement; const cancelBtn = modal.querySelector('.cancel-btn') as HTMLElement; const submitValue = () => { const value = input.value.trim(); this.respondToInteraction(request.id, 'submit', value); }; submitBtn.addEventListener('click', submitValue); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); submitValue(); } }); cancelBtn.addEventListener('click', () => { this.respondToInteraction(request.id, 'cancel'); }); } /** * Start timeout countdown */ private startTimeoutCountdown(modal: HTMLElement): void { const countdown = modal.querySelector('.timeout-countdown') as HTMLElement; if (!countdown) return; const timeout = parseInt(countdown.getAttribute('data-timeout') || '0'); if (timeout <= 0) return; const startTime = Date.now(); const interval = setInterval(() => { const elapsed = Date.now() - startTime; const remaining = Math.max(0, timeout - elapsed); const progress = (elapsed / timeout) * 100; // Update countdown bar const fill = countdown.querySelector('.countdown-fill') as HTMLElement; if (fill) { fill.style.width = `${Math.min(100, progress)}%`; } // Update countdown text const text = countdown.querySelector('.countdown-text') as HTMLElement; if (text) { text.textContent = `${Math.ceil(remaining / 1000)}s`; } // Stop when timeout reached if (remaining <= 0) { clearInterval(interval); } }, 100); // Store interval for cleanup countdown.setAttribute('data-interval', interval.toString()); } /** * Respond to interaction */ private respondToInteraction(id: string, response: string, value?: any): void { const callback = this.responseCallbacks.get(id); if (!callback) return; const interactionResponse: UserInteractionResponse = { id, response, value, timestamp: Date.now() }; // Clean up this.activeInteractions.delete(id); this.responseCallbacks.delete(id); this.hideModal(); // Call callback callback(interactionResponse); } /** * Handle interaction timeout */ private handleTimeout(id: string): void { this.respondToInteraction(id, 'timeout'); } /** * Cancel all active interactions */ public cancelAllInteractions(): void { const activeIds = Array.from(this.activeInteractions.keys()); activeIds.forEach(id => { this.respondToInteraction(id, 'cancel'); }); } /** * Check if there are active interactions */ public hasActiveInteractions(): boolean { return this.activeInteractions.size > 0; } /** * Get active interaction count */ public getActiveInteractionCount(): number { return this.activeInteractions.size; } } // Export types for use in other modules export type { UserInteractionRequest, UserInteractionResponse };