Download www/chatbot.js from jglowa/prosty-rag: direct link, hf CLI and curl.
- Browser
- Download file 30.3 kB
-
https://huggingface.co/jglowa/prosty-rag/resolve/main/www/chatbot.js
- Command line
-
hf download hf://jglowa/prosty-rag/www/chatbot.js
-
curl -L -o chatbot.js https://huggingface.co/jglowa/prosty-rag/resolve/main/www/chatbot.js
30.3 kB
| // Copyright 2024 Mozilla Foundation | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| const API_KEY = "your-api-key-here"; | |
| const DEFAULT_SYSTEM_PROMPT = | |
| "A chat between a curious human and an artificial " + | |
| "intelligence assistant. The assistant gives helpful, " + | |
| "detailed, and polite answers to the human's questions."; | |
| const DEFAULT_FLAGZ = { | |
| "model": null, | |
| "prompt": null, | |
| "nologo": false, | |
| "no_display_prompt": false, | |
| "frequency_penalty": 0, | |
| "presence_penalty": 0, | |
| "temperature": 0.8, | |
| "top_p": 0.95, | |
| "seed": null, | |
| "is_base_model": true, | |
| "completion_mode": false, | |
| }; | |
| const chatMessages = document.getElementById("chat-messages"); | |
| const chatInput = document.getElementById("chat-input"); | |
| const sendButton = document.getElementById("send-button"); | |
| const stopButton = document.getElementById("stop-button"); | |
| const settingsButton = document.getElementById("settings-button"); | |
| const settingsModal = document.getElementById("settings-modal"); | |
| const closeSettings = document.getElementById("close-settings"); | |
| const redoButton = document.getElementById('redo-button'); | |
| const chatInterface = document.getElementById("chat-interface"); | |
| const completionsInterface = document.getElementById("completions-interface"); | |
| const completionsInput = document.getElementById("completions-input"); | |
| const completeButton = document.getElementById("complete-button"); | |
| const completionsSettingsButton = document.getElementById("completions-settings-button"); | |
| const completionsStopButton = document.getElementById("completions-stop-button"); | |
| const uploadButton = document.getElementById("upload-button"); | |
| const fileUpload = document.getElementById("file-upload"); | |
| let abortController = null; | |
| let disableAutoScroll = false; | |
| let streamingMessageContent = []; | |
| let originalLength = 0; | |
| let uploadedFiles = []; | |
| let chatHistory = []; | |
| let flagz = null; | |
| function generateId() { | |
| return Date.now().toString(36) + Math.random().toString(36).substring(2); | |
| } | |
| function wrapMessageElement(messageElement, role) { | |
| const wrapper = document.createElement("div"); | |
| wrapper.appendChild(messageElement); | |
| if (role == "assistant") { | |
| const controlContainer = wrapper.appendChild(document.createElement("div")); | |
| controlContainer.appendChild(createCopyButton(() => messageElement.textContent, () => messageElement.innerHTML)); | |
| controlContainer.appendChild(infoButton(wrapper)); | |
| controlContainer.classList.add("message-controls"); | |
| } | |
| wrapper.classList.add("message-wrapper", role); | |
| return wrapper; | |
| } | |
| function infoButton(container, stats) { | |
| let button = container?.querySelector("#stats"); | |
| let statsElement = container?.querySelector("#info-container"); | |
| if (!button) { | |
| button = document.createElement("button"); | |
| button.id = "stats"; | |
| button.innerText = "i"; | |
| button.style.fontFamily = "monospace"; | |
| statsElement = document.createElement("div"); | |
| statsElement.id = "info-container"; | |
| statsElement.className = "hidden"; | |
| container.append(statsElement); | |
| button.addEventListener("click", () => { | |
| const show = !button.classList.contains("toggled"); | |
| statsElement.classList.toggle("hidden", !show); | |
| button.classList.toggle("toggled", show); | |
| if (show) | |
| requestAnimationFrame(() => scrollIntoViewIfNeeded(statsElement, container.parentElement)); | |
| }); | |
| } | |
| button.style.display = stats ? "" : "none"; | |
| if (stats) { | |
| const parts = []; | |
| const promptDurationMs = stats.firstContentTime - stats.startTime; | |
| const responseDurationMs = stats.endTime - stats.firstContentTime; | |
| if (promptDurationMs > 0 && stats.promptTokenCount > 0) { | |
| const tokensPerSecond = (stats.promptTokenCount / (promptDurationMs / 1000)).toFixed(2); | |
| const durationString = promptDurationMs >= 1000 ? `${(promptDurationMs / 1000).toFixed(2)}s` : `${promptDurationMs}ms`; | |
| parts.push(`Processed ${stats.promptTokenCount} input tokens in ${durationString} (${tokensPerSecond} tokens/s)`); | |
| } | |
| if (responseDurationMs > 0 && stats.reponseTokenCount > 0) { | |
| const tokensPerSecond = (stats.reponseTokenCount / (responseDurationMs / 1000)).toFixed(2); | |
| const durationString = responseDurationMs >= 1000 ? `${(responseDurationMs / 1000).toFixed(2)}s` : `${promptDurationMs}ms`; | |
| parts.push(`Generated ${stats.reponseTokenCount} tokens in ${durationString} (${tokensPerSecond} tokens/s)`) | |
| } else { | |
| parts.push("Incomplete"); | |
| } | |
| button.title = parts.join("\n"); | |
| statsElement.innerHTML = ""; | |
| parts.forEach(part => statsElement.appendChild(wrapInSpan(part + " "))); | |
| } | |
| return button; | |
| } | |
| function scrollIntoViewIfNeeded(elem, container) { | |
| let rectElem = elem.getBoundingClientRect(), rectContainer = container.getBoundingClientRect(); | |
| if (rectElem.bottom > rectContainer.bottom) elem.scrollIntoView(false); | |
| if (rectElem.top < rectContainer.top) elem.scrollIntoView(); | |
| } | |
| function wrapInSpan(innerText) { | |
| const span = document.createElement("span"); | |
| span.innerText = innerText; | |
| return span; | |
| } | |
| function createMessageElement(content) { | |
| const messageDiv = document.createElement("div"); | |
| messageDiv.classList.add("message"); | |
| let hdom = new HighlightDom(messageDiv); | |
| const high = new RenderMarkdown(hdom); | |
| high.feed(content); | |
| high.flush(); | |
| return messageDiv; | |
| } | |
| function scrollToBottom() { | |
| if (!disableAutoScroll) { | |
| document.getElementById("bottom").scrollIntoView({ behavior: "instant" }); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| } | |
| function onChatInput() { | |
| chatInput.style.height = "auto"; // computes scrollHeight | |
| chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + "px"; | |
| } | |
| function cleanupAfterMessage() { | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| chatInput.disabled = false; | |
| sendButton.style.display = "inline-block"; | |
| stopButton.style.display = "none"; | |
| abortController = null; | |
| if (!disableAutoScroll) { | |
| scrollToBottom(); | |
| chatInput.focus(); | |
| } | |
| disableAutoScroll = false; | |
| } | |
| function onWheel(e) { | |
| if (e.deltaY == undefined || e.deltaY < 0) | |
| disableAutoScroll = true; | |
| } | |
| async function handleChatStream(response, stats) { | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ""; | |
| let currentMessageElement = null; | |
| let currentMessageWrapper = null; | |
| let messageAppended = false; | |
| let finishReason = null; | |
| let hdom = null; | |
| let high = null; | |
| streamingMessageContent = []; | |
| const prefillStatus = document.getElementById('prefill-status'); | |
| const progressBar = prefillStatus.querySelector('.progress-bar'); | |
| try { | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split("\n"); | |
| // process all complete lines | |
| for (let i = 0; i < lines.length - 1; i++) { | |
| const line = lines[i].trim(); | |
| if (line.startsWith("data: ")) { | |
| const data = line.slice(6); | |
| if (data === "[DONE]") { | |
| prefillStatus.style.display = "none"; | |
| continue; | |
| } | |
| try { | |
| const parsed = JSON.parse(data); | |
| const content = parsed.choices[0]?.delta?.content || ""; | |
| finishReason = parsed.choices[0]?.finish_reason; | |
| // handle prefill progress | |
| if (parsed.x_prefill_progress !== undefined) { | |
| prefillStatus.style.display = "flex"; | |
| progressBar.style.width = `${parsed.x_prefill_progress * 100}%`; | |
| } else { | |
| if (content && !stats.firstContentTime) { | |
| // Finished parsing the prompt | |
| stats.firstContentTime = Date.now(); | |
| prefillStatus.style.display = "none"; | |
| } | |
| } | |
| if (content && !messageAppended) { | |
| currentMessageElement = createMessageElement(""); | |
| currentMessageWrapper = wrapMessageElement(currentMessageElement, "assistant"); | |
| chatMessages.appendChild(currentMessageWrapper); | |
| hdom = new HighlightDom(currentMessageElement); | |
| high = new RenderMarkdown(hdom); | |
| messageAppended = true; | |
| } | |
| if (messageAppended && content) { | |
| streamingMessageContent.push(content); | |
| high.feed(content); | |
| scrollToBottom(); | |
| } | |
| if (parsed.usage) { | |
| stats.endTime = Date.now() | |
| stats.promptTokenCount = parsed.usage.prompt_tokens | |
| stats.reponseTokenCount = parsed.usage.completion_tokens | |
| } | |
| } catch (e) { | |
| console.error("Error parsing JSON:", e); | |
| } | |
| } | |
| } | |
| // keep the last incomplete line in the buffer | |
| buffer = lines[lines.length - 1]; | |
| } | |
| } catch (error) { | |
| if (error.name !== "AbortError") { | |
| console.error("Error reading stream:", error); | |
| } | |
| } finally { | |
| if (messageAppended) { | |
| stats.firstContentTime = stats.firstContentTime ?? Date.now(); | |
| stats.endTime = stats.endTime ?? Date.now(); | |
| infoButton(currentMessageWrapper, stats); | |
| high.flush(); | |
| // we don't supply max_tokens, so "length" can | |
| // only mean that we ran out of context window | |
| if (finishReason === "length") { | |
| let img = document.createElement("IMG"); | |
| img.className = "ooc"; | |
| img.src = "ooc.svg"; | |
| img.alt = "🚫"; | |
| img.title = "Message truncated due to running out of context window. Consider tuning --ctx-size and/or --reserve-tokens"; | |
| img.width = 16; | |
| img.height = 16; | |
| hdom.lastElement.appendChild(img); | |
| } | |
| } | |
| prefillStatus.style.display = "none"; | |
| cleanupAfterMessage(); | |
| } | |
| } | |
| function stopMessage() { | |
| if (abortController) { | |
| abortController.abort(); | |
| cleanupAfterMessage(); | |
| } | |
| } | |
| function fixUploads(str) { | |
| str = uploadedFiles.reduce( | |
| (text, [from, to]) => text.replaceAll(from, to), | |
| str); | |
| return str; | |
| } | |
| async function sendMessage() { | |
| const message = fixUploads(chatInput.value.trim()); | |
| if (!message) | |
| return; | |
| // disable input while processing | |
| chatInput.value = ""; | |
| chatInput.disabled = true; | |
| onChatInput(); | |
| disableAutoScroll = false; | |
| sendButton.style.display = "none"; | |
| stopButton.style.display = "inline-block"; | |
| stopButton.focus(); | |
| abortController = new AbortController(); | |
| // add user message to chat | |
| const userMessageElement = createMessageElement(message); | |
| chatMessages.appendChild(wrapMessageElement(userMessageElement, "user")); | |
| scrollToBottom(); | |
| // Prosty RAG | |
| try { | |
| const res = await fetch("http://localhost:8081/?q=" + encodeURIComponent(message), {signal: abortController.signal}); | |
| if (res.ok) { | |
| const context = await res.text(); | |
| if (context) { | |
| chatHistory[0].content = chatHistory[0].content.replace(/\n\nKontekst:\n\n[\s\S]*$/g, '') + '\n\nKontekst:\n\n' + context.replace(/^\d+ \d\.\d+ /gm, ''); | |
| } | |
| } else { | |
| console.error("sendMessage() failed due to embedfile server error", response); | |
| chatMessages.appendChild(wrapMessageElement(createMessageElement( | |
| `Embedfile server replied with error code ${response.status} ${response.statusText}`), | |
| "system")); | |
| cleanupAfterMessage(); | |
| } | |
| } catch (error) { | |
| if (error.name !== "AbortError") { | |
| console.error("sendMessage() failed due to unexpected exception", error); | |
| chatMessages.appendChild(wrapMessageElement(createMessageElement( | |
| "There was an error processing your request."), | |
| "system")); | |
| } | |
| cleanupAfterMessage(); | |
| } | |
| // update chat history | |
| chatHistory.push({ role: "user", content: message }); | |
| const settings = loadSettings(); | |
| try { | |
| const stats = { | |
| startTime: Date.now(), // Timestamp when the request started | |
| firstContentTime: null, // Timestamp when the first content was received | |
| endTime: null, // Timestamp when the response was fully received | |
| promptTokenCount: 0, // Number of tokens in the prompt | |
| reponseTokenCount: 0 // Number of tokens in the response | |
| }; | |
| const response = await fetch("/v1/chat/completions", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": `Bearer ${API_KEY}` | |
| }, | |
| body: JSON.stringify({ | |
| model: flagz.model || "gpt-3.5-turbo", | |
| messages: chatHistory, | |
| temperature: settings.temperature, | |
| top_p: settings.top_p, | |
| presence_penalty: settings.presence_penalty, | |
| frequency_penalty: settings.frequency_penalty, | |
| stream: true, | |
| stream_options: { | |
| include_usage: true | |
| } | |
| }), | |
| signal: abortController.signal | |
| }); | |
| if (response.ok) { | |
| await handleChatStream(response, stats); | |
| const lastMessage = streamingMessageContent.join(""); | |
| if (lastMessage) | |
| chatHistory.push({ role: "assistant", content: lastMessage }); | |
| } else { | |
| console.error("sendMessage() failed due to server error", response); | |
| chatMessages.appendChild(wrapMessageElement(createMessageElement( | |
| `Server replied with error code ${response.status} ${response.statusText}`), | |
| "system")); | |
| cleanupAfterMessage(); | |
| } | |
| } catch (error) { | |
| if (error.name !== "AbortError") { | |
| console.error("sendMessage() failed due to unexpected exception", error); | |
| chatMessages.appendChild(wrapMessageElement(createMessageElement( | |
| "There was an error processing your request."), | |
| "system")); | |
| } | |
| cleanupAfterMessage(); | |
| } | |
| } | |
| function onDragBegin(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| chatInput.classList.add("drag-over"); | |
| } | |
| function onDragEnd(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| chatInput.classList.remove("drag-over"); | |
| } | |
| function onDrop(e) { | |
| const files = e.dataTransfer.files; | |
| [...files].forEach(onFile); | |
| } | |
| function onPaste(e) { | |
| const items = e.clipboardData.items; | |
| for (let item of items) { | |
| if (item.type.startsWith("image/")) { | |
| e.preventDefault(); | |
| onFile(item.getAsFile()); | |
| return; | |
| } | |
| } | |
| } | |
| // fixes image data uri | |
| // - convert to jpg if it's not jpg/png/gif | |
| // - reduce quality and/or downscale if too big | |
| async function fixImageDataUri(dataUri, maxLength = 1024 * 1024) { | |
| const mimeMatch = dataUri.match(/^data:([^;,]+)/); | |
| if (!mimeMatch) | |
| throw new Error("bad image data uri"); | |
| const mimeType = mimeMatch[1].toLowerCase(); | |
| const supported = ["image/jpeg", "image/jpg", "image/png", "image/gif"]; | |
| if (supported.includes(mimeType)) | |
| if (dataUri.length <= maxLength) | |
| return dataUri; | |
| const lossless = ["image/png", "image/gif"]; | |
| const quality = lossless.includes(mimeType) ? 0.92 : 0.8; | |
| function createScaledCanvas(img, scale) { | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = Math.floor(img.width * scale); | |
| canvas.height = Math.floor(img.height * scale); | |
| const ctx = canvas.getContext("2d"); | |
| ctx.imageSmoothingEnabled = true; | |
| ctx.imageSmoothingQuality = "high"; | |
| ctx.fillStyle = "white"; // in case of transparency | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0, canvas.width, canvas.height); | |
| return canvas; | |
| } | |
| return new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.onload = async () => { | |
| let scale = 1.0; | |
| let attempts = 0; | |
| const maxAttempts = 5; | |
| const initialCanvas = createScaledCanvas(img, scale); | |
| let result = initialCanvas.toDataURL("image/jpeg", quality); | |
| while (result.length > maxLength && attempts < maxAttempts) { | |
| attempts++; | |
| scale *= 0.7071; | |
| const scaledCanvas = createScaledCanvas(img, scale); | |
| result = scaledCanvas.toDataURL("image/jpeg", quality); | |
| result.length = result.length; | |
| } | |
| if (result.length <= maxLength) { | |
| resolve(result); | |
| } else { | |
| reject(new Error(`Could not reduce image to ${(maxLength/1024).toFixed(2)}kb after ${maxAttempts} attempts`)); | |
| } | |
| }; | |
| img.onerror = () => { | |
| reject(new Error('Failed to load image from data URI')); | |
| }; | |
| img.src = dataUri; | |
| }); | |
| } | |
| async function onFile(file) { | |
| const reader = new FileReader(); | |
| if (file.type.toLowerCase().startsWith('image/')) { | |
| reader.onloadend = async function() { | |
| const description = file.name; | |
| const realDataUri = await fixImageDataUri(reader.result); | |
| const fakeDataUri = 'data:,placeholder/' + generateId(); | |
| uploadedFiles.push([fakeDataUri, realDataUri]); | |
| insertText(chatInput, ``); | |
| }; | |
| reader.readAsDataURL(file); | |
| } else if (file.type.toLowerCase().startsWith('text/')) { | |
| reader.onloadend = function() { | |
| const content = reader.result; | |
| insertText(chatInput, `\`\`\`\n${content}\n\`\`\``); | |
| }; | |
| reader.readAsText(file); | |
| } else { | |
| alert('Only image and text files are supported'); | |
| return; | |
| } | |
| } | |
| function checkSurroundingNewlines(text, pos) { | |
| const beforeCaret = text.slice(0, pos); | |
| const afterCaret = text.slice(pos); | |
| const precedingNewlines = beforeCaret.match(/\n*$/)[0].length; | |
| const followingNewlines = afterCaret.match(/^\n*/)[0].length; | |
| return { precedingNewlines, followingNewlines }; | |
| } | |
| function insertText(elem, text) { | |
| const pos = elem.selectionStart; | |
| const isCodeBlock = text.includes('```'); | |
| if (isCodeBlock) { | |
| const { precedingNewlines, followingNewlines } = checkSurroundingNewlines(elem.value, pos); | |
| const needsLeadingNewlines = pos > 0 && precedingNewlines < 2 ? '\n'.repeat(2 - precedingNewlines) : ''; | |
| const needsTrailingNewlines = pos < elem.value.length && followingNewlines < 2 ? '\n'.repeat(2 - followingNewlines) : ''; | |
| text = needsLeadingNewlines + text + needsTrailingNewlines; | |
| } | |
| elem.value = elem.value.slice(0, pos) + text + elem.value.slice(pos); | |
| const newPos = pos + text.length; | |
| elem.setSelectionRange(newPos, newPos); | |
| elem.focus(); | |
| elem.dispatchEvent(new Event("input")); | |
| } | |
| function onKeyDown(e) { | |
| if (e.key === "Enter" && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| } | |
| async function fetchFlagz() { | |
| try { | |
| const response = await fetch("/flagz"); | |
| return await response.json(); | |
| } catch (error) { | |
| console.error("Could not fetch /flagz so using defaults", error); | |
| return DEFAULT_FLAGZ; | |
| } | |
| } | |
| function getSystemPrompt() { | |
| let defaultPrompt = flagz.prompt; | |
| if (!defaultPrompt) | |
| defaultPrompt = DEFAULT_SYSTEM_PROMPT; | |
| let promptsText = localStorage.getItem("v1.prompts"); | |
| if (!promptsText) | |
| return defaultPrompt; | |
| let prompts = JSON.parse(promptsText); | |
| let prompt = prompts[defaultPrompt]; | |
| if (!prompt) | |
| return defaultPrompt; | |
| return prompt; | |
| } | |
| function updateModelInfo() { | |
| if (flagz.model) { | |
| const modelName = flagz.model; | |
| document.title = `${modelName} - llamafile`; | |
| document.getElementById("model").textContent = modelName; | |
| document.getElementById("model-completions").textContent = modelName; | |
| } | |
| if (!flagz.nologo) { | |
| document.querySelectorAll(".logo").forEach(logo => logo.style.display = "inline-block"); | |
| } | |
| } | |
| function startChat(history) { | |
| chatHistory = history; | |
| chatMessages.innerHTML = ""; | |
| for (let i = 0; i < chatHistory.length; i++) { | |
| if (flagz.no_display_prompt && chatHistory[i].role == "system") | |
| continue; | |
| chatMessages.appendChild(wrapMessageElement(createMessageElement(chatHistory[i].content), | |
| chatHistory[i].role)); | |
| } | |
| scrollToBottom(); | |
| } | |
| function loadSettings() { | |
| const stored = localStorage.getItem('v1.modelSettings'); | |
| if (stored) { | |
| return JSON.parse(stored); | |
| } | |
| return { | |
| temperature: flagz.temperature, | |
| top_p: flagz.top_p, | |
| presence_penalty: flagz.presence_penalty, | |
| frequency_penalty: flagz.frequency_penalty, | |
| }; | |
| } | |
| function saveSettings(settings) { | |
| localStorage.setItem('v1.modelSettings', JSON.stringify(settings)); | |
| } | |
| function formatDoubleWithPlus(x) { | |
| return (x >= 0 ? "+" : "") + x.toFixed(2); | |
| } | |
| function updateSettingsDisplay(settings) { | |
| document.getElementById("temp-value").textContent = settings.temperature ? settings.temperature.toFixed(2) : "0.00 (deterministic)"; | |
| document.getElementById("top-p-value").textContent = settings.top_p.toFixed(2); | |
| document.getElementById("presence-value").textContent = formatDoubleWithPlus(settings.presence_penalty); | |
| document.getElementById("frequency-value").textContent = formatDoubleWithPlus(settings.frequency_penalty); | |
| document.getElementById("temperature").value = settings.temperature; | |
| document.getElementById("top-p").value = settings.top_p; | |
| document.getElementById("presence-penalty").value = settings.presence_penalty; | |
| document.getElementById("frequency-penalty").value = settings.frequency_penalty; | |
| // Handle top-p disabling - using a more reliable selector | |
| const topPSettingItem = document.querySelector('.setting-item:has(#top-p)'); | |
| if (settings.temperature === 0) { | |
| topPSettingItem.classList.add('disabled'); | |
| } else { | |
| topPSettingItem.classList.remove('disabled'); | |
| } | |
| // Update top-p description with percentage | |
| const topPDescription = topPSettingItem.querySelector('.setting-description'); | |
| if (settings.top_p >= 1) { | |
| topPDescription.textContent = "Disabled. All tokens will be considered by the sampler."; | |
| } else if (settings.top_p > .5) { | |
| const percentage = Math.round((1 - settings.top_p) * 100); | |
| topPDescription.textContent = `The bottom ${percentage}% tokens will be ignored by the sampler.`; | |
| } else { | |
| const percentage = Math.round(settings.top_p * 100); | |
| topPDescription.textContent = `Only the top ${percentage}% tokens will be considered by the sampler.`; | |
| } | |
| } | |
| function setupSettings() { | |
| settingsButton.addEventListener("click", () => { | |
| settingsModal.style.display = "flex"; | |
| updateSettingsDisplay(loadSettings()); | |
| }); | |
| closeSettings.addEventListener("click", () => { | |
| settingsModal.style.display = "none"; | |
| }); | |
| ["temperature", "top-p", "presence-penalty", "frequency-penalty"].forEach(id => { | |
| const element = document.getElementById(id); | |
| element.addEventListener("input", (e) => { | |
| const settings = loadSettings(); | |
| const value = parseFloat(e.target.value); | |
| const key = id.replace(/-/g, '_'); | |
| settings[key] = value; | |
| saveSettings(settings); | |
| updateSettingsDisplay(settings); | |
| }); | |
| }); | |
| settingsModal.addEventListener("mousedown", (e) => { | |
| if (e.target === settingsModal) { | |
| settingsModal.style.display = "none"; | |
| } | |
| }); | |
| document.addEventListener("keydown", (e) => { | |
| if (e.key === "Escape") { | |
| settingsModal.style.display = "none"; | |
| } | |
| }); | |
| } | |
| function setupCompletions() { | |
| completeButton.addEventListener("click", sendCompletion); | |
| completionsStopButton.addEventListener("click", stopCompletion); | |
| completionsSettingsButton.addEventListener("click", () => { | |
| settingsModal.style.display = "flex"; | |
| updateSettingsDisplay(loadSettings()); | |
| }); | |
| completionsInput.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter" && !e.shiftKey && (e.ctrlKey || e.metaKey)) { | |
| e.preventDefault(); | |
| sendCompletion(); | |
| } | |
| }); | |
| } | |
| function stopCompletion() { | |
| if (abortController) { | |
| abortController.abort(); | |
| cleanupAfterCompletion(); | |
| } | |
| } | |
| function cleanupAfterCompletion() { | |
| completeButton.style.display = "inline-block"; | |
| completionsStopButton.style.display = "none"; | |
| completeButton.disabled = false; | |
| abortController = null; | |
| // select newly added text and restore focus | |
| const textArea = completionsInput; | |
| textArea.focus(); | |
| textArea.selectionStart = originalLength || 0; | |
| textArea.selectionEnd = textArea.value.length; | |
| } | |
| async function sendCompletion() { | |
| const text = completionsInput.value; | |
| completeButton.style.display = "none"; | |
| completionsStopButton.style.display = "inline-block"; | |
| completeButton.disabled = true; | |
| abortController = new AbortController(); | |
| originalLength = text.length; | |
| completionsStopButton.focus(); | |
| const settings = loadSettings(); | |
| try { | |
| const response = await fetch("/v1/completions", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": `Bearer ${API_KEY}` | |
| }, | |
| body: JSON.stringify({ | |
| model: flagz.model || "gpt-3.5-turbo", | |
| prompt: text, | |
| temperature: settings.temperature, | |
| top_p: settings.top_p, | |
| presence_penalty: settings.presence_penalty, | |
| frequency_penalty: settings.frequency_penalty, | |
| stream: true | |
| }), | |
| signal: abortController.signal | |
| }); | |
| if (response.ok) { | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ""; | |
| try { | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) | |
| break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split("\n"); | |
| for (let i = 0; i < lines.length - 1; i++) { | |
| const line = lines[i].trim(); | |
| if (line.startsWith("data: ")) { | |
| const data = line.slice(6); | |
| if (data === "[DONE]") | |
| continue; | |
| try { | |
| const parsed = JSON.parse(data); | |
| const content = parsed.choices[0]?.text || ""; | |
| completionsInput.value += content; | |
| completionsInput.scrollTop = completionsInput.scrollHeight; | |
| } catch (e) { | |
| console.error("Error parsing JSON:", e); | |
| } | |
| } | |
| } | |
| buffer = lines[lines.length - 1]; | |
| } | |
| } catch (error) { | |
| if (error.name !== "AbortError") { | |
| console.error("Error reading stream:", error); | |
| } | |
| } | |
| } else { | |
| console.error("Completion failed:", response.statusText); | |
| } | |
| } catch (error) { | |
| if (error.name !== "AbortError") { | |
| console.error("Completion error:", error); | |
| } | |
| } finally { | |
| cleanupAfterCompletion(); | |
| } | |
| } | |
| function removeLastDirectChild(element) { | |
| if (element.lastElementChild) { | |
| element.removeChild(element.lastElementChild); | |
| } | |
| } | |
| function onRedo() { | |
| if (!chatHistory.length) | |
| return; | |
| removeLastDirectChild(chatMessages); | |
| let msg = chatHistory.pop(); | |
| if (msg.role === "assistant") { | |
| removeLastDirectChild(chatMessages); | |
| msg = chatHistory.pop(); | |
| } | |
| chatInput.value = msg.content; | |
| chatInput.focus(); | |
| chatInput.dispatchEvent(new Event("input")); // adjust textarea height | |
| } | |
| function setupMenu() { | |
| const triggers = document.querySelectorAll('.menu-trigger'); | |
| const menus = document.querySelectorAll('.menu'); | |
| const chatModeSwitch = document.getElementById('chat-mode-switch'); | |
| const completionsModeSwitch = document.getElementById('completions-mode-switch'); | |
| if (flagz.is_base_model) { | |
| completionsModeSwitch.classList.add('disabled'); | |
| completionsModeSwitch.title = "Chatbot mode isn't possible because this is a base model that hasn't had instruction fine tuning; try passing --chat-template chatml or llama2 if this is really an instruct model."; | |
| completionsModeSwitch.disabled = true; | |
| } | |
| triggers.forEach(trigger => { | |
| trigger.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const menu = trigger.nextElementSibling; | |
| menus.forEach(m => { | |
| if (m !== menu) | |
| m.classList.remove('show'); | |
| }); | |
| menu.classList.toggle('show'); | |
| }); | |
| }); | |
| document.addEventListener('click', () => { | |
| menus.forEach(menu => menu.classList.remove('show')); | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') | |
| menus.forEach(menu => menu.classList.remove('show')); | |
| }); | |
| chatModeSwitch.addEventListener('click', () => { | |
| flagz.completion_mode = true; | |
| setupCompletionsMode(); | |
| menus.forEach(menu => menu.classList.remove('show')); | |
| }); | |
| completionsModeSwitch.addEventListener('click', () => { | |
| if (!flagz.is_base_model) { | |
| flagz.completion_mode = false; | |
| setupChatCompletionsMode(); | |
| menus.forEach(menu => menu.classList.remove('show')); | |
| } | |
| }); | |
| } | |
| function setupChatCompletionsMode() { | |
| chatInterface.style.display = "flex"; | |
| completionsInterface.style.display = "none"; | |
| startChat([{ role: "system", content: getSystemPrompt() }]); | |
| chatInput.focus(); | |
| } | |
| function setupCompletionsMode() { | |
| chatInterface.style.display = "none"; | |
| completionsInterface.style.display = "flex"; | |
| completionsInput.focus(); | |
| } | |
| function onUploadButtonClick() { | |
| fileUpload.click(); | |
| } | |
| function onFileUploadChange(e) { | |
| if (e.target.files[0]) { | |
| onFile(e.target.files[0]); | |
| e.target.value = ''; | |
| } | |
| } | |
| async function chatbot() { | |
| flagz = await fetchFlagz(); | |
| updateModelInfo(); | |
| setupSettings(); | |
| setupCompletions(); | |
| setupMenu(); | |
| if (flagz.is_base_model || flagz.completion_mode) { | |
| setupCompletionsMode(); | |
| } else { | |
| setupChatCompletionsMode(); | |
| } | |
| sendButton.addEventListener("click", sendMessage); | |
| stopButton.addEventListener("click", stopMessage); | |
| redoButton.addEventListener("click", onRedo); | |
| chatInput.addEventListener("input", onChatInput); | |
| chatInput.addEventListener("keydown", onKeyDown); | |
| chatMessages.addEventListener("touchmove", onWheel); | |
| document.addEventListener("wheel", onWheel); | |
| document.addEventListener("dragenter", onDragBegin); | |
| document.addEventListener("dragover", onDragBegin); | |
| document.addEventListener("dragleave", onDragEnd); | |
| document.addEventListener("drop", onDragEnd); | |
| document.addEventListener("drop", onDrop); | |
| document.addEventListener("paste", onPaste); | |
| uploadButton.addEventListener("click", onUploadButtonClick); | |
| fileUpload.addEventListener("change", onFileUploadChange); | |
| } | |
| chatbot(); | |