/** * Message Manager - Handles chat messages, streaming, and code execution display */ import { Logger } from '../core/logger.js'; import { domManager } from './dom-manager.js'; export class MessageManager { constructor() { this.streamingMessages = new Map(); this.executionSpinners = new Map(); } addMessage(sender, content, iteration = null, promptDetails = null) { const messageDiv = document.createElement('div'); messageDiv.className = 'chat-message'; if (promptDetails) { messageDiv.classList.add('expandable-message'); } const avatarClass = sender === 'PIPS' || sender === 'PIPS System' ? 'avatar-pips' : sender === 'AI Code Reviewer' ? 'avatar-reviewer' : sender.includes('AI') ? 'avatar-llm' : 'avatar-system'; const avatarLetter = sender === 'PIPS' || sender === 'PIPS System' ? 'P' : sender === 'AI Code Reviewer' ? 'QA' : sender.includes('AI') ? 'AI' : 'S'; const iterationBadge = iteration ? `Iteration ${iteration}` : ''; // Create expand toggle if prompt details are available const expandToggle = promptDetails ? ` ` : ''; // Create expandable content if prompt details are available const expandableContent = promptDetails ? `
` : ''; messageDiv.innerHTML = ` `; domManager.getElement('chatArea').appendChild(messageDiv); // Re-highlight code blocks if (typeof Prism !== 'undefined') { Prism.highlightAll(); } // Replace feather icons for the new expand toggle if (promptDetails) { feather.replace(messageDiv); } this.smartScrollToBottom(); // Save message incrementally during solving this.saveMessageIncremental(sender, content, iteration, promptDetails); } displayFinalAnswer(answer) { Logger.debug('MessageManager', 'displayFinalAnswer called with:', answer); if (!answer || answer.trim() === '') { Logger.warn('MessageManager', 'Empty or null final answer provided'); return; } // Remove any existing final answer elements to avoid duplicates const existingAnswers = domManager.getElement('chatArea').querySelectorAll('.final-answer'); existingAnswers.forEach(el => el.remove()); const answerDiv = document.createElement('div'); answerDiv.className = 'final-answer'; if (typeof answer === 'string') { if (answer.includes('<') && answer.includes('>')) { answerDiv.innerHTML = answer; } else { answerDiv.textContent = answer; } } else { answerDiv.textContent = String(answer); } domManager.getElement('chatArea').appendChild(answerDiv); setTimeout(() => { this.smartScrollToBottom(); }, 100); } smartScrollToBottom() { const chatArea = domManager.getElement('chatArea'); const threshold = 100; const shouldAutoScroll = (chatArea.scrollTop + chatArea.clientHeight >= chatArea.scrollHeight - threshold); if (shouldAutoScroll) { chatArea.scrollTop = chatArea.scrollHeight; } } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // STREAMING MESSAGE METHODS showAIThinkingIndicator(iteration, senderName = 'AI Assistant') { // Remove any existing thinking indicator for this iteration and sender this.removeAIThinkingIndicator(iteration, senderName); const messageDiv = document.createElement('div'); messageDiv.className = 'chat-message ai-thinking'; messageDiv.setAttribute('data-iteration', iteration); messageDiv.setAttribute('data-sender', senderName); // Determine avatar based on sender let avatarClass, avatarLetter, thinkingText; if (senderName === 'AI Code Reviewer') { avatarClass = 'avatar-reviewer'; avatarLetter = 'QA'; thinkingText = 'Code reviewer is analyzing...'; } else { avatarClass = 'avatar-llm'; avatarLetter = 'AI'; thinkingText = 'AI is thinking...'; } messageDiv.innerHTML = ` `; domManager.getElement('chatArea').appendChild(messageDiv); this.smartScrollToBottom(); } removeAIThinkingIndicator(iteration, senderName = null) { const thinkingElements = domManager.getElement('chatArea').querySelectorAll('.ai-thinking'); thinkingElements.forEach(el => { const matchesIteration = !iteration || el.getAttribute('data-iteration') == iteration; const matchesSender = !senderName || el.getAttribute('data-sender') === senderName; if (matchesIteration && matchesSender) { el.remove(); } }); } updateStreamingMessage(token, iteration, sender) { // Create a unique identifier for this streaming message based on iteration and sender const streamingId = `${iteration}-${sender}`; // Find or create streaming message let streamingMessage = domManager.getElement('chatArea').querySelector(`[data-streaming-id="${streamingId}"]`); if (!streamingMessage) { // Remove thinking indicator if present for this specific sender this.removeAIThinkingIndicator(iteration, sender); // Create new streaming message streamingMessage = document.createElement('div'); streamingMessage.className = 'chat-message streaming-message'; streamingMessage.setAttribute('data-streaming-iteration', iteration); streamingMessage.setAttribute('data-streaming-id', streamingId); streamingMessage.setAttribute('data-sender', sender); // Determine avatar based on sender let avatarClass, avatarLetter; if (sender === 'AI Code Reviewer') { avatarClass = 'avatar-reviewer'; avatarLetter = 'QA'; } else { avatarClass = 'avatar-llm'; avatarLetter = 'AI'; } streamingMessage.innerHTML = ` `; domManager.getElement('chatArea').appendChild(streamingMessage); } // Update streaming content const streamingText = streamingMessage.querySelector('.streaming-text'); const currentContent = streamingText.getAttribute('data-content') || ''; const newContent = currentContent + token; streamingText.setAttribute('data-content', newContent); // Remove any existing typing indicators first const existingIndicators = streamingText.querySelectorAll('.typing-indicator'); existingIndicators.forEach(indicator => indicator.remove()); // Parse markdown if available if (typeof marked !== 'undefined') { streamingText.innerHTML = marked.parse(newContent); } else { streamingText.textContent = newContent; } // Add typing indicator at the very end of the content const typingIndicator = document.createElement('span'); typingIndicator.className = 'typing-indicator'; // Find the last element in the streaming text and append the cursor inline const lastElement = streamingText.lastElementChild; if (lastElement && (lastElement.tagName === 'P' || lastElement.tagName === 'DIV' || lastElement.tagName === 'SPAN')) { // Append to the last paragraph/div/span element to keep it inline lastElement.appendChild(typingIndicator); } else { // If no suitable element found, append directly to streaming text streamingText.appendChild(typingIndicator); } this.smartScrollToBottom(); } finalizeStreamingMessage(iteration, sender = null) { // If sender is specified, find the specific streaming message for that sender // Otherwise, finalize all streaming messages for the iteration (backward compatibility) let query; if (sender) { const streamingId = `${iteration}-${sender}`; query = `[data-streaming-id="${streamingId}"]`; } else { query = `[data-streaming-iteration="${iteration}"]`; } const streamingMessages = domManager.getElement('chatArea').querySelectorAll(query); streamingMessages.forEach(streamingMessage => { // Remove typing indicator const typingIndicator = streamingMessage.querySelector('.typing-indicator'); if (typingIndicator) { typingIndicator.remove(); } // Remove streaming attributes streamingMessage.classList.remove('streaming-message'); streamingMessage.removeAttribute('data-streaming-iteration'); streamingMessage.removeAttribute('data-streaming-id'); // Re-highlight code blocks if (typeof Prism !== 'undefined') { Prism.highlightAll(); } }); } // CODE EXECUTION METHODS showExecutionSpinner(iteration) { // Remove any existing execution spinner for this iteration this.removeExecutionSpinner(iteration); const spinnerDiv = document.createElement('div'); spinnerDiv.className = 'execution-spinner'; spinnerDiv.setAttribute('data-execution-iteration', iteration); spinnerDiv.innerHTML = ` Executing code... `; domManager.getElement('chatArea').appendChild(spinnerDiv); this.smartScrollToBottom(); } removeExecutionSpinner(iteration) { const spinners = domManager.getElement('chatArea').querySelectorAll('.execution-spinner'); spinners.forEach(spinner => { if (!iteration || spinner.getAttribute('data-execution-iteration') == iteration) { spinner.remove(); } }); } displayExecutionResult(result, iteration, isError = false) { const resultDiv = document.createElement('div'); resultDiv.className = `execution-result ${isError ? 'error' : ''}`; resultDiv.textContent = result; domManager.getElement('chatArea').appendChild(resultDiv); this.smartScrollToBottom(); } displayCode(code, iteration) { const codeDiv = document.createElement('div'); codeDiv.className = 'code-block'; codeDiv.innerHTML = `${this.escapeHtml(code)}`;
domManager.getElement('chatArea').appendChild(codeDiv);
if (typeof Prism !== 'undefined') {
Prism.highlightAll();
}
this.smartScrollToBottom();
}
toggleExpandMessage(button) {
const expandToggle = button;
const messageContent = button.closest('.message-content');
const expandableContent = messageContent.querySelector('.expandable-content');
if (!expandableContent) return;
const isExpanded = expandableContent.classList.contains('expanded');
if (isExpanded) {
expandableContent.classList.remove('expanded');
expandToggle.classList.remove('expanded');
expandToggle.innerHTML = `
Show Prompt
`;
} else {
expandableContent.classList.add('expanded');
expandToggle.classList.add('expanded');
expandToggle.innerHTML = `
Hide Prompt
`;
}
// Replace feather icons
feather.replace(expandToggle);
// Scroll to keep the message in view if needed
setTimeout(() => {
if (!isExpanded) {
this.smartScrollToBottom();
}
}, 300);
}
downloadChat() {
const chatContent = domManager.getElement('chatArea').innerHTML;
const blob = new Blob([`