├── 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 | Open Chat 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 | 15 | 19 |

Chat History

20 | 21 | 22 | 26 |

Settings

27 | 28 | 29 | 30 |
31 | 35 |
36 | 37 | 38 | 78 | 79 | 80 |
81 |
82 |
83 | 84 | 85 |
86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | background-color: #f0f2f5; 4 | margin: 0; 5 | padding: 0; 6 | display: flex; 7 | justify-content: center; 8 | align-items: stretch; 9 | height: 100vh; 10 | } 11 | 12 | .app { 13 | width: 100%; 14 | max-width: 1500px; 15 | background-color: #ffffff; 16 | border-radius: 8px; 17 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 18 | overflow: hidden; 19 | display: flex; 20 | flex-direction: row; 21 | height: 100%; 22 | } 23 | 24 | .left-panel { 25 | width: 200px; 26 | background-color: #f8f9fa; 27 | padding: 20px; 28 | border-right: 1px solid #e0e0e0; 29 | box-shadow: -1px 0 5px rgba(0, 0, 0, 0.05); 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | 34 | .controls-container { 35 | padding-bottom: 20px; 36 | border-bottom: 1px solid #e0e0e0; 37 | } 38 | 39 | .controls-container button { 40 | width: 100%; 41 | padding: 10px; 42 | margin-bottom: 10px; 43 | background-color: #007bff; 44 | color: #ffffff; 45 | border: none; 46 | border-radius: 4px; 47 | cursor: pointer; 48 | font-size: 16px; 49 | } 50 | 51 | .controls-container button:hover { 52 | background-color: #0056b3; 53 | } 54 | 55 | .clear-button { 56 | background-color: #ff4b4b; 57 | } 58 | 59 | .clear-button:hover { 60 | background-color: #d32f2f; 61 | } 62 | 63 | .sessions-container { 64 | flex-grow: 1; 65 | overflow-y: auto; 66 | } 67 | 68 | .session-list .session { 69 | padding: 10px; 70 | margin-top: 5px; 71 | background-color: #e0e0e0; 72 | cursor: pointer; 73 | border-radius: 4px; 74 | transition: background-color 0.3s; 75 | } 76 | 77 | .session-list .session:hover { 78 | background-color: #d0d0d0; 79 | } 80 | 81 | .settings-container { 82 | display: flex; 83 | flex-direction: column; 84 | flex-grow: 3; 85 | padding: 20px; 86 | border-left: 1px solid #e0e0e0; 87 | overflow-y: auto; 88 | } 89 | 90 | .settings-container label { 91 | font-size: 14px; 92 | color: #666; 93 | margin-bottom: 5px; 94 | } 95 | 96 | .settings-container input[type="text"], 97 | .settings-container input[type="number"] { 98 | padding: 10px; 99 | margin-bottom: 10px; 100 | border: 1px solid #ccc; 101 | border-radius: 4px; 102 | font-size: 16px; 103 | } 104 | 105 | .settings-container button { 106 | width: 100%; 107 | padding: 10px; 108 | background-color: #007bff; 109 | color: white; 110 | border: none; 111 | border-radius: 4px; 112 | cursor: pointer; 113 | font-size: 16px; 114 | margin-top: 10px; 115 | } 116 | 117 | .settings-container button:hover { 118 | background-color: #0056b3; 119 | } 120 | 121 | .chat-container { 122 | flex-grow: 3; 123 | padding: 20px; 124 | display: flex; 125 | flex-direction: column; 126 | } 127 | 128 | .chat-history { 129 | flex: 1; 130 | overflow-y: auto; 131 | margin: 10px; 132 | background-color: #ffffff; 133 | border: 1px solid #e0e0e0; 134 | } 135 | .message { 136 | margin: 10px; 137 | padding: 10px; 138 | border-radius: 5px; 139 | background-color: #f4f4f4; 140 | word-wrap: break-word; 141 | line-height: 1.8; 142 | } 143 | .user { 144 | align-self: flex-end; 145 | background-color: #dcf8c6; 146 | } 147 | .assistant { 148 | align-self: flex-start; 149 | } 150 | .input-container { 151 | display: flex; 152 | padding: 10px; 153 | align-items: center; 154 | border-top: 1px solid #e0e0e0; 155 | } 156 | 157 | .input-container textarea { 158 | flex-grow: 1; 159 | padding: 10px; 160 | border: 1px solid #ccc; 161 | border-radius: 4px; 162 | margin-right: 10px; 163 | font-size: 16px; 164 | } 165 | 166 | .input-container button { 167 | padding: 10px 15px; 168 | background-color: #007bff; 169 | color: #ffffff; 170 | border: none; 171 | border-radius: 4px; 172 | cursor: pointer; 173 | font-size: 16px; 174 | } 175 | .switch { 176 | position: relative; 177 | display: inline-block; 178 | width: 60px; 179 | height: 34px; 180 | } 181 | 182 | .switch input { 183 | opacity: 0; 184 | width: 0; 185 | height: 0; 186 | } 187 | 188 | .slider { 189 | position: absolute; 190 | cursor: pointer; 191 | top: 0; 192 | left: 0; 193 | right: 0; 194 | bottom: 0; 195 | background-color: #ccc; 196 | transition: 0.4s; 197 | border-radius: 34px; 198 | } 199 | 200 | .slider:before { 201 | position: absolute; 202 | content: ""; 203 | height: 26px; 204 | width: 26px; 205 | left: 4px; 206 | bottom: 4px; 207 | background-color: white; 208 | transition: 0.4s; 209 | border-radius: 50%; 210 | } 211 | 212 | input:checked + .slider { 213 | background-color: #2196f3; 214 | } 215 | 216 | input:focus + .slider { 217 | box-shadow: 0 0 1px #2196f3; 218 | } 219 | 220 | input:checked + .slider:before { 221 | transform: translateX(26px); 222 | } 223 | 224 | .settings-container { 225 | transition: transform 0.3s ease-in-out; 226 | transform: translateX(0); 227 | } 228 | 229 | .hidden { 230 | display: none; 231 | } 232 | 233 | .regenerate-button { 234 | bottom: 5px; 235 | right: 5px; 236 | padding: 5px 10px; 237 | background-color: #007bff; 238 | color: white; 239 | border: none; 240 | border-radius: 5px; 241 | cursor: pointer; 242 | font-size: 12px; 243 | } 244 | 245 | .regenerate-button:hover { 246 | background-color: #45a049; 247 | } 248 | .setting-row { 249 | display: flex; 250 | align-items: center; 251 | margin-bottom: 10px; 252 | } 253 | 254 | .setting-row label:last-child { 255 | margin-left: 10px; 256 | margin-bottom: 0; 257 | color: #333; 258 | font-size: 16px; 259 | } 260 | 261 | .setting-row .switch { 262 | margin-right: 0; 263 | } 264 | --------------------------------------------------------------------------------