/** * 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 = `
`; // 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 feedbackNo 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) => `${message}
Solution completed successfully!
Add General Comment