<!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>