|
|
|
|
|
|
|
|
|
|
|
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 ? |
|
|
`<span class="iteration-badge">Iteration ${iteration}</span>` : ''; |
|
|
|
|
|
|
|
|
const expandToggle = promptDetails ? ` |
|
|
<button class="expand-toggle" onclick="window.pipsApp.toggleExpandMessage(this)"> |
|
|
<i data-feather="chevron-down" style="width: 12px; height: 12px;"></i> |
|
|
Show Prompt |
|
|
</button> |
|
|
` : ''; |
|
|
|
|
|
|
|
|
const expandableContent = promptDetails ? ` |
|
|
<div class="expandable-content"> |
|
|
<div class="expandable-content-inner"> |
|
|
${promptDetails.description ? `<div class="prompt-description">${this.escapeHtml(promptDetails.description)}</div>` : ''} |
|
|
<div class="prompt-conversation"> |
|
|
${promptDetails.conversation.map(msg => { |
|
|
// Format content based on its structure |
|
|
let formattedContent = ''; |
|
|
if (typeof msg.content === 'string') { |
|
|
// Check if content looks like structured data or contains code blocks |
|
|
if (msg.content.includes('```') || msg.content.includes('{') || msg.content.includes('[')) { |
|
|
// Use markdown parsing for structured content |
|
|
formattedContent = marked ? marked.parse(msg.content) : msg.content.replace(/\n/g, '<br>'); |
|
|
} else { |
|
|
// Escape HTML but preserve line breaks for simple text |
|
|
formattedContent = this.escapeHtml(msg.content).replace(/\n/g, '<br>'); |
|
|
} |
|
|
} else if (Array.isArray(msg.content)) { |
|
|
// Handle multimodal content (like image + text) |
|
|
formattedContent = msg.content.map(item => { |
|
|
if (item.type === 'text') { |
|
|
return this.escapeHtml(item.text).replace(/\n/g, '<br>'); |
|
|
} else if (item.type === 'image_url') { |
|
|
return '<div class="prompt-image">[Image content]</div>'; |
|
|
} |
|
|
return this.escapeHtml(JSON.stringify(item)); |
|
|
}).join(''); |
|
|
} else { |
|
|
// Fallback for other content types |
|
|
formattedContent = this.escapeHtml(JSON.stringify(msg.content, null, 2)).replace(/\n/g, '<br>'); |
|
|
} |
|
|
|
|
|
return ` |
|
|
<div class="prompt-message ${msg.role}"> |
|
|
<div class="prompt-role">${msg.role}</div> |
|
|
<div class="prompt-content">${formattedContent}</div> |
|
|
</div> |
|
|
`; |
|
|
}).join('')} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
` : ''; |
|
|
|
|
|
messageDiv.innerHTML = ` |
|
|
<div class="message-header"> |
|
|
<div class="message-avatar ${avatarClass}">${avatarLetter}</div> |
|
|
<span class="message-sender">${this.escapeHtml(sender)}</span> |
|
|
${iterationBadge} |
|
|
</div> |
|
|
<div class="message-content"> |
|
|
${marked ? marked.parse(content) : content} |
|
|
${expandToggle} |
|
|
${expandableContent} |
|
|
</div> |
|
|
`; |
|
|
|
|
|
domManager.getElement('chatArea').appendChild(messageDiv); |
|
|
|
|
|
|
|
|
if (typeof Prism !== 'undefined') { |
|
|
Prism.highlightAll(); |
|
|
} |
|
|
|
|
|
|
|
|
if (promptDetails) { |
|
|
feather.replace(messageDiv); |
|
|
} |
|
|
|
|
|
this.smartScrollToBottom(); |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
showAIThinkingIndicator(iteration, senderName = 'AI Assistant') { |
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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 = ` |
|
|
<div class="message-header"> |
|
|
<div class="message-avatar ${avatarClass}">${avatarLetter}</div> |
|
|
<span class="message-sender">${senderName}</span> |
|
|
${iteration ? `<span class="iteration-badge">Iteration ${iteration}</span>` : ''} |
|
|
</div> |
|
|
<div class="message-content"> |
|
|
<div class="streaming-indicator"> |
|
|
<div class="spinner"></div> |
|
|
<span>${thinkingText}</span> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
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) { |
|
|
|
|
|
const streamingId = `${iteration}-${sender}`; |
|
|
|
|
|
|
|
|
let streamingMessage = domManager.getElement('chatArea').querySelector(`[data-streaming-id="${streamingId}"]`); |
|
|
|
|
|
if (!streamingMessage) { |
|
|
|
|
|
this.removeAIThinkingIndicator(iteration, sender); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
let avatarClass, avatarLetter; |
|
|
if (sender === 'AI Code Reviewer') { |
|
|
avatarClass = 'avatar-reviewer'; |
|
|
avatarLetter = 'QA'; |
|
|
} else { |
|
|
avatarClass = 'avatar-llm'; |
|
|
avatarLetter = 'AI'; |
|
|
} |
|
|
|
|
|
streamingMessage.innerHTML = ` |
|
|
<div class="message-header"> |
|
|
<div class="message-avatar ${avatarClass}">${avatarLetter}</div> |
|
|
<span class="message-sender">${sender}</span> |
|
|
${iteration ? `<span class="iteration-badge">Iteration ${iteration}</span>` : ''} |
|
|
</div> |
|
|
<div class="message-content"> |
|
|
<div class="streaming-text" data-content=""></div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
domManager.getElement('chatArea').appendChild(streamingMessage); |
|
|
} |
|
|
|
|
|
|
|
|
const streamingText = streamingMessage.querySelector('.streaming-text'); |
|
|
const currentContent = streamingText.getAttribute('data-content') || ''; |
|
|
const newContent = currentContent + token; |
|
|
streamingText.setAttribute('data-content', newContent); |
|
|
|
|
|
|
|
|
const existingIndicators = streamingText.querySelectorAll('.typing-indicator'); |
|
|
existingIndicators.forEach(indicator => indicator.remove()); |
|
|
|
|
|
|
|
|
if (typeof marked !== 'undefined') { |
|
|
streamingText.innerHTML = marked.parse(newContent); |
|
|
} else { |
|
|
streamingText.textContent = newContent; |
|
|
} |
|
|
|
|
|
|
|
|
const typingIndicator = document.createElement('span'); |
|
|
typingIndicator.className = 'typing-indicator'; |
|
|
|
|
|
|
|
|
const lastElement = streamingText.lastElementChild; |
|
|
if (lastElement && (lastElement.tagName === 'P' || lastElement.tagName === 'DIV' || lastElement.tagName === 'SPAN')) { |
|
|
|
|
|
lastElement.appendChild(typingIndicator); |
|
|
} else { |
|
|
|
|
|
streamingText.appendChild(typingIndicator); |
|
|
} |
|
|
|
|
|
this.smartScrollToBottom(); |
|
|
} |
|
|
|
|
|
finalizeStreamingMessage(iteration, sender = null) { |
|
|
|
|
|
|
|
|
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 => { |
|
|
|
|
|
const typingIndicator = streamingMessage.querySelector('.typing-indicator'); |
|
|
if (typingIndicator) { |
|
|
typingIndicator.remove(); |
|
|
} |
|
|
|
|
|
|
|
|
streamingMessage.classList.remove('streaming-message'); |
|
|
streamingMessage.removeAttribute('data-streaming-iteration'); |
|
|
streamingMessage.removeAttribute('data-streaming-id'); |
|
|
|
|
|
|
|
|
if (typeof Prism !== 'undefined') { |
|
|
Prism.highlightAll(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
showExecutionSpinner(iteration) { |
|
|
|
|
|
this.removeExecutionSpinner(iteration); |
|
|
|
|
|
const spinnerDiv = document.createElement('div'); |
|
|
spinnerDiv.className = 'execution-spinner'; |
|
|
spinnerDiv.setAttribute('data-execution-iteration', iteration); |
|
|
spinnerDiv.innerHTML = ` |
|
|
<div class="spinner"></div> |
|
|
<span>Executing code...</span> |
|
|
`; |
|
|
|
|
|
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 = `<pre><code class="language-python">${this.escapeHtml(code)}</code></pre>`; |
|
|
|
|
|
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 = ` |
|
|
<i data-feather="chevron-down" style="width: 12px; height: 12px;"></i> |
|
|
Show Prompt |
|
|
`; |
|
|
} else { |
|
|
expandableContent.classList.add('expanded'); |
|
|
expandToggle.classList.add('expanded'); |
|
|
expandToggle.innerHTML = ` |
|
|
<i data-feather="chevron-up" style="width: 12px; height: 12px;"></i> |
|
|
Hide Prompt |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
feather.replace(expandToggle); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
if (!isExpanded) { |
|
|
this.smartScrollToBottom(); |
|
|
} |
|
|
}, 300); |
|
|
} |
|
|
|
|
|
downloadChat() { |
|
|
const chatContent = domManager.getElement('chatArea').innerHTML; |
|
|
const blob = new Blob([` |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>PIPS Chat Export</title> |
|
|
<style> |
|
|
body { font-family: Arial, sans-serif; margin: 20px; } |
|
|
.chat-message { margin-bottom: 20px; } |
|
|
.message-header { font-weight: bold; margin-bottom: 5px; } |
|
|
.message-content { margin-left: 20px; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<h1>PIPS Chat Export</h1> |
|
|
<div class="chat-area">${chatContent}</div> |
|
|
</body> |
|
|
</html> |
|
|
`], { type: 'text/html' }); |
|
|
|
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = `pips_chat_${new Date().toISOString().split('T')[0]}.html`; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
URL.revokeObjectURL(url); |
|
|
} |
|
|
|
|
|
|
|
|
getCurrentChatHistory() { |
|
|
const chatArea = domManager.getElement('chatArea'); |
|
|
if (!chatArea) { |
|
|
Logger.warn('MessageManager', 'Chat area not found'); |
|
|
return []; |
|
|
} |
|
|
|
|
|
const messages = chatArea.querySelectorAll('.chat-message'); |
|
|
const history = []; |
|
|
|
|
|
messages.forEach(message => { |
|
|
const senderElement = message.querySelector('.message-sender'); |
|
|
const contentElement = message.querySelector('.message-content'); |
|
|
const iterationElement = message.querySelector('.iteration-badge'); |
|
|
|
|
|
if (!senderElement || !contentElement) { |
|
|
Logger.debug('MessageManager', 'Skipping malformed message'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const sender = senderElement.textContent || 'Unknown'; |
|
|
let content = ''; |
|
|
|
|
|
|
|
|
let contentToSave = ''; |
|
|
const contentChildren = Array.from(contentElement.children); |
|
|
|
|
|
|
|
|
contentChildren.forEach(child => { |
|
|
if (!child.classList.contains('expand-toggle') && |
|
|
!child.classList.contains('expandable-content')) { |
|
|
contentToSave += child.outerHTML; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if (!contentToSave) { |
|
|
|
|
|
const clonedContent = contentElement.cloneNode(true); |
|
|
const expandToggle = clonedContent.querySelector('.expand-toggle'); |
|
|
const expandableContent = clonedContent.querySelector('.expandable-content'); |
|
|
if (expandToggle) expandToggle.remove(); |
|
|
if (expandableContent) expandableContent.remove(); |
|
|
contentToSave = clonedContent.innerHTML.trim() || clonedContent.textContent.trim(); |
|
|
} |
|
|
|
|
|
content = contentToSave; |
|
|
|
|
|
const iteration = iterationElement ? iterationElement.textContent : null; |
|
|
|
|
|
|
|
|
if (sender === 'PIPS System' && content.includes('Welcome to PIPS')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (!content || content === '') { |
|
|
Logger.debug('MessageManager', 'Skipping empty message'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (message.classList.contains('ai-thinking') || |
|
|
message.classList.contains('streaming-message') || |
|
|
content.includes('AI is thinking...') || |
|
|
content.includes('Executing code...')) { |
|
|
Logger.debug('MessageManager', 'Skipping active streaming indicator'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const expandableContent = message.querySelector('.expandable-content'); |
|
|
let promptDetails = null; |
|
|
|
|
|
if (expandableContent) { |
|
|
|
|
|
const promptDescription = expandableContent.querySelector('.prompt-description'); |
|
|
const promptMessages = expandableContent.querySelectorAll('.prompt-message'); |
|
|
|
|
|
if (promptMessages.length > 0) { |
|
|
promptDetails = { |
|
|
description: promptDescription ? promptDescription.textContent : '', |
|
|
conversation: Array.from(promptMessages).map(promptMsg => ({ |
|
|
role: promptMsg.querySelector('.prompt-role').textContent.toLowerCase(), |
|
|
content: promptMsg.querySelector('.prompt-content').textContent |
|
|
})) |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
history.push({ |
|
|
sender, |
|
|
content, |
|
|
iteration, |
|
|
promptDetails, |
|
|
timestamp: new Date().toISOString() |
|
|
}); |
|
|
}); |
|
|
|
|
|
Logger.debug('MessageManager', `Extracted ${history.length} messages from chat`); |
|
|
return history; |
|
|
} |
|
|
|
|
|
loadChatHistory(history) { |
|
|
const chatArea = domManager.getElement('chatArea'); |
|
|
|
|
|
|
|
|
let welcomeMessage = null; |
|
|
const existingMessages = chatArea.querySelectorAll('.chat-message'); |
|
|
existingMessages.forEach(msg => { |
|
|
const sender = msg.querySelector('.message-sender'); |
|
|
const content = msg.querySelector('.message-content'); |
|
|
if (sender && content && |
|
|
sender.textContent === 'PIPS System' && |
|
|
content.textContent.includes('Welcome to PIPS')) { |
|
|
welcomeMessage = msg.cloneNode(true); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
chatArea.innerHTML = ''; |
|
|
|
|
|
|
|
|
if (welcomeMessage) { |
|
|
chatArea.appendChild(welcomeMessage); |
|
|
} |
|
|
|
|
|
|
|
|
if (history && history.length > 0) { |
|
|
Logger.debug('MessageManager', `Loading ${history.length} messages from history`); |
|
|
|
|
|
history.forEach((msg, index) => { |
|
|
if (!msg || !msg.sender || !msg.content) { |
|
|
Logger.warn('MessageManager', `Skipping invalid message at index ${index}:`, msg); |
|
|
return; |
|
|
} |
|
|
|
|
|
const messageDiv = document.createElement('div'); |
|
|
messageDiv.className = 'chat-message'; |
|
|
|
|
|
const avatarClass = msg.sender === 'PIPS' || msg.sender === 'PIPS System' ? 'avatar-pips' : |
|
|
msg.sender === 'AI Code Reviewer' ? 'avatar-reviewer' : |
|
|
msg.sender.includes('AI') ? 'avatar-llm' : 'avatar-system'; |
|
|
const avatarLetter = msg.sender === 'PIPS' || msg.sender === 'PIPS System' ? 'P' : |
|
|
msg.sender === 'AI Code Reviewer' ? 'QA' : |
|
|
msg.sender.includes('AI') ? 'AI' : 'S'; |
|
|
|
|
|
const iterationBadge = msg.iteration ? |
|
|
`<span class="iteration-badge">${this.escapeHtml(msg.iteration)}</span>` : ''; |
|
|
|
|
|
|
|
|
const expandToggle = msg.promptDetails ? ` |
|
|
<button class="expand-toggle" onclick="window.pipsApp.toggleExpandMessage(this)"> |
|
|
<i data-feather="chevron-down" style="width: 12px; height: 12px;"></i> |
|
|
Show Prompt |
|
|
</button> |
|
|
` : ''; |
|
|
|
|
|
const expandableContent = msg.promptDetails ? ` |
|
|
<div class="expandable-content"> |
|
|
<div class="expandable-content-inner"> |
|
|
${msg.promptDetails.description ? `<div class="prompt-description">${this.escapeHtml(msg.promptDetails.description)}</div>` : ''} |
|
|
<div class="prompt-conversation"> |
|
|
${msg.promptDetails.conversation.map(promptMsg => ` |
|
|
<div class="prompt-message ${promptMsg.role}"> |
|
|
<div class="prompt-role">${promptMsg.role}</div> |
|
|
<div class="prompt-content">${this.escapeHtml(promptMsg.content)}</div> |
|
|
</div> |
|
|
`).join('')} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
` : ''; |
|
|
|
|
|
if (msg.promptDetails) { |
|
|
messageDiv.classList.add('expandable-message'); |
|
|
} |
|
|
|
|
|
messageDiv.innerHTML = ` |
|
|
<div class="message-header"> |
|
|
<div class="message-avatar ${avatarClass}">${avatarLetter}</div> |
|
|
<span class="message-sender">${this.escapeHtml(msg.sender)}</span> |
|
|
${iterationBadge} |
|
|
</div> |
|
|
<div class="message-content"> |
|
|
${msg.content} |
|
|
${expandToggle} |
|
|
${expandableContent} |
|
|
</div> |
|
|
`; |
|
|
|
|
|
chatArea.appendChild(messageDiv); |
|
|
}); |
|
|
|
|
|
|
|
|
if (typeof feather !== 'undefined') { |
|
|
feather.replace(chatArea); |
|
|
} |
|
|
} else { |
|
|
Logger.debug('MessageManager', 'No chat history to load'); |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof Prism !== 'undefined') { |
|
|
Prism.highlightAll(); |
|
|
} |
|
|
|
|
|
this.smartScrollToBottom(); |
|
|
} |
|
|
|
|
|
clearChatAndRestoreWelcome() { |
|
|
const chatArea = domManager.getElement('chatArea'); |
|
|
chatArea.innerHTML = ''; |
|
|
|
|
|
|
|
|
const welcomeDiv = document.createElement('div'); |
|
|
welcomeDiv.className = 'chat-message'; |
|
|
welcomeDiv.innerHTML = ` |
|
|
<div class="message-header"> |
|
|
<div class="message-avatar avatar-pips">P</div> |
|
|
<span class="message-sender">PIPS System</span> |
|
|
</div> |
|
|
<div class="message-content"> |
|
|
Welcome to PIPS! Enter a problem in the left panel and click "Solve Problem" to get started. |
|
|
Don't forget to configure your model settings first. |
|
|
</div> |
|
|
`; |
|
|
chatArea.appendChild(welcomeDiv); |
|
|
} |
|
|
|
|
|
|
|
|
cleanupAllActiveIndicators() { |
|
|
Logger.debug('MessageManager', 'Cleaning up all active indicators'); |
|
|
|
|
|
|
|
|
const thinkingElements = domManager.getElement('chatArea').querySelectorAll('.ai-thinking'); |
|
|
thinkingElements.forEach(el => el.remove()); |
|
|
|
|
|
|
|
|
const executionSpinners = domManager.getElement('chatArea').querySelectorAll('.execution-spinner'); |
|
|
executionSpinners.forEach(el => el.remove()); |
|
|
|
|
|
|
|
|
const streamingMessages = domManager.getElement('chatArea').querySelectorAll('.streaming-message'); |
|
|
streamingMessages.forEach(streamingMessage => { |
|
|
|
|
|
const typingIndicator = streamingMessage.querySelector('.typing-indicator'); |
|
|
if (typingIndicator) { |
|
|
typingIndicator.remove(); |
|
|
} |
|
|
|
|
|
|
|
|
streamingMessage.classList.remove('streaming-message'); |
|
|
streamingMessage.removeAttribute('data-streaming-iteration'); |
|
|
streamingMessage.removeAttribute('data-streaming-id'); |
|
|
}); |
|
|
|
|
|
|
|
|
if (typeof Prism !== 'undefined') { |
|
|
Prism.highlightAll(); |
|
|
} |
|
|
|
|
|
Logger.debug('MessageManager', 'All active indicators cleaned up'); |
|
|
} |
|
|
|
|
|
|
|
|
saveMessageIncremental(sender, content, iteration = null, promptDetails = null) { |
|
|
|
|
|
|
|
|
import('./session-manager.js').then(({ sessionManager }) => { |
|
|
if (window.appState && window.appState.currentSessionData) { |
|
|
|
|
|
window.appState.currentSessionData.chatHistory = this.getCurrentChatHistory(); |
|
|
window.appState.currentSessionData.lastUsed = new Date().toISOString(); |
|
|
|
|
|
|
|
|
sessionManager.saveCurrentSessionToStorage(); |
|
|
|
|
|
Logger.debug('MessageManager', `Incrementally saved message from ${sender} to session`); |
|
|
} |
|
|
}).catch(err => { |
|
|
Logger.warn('MessageManager', 'Could not save message incrementally:', err); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export const messageManager = new MessageManager(); |