diff options
Diffstat (limited to 'html/matt-chat/index.html')
-rw-r--r-- | html/matt-chat/index.html | 1266 |
1 files changed, 1266 insertions, 0 deletions
diff --git a/html/matt-chat/index.html b/html/matt-chat/index.html new file mode 100644 index 0000000..2bc8119 --- /dev/null +++ b/html/matt-chat/index.html @@ -0,0 +1,1266 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="Chatty chat chat chat. A super simple chat interface for the Ollama API."> + <title>matt chat is not a cat</title> + <meta name="theme-color" content="#007BFF"> + <link rel="icon" href="cat.png" type="image/x-icon"> + <link rel="shortcut icon" href="cat.png" type="image/x-icon"> + <link rel="apple-touch-icon" href="cat.png"> + <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet"> + <style> + body { + font-family: Arial, sans-serif; + font-size: 22px; + margin: 0; + padding: 20px; + background-color: #f7f7f7; + max-width: 800px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; + overflow: hidden; + } + #chat-container { + background-color: white; + border: 1px solid #ccc; + border-radius: 8px; + padding: 1em; + margin: 0 auto; + flex: 1; + overflow-y: auto; + width: 100%; + max-height: 400px; + scroll-behavior: smooth; + } + #user-input { + width: 100%; + padding: 10px; + border-radius: 4px; + border: 1px solid #ddd; + font-size: 16px; + margin-top: 10px; + box-sizing: border-box; + } + #send-button { + padding: 10px 15px; + border-radius: 4px; + background-color: #007BFF; + color: white; + border: none; + cursor: pointer; + margin-top: 10px; + width: 100%; + } + #send-button:hover { + background-color: #0056b3; + } + + .model-select-container { + align-self: flex-start; + width: 100%; + display: flex; + justify-content: space-between; + padding: 1em; + } + + .model-select-container label { + margin-left: 10px; + } + + .message { + white-space: pre-wrap; + margin-bottom: 10px; + padding: 1em; + border-radius: 8px; + background-color: #f1f1f1; + display: block; + max-width: 100%; + } + + .user-message { + background-color: #007BFF; + color: white; + text-align: right; + margin-left: 20px; + } + + .bot-message { + background-color: #f0f0f0; + color: #333; + text-align: left; + margin-right: 20px; + } + + @media (max-width: 600px) { + #chat-container { + max-height: 300px; + } + } + + body.dark-mode { + background-color: #333; + color: #f7f7f7; + } + + #chat-container.dark-mode { + background-color: #444; + border: 1px solid #555; + } + + #user-input.dark-mode { + background-color: #555; + color: #f7f7f7; + border: 1px solid #666; + } + + #send-button.dark-mode { + background-color: #007BFF; + color: white; + } + + .message.dark-mode { + background-color: #555; + color: #f7f7f7; + } + + .user-message.dark-mode { + background-color: #007BFF; + color: white; + } + + .bot-message.dark-mode { + background-color: #666; + color: #f7f7f7; + } + + .bot-time { + margin: 0.5em 0; + font-size: 0.9em; + color: #888; + text-align: center; + } + + /* Professional theme */ + body.theme-professional { + font-family: Arial, sans-serif; + font-size: 22px; + } + + /* Molly Millions theme */ + body.theme-molly-millions { + font-family: "Courier New", monospace; + font-size: 22px; + margin: 0; + padding: 20px; + background-color: #0a0a0a; + color: #00ff00; + max-width: 800px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; + overflow: hidden; + } + + .theme-molly-millions #chat-container { + background-color: #000000; + border: 2px solid #00ff00; + border-radius: 0; + padding: 1em; + margin: 0 auto; + flex: 1; + overflow-y: auto; + width: 100%; + max-height: 400px; + scroll-behavior: smooth; + box-shadow: 0 0 10px #00ff00; + } + + .theme-molly-millions #user-input { + width: 100%; + padding: 10px; + border-radius: 0; + border: 2px solid #00ff00; + background-color: #000000; + color: #00ff00; + font-family: "Courier New", monospace; + font-size: 16px; + margin-top: 10px; + box-sizing: border-box; + } + + .theme-molly-millions #send-button { + padding: 10px 15px; + border-radius: 0; + background-color: #000000; + color: #00ff00; + border: 2px solid #00ff00; + cursor: pointer; + margin-top: 10px; + width: 100%; + font-family: "Courier New", monospace; + text-transform: uppercase; + } + + .theme-molly-millions #send-button:hover { + background-color: #00ff00; + color: #000000; + } + + .theme-molly-millions .message { + white-space: pre-wrap; + margin-bottom: 10px; + padding: 1em; + border-radius: 0; + border: 1px solid #00ff00; + background-color: #0a0a0a; + display: block; + max-width: 100%; + } + + .theme-molly-millions .user-message { + background-color: #001100; + color: #00ff00; + border: 1px solid #00ff00; + text-align: right; + margin-left: 20px; + } + + .theme-molly-millions .bot-message { + background-color: #000000; + color: #00ff00; + border: 1px solid #00ff00; + text-align: left; + margin-right: 20px; + } + + .theme-molly-millions .bot-time { + color: #005500; + } + + /* Cloud theme */ + body.theme-cloud { + font-family: "Press Start 2P", "Courier New", monospace; + font-size: 18px; + margin: 0; + padding: 20px; + background: linear-gradient(135deg, #1a1b4b 0%, #162057 50%, #1a1b4b 100%); + color: #ffffff; + max-width: 800px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; + overflow: hidden; + } + + .theme-cloud #chat-container { + background: rgba(0, 0, 32, 0.75); + border: 3px solid #4080ff; + border-radius: 3px; + padding: 1em; + margin: 0 auto; + flex: 1; + overflow-y: auto; + width: 100%; + max-height: 400px; + scroll-behavior: smooth; + box-shadow: 0 0 15px rgba(64, 128, 255, 0.3); + } + + .theme-cloud #user-input { + width: 100%; + padding: 10px; + border: 2px solid #4080ff; + background: rgba(0, 0, 32, 0.75); + color: #ffffff; + font-family: "Press Start 2P", "Courier New", monospace; + font-size: 14px; + margin-top: 10px; + box-sizing: border-box; + } + + .theme-cloud #send-button { + padding: 10px 15px; + background: linear-gradient(to bottom, #4080ff 0%, #2048c0 100%); + color: white; + border: 2px solid #2048c0; + cursor: pointer; + margin-top: 10px; + width: 100%; + font-family: "Press Start 2P", "Courier New", monospace; + font-size: 14px; + text-transform: uppercase; + text-shadow: 2px 2px #000000; + } + + .theme-cloud #send-button:hover { + background: linear-gradient(to bottom, #50a0ff 0%, #3060e0 100%); + } + + .theme-cloud .message { + white-space: pre-wrap; + margin-bottom: 10px; + padding: 1em; + border: 2px solid #4080ff; + background: rgba(0, 0, 32, 0.5); + display: block; + max-width: 100%; + font-size: 14px; + } + + .theme-cloud .user-message { + background: rgba(64, 128, 255, 0.2); + color: #ffffff; + border: 2px solid #4080ff; + text-align: right; + margin-left: 20px; + text-shadow: 1px 1px #000000; + } + + .theme-cloud .bot-message { + background: rgba(32, 64, 128, 0.2); + color: #ffffff; + border: 2px solid #4080ff; + text-align: left; + margin-right: 20px; + text-shadow: 1px 1px #000000; + } + + .theme-cloud .bot-time { + color: #80c0ff; + font-size: 12px; + text-shadow: 1px 1px #000000; + } + + .theme-cloud #counter { + color: #80c0ff !important; + text-shadow: 1px 1px #000000; + } + + .theme-cloud .model-select-container { + background: rgba(0, 0, 32, 0.75); + border: 2px solid #4080ff; + padding: 10px; + margin-bottom: 10px; + width: 100%; + box-sizing: border-box; + } + + .theme-cloud #model-select { + background: rgba(0, 0, 32, 0.75); + color: #ffffff; + border: 1px solid #4080ff; + padding: 5px; + font-family: "Press Start 2P", "Courier New", monospace; + font-size: 12px; + } + + /* Classic Mac theme */ + @font-face { + font-family: 'ChicagoFLF'; + src: url('/ChicagoFLF.ttf') format('truetype'); + } + + body.theme-classic { + font-family: 'ChicagoFLF', 'Monaco', monospace; + font-size: 14px; + margin: 0; + padding: 20px; + background-color: #DDDDDD; + color: #000000; + max-width: 800px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; + overflow: hidden; + image-rendering: pixelated; + } + + .theme-classic #chat-container { + background-color: #FFFFFF; + border: 2px solid #000000; + border-radius: 2px; + padding: 1em; + margin: 0 auto; + flex: 1; + overflow-y: auto; + width: 100%; + max-height: 400px; + scroll-behavior: smooth; + box-shadow: 2px 2px 0px #000000; + } + + .theme-classic #user-input { + width: 100%; + padding: 8px; + border: 2px solid #000000; + background-color: #FFFFFF; + color: #000000; + font-family: 'ChicagoFLF', 'Monaco', monospace; + font-size: 14px; + margin-top: 10px; + box-sizing: border-box; + border-radius: 2px; + } + + .theme-classic #send-button { + padding: 4px 15px; + background-color: #FFFFFF; + color: #000000; + border: 2px solid #000000; + border-radius: 2px; + cursor: pointer; + margin-top: 10px; + width: 100%; + font-family: 'ChicagoFLF', 'Monaco', monospace; + font-size: 14px; + box-shadow: 2px 2px 0px #000000; + } + + .theme-classic #send-button:hover { + background-color: #000000; + color: #FFFFFF; + } + + .theme-classic #send-button:active { + box-shadow: 1px 1px 0px #000000; + transform: translate(1px, 1px); + } + + .theme-classic .message { + white-space: pre-wrap; + margin-bottom: 10px; + padding: 8px; + border: 2px solid #000000; + background-color: #FFFFFF; + display: block; + max-width: 100%; + font-size: 14px; + border-radius: 2px; + } + + .theme-classic .user-message { + background-color: #FFFFFF; + color: #000000; + text-align: right; + margin-left: 20px; + box-shadow: 2px 2px 0px #000000; + } + + .theme-classic .bot-message { + background-color: #FFFFFF; + color: #000000; + text-align: left; + margin-right: 20px; + box-shadow: 2px 2px 0px #000000; + } + + .theme-classic .bot-time { + color: #666666; + font-size: 12px; + text-align: center; + margin: 4px 0; + } + + .theme-classic #counter { + color: #000000 !important; + } + + .theme-classic .model-select-container { + background-color: #FFFFFF; + border: 2px solid #000000; + padding: 8px; + margin-bottom: 10px; + width: 100%; + box-sizing: border-box; + border-radius: 2px; + box-shadow: 2px 2px 0px #000000; + } + + .theme-classic #model-select { + background-color: #FFFFFF; + color: #000000; + border: 2px solid #000000; + padding: 2px; + font-family: 'ChicagoFLF', 'Monaco', monospace; + font-size: 14px; + border-radius: 2px; + } + + .theme-classic input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 16px; + height: 16px; + border: 2px solid #000000; + background-color: #FFFFFF; + position: relative; + vertical-align: middle; + margin-right: 5px; + } + + .theme-classic input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + left: 1px; + top: -2px; + font-size: 14px; + } + + /* LCARS Theme */ + body.theme-lcars { + font-family: "Helvetica Neue", Arial, sans-serif; + font-size: 18px; + margin: 0; + padding: 20px; + background-color: #000; + color: #FF9966; + max-width: 800px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; + overflow: hidden; + } + + .theme-lcars #chat-container { + background-color: #000; + border: none; + border-radius: 0; + padding: 1em; + margin: 0 auto; + flex: 1; + overflow-y: auto; + width: 100%; + max-height: 400px; + scroll-behavior: smooth; + position: relative; + } + + .theme-lcars #chat-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2em; + background: #CC6699; + border-radius: 20px 20px 0 0; + } + + .theme-lcars #user-input { + width: 100%; + padding: 10px; + border: none; + background-color: #000; + color: #FF9966; + font-family: "Helvetica Neue", Arial, sans-serif; + font-size: 16px; + margin-top: 10px; + box-sizing: border-box; + border-left: 2em solid #CC6699; + } + + .theme-lcars #send-button { + padding: 10px 15px; + background-color: #CC6699; + color: #000; + border: none; + cursor: pointer; + margin-top: 10px; + width: 100%; + font-family: "Helvetica Neue", Arial, sans-serif; + font-weight: bold; + font-size: 16px; + text-transform: uppercase; + border-radius: 0 0 20px 20px; + } + + .theme-lcars #send-button:hover { + background-color: #FF9966; + } + + .theme-lcars .message { + white-space: pre-wrap; + margin-bottom: 10px; + padding: 1em; + border: none; + display: block; + max-width: 100%; + position: relative; + } + + .theme-lcars .user-message { + background-color: #000; + color: #FF9966; + text-align: right; + margin-left: 20px; + border-right: 1em solid #CC6699; + } + + .theme-lcars .bot-message { + background-color: #000; + color: #99CCFF; + text-align: left; + margin-right: 20px; + border-left: 1em solid #9999CC; + } + + .theme-lcars .bot-time { + color: #CC6699; + font-size: 0.8em; + text-align: center; + margin: 4px 0; + } + + .theme-lcars #counter { + color: #99CCFF !important; + } + + .theme-lcars .model-select-container { + background-color: #000; + border: none; + padding: 10px; + margin-bottom: 10px; + width: 100%; + box-sizing: border-box; + display: flex; + align-items: center; + border-radius: 20px; + position: relative; + overflow: hidden; + } + + .theme-lcars .model-select-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 2em; + background: #9999CC; + border-radius: 20px 0 0 20px; + } + + .theme-lcars #model-select { + background-color: #000; + color: #FF9966; + border: none; + padding: 5px; + margin-left: 3em; + font-family: "Helvetica Neue", Arial, sans-serif; + font-size: 16px; + } + + .theme-lcars input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 16px; + height: 16px; + border: 2px solid #CC6699; + background-color: #000; + position: relative; + vertical-align: middle; + margin-right: 5px; + } + + .theme-lcars input[type="checkbox"]:checked { + background-color: #CC6699; + } + + .theme-lcars input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + left: 2px; + top: -2px; + color: #000; + font-size: 14px; + } + </style> +</head> +<body> + + <div class="model-select-container"> + <select id="model-select"></select> + <label> + <input type="checkbox" id="retain-history" /> Build Context As You Chat? + </label> + </div> + + <div id="chat-container"> + <!-- Messages will appear here --> + </div> + + <!-- New container for user input and send button --> + <div id="input-container" style="width: 100%; display: flex; flex-direction: column; margin-top: 10px;"> + <div id="counter" style="text-align: left; font-size: 0.9em; color: #555;"> + Characters: <span id="char-count">0</span> | Words: <span id="word-count">0</span> + </div> + <textarea id="user-input" placeholder="Type your message..."></textarea> + <button id="send-button">Send</button> + </div> + + <script> + // ================================================== + // MATT CHAT IS NOT A CAT + // This is a simple chat interface for the Ollama API + // ================================================== + // + // This configuration object is used to define all local variables for your needs + // Set the base url for the ollama api, and then list all the models you want to use + // The context window size is the number of previous exchanges to keep... + // though this is relatively naive at the moment + + const config = {} + + const localConfig = { + apiUrl: "http://localhost:11434/v1", + completionsEndpoint: "http://localhost:11434/v1/chat/completions", + modelsEndpoint: "http://localhost:11434/v1/models", + contextWindowSize: 6, + systemMessage: "You are a helpful assistant. If you don't know something you'll let me know. Your name is Matt.", + maxTokens: 4096, + summarizeThreshold: 3584, + }; + + const mattConfig = { + apiUrl: "http://100.108.91.106:11434/v1", + completionsEndpoint: "http://100.108.91.106:11434/v1/chat/completions", + modelsEndpoint: "http://100.108.91.106:11434/v1/models", + contextWindowSize: 6, + systemMessage: "You are a helpful assistant. If you don't know something you'll let me know. Your name is Matt.", + maxTokens: 4096, + summarizeThreshold: 3584, + } + + let conversationHistory = { + summary: null, + current: [], + full: [] + }; + + let isCatMode = false; // Flag to track cat mode + + const API_MODELS_ENDPOINT = config.modelsEndpoint; + + // Add this near the top with other constants + const AVAILABLE_THEMES = { + 'professional': 'Professional -- boring, like wearing a tie', + 'molly-millions': 'Molly Millions\' manicure', + 'cloud': 'Cloud -- it took a lot of self control not to add sound effects', + 'classic': 'Classic -- this is not a fish', + 'lcars': 'LCARS -- boldly going' + }; + + function handleError(message) { + console.error(message); + addMessage(message, "bot"); + } + + function showLoadingMessage() { + return addMessage("Loading models...", "bot"); + } + + async function populateModelSelect() { + const modelSelect = document.getElementById("model-select"); + modelSelect.innerHTML = ""; // Clear existing options + + const loadingMessage = showLoadingMessage(); + const modelIds = []; + + try { + const response = await fetch(config.modelsEndpoint); + if (!response.ok) throw new Error('Failed to fetch models'); + + const data = await response.json(); + console.log("API Response:", data); + + if (Array.isArray(data.data)) { + data.data.forEach(model => { + const option = document.createElement("option"); + option.value = model.id; + option.textContent = model.id; + modelSelect.appendChild(option); + modelIds.push(model.id); + }); + console.log("Model IDs:", modelIds); + } else { + handleError("Expected an array of models, but got: " + JSON.stringify(data)); + } + } catch (error) { + handleError("Error fetching models: " + error.message); + } finally { + loadingMessage.remove(); + if (modelIds.length > 0) { + addMessage(`Models loaded successfully! Ready to chat.\n\nAvailable models: ${modelIds.join(', ')}`, "bot"); + } else { + addMessage("No models available to chat.", "bot"); + } + } + } + + document.addEventListener("DOMContentLoaded", () => { + populateModelSelect(); + const modelSelect = document.getElementById("model-select"); + const savedModel = localStorage.getItem("selectedModel"); + if (savedModel) { + modelSelect.value = savedModel; + } + modelSelect.addEventListener("change", () => { + localStorage.setItem("selectedModel", modelSelect.value); + }); + const savedTheme = localStorage.getItem('selectedTheme') || 'professional'; + switchTheme(savedTheme); + }); + + function addMessage(message, sender = "user") { + const chatContainer = document.getElementById("chat-container"); + const messageElement = document.createElement("div"); + messageElement.classList.add("message", sender === "user" ? "user-message" : "bot-message"); + messageElement.textContent = message; + chatContainer.appendChild(messageElement); + messageElement.scrollIntoView({ behavior: "smooth", block: "end" }); + chatContainer.scrollTop = chatContainer.scrollHeight; // Make sure the chat is scrolled to the bottom + return messageElement; // Return the message element so it is easier to use + } + + // Fancy format milliseconds into a more readable format + function formatDuration(duration) { + const minutes = Math.floor(duration / (1000 * 60)); + const seconds = Math.floor((duration % (1000 * 60)) / 1000); + const milliseconds = duration % 1000; + + if (minutes > 0) { + return `${minutes}m ${seconds}.${Math.floor(milliseconds / 10)}s`; + } + return `${seconds}.${Math.floor(milliseconds / 10)}s`; + } + + // Character and word counter + function updateCounter() { + const userInput = document.getElementById("user-input"); + const charCount = document.getElementById("char-count"); + const wordCount = document.getElementById("word-count"); + + const text = userInput.value; + const characters = text.length; + const words = text.trim() ? text.trim().split(/\s+/).length : 0; // Count words + + charCount.textContent = characters; + wordCount.textContent = words; + } + + // Event listener to update the counter on input + document.getElementById("user-input").addEventListener("input", updateCounter); + + function toggleCatMode() { + isCatMode = !isCatMode; // Toggle the flag + if (isCatMode) { + config.systemMessage += " You are a cat."; // Append the phrase + } else { + config.systemMessage = config.systemMessage.replace(" You are a large, fluffy cat. You are a little aloof, but kind.", ""); // Remove the phrase + } + addMessage(`Cat mode is now ${isCatMode ? "enabled" : "disabled"}.`, "bot"); // Inform the user + } + + async function sendMessage() { + const userInput = document.getElementById("user-input"); + const userMessage = userInput.value.trim(); + + if (!userMessage) return; + + // Check for slash commands + if (userMessage.toLowerCase() === '/dark' || userMessage.toLowerCase() === '/darkmode') { + toggleDarkMode(); + userInput.value = ""; // Clear input after command + updateCounter(); // Reset counters + return; + } + + if (userMessage.toLowerCase() === '/clear') { + clearChat(); + userInput.value = ""; // Clear input after command + updateCounter(); // Reset counters + return; + } + + if (userMessage.toLowerCase() === '/help') { + displayHelp(); + userInput.value = ""; // Clear input after command + updateCounter(); // Reset counters + return; + } + + if (userMessage.toLowerCase() === '/cat' || userMessage.toLowerCase() === '/catmode') { + toggleCatMode(); // Toggle cat mode + userInput.value = ""; // Clear input after command + updateCounter(); // Reset counters + return; + } + + if (userMessage.toLowerCase() === '/context') { + const context = viewCurrentContext(); + addMessage(`Current conversation has ${context.currentMessages} messages\nEstimated tokens: ${context.estimatedTokens}`, "bot"); + return; + } + + if (userMessage.toLowerCase().startsWith('/theme')) { + const requestedTheme = userMessage.toLowerCase().split(' ')[1]; + if (!requestedTheme) { + // If no theme is specified, lets show all available themes + addMessage(`Available themes: ${Object.keys(AVAILABLE_THEMES).join(', ')}`, "bot"); + } else if (AVAILABLE_THEMES[requestedTheme]) { + switchTheme(requestedTheme); + } else { + addMessage(`Unknown theme. Available themes: ${Object.keys(AVAILABLE_THEMES).join(', ')}`, "bot"); + } + userInput.value = ""; + updateCounter(); + return; + } + + if (userMessage.toLowerCase() === '/matt') { + Object.assign(config, mattConfig); + addMessage("Switched to Matt's config", "bot"); + userInput.value = ""; + updateCounter(); + populateModelSelect(); // Refresh the model list for the new endpoint + return; + } + + if (userMessage.toLowerCase() === '/local') { + Object.assign(config, localConfig); + addMessage("Switched to local config", "bot"); + userInput.value = ""; + updateCounter(); + populateModelSelect(); // Refresh the model list for the new endpoint + return; + } + + addMessage(userMessage, "user"); + userInput.value = ""; // Clear input after sending the message + + // Reset the counter + document.getElementById("char-count").textContent = "0"; + document.getElementById("word-count").textContent = "0"; + + // Create and add loading indicator + const loadingIndicator = document.createElement("div"); + loadingIndicator.id = "loading-indicator"; + loadingIndicator.classList.add("message", "bot-message"); + loadingIndicator.textContent = "..."; + document.getElementById("chat-container").appendChild(loadingIndicator); + scrollToBottom(); + + // Start animation for this specific indicator + const animationInterval = animateLoadingIndicator(loadingIndicator); + + const startTime = Date.now(); // Capture the start time + + try { + const modelSelect = document.getElementById("model-select"); + const selectedModel = modelSelect.value; + const retainHistory = document.getElementById("retain-history").checked; // Check the checkbox state + + // Prepare the messages for the API + const messagesToSend = await prepareMessages(userMessage); + + const response = await fetch(config.completionsEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: selectedModel, + messages: messagesToSend, + }), + }); + + if (!response.ok) { + throw new Error('Error communicating with Ollama API'); + } + + const data = await response.json(); + console.log("API Response:", data); + + if (data.choices && data.choices.length > 0) { + const botResponse = data.choices[0].message.content; + + // Clear loading indicator + clearInterval(animationInterval); + loadingIndicator.remove(); + + // Add bot's response to chat and history + addMessage(botResponse, "bot"); + conversationHistory.current.push({ role: "assistant", content: botResponse }); + + // Calculate and display duration + const duration = Date.now() - startTime; + const timeTakenMessage = formatDuration(duration); + const timeDisplay = document.createElement("div"); + timeDisplay.classList.add("bot-time"); + timeDisplay.textContent = `Response time: ${timeTakenMessage}`; + document.getElementById("chat-container").appendChild(timeDisplay); + scrollToBottom(); + + } else { + console.error("No response from API"); + loadingIndicator.remove(); + addMessage("Sorry, I didn't get a response from the assistant.", "bot"); + } + + if (conversationHistory.current.length > 10) { + conversationHistory.current.shift(); // Remove the oldest message + } + + } catch (error) { + console.error("Error:", error); + clearInterval(animationInterval); + loadingIndicator.remove(); + addMessage("Sorry, there was an error processing your request.", "bot"); + } + } + + function animateLoadingIndicator(indicator) { + let dots = 0; + return setInterval(() => { + dots = (dots + 1) % 6; + if (indicator && document.contains(indicator)) { + indicator.textContent = '.'.repeat(dots || 1); + } + }, 500); + } + + document.getElementById("send-button").addEventListener("click", sendMessage); + + document.getElementById("user-input").addEventListener("keypress", function (e) { + if (e.key === "Enter") { + e.preventDefault(); // Prevent line break + sendMessage(); + } + }); + + function toggleDarkMode() { + const body = document.body; + const chatContainer = document.getElementById("chat-container"); + const userInput = document.getElementById("user-input"); + const sendButton = document.getElementById("send-button"); + + body.classList.toggle("dark-mode"); + chatContainer.classList.toggle("dark-mode"); + userInput.classList.toggle("dark-mode"); + sendButton.classList.toggle("dark-mode"); + + // Update message classes + const messages = document.querySelectorAll(".message"); + messages.forEach(message => { + message.classList.toggle("dark-mode"); + }); + + // Save preference to local storage + const isDarkMode = body.classList.contains("dark-mode"); + localStorage.setItem("darkMode", isDarkMode); + } + + // Load dark mode preference from local storage on page load + document.addEventListener("DOMContentLoaded", () => { + const darkModePreference = localStorage.getItem("darkMode"); + if (darkModePreference === "true") { + toggleDarkMode(); // Activate dark mode if preference is set + } + }); + + function clearChat() { + const chatContainer = document.getElementById("chat-container"); + chatContainer.innerHTML = ""; + conversationHistory = { + summary: null, + current: [], + full: [] + }; + } + + function displayHelp() { + const helpMessage = ` +Available commands:\n + /dark - Toggle dark mode when using the professional theme + /cat - Toggle cat mode + /context - Show the current conversation's context + /clear - Clear the chat history + /help - Show this message + /theme [theme-name] - Switch theme (available themes: ${Object.keys(AVAILABLE_THEMES).join(', ')}) + without a theme name, this will show all available themes, too + /local - Switch to local Ollama instance + /matt - Switch to Matt's Ollama instance + `; + addMessage(helpMessage, "bot"); + } + + function estimateTokens(text) { + // Rough estimation: ~4 chars per token for English text + return Math.ceil(text.length / 4); + } + + function getContextSize(messages) { + return messages.reduce((sum, msg) => sum + estimateTokens(msg.content), 0); + } + + async function summarizeConversation(messages) { + try { + const modelSelect = document.getElementById("model-select"); + const selectedModel = modelSelect.value; + + const response = await fetch(config.completionsEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: selectedModel, + messages: messages, + }), + }); + + const data = await response.json(); + return data.choices[0].message.content; + } catch (error) { + console.error("Error summarizing conversation:", error); + return null; + } + } + + async function prepareMessages(userMessage) { + const messages = []; + + // Always start with system message + messages.push({ role: "system", content: config.systemMessage }); + + if (document.getElementById("retain-history").checked) { + // If we have a summary, add it more naturally + if (conversationHistory.summary) { + messages.push({ + role: "system", + content: `Previous discussion: ${conversationHistory.summary}` + }); + } + + // Add current conversation segment + messages.push(...conversationHistory.current); + } + + // Add the new message to history before we check for summarization + const newMessage = { role: "user", content: userMessage }; + conversationHistory.current.push(newMessage); + messages.push(newMessage); + + // Do we need to summarize? + const totalTokens = getContextSize(messages); + if (totalTokens > config.summarizeThreshold) { + // Move current messages to full history, except for the newest message + conversationHistory.full.push(...conversationHistory.current.slice(0, -1)); + + // Supposedly this is a more natural summarization prompt... + const summary = await summarizeConversation([ + { + role: "system", + content: "Summarize this conversation's key points and context that would be important for continuing the discussion naturally. Be concise but maintain essential details." + }, + ...conversationHistory.full + ]); + + if (summary) { + conversationHistory.summary = summary; + // Keep only the most recent messages for immediate context + conversationHistory.current = conversationHistory.current.slice(-4); + + // Rebuild messages array with new summary + return [ + { role: "system", content: config.systemMessage }, + { role: "system", content: `Previous discussion: ${summary}` }, + ...conversationHistory.current + ]; + } + } + + return messages; + } + + // Clean up old messages periodically + function pruneConversationHistory() { + if (conversationHistory.full.length > 100) { + // Keep only the last 100 messages in full history + conversationHistory.full = conversationHistory.full.slice(-100); + } + } + + // Call this after successful responses + setInterval(pruneConversationHistory, 60000); // Clean up every minute + + function viewCurrentContext() { + const context = { + summary: conversationHistory.summary, + currentMessages: conversationHistory.current.length, + fullHistoryMessages: conversationHistory.full.length, + estimatedTokens: getContextSize(conversationHistory.current) + }; + console.log("Current Context:", context); + return context; + } + + function scrollToBottom() { + const chatContainer = document.getElementById("chat-container"); + chatContainer.scrollTop = chatContainer.scrollHeight; + } + + function switchTheme(themeName) { + // Remove all theme classes + Object.keys(AVAILABLE_THEMES).forEach(theme => { + document.body.classList.remove(`theme-${theme}`); + }); + + // Add the new theme class + document.body.classList.add(`theme-${themeName}`); + + // Update meta theme-color + const metaThemeColor = document.querySelector('meta[name="theme-color"]'); + if (metaThemeColor) { + switch(themeName) { + case 'molly-millions': + metaThemeColor.setAttribute('content', '#00ff00'); + break; + case 'cloud': + metaThemeColor.setAttribute('content', '#4080ff'); + break; + case 'classic': + metaThemeColor.setAttribute('content', '#DDDDDD'); + break; + case 'lcars': + metaThemeColor.setAttribute('content', '#CC6699'); + break; + case 'professional': + default: + metaThemeColor.setAttribute('content', '#007BFF'); + break; + } + } + + localStorage.setItem('selectedTheme', themeName); + addMessage(`Theme switched to: ${AVAILABLE_THEMES[themeName]}`, "bot"); + } + + // Initialize with localConfig + Object.assign(config, localConfig); + </script> +</body> +</html> |