Created
October 23, 2024 14:13
-
-
Save mseri/91b258a090262fe8a5fa6fcf0002763a to your computer and use it in GitHub Desktop.
Simple html chat interface, made with Claude, used to interact with LM Studio, Ollama or any other openai compatible server (I am using it with firefox new ai panel)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>LM Studio Chat Interface</title> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", | |
| Roboto, sans-serif; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background: #f5f5f5; | |
| } | |
| #setup-form { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| margin-bottom: 20px; | |
| } | |
| #base-url { | |
| width: 100%; | |
| padding: 8px; | |
| margin-bottom: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| } | |
| #model-select { | |
| width: 100%; | |
| padding: 8px; | |
| margin-bottom: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| } | |
| #api-type { | |
| width: 100%; | |
| padding: 8px; | |
| margin-bottom: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| } | |
| #chat-container { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| min-height: 400px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| #messages { | |
| flex-grow: 1; | |
| overflow-y: auto; | |
| margin-bottom: 20px; | |
| padding: 10px; | |
| background: #f9f9f9; | |
| border-radius: 4px; | |
| min-height: 300px; | |
| } | |
| .message { | |
| margin-bottom: 10px; | |
| padding: 10px; | |
| border-radius: 4px; | |
| white-space: pre-wrap; | |
| } | |
| .user-message { | |
| background: #e3f2fd; | |
| margin-left: 20px; | |
| } | |
| .assistant-message { | |
| background: #f5f5f5; | |
| margin-right: 20px; | |
| } | |
| .error-message { | |
| background: #ffebee; | |
| margin-right: 20px; | |
| color: #d32f2f; | |
| } | |
| .system-message { | |
| background: #e8f5e9; | |
| margin: 10px 0; | |
| font-style: italic; | |
| } | |
| #input-container { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| #user-input { | |
| flex-grow: 1; | |
| padding: 8px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| resize: vertical; | |
| } | |
| button { | |
| padding: 8px 16px; | |
| background: #2196f3; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| button:hover { | |
| background: #1976d2; | |
| } | |
| button:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .error { | |
| color: #d32f2f; | |
| margin-top: 10px; | |
| } | |
| #status { | |
| margin-top: 10px; | |
| padding: 10px; | |
| border-radius: 4px; | |
| display: none; | |
| } | |
| .status-error { | |
| background: #ffebee; | |
| color: #d32f2f; | |
| } | |
| .status-success { | |
| background: #e8f5e9; | |
| color: #2e7d32; | |
| } | |
| #system-prompt { | |
| width: 100%; | |
| padding: 8px; | |
| margin-bottom: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| display: none; | |
| } | |
| .debug-panel { | |
| background: #f8f9fa; | |
| padding: 10px; | |
| border-radius: 4px; | |
| margin-top: 10px; | |
| font-family: monospace; | |
| white-space: pre-wrap; | |
| display: none; | |
| } | |
| #debug-toggle { | |
| margin-top: 10px; | |
| background: #6c757d; | |
| } | |
| .debug-message { | |
| background: #e9ecef; | |
| padding: 10px; | |
| margin: 5px 0; | |
| border-radius: 4px; | |
| font-size: 0.9em; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="setup-form"> | |
| <input | |
| type="text" | |
| id="base-url" | |
| value="http://localhost:123456" | |
| placeholder="Base URL (e.g., http://localhost:123456)" | |
| /> | |
| <select id="api-type" onchange="toggleSystemPrompt()"> | |
| <option value="chat">Chat Completions</option> | |
| <option value="completions">Completions</option> | |
| </select> | |
| <select id="model-select"> | |
| <option value="">Loading models...</option> | |
| </select> | |
| <textarea | |
| id="system-prompt" | |
| rows="2" | |
| placeholder="System prompt (optional)" | |
| ></textarea> | |
| <button onclick="initialize()">Connect</button> | |
| <button id="debug-toggle" onclick="toggleDebug()"> | |
| Show Debug Info | |
| </button> | |
| <div id="status"></div> | |
| <div id="debug-panel" class="debug-panel"></div> | |
| </div> | |
| <div id="chat-container"> | |
| <div id="messages"></div> | |
| <div id="input-container"> | |
| <textarea | |
| id="user-input" | |
| rows="3" | |
| placeholder="Type your message..." | |
| disabled | |
| ></textarea> | |
| <button onclick="sendMessage()" id="send-button" disabled> | |
| Send | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| let messages = []; | |
| let baseUrl = ""; | |
| let selectedModel = ""; | |
| let apiType = "chat"; | |
| let debugMode = false; | |
| function logDebug(message, data) { | |
| const debugPanel = document.getElementById("debug-panel"); | |
| const debugMessage = document.createElement("div"); | |
| debugMessage.className = "debug-message"; | |
| debugMessage.textContent = `${message}\n${JSON.stringify(data, null, 2)}`; | |
| debugPanel.insertBefore(debugMessage, debugPanel.firstChild); | |
| } | |
| function toggleDebug() { | |
| debugMode = !debugMode; | |
| const debugPanel = document.getElementById("debug-panel"); | |
| const debugToggle = document.getElementById("debug-toggle"); | |
| debugPanel.style.display = debugMode ? "block" : "none"; | |
| debugToggle.textContent = debugMode | |
| ? "Hide Debug Info" | |
| : "Show Debug Info"; | |
| } | |
| // Previous helper functions remain the same | |
| function showStatus(message, isError = false) { | |
| const status = document.getElementById("status"); | |
| status.textContent = message; | |
| status.style.display = "block"; | |
| status.className = isError ? "status-error" : "status-success"; | |
| } | |
| function toggleSystemPrompt() { | |
| const apiType = document.getElementById("api-type").value; | |
| const systemPrompt = document.getElementById("system-prompt"); | |
| systemPrompt.style.display = | |
| apiType === "chat" ? "block" : "none"; | |
| } | |
| async function fetchModels() { | |
| const baseUrl = document | |
| .getElementById("base-url") | |
| .value.trim(); | |
| try { | |
| const response = await fetch(`${baseUrl}/v1/models`); | |
| if (!response.ok) | |
| throw new Error( | |
| `HTTP error! status: ${response.status}`, | |
| ); | |
| const data = await response.json(); | |
| if (debugMode) logDebug("Models response:", data); | |
| const select = document.getElementById("model-select"); | |
| select.innerHTML = data.data | |
| .map( | |
| (model) => | |
| `<option value="${model.id}">${model.id}</option>`, | |
| ) | |
| .join(""); | |
| } catch (error) { | |
| showStatus(`Error fetching models: ${error.message}`, true); | |
| } | |
| } | |
| async function sendMessage(isInitial = false) { | |
| const userInput = isInitial | |
| ? "" | |
| : document.getElementById("user-input").value.trim(); | |
| if (!isInitial && !userInput) return; | |
| const sendButton = document.getElementById("send-button"); | |
| const userInputElem = document.getElementById("user-input"); | |
| sendButton.disabled = true; | |
| userInputElem.disabled = true; | |
| if (!isInitial) { | |
| if (apiType === "chat") { | |
| messages.push({ | |
| role: "user", | |
| content: userInput, | |
| }); | |
| } else { | |
| messages.push({ | |
| role: "user", | |
| content: userInput, | |
| }); | |
| } | |
| userInputElem.value = ""; | |
| } | |
| displayMessages(); | |
| try { | |
| let endpoint = `${baseUrl}/v1/${apiType === "chat" ? "chat/completions" : "completions"}`; | |
| let payload = { | |
| model: selectedModel, | |
| stream: false, | |
| }; | |
| if (apiType === "chat") { | |
| payload.messages = messages; | |
| } else { | |
| payload.prompt = messages | |
| .map((m) => m.content) | |
| .join("\n"); | |
| } | |
| if (debugMode) logDebug("Request payload:", payload); | |
| const response = await fetch(endpoint, { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error( | |
| `HTTP error! status: ${response.status}, body: ${errorText}`, | |
| ); | |
| } | |
| const data = await response.json(); | |
| if (debugMode) logDebug("Response data:", data); | |
| if (apiType === "chat") { | |
| // Handle different response formats | |
| if (data.choices?.[0]?.message) { | |
| messages.push(data.choices[0].message); | |
| } else if (data.choices?.[0]?.content) { | |
| messages.push({ | |
| role: "assistant", | |
| content: data.choices[0].content, | |
| }); | |
| } else if (data.response) { | |
| messages.push({ | |
| role: "assistant", | |
| content: data.response, | |
| }); | |
| } else { | |
| throw new Error( | |
| "Unexpected response format: " + | |
| JSON.stringify(data), | |
| ); | |
| } | |
| } else { | |
| messages.push({ | |
| role: "assistant", | |
| content: | |
| data.choices?.[0]?.text || | |
| data.response || | |
| data.content || | |
| JSON.stringify(data), | |
| }); | |
| } | |
| displayMessages(); | |
| } catch (error) { | |
| messages.push({ | |
| role: "error", | |
| content: `Error: ${error.message}`, | |
| }); | |
| displayMessages(); | |
| } finally { | |
| sendButton.disabled = false; | |
| userInputElem.disabled = false; | |
| userInputElem.focus(); | |
| } | |
| } | |
| // Previous initialization and event handling code remains the same | |
| function initialize() { | |
| baseUrl = document.getElementById("base-url").value.trim(); | |
| selectedModel = document.getElementById("model-select").value; | |
| apiType = document.getElementById("api-type").value; | |
| if (!baseUrl || !selectedModel) { | |
| showStatus( | |
| "Please enter base URL and select a model", | |
| true, | |
| ); | |
| return; | |
| } | |
| document.getElementById("user-input").disabled = false; | |
| document.getElementById("send-button").disabled = false; | |
| document.getElementById("base-url").disabled = true; | |
| document.getElementById("model-select").disabled = true; | |
| document.getElementById("api-type").disabled = true; | |
| const systemPrompt = document | |
| .getElementById("system-prompt") | |
| .value.trim(); | |
| if (apiType === "chat" && systemPrompt) { | |
| messages = [ | |
| { | |
| role: "system", | |
| content: systemPrompt, | |
| }, | |
| ]; | |
| displayMessages(); | |
| } | |
| showStatus("Connected successfully", false); | |
| const initialMessage = getQueryParam("q"); | |
| if (initialMessage) { | |
| const decodedMessage = decodeURIComponent(initialMessage); | |
| messages.push({ | |
| role: "user", | |
| content: decodedMessage, | |
| }); | |
| displayMessages(); | |
| sendMessage(true); | |
| } | |
| } | |
| function getQueryParam(param) { | |
| const urlParams = new URLSearchParams(window.location.search); | |
| return urlParams.get(param); | |
| } | |
| function displayMessages() { | |
| const messagesDiv = document.getElementById("messages"); | |
| messagesDiv.innerHTML = messages | |
| .map( | |
| (msg) => ` | |
| <div class="${msg.role}-message message"> | |
| <strong>${msg.role}:</strong> ${msg.content} | |
| </div> | |
| `, | |
| ) | |
| .join(""); | |
| messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
| } | |
| document.addEventListener("DOMContentLoaded", () => { | |
| fetchModels(); | |
| toggleSystemPrompt(); | |
| }); | |
| document | |
| .getElementById("user-input") | |
| .addEventListener("keydown", function (e) { | |
| if (e.key === "Enter" && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment