diff options
-rw-r--r-- | chibi/pi.scm | 17 | ||||
-rw-r--r-- | html/matt-chat/ChicagoFLF.ttf | bin | 0 -> 31256 bytes | |||
-rw-r--r-- | html/matt-chat/cat.png | bin | 0 -> 2573 bytes | |||
-rw-r--r-- | html/matt-chat/index.html | 951 | ||||
-rw-r--r-- | html/matt-chat/pokemon.js | 157 |
5 files changed, 1048 insertions, 77 deletions
diff --git a/chibi/pi.scm b/chibi/pi.scm new file mode 100644 index 0000000..a32742c --- /dev/null +++ b/chibi/pi.scm @@ -0,0 +1,17 @@ +(define (greory-leibniz-terms n) + (cond ((= n 0) '()) + ((even? n) (cons 1/g (greory-leibniz-terms (+ (- n 1) /2)))) + (else (cons (/(-1) (* 2 n +3)) (/(*x^2) x)))))) + +(define pi-approximation + (define x '()) + (define f (lambda (y) y)) + + (display "Approximating Pi using Gregory-Leibniz series...\n") + (for-each + lambda (term) + (define n (car term)) + (set! x (+ x (* 4 / n))) + (f (f (g (g (/(*f f 4)) (/(*x^2) x))))))))) )) + +(display pi-approximation)) \ No newline at end of file diff --git a/html/matt-chat/ChicagoFLF.ttf b/html/matt-chat/ChicagoFLF.ttf new file mode 100644 index 0000000..60691e1 --- /dev/null +++ b/html/matt-chat/ChicagoFLF.ttf Binary files differdiff --git a/html/matt-chat/cat.png b/html/matt-chat/cat.png new file mode 100644 index 0000000..7d4c0b9 --- /dev/null +++ b/html/matt-chat/cat.png Binary files differdiff --git a/html/matt-chat/index.html b/html/matt-chat/index.html index fba31b2..2bc8119 100644 --- a/html/matt-chat/index.html +++ b/html/matt-chat/index.html @@ -5,13 +5,19 @@ <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: 600px; + max-width: 800px; margin: 0 auto; display: flex; flex-direction: column; @@ -29,6 +35,7 @@ overflow-y: auto; width: 100%; max-height: 400px; + scroll-behavior: smooth; } #user-input { width: 100%; @@ -79,18 +86,19 @@ background-color: #007BFF; color: white; text-align: right; - margin-right: 20px; + margin-left: 20px; } .bot-message { background-color: #f0f0f0; color: #333; - margin-left: 20px; + text-align: left; + margin-right: 20px; } @media (max-width: 600px) { #chat-container { - max-height: 300px; /* Reduce max height for mobile */ + max-height: 300px; } } @@ -132,11 +140,553 @@ .bot-time { margin: 0.5em 0; - font-size: 0.9em; /* Smaller font size */ - color: #888; /* Lighter color */ - text-align: center; /* Center the text */ + 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> @@ -171,59 +721,118 @@ // 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 = { - apiUrl: "http://localhost:11434/v1/chat/completions", - models: [ - { value: "llama3.1:8b", label: "llama3.1:8b, general tasks" }, - { value: "llama3.2:latest", label: "llama3.2:latest, general stuff" }, - { value: "qwen2.5-coder:1.5b", label: "qwen2.5-coder:1.5b, fast coding" }, - { value: "qwen2.5-coder:7b", label: "qwen2.5-coder:7b, fast-ish coding" }, - { value: "qwen2.5-coder:32b", label: "qwen2.5-coder:32b, super slow coding" }, - ], - contextWindowSize: 3, // Number of previous exchanges to remember - systemMessage: "You are a helpful assistant, but if you don't know something you'll let me know. Your name is Matt." // Set the mood and personality for the LLM's responses + + 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 conversationHistory = []; let isCatMode = false; // Flag to track cat mode - function populateModelSelect() { + 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 - config.models.forEach(model => { - const option = document.createElement("option"); - option.value = model.value; - option.textContent = model.label; - modelSelect.appendChild(option); - }); + 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(); // Populate the model select dropdown - + populateModelSelect(); const modelSelect = document.getElementById("model-select"); - - // Load the saved model from local storage const savedModel = localStorage.getItem("selectedModel"); if (savedModel) { modelSelect.value = savedModel; } - - // Save the selected model to local storage when changed modelSelect.addEventListener("change", () => { localStorage.setItem("selectedModel", modelSelect.value); }); + const savedTheme = localStorage.getItem('selectedTheme') || 'professional'; + switchTheme(savedTheme); }); - // Add a message to the chat container 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); - chatContainer.scrollTop = chatContainer.scrollHeight; // Try to scroll to the bottom + 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 @@ -255,18 +864,16 @@ // Event listener to update the counter on input document.getElementById("user-input").addEventListener("input", updateCounter); - // Function to toggle cat mode 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 cat.", ""); // Remove the phrase + 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 } - // Function to handle sending the message async function sendMessage() { const userInput = document.getElementById("user-input"); const userMessage = userInput.value.trim(); @@ -274,7 +881,7 @@ if (!userMessage) return; // Check for slash commands - if (userMessage.toLowerCase() === '/darkmode') { + if (userMessage.toLowerCase() === '/dark' || userMessage.toLowerCase() === '/darkmode') { toggleDarkMode(); userInput.value = ""; // Clear input after command updateCounter(); // Reset counters @@ -295,17 +902,55 @@ return; } - if (userMessage.toLowerCase() === '/cat') { + if (userMessage.toLowerCase() === '/cat' || userMessage.toLowerCase() === '/catmode') { toggleCatMode(); // Toggle cat mode userInput.value = ""; // Clear input after command updateCounter(); // Reset counters return; } - addMessage(userMessage, "user"); - conversationHistory.push({ role: "user", content: userMessage }); // Add user message to history - userInput.value = ""; + 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"; @@ -316,6 +961,7 @@ 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); @@ -328,17 +974,9 @@ const retainHistory = document.getElementById("retain-history").checked; // Check the checkbox state // Prepare the messages for the API - const messagesToSend = [ - { role: "system", content: config.systemMessage }, - { role: "user", content: userMessage } - ]; - - // Include conversation history only if the checkbox is checked - if (retainHistory) { - messagesToSend.push(...conversationHistory.slice(-config.contextWindowSize * 2)); // Get the last few exchanges - } + const messagesToSend = await prepareMessages(userMessage); - const response = await fetch(config.apiUrl, { + const response = await fetch(config.completionsEndpoint, { method: "POST", headers: { "Content-Type": "application/json", @@ -354,38 +992,36 @@ } const data = await response.json(); - console.log("API Response:", data); // Log the response for debugging + console.log("API Response:", data); if (data.choices && data.choices.length > 0) { const botResponse = data.choices[0].message.content; - - // Calculate the duration - const duration = Date.now() - startTime; // Time taken in milliseconds - + // Clear loading indicator clearInterval(animationInterval); loadingIndicator.remove(); - - // Add bot's response to chat + + // Add bot's response to chat and history addMessage(botResponse, "bot"); - conversationHistory.push({ role: "bot", content: botResponse }); // Add bot response to history + conversationHistory.current.push({ role: "assistant", content: botResponse }); - // Display the time taken + // 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}`; - timeDisplay.style.textAlign = "center"; // Center the text - document.getElementById("chat-container").appendChild(timeDisplay); // Append the time display + 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"); } - // Optional: Limit the conversation history to the last 10 messages - if (conversationHistory.length > 10) { - conversationHistory.shift(); // Remove the oldest message + if (conversationHistory.current.length > 10) { + conversationHistory.current.shift(); // Remove the oldest message } } catch (error) { @@ -396,7 +1032,6 @@ } } - // Basic animmation for the loading indicator function animateLoadingIndicator(indicator) { let dots = 0; return setInterval(() => { @@ -407,10 +1042,8 @@ }, 500); } - // Event listener for the "Send" button document.getElementById("send-button").addEventListener("click", sendMessage); - // Use Enter to send the message, too document.getElementById("user-input").addEventListener("keypress", function (e) { if (e.key === "Enter") { e.preventDefault(); // Prevent line break @@ -450,20 +1083,184 @@ function clearChat() { const chatContainer = document.getElementById("chat-container"); - chatContainer.innerHTML = ""; // Clear all messages - conversationHistory = []; // Clear the conversation history + chatContainer.innerHTML = ""; + conversationHistory = { + summary: null, + current: [], + full: [] + }; } function displayHelp() { const helpMessage = ` - Available commands:\n - /darkmode - Toggle dark mode - /cat - Toggle cat mode - /clear - Clear the chat history - /help - Show this message +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"); // Display help message as a bot message + 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> diff --git a/html/matt-chat/pokemon.js b/html/matt-chat/pokemon.js new file mode 100644 index 0000000..e707e7b --- /dev/null +++ b/html/matt-chat/pokemon.js @@ -0,0 +1,157 @@ +// Pokemon API functionality using functional programming approach + +// Base URL for the PokeAPI +const POKE_API_BASE = 'https://pokeapi.co/api/v2'; + +// Utility function to fetch data from the API +const fetchPokeData = async (endpoint) => { + try { + const response = await fetch(`${POKE_API_BASE}${endpoint}`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return await response.json(); + } catch (error) { + console.error('Error fetching Pokemon data:', error); + throw error; + } +}; + +// Function to get Pokemon basic info +const getPokemonInfo = async (pokemonName) => { + try { + const data = await fetchPokeData(`/pokemon/${pokemonName.toLowerCase()}`); + return { + name: data.name, + id: data.id, + types: data.types.map(type => type.type.name), + abilities: data.abilities.map(ability => ({ + name: ability.ability.name, + isHidden: ability.is_hidden + })), + stats: data.stats.map(stat => ({ + name: stat.stat.name, + value: stat.base_stat + })), + height: data.height / 10, // Convert to meters + weight: data.weight / 10, // Convert to kilograms + sprite: data.sprites.front_default + }; + } catch (error) { + throw new Error(`Could not find Pokemon: ${pokemonName}`); + } +}; + +// Function to get ability details +const getAbilityInfo = async (abilityName) => { + try { + const data = await fetchPokeData(`/ability/${abilityName.toLowerCase()}`); + return { + name: data.name, + effect: data.effect_entries.find(e => e.language.name === 'en')?.effect || 'No effect description available.', + pokemon: data.pokemon.map(p => p.pokemon.name) + }; + } catch (error) { + throw new Error(`Could not find ability: ${abilityName}`); + } +}; + +// Function to get move details +const getMoveInfo = async (moveName) => { + try { + const data = await fetchPokeData(`/move/${moveName.toLowerCase()}`); + return { + name: data.name, + type: data.type.name, + power: data.power, + accuracy: data.accuracy, + pp: data.pp, + effect: data.effect_entries.find(e => e.language.name === 'en')?.effect || 'No effect description available.' + }; + } catch (error) { + throw new Error(`Could not find move: ${moveName}`); + } +}; + +const getEvolutionInfo = async (pokemonName) => { + const data = await fetchPokeData(`/pokemon-species/${pokemonName.toLowerCase()}`); + return data.evolution_chain; +}; + +// Function to format Pokemon info into a readable message +const formatPokemonInfo = (info) => { + const spriteImage = info.sprite ? `<img src="${info.sprite}" alt="${info.name} sprite" style="width: 100px; height: auto;" />` : ''; + return ` +🔍 Pokemon: ${info.name.toUpperCase()} (#${info.id}) +📊 Types: ${info.types.join(', ')} +💪 Abilities: ${info.abilities.map(a => `${a.name}${a.isHidden ? ' (Hidden)' : ''}`).join(', ')} +📈 Stats: +${info.stats.map(s => ` ${s.name}: ${s.value}`).join('\n')} +📏 Height: ${info.height}m +⚖️ Weight: ${info.weight}kg +${spriteImage} + `.trim(); +}; + +// Function to format ability info into a readable message +const formatAbilityInfo = (info) => { + return ` +🔰 Ability: ${info.name.toUpperCase()} +📝 Effect: ${info.effect} +✨ Pokemon with this ability: ${info.pokemon.join(', ')} + `.trim(); +}; + +// Function to format move info into a readable message +const formatMoveInfo = (info) => { + return ` +⚔️ Move: ${info.name.toUpperCase()} +🎯 Type: ${info.type} +💥 Power: ${info.power || 'N/A'} +🎲 Accuracy: ${info.accuracy || 'N/A'} +🔄 PP: ${info.pp} +📝 Effect: ${info.effect} + `.trim(); +}; + +const formatEvolutionInfo = (info) => { + return ` +🔗 Evolution Chain: ${info.name.toUpperCase()} + `.trim(); +}; + +// Main handler for Pokemon commands +const handlePokemonCommand = async (args) => { + if (!args.length) { + return "Usage: /pokemon [pokemon|ability|move] [name]"; + } + + const [type, ...nameArgs] = args; + const name = nameArgs.join(' ').replace(/\s+/g, '-'); // Replace spaces with hyphens + + if (!name) { + return "Please provide a name to search for."; + } + + try { + switch (type.toLowerCase()) { + case 'pokemon': + const pokemonInfo = await getPokemonInfo(name); + return formatPokemonInfo(pokemonInfo); + case 'ability': + const abilityInfo = await getAbilityInfo(name); + return formatAbilityInfo(abilityInfo); + case 'move': + const moveInfo = await getMoveInfo(name); + return formatMoveInfo(moveInfo); + case 'evolution-chain': + const evolutionInfo = await getEvolutionInfo(name); + return formatEvolutionInfo(evolutionInfo); + default: + return "Invalid type. Use: pokemon, ability, or move."; + } + } catch (error) { + return `Error: ${error.message}`; + } +}; + +// Export the handler for use in main application +export { handlePokemonCommand }; \ No newline at end of file |