diff --git a/public/js/chat-local.js b/public/js/chat-local.js new file mode 100644 index 0000000..b628cba --- /dev/null +++ b/public/js/chat-local.js @@ -0,0 +1,238 @@ + // Handles key down event to send message on Enter + function handleKeyDown(event) { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + sendMessage(); + } +} + +// Sends a message to the chat API +async function sendMessage() { + const messageInput = document.getElementById('messageInput'); + let message = messageInput.value.trim(); + + if (message === '') return; + + // Encode the message to avoid XSS attacks + message = he.encode(message); + + // Display the user's message in the chat + displayMessage(message, 'user'); + messageInput.value = ''; // Clear the input + toggleLoading(true); // Show loading indicator + + try { + const response = await fetch('https://infer.x64.world/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ message: message }) + }); + + if (response.ok) { + const data = await response.json(); + displayMessage(data, 'assistant'); + } else { + handleErrorResponse(response.status); + } + } catch (error) { + displayMessage('Error: ' + error.message, 'assistant'); + } finally { + toggleLoading(false); // Hide loading indicator + } +} + +// Toggles the loading indicator +function toggleLoading(show) { + const loadingElement = document.getElementById('loading'); + loadingElement.style.display = show ? 'block' : 'none'; +} + +// Displays a message in the chat window +function displayMessage(content, sender) { + const messages = document.getElementById('messages'); + const messageElement = document.createElement('div'); + messageElement.classList.add('message', sender); + + // Decode HTML entities and render Markdown + const decodedContent = he.decode(content); + const htmlContent = marked(decodedContent); + messageElement.innerHTML = htmlContent; + + messages.appendChild(messageElement); + messages.scrollTop = messages.scrollHeight; // Scroll to the bottom of the chat + + // Highlight code blocks if any + document.querySelectorAll('pre code').forEach((block) => { + hljs.highlightElement(block); + if (sender === 'assistant') { + addCopyButton(block); // Add copy button to code blocks + } + }); + + // Add "Copy Full Response" button after each assistant response + if (sender === 'assistant') { + addCopyFullResponseButton(messages, messageElement); + } +} + +// Adds a copy button to a code block +function addCopyButton(block) { + const button = document.createElement('button'); + button.classList.add('copy-button-code'); + button.textContent = 'Copy'; + button.addEventListener('click', () => copyToClipboard(block)); + block.parentNode.appendChild(button); +} + +// Adds "Copy Full Response" button below the assistant response +function addCopyFullResponseButton(messagesContainer, messageElement) { + const copyFullResponseButton = document.createElement('button'); + copyFullResponseButton.classList.add('copy-button'); + copyFullResponseButton.textContent = 'Copy Full Response'; + copyFullResponseButton.addEventListener('click', () => copyFullResponse(messageElement)); + + messagesContainer.appendChild(copyFullResponseButton); +} + +// Copies code block content to the clipboard +function copyToClipboard(block) { + const text = block.innerText; + navigator.clipboard.writeText(text).then(() => { + displayAlert('success', 'The code block was copied to the clipboard!'); + }).catch((err) => { + displayAlert('error', 'Failed to copy code: ' + err); + }); +} + +// Copies the full response content to the clipboard in Markdown format +function copyFullResponse(messageElement) { + const markdownContent = convertToMarkdown(messageElement); + navigator.clipboard.writeText(markdownContent).then(() => { + displayAlert('success', 'Full response copied to clipboard!'); + }).catch((err) => { + displayAlert('error', 'Failed to copy response: ' + err); + }); +} + +// Converts the HTML content of the response to Markdown, including language identifier +function convertToMarkdown(element) { + let markdown = ''; + const nodes = element.childNodes; + + nodes.forEach(node => { + if (node.nodeName === 'P') { + markdown += `${node.innerText}\n\n`; + } else if (node.nodeName === 'PRE') { + const codeBlock = node.querySelector('code'); + const languageClass = codeBlock.className.match(/language-(\w+)/); // Extract language from class if available + const language = languageClass ? languageClass[1] : ''; // Default to empty if no language found + const codeText = codeBlock.innerText; + + // Add language identifier to the Markdown code block + markdown += `\`\`\`${language}\n${codeText}\n\`\`\`\n\n`; + } + }); + + return markdown; +} + +// Displays an alert message with animation +function displayAlert(type, message) { + const alertElement = document.getElementById(`${type}-alert`); + alertElement.textContent = message; + alertElement.style.display = 'flex'; // Show the alert + alertElement.classList.remove('fade-out'); // Remove fade-out class if present + alertElement.style.opacity = '1'; // Ensure it's fully visible + + // Automatically hide the alert after 3 seconds with animation + setTimeout(() => { + alertElement.classList.add('fade-out'); // Add fade-out class to trigger animation + setTimeout(() => { + alertElement.style.display = 'none'; // Hide after animation finishes + }, 500); // Match this time to the animation duration + }, 3000); // Show the alert for 3 seconds before hiding +} + +// Handles error responses based on status code +function handleErrorResponse(status) { + const messages = document.getElementById('messages'); + const lastMessage = messages.lastElementChild; + + if (status === 429) { + displayAlert('error', 'Sorry, I am currently too busy at the moment!'); + // Remove the last user message if the status is 429 + if (lastMessage && lastMessage.classList.contains('user')) { + messages.removeChild(lastMessage); + } + } else { + displayMessage('Error: ' + status, 'assistant'); + // Remove the last user message for any other errors + if (lastMessage && lastMessage.classList.contains('user')) { + messages.removeChild(lastMessage); + } + } +} + +// Helper function to reset the conversation on the server +async function sendResetRequest() { + const response = await fetch('https://infer.x64.world/reset-conversation', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + return response; +} + +// Resets the chat messages and optionally displays a success message +async function resetChat(displaySuccessMessage = true) { + try { + const response = await sendResetRequest(); + + if (response.ok) { + const messagesContainer = document.getElementById('messages'); + messagesContainer.innerHTML = ''; // Clear all messages + + if (displaySuccessMessage) { + displayAlert('success', 'Messages Cleared!'); + } + } else { + displayAlert('error', 'Failed to reset conversation. Please try again.'); + } + } catch (error) { + displayAlert('error', 'An error occurred while resetting the conversation.'); + } +} + +// Resets the chat on page load without displaying the success message +document.addEventListener('DOMContentLoaded', function () { + resetChat(false); +}); + +function openWindow(url, windowName, width, height) { + window.open(url, windowName, `width=${width},height=${height},menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes`); +} + +function openGpuStats() { + openWindow('https://raven-scott.fyi/smi.html', 'gpuStatsWindow', 800, 600); +} + +function openLiveLog() { + openWindow('https://llama-live-log.x64.world/', 'liveLogWindow', 600, 600); +} + +function openTop() { + openWindow('https://ai-top.x64.world/', 'liveLogGtopWindow', 800, 600); +} + +function openNetdata() { + openWindow('https://ai-monitor.x64.world/', 'netDataWindow', 800, 650); +} + +function openAbout() { + openWindow('https://raven-scott.fyi/about-rayai', 'aboutRAIWindow', 800, 650); +} + + diff --git a/public/js/chat.js b/public/js/chat.js index 1a0eb59..b628cba 100644 --- a/public/js/chat.js +++ b/public/js/chat.js @@ -32,7 +32,7 @@ async function sendMessage() { if (response.ok) { const data = await response.json(); - displayMessage(data.content, 'assistant'); + displayMessage(data, 'assistant'); } else { handleErrorResponse(response.status); }