/** * Interactive Feedback UI for PIPS Interactive Mode * * This module handles the user interface for providing feedback on * AI-generated code and critic suggestions during interactive solving. */ class InteractiveFeedback { constructor() { this.feedbackPanel = null; this.currentIteration = null; this.currentCode = ''; this.currentSymbols = {}; this.criticText = ''; this.selectedRanges = []; this.isVisible = false; this.isResizing = false; this.sidebarWidth = 380; // Default width this.minWidth = 300; this.maxWidth = 800; this.feedbackCounter = 0; this.isMinimized = false; this.restoreButton = null; // Store panel state for restoration this.panelState = null; this.initializeEventHandlers(); } initializeEventHandlers() { // Socket event handlers - Note: We don't handle these here anymore // They are handled by the main socket event handlers in socket-handlers.js // This class is called by those handlers when needed // Add global mouse events for resizing document.addEventListener('mousemove', (e) => this.handleMouseMove(e)); document.addEventListener('mouseup', () => this.handleMouseUp()); } showFeedbackPanel(data) { const { iteration, critic_text, code, symbols } = data; this.currentIteration = iteration; this.currentCode = code; this.currentSymbols = symbols; this.criticText = critic_text; this.selectedRanges = []; this.feedbackCounter = 0; this.isMinimized = false; // Store panel state for potential restoration this.panelState = { iteration, critic_text, code, symbols }; // Remove any existing restore button this.removeRestoreButton(); this.renderFeedbackPanel(); } renderFeedbackPanel() { // Remove existing panel if any this.removeFeedbackPanel(); // Create compact sidebar panel this.feedbackPanel = document.createElement('div'); this.feedbackPanel.className = 'feedback-sidebar'; this.feedbackPanel.style.width = `${this.sidebarWidth}px`; this.feedbackPanel.innerHTML = `

Interactive Review

Iteration ${this.currentIteration}
Extracted Symbols
${this.renderSymbolsJSON()}
Generated Code
${this.escapeHtml(this.truncateCode(this.currentCode))}
AI Analysis
${this.formatCriticSummary(this.criticText)}
Your Feedback
0 items

No feedback added yet

Highlight code or symbols to add feedback
`; // Insert panel into the body (overlay) document.body.appendChild(this.feedbackPanel); // Add event listeners this.attachPanelEventListeners(); // Initialize feather icons if (typeof feather !== 'undefined') { feather.replace(); } // Show panel with animation setTimeout(() => { this.feedbackPanel.classList.add('visible'); this.isVisible = true; }, 10); } renderSymbolsJSON() { if (!this.currentSymbols || Object.keys(this.currentSymbols).length === 0) { return '

No symbols extracted

'; } const jsonString = JSON.stringify(this.currentSymbols, null, 2); const truncatedJson = jsonString.length > 200 ? jsonString.substring(0, 200) + '\n ...\n}' : jsonString; return `
${this.escapeHtml(truncatedJson)}
`; } attachPanelEventListeners() { // Resize handle document.getElementById('resize-handle').addEventListener('mousedown', (e) => { this.startResize(e); }); // Close button with confirmation document.getElementById('feedback-close').addEventListener('click', () => { this.confirmCloseFeedbackPanel(); }); // Expand symbols button document.getElementById('expand-symbols').addEventListener('click', () => { this.showSymbolsModal(); }); // Expand code button document.getElementById('expand-code').addEventListener('click', () => { this.showCodeModal(); }); // Add comment button document.getElementById('add-comment').addEventListener('click', () => { this.showCommentsSection(); }); // Finish button document.getElementById('finish-here').addEventListener('click', () => { this.submitFeedback(); }); // Comment actions document.getElementById('save-comment').addEventListener('click', () => { this.addGeneralComment(); }); document.getElementById('cancel-comment').addEventListener('click', () => { this.hideCommentsSection(); document.getElementById('user-comments').value = ''; }); // Modal close buttons document.getElementById('close-symbols-modal').addEventListener('click', () => { this.hideSymbolsModal(); }); document.getElementById('close-code-modal').addEventListener('click', () => { this.hideCodeModal(); }); // Click outside to close modals document.getElementById('symbols-modal').addEventListener('click', (e) => { if (e.target.id === 'symbols-modal') { this.hideSymbolsModal(); } }); document.getElementById('code-modal').addEventListener('click', (e) => { if (e.target.id === 'code-modal') { this.hideCodeModal(); } }); // Dialogue close buttons document.getElementById('close-symbol-dialogue')?.addEventListener('click', () => { this.hideSymbolDialogue(); }); document.getElementById('close-code-dialogue')?.addEventListener('click', () => { this.hideCodeDialogue(); }); // Dialogue action buttons document.getElementById('save-symbol-feedback')?.addEventListener('click', () => { this.saveSymbolFeedback(); }); document.getElementById('cancel-symbol-feedback')?.addEventListener('click', () => { this.hideSymbolDialogue(); }); document.getElementById('save-code-feedback')?.addEventListener('click', () => { this.saveCodeFeedback(); }); document.getElementById('cancel-code-feedback')?.addEventListener('click', () => { this.hideCodeDialogue(); }); // Preview click handlers document.querySelector('.hoverable-code').addEventListener('click', () => { this.showCodeModal(); }); document.querySelector('.selectable-json')?.addEventListener('click', () => { this.showSymbolsModal(); }); } startResize(e) { this.isResizing = true; this.startX = e.clientX; this.startWidth = this.sidebarWidth; // Add visual feedback document.body.style.cursor = 'ew-resize'; this.feedbackPanel.classList.add('resizing'); e.preventDefault(); } handleMouseMove(e) { if (!this.isResizing) return; const deltaX = this.startX - e.clientX; const newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, this.startWidth + deltaX)); this.sidebarWidth = newWidth; this.feedbackPanel.style.width = `${newWidth}px`; } handleMouseUp() { if (!this.isResizing) return; this.isResizing = false; document.body.style.cursor = ''; this.feedbackPanel.classList.remove('resizing'); } showSymbolsModal() { const modal = document.getElementById('symbols-modal'); modal.style.display = 'flex'; // Initialize JSON selection setTimeout(() => { this.initializeJSONSelection(); }, 10); } hideSymbolsModal() { const modal = document.getElementById('symbols-modal'); modal.style.display = 'none'; this.hideSymbolDialogue(); } showCodeModal() { const modal = document.getElementById('code-modal'); modal.style.display = 'flex'; // Add line numbers and initialize code selection setTimeout(() => { this.addLineNumbers(); this.initializeCodeSelection(); }, 10); } hideCodeModal() { const modal = document.getElementById('code-modal'); modal.style.display = 'none'; this.hideCodeDialogue(); } initializeJSONSelection() { const jsonElement = document.getElementById('symbols-json'); if (jsonElement) { jsonElement.addEventListener('mouseup', () => { this.handleJSONSelection(); }); } } initializeCodeSelection() { const codeDisplay = document.getElementById('code-display'); if (codeDisplay) { codeDisplay.addEventListener('mouseup', () => { this.handleCodeSelection(); }); } } handleJSONSelection() { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const selectedText = selection.toString().trim(); if (selectedText) { this.showSymbolDialogue(selectedText); } } } handleCodeSelection() { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const selectedText = selection.toString().trim(); if (selectedText) { this.showCodeDialogue(selectedText); } } } showSymbolDialogue(selectedText) { const dialogue = document.getElementById('symbol-dialogue'); const preview = document.getElementById('symbol-highlight-preview'); preview.innerHTML = `
${this.escapeHtml(selectedText)}
`; dialogue.style.display = 'block'; // Focus on textarea document.getElementById('symbol-feedback-text').focus(); // Store selected text this.currentSelection = { type: 'symbol', text: selectedText }; } showCodeDialogue(selectedText) { const dialogue = document.getElementById('code-dialogue'); const preview = document.getElementById('code-highlight-preview'); preview.innerHTML = `
${this.escapeHtml(selectedText)}
`; dialogue.style.display = 'block'; // Focus on textarea document.getElementById('code-feedback-text').focus(); // Store selected text this.currentSelection = { type: 'code', text: selectedText }; } hideSymbolDialogue() { const dialogue = document.getElementById('symbol-dialogue'); dialogue.style.display = 'none'; document.getElementById('symbol-feedback-text').value = ''; window.getSelection().removeAllRanges(); } hideCodeDialogue() { const dialogue = document.getElementById('code-dialogue'); dialogue.style.display = 'none'; document.getElementById('code-feedback-text').value = ''; window.getSelection().removeAllRanges(); } saveSymbolFeedback() { const feedbackText = document.getElementById('symbol-feedback-text').value.trim(); if (feedbackText && this.currentSelection) { this.addFeedbackItem('symbol', this.currentSelection.text, feedbackText); this.hideSymbolDialogue(); this.showNotification('Symbol feedback added'); } } saveCodeFeedback() { const feedbackText = document.getElementById('code-feedback-text').value.trim(); if (feedbackText && this.currentSelection) { this.addFeedbackItem('code', this.currentSelection.text, feedbackText); this.hideCodeDialogue(); this.showNotification('Code feedback added'); } } addGeneralComment() { const comment = document.getElementById('user-comments').value.trim(); if (comment) { this.addFeedbackItem('general', '', comment); this.hideCommentsSection(); document.getElementById('user-comments').value = ''; this.showNotification('General comment added'); } } addFeedbackItem(type, selectedText, comment) { const feedback = { id: ++this.feedbackCounter, type: type, text: selectedText, comment: comment, timestamp: new Date().toLocaleTimeString() }; this.selectedRanges.push(feedback); this.updateFeedbackCart(); } updateFeedbackCart() { const cartItems = document.getElementById('cart-items'); const cartCount = document.getElementById('cart-count'); cartCount.textContent = `${this.selectedRanges.length} item${this.selectedRanges.length !== 1 ? 's' : ''}`; if (this.selectedRanges.length === 0) { cartItems.innerHTML = `

No feedback added yet

Highlight code or symbols to add feedback
`; if (typeof feather !== 'undefined') { feather.replace(); } return; } const items = this.selectedRanges.map(item => { const typeIcon = item.type === 'code' ? 'code' : item.type === 'symbol' ? 'hash' : 'message-circle'; const typeLabel = item.type === 'code' ? 'Code' : item.type === 'symbol' ? 'Symbol' : 'General'; const preview = item.text ? (item.text.length > 50 ? item.text.substring(0, 50) + '...' : item.text) : ''; return `
${typeLabel} ${item.timestamp}
${preview ? `
${this.escapeHtml(preview)}
` : ''}
${this.escapeHtml(item.comment)}
`; }).join(''); cartItems.innerHTML = items; // Re-initialize feather icons if (typeof feather !== 'undefined') { feather.replace(); } } editFeedback(id) { const feedback = this.selectedRanges.find(item => item.id === id); if (!feedback) return; const newComment = prompt(`Edit your feedback:\n\n${feedback.text ? 'Selected: ' + feedback.text + '\n\n' : ''}Current feedback:`, feedback.comment); if (newComment !== null && newComment.trim() !== '') { feedback.comment = newComment.trim(); this.updateFeedbackCart(); this.showNotification('Feedback updated'); } } removeFeedback(id) { this.selectedRanges = this.selectedRanges.filter(item => item.id !== id); this.updateFeedbackCart(); this.showNotification('Feedback removed'); } showCommentsSection() { const section = document.getElementById('comments-section'); section.style.display = 'block'; document.getElementById('user-comments').focus(); } hideCommentsSection() { const section = document.getElementById('comments-section'); section.style.display = 'none'; } confirmCloseFeedbackPanel() { const hasUnsavedFeedback = this.selectedRanges.length > 0; let message = 'Are you sure you want to close the feedback panel?'; if (hasUnsavedFeedback) { message += '\n\nYou have unsaved feedback that will be lost. The interactive session will not be able to continue without your feedback.'; } else { message += '\n\nWithout providing feedback, the interactive session cannot continue.'; } if (confirm(message)) { this.hideFeedbackPanel(); } } hideFeedbackPanel() { if (this.feedbackPanel) { this.feedbackPanel.classList.remove('visible'); setTimeout(() => { this.removeFeedbackPanel(); this.showRestoreButton(); }, 300); } } showRestoreButton() { // Remove existing restore button if any this.removeRestoreButton(); // Create restore button in chat area this.restoreButton = document.createElement('div'); this.restoreButton.className = 'feedback-restore-container'; this.restoreButton.innerHTML = `
`; // Add to chat container const chatContainer = document.getElementById('chat-container') || document.getElementById('chatArea'); if (chatContainer) { chatContainer.appendChild(this.restoreButton); } // Add event listeners document.getElementById('restore-feedback-btn').addEventListener('click', () => { this.restoreFeedbackPanel(); }); document.getElementById('terminate-session-btn').addEventListener('click', () => { this.terminateInteractiveSession(); }); // Initialize feather icons if (typeof feather !== 'undefined') { feather.replace(); } } removeRestoreButton() { if (this.restoreButton && document.body.contains(this.restoreButton)) { this.restoreButton.remove(); } this.restoreButton = null; } restoreFeedbackPanel() { if (this.panelState) { // Remove restore button this.removeRestoreButton(); // Restore the panel with saved state if (this.isMinimized && this.feedbackPanel) { // Panel exists but is hidden, just show it this.feedbackPanel.style.display = 'block'; this.isMinimized = false; this.isVisible = true; } else { // Panel was completely removed, recreate it this.showFeedbackPanel(this.panelState); } this.showNotification('Welcome back! Ready to continue reviewing the AI\'s work.'); } } terminateInteractiveSession() { if (confirm('Are you sure you want to end the interactive session?\n\nThis will stop the AI from waiting for feedback and provide the current solution as final.')) { // Remove restore button this.removeRestoreButton(); // Send termination signal import('../network/socket.js').then(({ socketManager }) => { socketManager.send('terminate_session'); }); this.showNotification('Session ended. The AI will finalize the current solution.'); } } truncateCode(code) { const lines = code.split('\n'); if (lines.length <= 8) { return code; } return lines.slice(0, 8).join('\n') + '\n... (click to expand)'; } formatCriticSummary(text) { if (!text || text.trim() === '') { return '

No issues found by AI critic.

'; } // Extract first sentence or first 100 characters const summary = text.length > 100 ? text.substring(0, 100) + '...' : text; return `

${this.escapeHtml(summary)}

`; } addLineNumbers() { const codeDisplay = document.getElementById('code-display'); const codeGutter = document.getElementById('code-gutter'); if (codeDisplay && codeGutter) { const lines = this.currentCode.split('\n'); const gutterHTML = lines.map((_, index) => `
${index + 1}
` ).join(''); codeGutter.innerHTML = gutterHTML; } } showNotification(message) { const notification = document.createElement('div'); notification.className = 'feedback-notification'; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.classList.add('visible'); }, 10); setTimeout(() => { notification.classList.remove('visible'); setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification); } }, 300); }, 2000); } submitFeedback() { const acceptCritic = document.getElementById('accept-critic').checked; this.disableButtons(); this.showLoadingState('Submitting feedback...'); // Convert feedback to the expected format const quotedRanges = this.selectedRanges.map(item => { if (item.type === 'symbol') { return { text: `Symbol JSON: ${item.text}`, comment: item.comment }; } else if (item.type === 'code') { return { text: item.text, comment: item.comment }; } else { return { text: 'General Comment', comment: item.comment }; } }); // Import socket manager and send feedback import('../network/socket.js').then(({ socketManager }) => { socketManager.send('provide_feedback', { accept_critic: acceptCritic, extra_comments: '', quoted_ranges: quotedRanges, terminate: false // Continue the process, don't terminate }); }); // Clean up the panel completely after submitting feedback this.removeFeedbackPanel(); this.removeRestoreButton(); } disableButtons() { const buttons = this.feedbackPanel.querySelectorAll('button'); buttons.forEach(btn => btn.disabled = true); } showLoadingState(message) { // Show loading indicator in the sidebar const content = this.feedbackPanel.querySelector('.feedback-sidebar-content'); if (content) { content.innerHTML = `

${message}

`; } } removeFeedbackPanel() { if (this.feedbackPanel && document.body.contains(this.feedbackPanel)) { document.body.removeChild(this.feedbackPanel); } this.feedbackPanel = null; this.isVisible = false; } showFinalArtifacts(data) { // Show final artifacts in a compact way const artifactsPanel = document.createElement('div'); artifactsPanel.className = 'final-artifacts-compact'; artifactsPanel.innerHTML = `

Final Solution

Solution completed successfully!

`; // Add to chat area const chatContainer = document.getElementById('chat-container'); if (chatContainer) { chatContainer.appendChild(artifactsPanel); } // Initialize feather icons if (typeof feather !== 'undefined') { feather.replace(); } } handleModeSwitched(data) { // Handle mode switching if needed this.updateModeIndicator(data.mode); } updateModeIndicator(mode) { // Update any mode indicators in the UI const indicators = document.querySelectorAll('.mode-badge'); indicators.forEach(indicator => { indicator.textContent = mode; indicator.className = `mode-badge mode-${mode.toLowerCase()}`; }); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // Create global instance window.interactiveFeedback = new InteractiveFeedback();