Skip to content

Instantly share code, notes, and snippets.

@mseri
Created October 23, 2024 14:13
Show Gist options
  • Select an option

  • Save mseri/91b258a090262fe8a5fa6fcf0002763a to your computer and use it in GitHub Desktop.

Select an option

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