const MSG_OPERATION_STARTED = 0;
const MSG_OPERATION_ENDED = 255;

// Chat messages
const MSG_CHAT_SYSTEM = 1;
const MSG_CHAT_USER = 2;
const MSG_CHAT_THINK = 3;
const MSG_AGENT_SWITCH = 4;
const MSG_AUTHORIZATION_REQUEST = 5;
const MSG_AUTHORIZATION_RESPONSE = 6;

// Command messages
const MSG_COMMAND_START = 10;
const MSG_COMMAND_OUTPUT = 11;
const MSG_COMMAND_SUCCESS = 12;
const MSG_COMMAND_ERROR = 13;

// Download messages
const MSG_DOWNLOAD_START = 20;
const MSG_UPLOAD_START = 21;
const MSG_PROGRESS = 22;
const MSG_PROGRESS_SUCCESS = 23;
const MSG_PROGRESS_ERROR = 24;

const MSG_PROMPT_INPUT = 100;
const MSG_COMMAND_INPUT = 101;


// --- Message Rendering ---

const chatHistory = document.querySelector('.chat-history');
const promptInput = document.querySelector('.chat-input textarea');
const sendButton = document.querySelector('.chat-input button');
const sendIcon = sendButton ? sendButton.querySelector('i') : null;

window.showThinkingEnabled = false;
window.synthesizeSpeechEnabled = false;

const thinkingToggle = document.getElementById('toggle-show-thinking');
const speechToggle = document.getElementById('toggle-synthesize-speech');
const dropdownToggleButton = document.getElementById('model-settings-dropdown');
const dropdownMenu = document.querySelector('.gear-dropdown .dropdown-menu');

const attachToggle = (element, propertyName, defaultValue, onChange) => {
	  if (!element) {
		  window[propertyName] = defaultValue;
		  if (typeof onChange === 'function') {
			  onChange(window[propertyName]);
		  }
		  return;
	  }
	  const updateState = (value) => {
		  window[propertyName] = value;
		  if (typeof onChange === 'function') {
			  onChange(value);
		  }
	  };
	  updateState(element.checked);
	  element.addEventListener('change', (event) => {
		  updateState(event.target.checked);
	  });
};

const updateThinkingVisibility = (visible) => {
	  document.querySelectorAll('.vdu-container').forEach((el) => {
		  if (visible) {
			  el.classList.remove('hideThinking');
			  if (!el.classList.contains('d-flex')) {
				  el.classList.add('d-flex');
			  }
		  } else {
			  el.classList.add('hideThinking');
			  el.classList.remove('d-flex');
		  }
	  });
};

attachToggle(thinkingToggle, 'showThinkingEnabled', true, () => {
	  updateThinkingVisibility(window.showThinkingEnabled);
});
attachToggle(speechToggle, 'synthesizeSpeechEnabled', false);

const setSendSpinner = (active) => {
	if (!sendIcon || !sendButton) {
		return;
	}
	if (active) {
		sendIcon.classList.remove('fa-paper-plane');
		sendIcon.classList.add('fa-circle-notch', 'fa-spin');
	} else {
		sendIcon.classList.remove('fa-circle-notch', 'fa-spin');
		if (!sendIcon.classList.contains('fa-paper-plane')) {
			sendIcon.classList.add('fa-paper-plane');
		}
	}
};

setSendSpinner(false);

if (dropdownToggleButton && dropdownMenu) {
	const toggleDropdownMenu = (event) => {
		event.preventDefault();
		event.stopPropagation();
		dropdownMenu.classList.toggle('show');
		const expanded = dropdownMenu.classList.contains('show');
		dropdownToggleButton.setAttribute('aria-expanded', expanded ? 'true' : 'false');
	};

	dropdownToggleButton.addEventListener('click', toggleDropdownMenu);
	document.addEventListener('click', (event) => {
		if (dropdownMenu.classList.contains('show') && !dropdownMenu.contains(event.target) && !dropdownToggleButton.contains(event.target)) {
			dropdownMenu.classList.remove('show');
			dropdownToggleButton.setAttribute('aria-expanded', 'false');
		}
	});
}

const adjustTextareaHeight = () => {
	if (!promptInput) {
		return;
	}
	promptInput.style.height = 'auto';
	promptInput.style.height = `${promptInput.scrollHeight}px`;
};

if (promptInput) {
	promptInput.addEventListener('input', adjustTextareaHeight);
}

if (sendButton && promptInput) {
	sendButton.addEventListener('click', () => {
		setTimeout(() => {
			promptInput.style.height = 'auto';
		}, 10);
	});
}

const md = window.markdownit();

const markdownToPlainText = (source) => {
	if (!source) {
		return '';
	}
	const temp = document.createElement('div');
	temp.innerHTML = md.render(source);
	return temp.textContent?.trim() ?? '';
};

const progress = (uuid, val) => {
	
	const progressBar = document.getElementById(uuid);
	progressBar.setAttribute('aria-valuenow', val);
	var el = progressBar.getElementsByClassName('progress-bar')[0];
	el.textContent = val + '%';
	el.style = `width: ${val}%`;
	return el;
}

const progressComplete = (uuid) => {
	progress(uuid, 100).classList.add('bg-success');
}

const progressError = (uuid) => {
	progress(uuid, 100).classList.add('bg-error');
}

const addTermToggle = (element) => {
	
	var el = document.createElement('a');
	el.setAttribute('href', '#');
	el.classList.add('toggle-output', 'float-end', 'ms-2', 'text-dark');
	var icon = document.createElement('i');
	icon.classList.add('fa-solid', 'fa-square-caret-down');
	el.appendChild(icon);
	element.before(el);
}

const commandComplete = (uuid) => {
	
	document.getElementById(uuid).getElementsByTagName("span")[0].classList.remove('bg-primary');
	document.getElementById(uuid).getElementsByTagName("span")[0].classList.add('bg-success');
	document.getElementById(uuid).getElementsByTagName("span")[0].innerHTML = '<i class="fa-solid fa-check"></i> Done';
	
	addTermToggle(document.getElementById(uuid).getElementsByTagName("span")[0]);
}

const commandError = (uuid) => {
	
	document.getElementById(uuid).getElementsByTagName("span")[0].classList.remove('bg-primary');
	document.getElementById(uuid).getElementsByTagName("span")[0].classList.add('bg-danger');
	document.getElementById(uuid).getElementsByTagName("span")[0].innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i> Error';

	addTermToggle(document.getElementById(uuid).getElementsByTagName("span")[0]);
}

const commandOutput = (uuid, output) => {
	
	const terminalElement = document.getElementById(uuid).getElementsByClassName("term")[0];
	terminalElement.xtermInstance.write(output);
}

// This listener ensures the voices are loaded from the OS/Browser
window.speechSynthesis.onvoiceschanged = () => {
    console.log("WOPR Voice Cores Online. Available voices:", window.speechSynthesis.getVoices().length);
};

// Call this once on page load to "kick" the service
window.speechSynthesis.getVoices();


const speak = (text) => {
    if (!window.synthesizeSpeechEnabled) return;
    const plainText = markdownToPlainText(text);
    if (!plainText) return;

    // Cancel any current speech to prevent overlapping "robotic" stutter
    window.speechSynthesis.cancel();

    const utterance = new SpeechSynthesisUtterance(plainText);
    
    // 1. Target the 'Google US English' or 'Samantha' (MacOS) voices
    // These are cleaner and less raspy than the 'Microsoft' or 'Alex' defaults.
    const voices = window.speechSynthesis.getVoices();
    
    // Look for high-quality "Natural" or "Google" voices first
    const preferredVoice = voices.find(v => v.name === 'Google US English') 
                        || voices.find(v => v.name === 'Samantha')
                        || voices.find(v => v.lang === 'en-US');

    if (preferredVoice) {
        utterance.voice = preferredVoice;
    }

    // 2. THE WOPR "CADENCE" SETTINGS
    utterance.rate = 0.85;  // Slightly slower for that deliberate mainframe feel
    utterance.pitch = 0.5; // Lower pitch = more "1980s hardware" vibe
    utterance.volume = 1.0;

    window.speechSynthesis.speak(utterance);
};
				
const createMessageBubble = (ai, buildFunc) => {
	const messageContainer = document.createElement('div');
	const messageBubble = document.createElement('div');

	messageBubble.classList.add('message-bubble');
	
	if(!ai) {
		messageContainer.classList.add('d-flex', 'justify-content-end');
		messageBubble.classList.add('user-message');
	} else {
		messageContainer.classList.add('d-flex', 'justify-content-start');
		messageBubble.classList.add('ai-message', 'd-flex', 'align-items-start');
	}
	
	buildFunc(messageContainer, messageBubble);

  	messageContainer.appendChild(messageBubble);
	chatHistory.appendChild(messageContainer);
	
	return messageContainer;
}
// Function to add a message to the chat history
const addMessage = (sender, type, uuid, content) => {

	if (sender === 'user') {
		createMessageBubble(false, function(messageContainer, messageBubble) {
			iconHtml = '<i class="fa-solid fa-comment-dots icon text-light"></i>';
			contentHtml = `<p class="mb-0">${content}</p>`;
			messageBubble.innerHTML = `<p class="mb-0"><i class="me-2 fa-solid  fa-comment-dots icon text-light"></i>${content}</p>`;
		});
	
	} else if (sender === 'agent') {
		const html = `<div class="vdu-log">[LOAD]: @${content} OK</div>`;
		chatHistory.insertAdjacentHTML('beforeend', html);
	} else if (sender === 'ai') {

		switch (type) {
			case 'comment':
				createMessageBubble(true, function(messageContainer, messageBubble) {
					messageBubble.innerHTML = `<i class="fa-solid fa-comment-dots icon text-light"></i><div>${md.render(content)}</div>`;
				});
				speak(content); // Strip HTML tags for speech
				break;
			case 'think':
			case 'reason':
				var c = 'hideThinking';
				if(window.showThinkingEnabled) {
					c = 'd-flex';
				}
				const tracePill = `
				<div class="vdu-container ${c} justify-content-start$">
				        <div class="vdu-log" style="color: #00ffcc; border-color: #00ffcc; text-shadow: 0 0 5px rgba(0, 255, 204, 0.7);">
				            <i class="fa-solid fa-microchip me-2"></i>
				            [THINK]: ${content}
				        </div>
				    </div>
				`;

				chatHistory.insertAdjacentHTML('beforeend', tracePill);	
				break;
			case 'command':
				messageContainer = createMessageBubble(true, function(messageContainer, messageBubble) {
					messageBubble.classList.add('ai-action');
					messageBubble.innerHTML = `
					<div>
					    <i class="fa-solid fa-terminal icon text-light"></i>
					</div>
					<div id="${uuid}">
                        <strong>Executing command:</strong> <code class="ms-2">${content}</code>
						<span class="ms-2 float-end badge bg-primary"><i class="fa-solid  fa-spinner fa-spin"></i> Running...</span>
						<div class="term mt-2 d-none"></div>
                    </div>
					`;
				});
				
				const terminalElement = messageContainer.getElementsByClassName('term')[0];
				const term = new Terminal({
					rows: 2,
					cols: 80,
		            cursorBlink: false,
		            convertEol: true,
		            scrollback: 1000 
				});
				const func = () => {
					// The cursor Y position is 0-indexed. Add 1 to get the number of lines with content.
		            const contentRows = term.buffer.active.baseY + 1;
		            
		            // Calculate the desired number of rows, clamping between MIN and MAX.
		            const desiredRows = Math.max(contentRows, 2);
		            const newRows = Math.min(desiredRows, 20);

		            // Resize only if the number of rows has changed
		            if (newRows !== term.rows) {
		                term.resize(80, newRows);
		            }
				};
		        term.onLineFeed(func);
		        term.onKey(func);
				term.open(terminalElement);
				term.onData((data) => {
				        createBinaryMessage(MSG_COMMAND_INPUT, data);
				 });
				terminalElement.xtermInstance = term;
				break;
			case 'download':
				createMessageBubble(true, function(messageContainer, messageBubble) {
					messageBubble.classList.add('ai-action');
					messageBubble.innerHTML = `
					<div><i class="fa-solid fa-cloud-download-alt icon text-info"></i></div>
					<div>
                        <p class="mb-1"><strong>Downloading file:</strong> <code>${content}</code></p>
                        <div class="progress" id="${uuid}" role="progressbar" aria-label="Download Progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
                            <div class="progress-bar" style="width: 0%">0%</div>
                        </div>
                    </div>
					`;
				});	
				break;
			case 'upload':
				createMessageBubble(true, function(messageContainer, messageBubble) {
					messageBubble.classList.add('ai-action');
					messageBubble.innerHTML = `
					<div><i class="fa-solid fa-cloud-arrow-up icon text-info"></i></div>
					<div>
                        <p class="mb-1"><strong>Downloading file:</strong> <code>${content}</code></p>
                        <div class="progress" id="${uuid}" role="progressbar" aria-label="Download Progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
                            <div class="progress-bar" style="width: 0%">0%</div>
                        </div>
                    </div>
					`;
				});	
			default:
				createMessageBubble(true, function(messageContainer, messageBubble) {
					messageBubble.innerHTML = `
					<i class="fa-solid fa-comment-dots icon text-light"></i>
					<p class="mb-0">${content}</p>`;
				});
				break;
		}
	}

	console.log("scrolling");
	chatHistory.scrollTo({
	  top: chatHistory.scrollHeight,
	  behavior: 'smooth'
	});
	
	
};



// --- Connection Setup ---
// Replace with your WebSocket server URL.
const ws = new WebSocket('wss://'
	+ window.location.hostname
	+ ':'
	+ window.location.port
	+ '/socket/chat-socket?uuid='
    + document.getElementsByTagName("body")[0].getAttribute("uuid"));

// Set the WebSocket to receive binary data as an ArrayBuffer.
ws.binaryType = 'arraybuffer';

ws.onopen = () => {
	console.log('Connected to WebSocket server.');
	// You can send a message to the server here if needed.
	// ws.send('Hello from client');
};

ws.onerror = (error) => {
	console.error('WebSocket Error:', error);
};

// --- Message Handling ---
ws.onmessage = (event) => {
	// Check if the received data is an ArrayBuffer (binary data).
	// Note: The ws.binaryType = 'arraybuffer'; line above ensures this.
	if (event.data instanceof ArrayBuffer) {
		// Use a DataView to read different data types from the ArrayBuffer.
		const dataView = new DataView(event.data);

		// Track the current position in the buffer.
		let offset = 0;

		// 1. Read the Byte ID (1 byte)
		const messageId = dataView.getUint8(offset);
		offset += 1;

		// 2. Read the UUID length (e.g., as a 16-bit integer)
		// You can change `getUint16` to `getUint8` or `getUint32` depending on how
		// the length is encoded on the server side.
		const uuidLength = dataView.getUint32(offset);
		offset += 4;

		// 3. Read the UUID string (uuidLength bytes)
		// Create a new ArrayBuffer for just the UUID part.
		const uuidBuffer = event.data.slice(offset, offset + uuidLength);

		// Use TextDecoder to convert the byte array to a UTF-8 string.
		const uuid = new TextDecoder().decode(uuidBuffer);
		offset += uuidLength;
	    debugger;
		// Handle different message types based on the ID.
		switch (messageId) {
			case MSG_OPERATION_STARTED:
				console.log('Message Type: MSG_OPERATION_STARTED');
				
				sendButton.setAttribute('disabled', 'true');
				promptInput.setAttribute('disabled', 'true');
				setSendSpinner(true);

			break;
			case MSG_OPERATION_ENDED:
				console.log('Message Type: MSG_OPERATION_ENDED');
				
				sendButton.removeAttribute('disabled');
				promptInput.removeAttribute('disabled');
				setSendSpinner(false);

				break;
			case MSG_CHAT_SYSTEM:
			case MSG_CHAT_USER:
			{
			    var type = messageId === MSG_CHAT_USER ? 'user' : 'ai';
				console.log('Message Type: ' + type);
				
				const textLength = dataView.getUint32(offset);
				offset += 4;

				// 3. Read the UUID string (uuidLength bytes)
				// Create a new ArrayBuffer for just the UUID part.
				const textBuffer = event.data.slice(offset, offset + textLength);

				// Use TextDecoder to convert the byte array to a UTF-8 string.
				const text = new TextDecoder().decode(textBuffer);
				addMessage(type, 'comment', uuid, text);
				break;
			}
			case MSG_CHAT_THINK:
			{
				var type = messageId === MSG_CHAT_USER ? 'user' : 'ai';
				console.log('Message Type: ' + type);
				
				const textLength = dataView.getUint32(offset);
				offset += 4;

				// 3. Read the UUID string (uuidLength bytes)
				// Create a new ArrayBuffer for just the UUID part.
				const textBuffer = event.data.slice(offset, offset + textLength);

				// Use TextDecoder to convert the byte array to a UTF-8 string.
				const text = new TextDecoder().decode(textBuffer);
				addMessage(type, 'think', uuid, text);
				break;
			}
			case MSG_AGENT_SWITCH:
			{
				
				console.log('Message Type: ' + type);
				
				var textLength = dataView.getUint32(offset);
				offset += 4;

				// Use TextDecoder to convert the byte array to a UTF-8 string.
				const name = new TextDecoder().decode(event.data.slice(offset, offset + textLength));
				
				offset += textLength;
				textLength = dataView.getUint32(offset);
				offset += 4;
				
				const reason = new TextDecoder().decode(event.data.slice(offset, offset + textLength));
				addMessage('agent', 'agent', uuid, name);

				break;
			}
			case MSG_AUTHORIZATION_REQUEST:
			{
				var toolNameLength = dataView.getUint32(offset);
				offset += 4;
				const toolName = new TextDecoder().decode(event.data.slice(offset, offset + toolNameLength));
				offset += toolNameLength;

				var authMsgLength = dataView.getUint32(offset);
				offset += 4;
				const authMessage = new TextDecoder().decode(event.data.slice(offset, offset + authMsgLength));
				offset += authMsgLength;

				createMessageBubble(true, function(messageContainer, messageBubble) {
					messageBubble.classList.add('ai-action');

					messageBubble.innerHTML = `
						<div><i class="fa-solid fa-shield-halved icon text-warning"></i></div>
						<div class="w-100">
							<p class="mb-1"><strong>Authorization Request:</strong> <code>${toolName}</code></p>
							<div class="mb-2">${md.render(authMessage)}</div>
							<div class="mt-2 d-flex gap-2 flex-wrap auth-buttons">
								<button class="btn btn-sm btn-success auth-btn" data-option="APPROVED_ONCE">
									<i class="fa-solid fa-check me-1"></i>Approve Once
								</button>
								<button class="btn btn-sm btn-primary auth-btn" data-option="APPROVED_IN_SESSION">
									<i class="fa-solid fa-check-double me-1"></i>Approve in Session
								</button>
								<button class="btn btn-sm btn-danger auth-btn" data-option="REJECTED">
									<i class="fa-solid fa-xmark me-1"></i>Reject
								</button>
							</div>
							<div class="mt-2 rejection-input d-none">
								<textarea class="form-control mb-2" rows="2" placeholder="Reason for rejection (optional)..."></textarea>
								<button class="btn btn-sm btn-danger confirm-reject-btn">
									<i class="fa-solid fa-xmark me-1"></i>Confirm Rejection
								</button>
							</div>
						</div>
					`;

					const authButtons = messageBubble.querySelectorAll('.auth-btn');
					const rejectionInput = messageBubble.querySelector('.rejection-input');
					const confirmRejectBtn = messageBubble.querySelector('.confirm-reject-btn');
					const rejectionTextarea = messageBubble.querySelector('.rejection-input textarea');
					const authButtonsContainer = messageBubble.querySelector('.auth-buttons');

					const capturedUuid = uuid;
					const capturedToolName = toolName;

					const disableControls = () => {
						authButtonsContainer.classList.add('d-none');
						rejectionInput.classList.add('d-none');
					};

					authButtons.forEach(btn => {
						btn.addEventListener('click', () => {
							const option = btn.getAttribute('data-option');
							if (option === 'REJECTED') {
								rejectionInput.classList.remove('d-none');
								rejectionTextarea.focus();
							} else {
								disableControls();
								handleAuthorizationResponse(capturedUuid, capturedToolName, option, '');
							}
						});
					});

					confirmRejectBtn.addEventListener('click', () => {
						const answerText = rejectionTextarea.value.trim();
						disableControls();
						handleAuthorizationResponse(capturedUuid, capturedToolName, 'REJECTED', answerText);
					});
				});

				break;
			}
			case MSG_COMMAND_START:
				console.log('Message Type: MSG_COMMAND_START');
	
				const cmdLength = dataView.getUint32(offset);
				offset += 4;

				// 3. Read the UUID string (uuidLength bytes)
				// Create a new ArrayBuffer for just the UUID part.
				const cmdBuffer = event.data.slice(offset, offset + cmdLength);

				// Use TextDecoder to convert the byte array to a UTF-8 string.
				const cmd = new TextDecoder().decode(cmdBuffer);
				addMessage('ai', 'command', uuid, cmd);
				break;
			case MSG_COMMAND_OUTPUT:
				console.log('Message Type: MSG_COMMAND_OUTPUT');

				const outputLength = dataView.getUint32(offset);
				offset += 4;
	
				// 3. Read the UUID string (uuidLength bytes)
				// Create a new ArrayBuffer for just the UUID part.
				const outputBuffer = event.data.slice(offset, offset + outputLength);
	
				// Use TextDecoder to convert the byte array to a UTF-8 string.
				const output = new TextDecoder().decode(outputBuffer);
				commandOutput(uuid, output);
				break;
			case MSG_COMMAND_SUCCESS:
				console.log('Message Type: MSG_COMMAND_SUCCESS');
                commandComplete(uuid);
				break;
			case MSG_COMMAND_ERROR:
				console.log('Message Type: MSG_COMMAND_ERROR');
				commandError(uuid);
				break;
			case MSG_DOWNLOAD_START:
			case MSG_UPLOAD_START:
				console.log('Message Type: MSG_DOWNLOAD_START | MSG_UPLOAD_START');

				const filenameLength = dataView.getUint32(offset);
				offset += 4;

				// 3. Read the UUID string (uuidLength bytes)
				// Create a new ArrayBuffer for just the UUID part.
				const filenameBuffer = event.data.slice(offset, offset + filenameLength);
				offset += filenameLength;
				// Use TextDecoder to convert the byte array to a UTF-8 string.
				const filename = new TextDecoder().decode(filenameBuffer);
				addMessage('ai', messageId==MSG_UPLOAD_START ? 'upload' : 'download', uuid, filename);
				break;
			case MSG_PROGRESS:
				console.log('Message Type: MSG_PROGRESS');
                progress(uuid, dataView.getUint32(offset))
				break;
			case MSG_PROGRESS_SUCCESS:
				console.log('Message Type: MSG_PROGRESS_SUCCESS');
				progressComplete(uuid);
				break;
			case MSG_PROGRESS_ERROR:
				console.log('Message Type: MSG_DOWNLOAD_ERROR');
				progressError(uuid);
				break;
			default:
				console.log('Message Type: Unknown');
				break;
		}

	} else {
		// Handle non-binary (text) messages if necessary.
		console.log('Received Text Message:', event.data);
	}
};

ws.onclose = () => {
	console.log('Disconnected from WebSocket server.');
};

// Function to create the binary message
function createBinaryMessage(messageId, payloadString) {
    // 1. Encode the string to get its byte length
    const encoder = new TextEncoder();
    const stringBytes = encoder.encode(payloadString);
    const stringLength = stringBytes.length;

    // 2. Calculate the total buffer size
    //   - 1 byte for messageId
    //   - 4 bytes for stringLength (using a 32-bit integer)
    //   - N bytes for the string itself
    const bufferSize = 1 + 4 + stringLength;
    
    // 3. Create a new ArrayBuffer
    const buffer = new ArrayBuffer(bufferSize);
    
    // 4. Create a DataView to write data into the buffer
    const view = new DataView(buffer);

    // 5. Write the messageId (single byte) at the start
    //    Position: 0
    view.setUint8(0, messageId);

    // 6. Write the stringLength (4 bytes)
    //    Position: 1
    //    Use false for little-endian, true for big-endian.
    //    Make sure this matches your server's byte order.
    //    We'll use big-endian here as it's common for network protocols.
    view.setUint32(1, stringLength, false); 

    // 7. Write the string bytes
    //    Position: 1 (start of length) + 4 (size of length) = 5
    const stringDataView = new Uint8Array(buffer, 5, stringLength);
    stringDataView.set(stringBytes);

	if (ws.readyState === WebSocket.OPEN) {
        ws.send(buffer);
    } else {
        console.error('WebSocket is not open.');
    }
}

// Function to create a binary message with multiple string payloads.
// Each string is length-prefixed with a 4-byte big-endian uint32, matching ByteArrayReader.readString() on the server.
function createBinaryMessageMulti(messageId, strings) {
    const encoder = new TextEncoder();
    const encoded = strings.map(s => encoder.encode(s ?? ''));

    // 1 byte messageId + for each string: 4 bytes length + N bytes data
    const bufferSize = 1 + encoded.reduce((sum, b) => sum + 4 + b.length, 0);

    const buffer = new ArrayBuffer(bufferSize);
    const view = new DataView(buffer);
    const bytes = new Uint8Array(buffer);

    let offset = 0;
    view.setUint8(offset, messageId);
    offset += 1;

    for (const stringBytes of encoded) {
        view.setUint32(offset, stringBytes.length, false);
        offset += 4;
        bytes.set(stringBytes, offset);
        offset += stringBytes.length;
    }

    if (ws.readyState === WebSocket.OPEN) {
        ws.send(buffer);
    } else {
        console.error('WebSocket is not open.');
    }
}

const handleAuthorizationResponse = (uuid, toolName, authOption, answerText) => {
    createBinaryMessageMulti(MSG_AUTHORIZATION_RESPONSE, [uuid, toolName, authOption, answerText]);
};

function handleSendAction() {
	// 3. Get the value of the text input
	    const inputValue = promptInput.value.trim();
		if(!inputValue) {
			promptInput.value = '';
			return;
		}
		promptInput.value = '';
		
	    // Optional: Log the value to the console to verify it's working
	    console.log('The input value is:', inputValue);
	    createBinaryMessage(MSG_PROMPT_INPUT, inputValue);
}

// 2. Attach an event listener to the button
sendButton.addEventListener('click', function() {
    handleSendAction();
});

promptInput.addEventListener('keydown', function(event) {
	
	if(!promptInput.getAttribute('disabled')) {
	    // Check if the pressed key is "Enter"
	    // The event.key property is the modern and recommended way
	    if (event.key === 'Enter') {
	        // Prevent the default form submission behavior, if the input is inside a form
	        event.preventDefault(); 
	        
	        // Call the same function that the button click uses
	        handleSendAction();
	    }
	}
});

document.addEventListener('click', function (event) {

    // Use .closest() to find the nearest ancestor with the class 'toggle-output'
    // This works even if you click on a child element inside your target element
    const toggleElement = event.target.closest('.toggle-output');

    // If a matching element was found, run your code
    if (toggleElement) {
		event.preventDefault(); 
		const parent = toggleElement.parentElement;
		                
        // From the parent, find the sibling with the class "term"
        const termSibling = parent.querySelector('.term');
        
        // If the sibling exists, toggle its "d-none" class
        if (termSibling) {
			const icon = toggleElement.querySelector('i');
			if(icon) {
				icon.classList.toggle('fa-square-caret-up');
				icon.classList.toggle('fa-square-caret-down');
			}
            termSibling.classList.toggle('d-none');
        }
	}
});