├── README.md ├── app.js ├── index.html └── styles.css /README.md: -------------------------------------------------------------------------------- 1 | # Chat UI 2 | 3 | This repository contains a lightweight and straightforward user interface that provides a quick and simple chat interface for interacting with the OpenAI-like API. 4 | 5 | ## Live Demo 6 | 7 | The application is hosted on GitHub Pages, and you can access it directly at: [https://mzbac.github.io/open-chat/](https://mzbac.github.io/open-chat/) 8 | 9 | ## Usage 10 | 11 | 1. Open the application in your web browser by visiting the live demo link above. 12 | 2. In the settings panel, enter your API endpoint URL and any other desired settings (e.g., stop word, max tokens, model, temperature, top_p). 13 | 3. Click the "Save Settings" button to save your configuration. 14 | 4. Type your prompt in the input field and click the "Send" button to send it to the API. 15 | 5. The generated response from the API will be displayed in the chat history. 16 | 17 | ## Features 18 | 19 | - Quick and simple chat UI for interacting with the OpenAI API 20 | - Chat history management 21 | - Session saving and loading 22 | - Customizable settings for API parameters (endpoint, stop word, max tokens, model, temperature, top_p) 23 | 24 | ## License 25 | 26 | This project is licensed under the [MIT License](LICENSE). 27 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", (event) => { 2 | const chatHistory = document.querySelector(".chat-history"); 3 | const input = document.querySelector(".input-container textarea"); 4 | const sendButton = document.querySelector(".input-container button"); 5 | const clearButton = document.querySelector(".clear-button"); 6 | const historyButton = document.querySelector(".history-button"); 7 | const endpointInput = document.getElementById("endpoint"); 8 | const stopWordInput = document.getElementById("stopWord"); 9 | const maxTokensInput = document.getElementById("maxTokens"); 10 | const modelInput = document.getElementById("model"); 11 | const temperatureInput = document.getElementById("temperature"); 12 | const topPInput = document.getElementById("topP"); 13 | const apiKeyInput = document.getElementById("apiKey"); 14 | const saveSettingsButton = document.getElementById("saveSettings"); 15 | const settingsContainer = document.querySelector(".settings-container"); 16 | const settingsToggle = document.getElementById("settings-toggle"); 17 | const chatHistoryToggle = document.getElementById("chat-history-toggle"); 18 | const sessionsContainer = document.querySelector(".sessions-container"); 19 | const sessionListContainer = document.querySelector(".session-list"); 20 | const apiKeyHeaderToggle = document.getElementById("apiKeyHeaderToggle"); 21 | 22 | settingsToggle.addEventListener("change", () => { 23 | settingsContainer.classList.toggle("hidden"); 24 | }); 25 | 26 | chatHistoryToggle.addEventListener("click", () => { 27 | sessionsContainer.classList.toggle("hidden"); 28 | }); 29 | 30 | let messages = JSON.parse(localStorage.getItem("chatHistory")) || []; 31 | const settings = JSON.parse(localStorage.getItem("settings")) || { 32 | endpoint: "", 33 | stopWord: "", 34 | maxTokens: 200, 35 | model: "meta-llama/Meta-Llama-3-8B-Instruct", 36 | temperature: 0.0, 37 | topP: 0.95, 38 | apiKey: "", 39 | useApiKeyHeader: false, 40 | }; 41 | 42 | const throttle = (func, limit) => { 43 | let lastFunc; 44 | let lastRan; 45 | return function () { 46 | const context = this; 47 | const args = arguments; 48 | if (!lastRan) { 49 | func.apply(context, args); 50 | lastRan = Date.now(); 51 | } else { 52 | clearTimeout(lastFunc); 53 | lastFunc = setTimeout(function () { 54 | if (Date.now() - lastRan >= limit) { 55 | func.apply(context, args); 56 | lastRan = Date.now(); 57 | } 58 | }, limit - (Date.now() - lastRan)); 59 | } 60 | }; 61 | }; 62 | 63 | function clearErrorMessages() { 64 | messages = messages.filter((msg) => msg.role !== "error"); 65 | localStorage.setItem("chatHistory", JSON.stringify(messages)); 66 | throttledRenderMessages(); 67 | } 68 | 69 | const renderMessages = () => { 70 | if (!marked) { 71 | console.error("Marked library is not loaded correctly."); 72 | return; 73 | } 74 | 75 | chatHistory.innerHTML = ""; 76 | messages.forEach((msg, index) => { 77 | const messageDiv = document.createElement("div"); 78 | messageDiv.className = `message ${msg.role}`; 79 | messageDiv.dataset.index = index; 80 | 81 | if (msg.role === "assistant") { 82 | messageDiv.innerHTML = marked.parse(msg.content); 83 | const regenerateButton = document.createElement("button"); 84 | regenerateButton.className = "regenerate-button"; 85 | regenerateButton.dataset.index = index; 86 | regenerateButton.textContent = "Regenerate"; 87 | messageDiv.appendChild(regenerateButton); 88 | } else { 89 | const editableDiv = document.createElement("div"); 90 | editableDiv.contentEditable = true; 91 | editableDiv.innerHTML = marked.parse(msg.content); 92 | editableDiv.className = "editable-user-input"; 93 | editableDiv.dataset.originalContent = msg.content; 94 | messageDiv.appendChild(editableDiv); 95 | } 96 | 97 | chatHistory.appendChild(messageDiv); 98 | }); 99 | 100 | chatHistory.scrollTop = chatHistory.scrollHeight; 101 | }; 102 | 103 | const throttledRenderMessages = throttle(renderMessages, 100); 104 | 105 | chatHistory.addEventListener("click", async (event) => { 106 | if (event.target.classList.contains("regenerate-button")) { 107 | const index = parseInt(event.target.dataset.index); 108 | if (messages[index].role === "assistant") { 109 | await regenerateAssistantMessage(index); 110 | } 111 | } 112 | }); 113 | 114 | chatHistory.addEventListener( 115 | "blur", 116 | async (event) => { 117 | if (event.target.classList.contains("editable-user-input")) { 118 | const index = parseInt(event.target.parentNode.dataset.index); 119 | const originalContent = event.target.dataset.originalContent; 120 | const currentContent = event.target.innerText; 121 | 122 | if (currentContent !== originalContent) { 123 | messages[index].content = currentContent; 124 | localStorage.setItem("chatHistory", JSON.stringify(messages)); 125 | await regenerateFromUserEdit(index); 126 | } 127 | } 128 | }, 129 | true 130 | ); 131 | 132 | async function regenerateAssistantMessage(index) { 133 | clearErrorMessages(); 134 | messages.splice(index, 1); 135 | const updatedMessages = messages.slice(0, index); 136 | await regenerateResponse(updatedMessages, index); 137 | } 138 | 139 | async function regenerateFromUserEdit(index) { 140 | clearErrorMessages(); 141 | messages = messages.slice(0, index + 1); 142 | await regenerateResponse(messages, index + 1); 143 | } 144 | 145 | async function regenerateResponse(messageList, startIndex) { 146 | sendButton.disabled = true; 147 | sendButton.textContent = "Generating..."; 148 | 149 | try { 150 | const headers = { 151 | "Content-Type": "application/json", 152 | }; 153 | 154 | if (settings.useApiKeyHeader) { 155 | headers["api-key"] = settings.apiKey; 156 | } else { 157 | headers["Authorization"] = `Bearer ${settings.apiKey}`; 158 | } 159 | 160 | const response = await fetch(settings.endpoint, { 161 | method: "POST", 162 | headers: headers, 163 | body: JSON.stringify({ 164 | model: settings.model, 165 | messages: messageList, 166 | stream: true, 167 | stop: settings.stopWord ? settings.stopWord : undefined, 168 | max_tokens: settings.maxTokens, 169 | temperature: settings.temperature, 170 | top_p: settings.topP, 171 | }), 172 | }); 173 | 174 | if (!response.ok) { 175 | throw new Error(`Failed to generate response: ${response.statusText}`); 176 | } 177 | const reader = response.body.getReader(); 178 | let currentResponse = ""; 179 | let initial = true; 180 | 181 | while (true) { 182 | const { value, done } = await reader.read(); 183 | if (done) break; 184 | const chunk = new TextDecoder().decode(value); 185 | if (chunk.trim()) { 186 | const lines = chunk.split("\n"); 187 | for (let line of lines) { 188 | if (line.startsWith("data:")) { 189 | if (line === "data: [DONE]") break; 190 | 191 | const data = JSON.parse(line.substring(5)); 192 | if (data.choices) { 193 | data.choices.forEach((choice) => { 194 | currentResponse += choice.delta.content ?? ""; 195 | }); 196 | } 197 | } 198 | } 199 | if (initial) { 200 | messages.splice(startIndex, 0, { 201 | role: "assistant", 202 | content: currentResponse, 203 | }); 204 | initial = false; 205 | } else { 206 | messages[startIndex].content = currentResponse; 207 | } 208 | throttledRenderMessages(); 209 | } 210 | } 211 | } catch (error) { 212 | console.error("Error processing message:", error); 213 | messages.push({ 214 | role: "error", 215 | content: "An error occurred while processing your message.", 216 | }); 217 | throttledRenderMessages(); 218 | } finally { 219 | sendButton.disabled = false; 220 | sendButton.textContent = "Send"; 221 | localStorage.setItem("chatHistory", JSON.stringify(messages)); 222 | } 223 | } 224 | 225 | clearButton.addEventListener("click", () => { 226 | localStorage.removeItem("chatHistory"); 227 | sessionListContainer.innerHTML = ""; 228 | messages = []; 229 | throttledRenderMessages(); 230 | }); 231 | 232 | historyButton.addEventListener("click", () => { 233 | const now = new Date(); 234 | const defaultSessionName = `Session ${now.getFullYear()}-${ 235 | now.getMonth() + 1 236 | }-${now.getDate()} ${now.getHours()}:${now.getMinutes()}`; 237 | const sessionName = prompt( 238 | "Please enter a name for this session:", 239 | defaultSessionName 240 | ); 241 | if (sessionName) { 242 | localStorage.setItem(sessionName, JSON.stringify(messages)); 243 | addSessionToList(sessionName); 244 | } 245 | messages = []; 246 | throttledRenderMessages(); 247 | }); 248 | 249 | const addSessionToList = (sessionName) => { 250 | const sessionDiv = document.createElement("div"); 251 | sessionDiv.className = "session"; 252 | sessionDiv.textContent = sessionName; 253 | sessionDiv.onclick = function () { 254 | messages = JSON.parse(localStorage.getItem(sessionName)); 255 | throttledRenderMessages(); 256 | }; 257 | sessionListContainer.appendChild(sessionDiv); 258 | }; 259 | 260 | Object.keys(localStorage).forEach((key) => { 261 | if (key !== "settings" && key !== "chatHistory") { 262 | addSessionToList(key); 263 | } 264 | }); 265 | 266 | saveSettingsButton.addEventListener("click", () => { 267 | settings.endpoint = endpointInput.value.trim(); 268 | settings.stopWord = stopWordInput.value.trim(); 269 | settings.maxTokens = parseInt(maxTokensInput.value); 270 | settings.model = modelInput.value.trim(); 271 | settings.temperature = parseFloat(temperatureInput.value); 272 | settings.topP = parseFloat(topPInput.value); 273 | settings.apiKey = document.getElementById("apiKey").value.trim(); 274 | settings.useApiKeyHeader = apiKeyHeaderToggle.checked; 275 | localStorage.setItem("settings", JSON.stringify(settings)); 276 | }); 277 | 278 | const sendMessage = async () => { 279 | clearErrorMessages(); 280 | const userInput = input.value.trim(); 281 | if (userInput === "") return; 282 | 283 | const userMessage = { role: "user", content: userInput }; 284 | messages.push(userMessage); 285 | throttledRenderMessages(); 286 | input.value = ""; 287 | 288 | await regenerateResponse(messages, messages.length); 289 | }; 290 | 291 | sendButton.addEventListener("click", sendMessage); 292 | 293 | input.addEventListener("keydown", (event) => { 294 | if (event.key === "Enter" && !event.shiftKey) { 295 | event.preventDefault(); 296 | sendMessage(); 297 | } 298 | }); 299 | 300 | throttledRenderMessages(); 301 | 302 | endpointInput.value = settings.endpoint; 303 | stopWordInput.value = settings.stopWord; 304 | maxTokensInput.value = settings.maxTokens; 305 | modelInput.value = settings.model; 306 | temperatureInput.value = settings.temperature; 307 | topPInput.value = settings.topP; 308 | apiKeyInput.value = settings.apiKey ?? ""; 309 | apiKeyHeaderToggle.checked = settings.useApiKeyHeader; 310 | }); 311 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |Chat History
20 | 21 | 22 | 26 |Settings
27 | 28 | 29 | 30 |