Spaces:
Configuration error
Configuration error
/* | |
https://github.com/david-haerer/chatapi | |
MIT License | |
Copyright (c) 2023 David Härer | |
Copyright (c) 2024 Ettore Di Giacinto | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
*/ | |
function submitKey(event) { | |
event.preventDefault(); | |
localStorage.setItem("key", document.getElementById("apiKey").value); | |
document.getElementById("apiKey").blur(); | |
} | |
function submitSystemPrompt(event) { | |
event.preventDefault(); | |
localStorage.setItem("system_prompt", document.getElementById("systemPrompt").value); | |
document.getElementById("systemPrompt").blur(); | |
} | |
var image = ""; | |
function submitPrompt(event) { | |
event.preventDefault(); | |
const input = document.getElementById("input").value; | |
Alpine.store("chat").add("user", input, image); | |
document.getElementById("input").value = ""; | |
const key = localStorage.getItem("key"); | |
const systemPrompt = localStorage.getItem("system_prompt"); | |
promptGPT(systemPrompt, key, input); | |
} | |
function readInputImage() { | |
if (!this.files || !this.files[0]) return; | |
const FR = new FileReader(); | |
FR.addEventListener("load", function(evt) { | |
image = evt.target.result; | |
}); | |
FR.readAsDataURL(this.files[0]); | |
} | |
async function promptGPT(systemPrompt, key, input) { | |
const model = document.getElementById("chat-model").value; | |
// Set class "loader" to the element with "loader" id | |
//document.getElementById("loader").classList.add("loader"); | |
// Make the "loader" visible | |
document.getElementById("loader").style.display = "block"; | |
document.getElementById("input").disabled = true; | |
document.getElementById('messages').scrollIntoView(false) | |
messages = Alpine.store("chat").messages(); | |
// if systemPrompt isn't empty, push it at the start of messages | |
if (systemPrompt) { | |
messages.unshift({ | |
role: "system", | |
content: systemPrompt | |
}); | |
} | |
// loop all messages, and check if there are images. If there are, we need to change the content field | |
messages.forEach((message) => { | |
if (message.image) { | |
// The content field now becomes an array | |
message.content = [ | |
{ | |
"type": "text", | |
"text": message.content | |
} | |
] | |
message.content.push( | |
{ | |
"type": "image_url", | |
"image_url": { | |
"url": message.image, | |
} | |
} | |
); | |
// remove the image field | |
delete message.image; | |
} | |
}); | |
// reset the form and the image | |
image = ""; | |
document.getElementById("input_image").value = null; | |
document.getElementById("fileName").innerHTML = ""; | |
// if (image) { | |
// // take the last element content's and add the image | |
// last_message = messages[messages.length - 1] | |
// // The content field now becomes an array | |
// last_message.content = [ | |
// { | |
// "type": "text", | |
// "text": last_message.content | |
// } | |
// ] | |
// last_message.content.push( | |
// { | |
// "type": "image_url", | |
// "image_url": { | |
// "url": image, | |
// } | |
// } | |
// ); | |
// // and we replace it in the messages array | |
// messages[messages.length - 1] = last_message | |
// // reset the form and the image | |
// image = ""; | |
// document.getElementById("input_image").value = null; | |
// document.getElementById("fileName").innerHTML = ""; | |
// } | |
// Source: https://stackoverflow.com/a/75751803/11386095 | |
const response = await fetch("/v1/chat/completions", { | |
method: "POST", | |
headers: { | |
Authorization: `Bearer ${key}`, | |
"Content-Type": "application/json", | |
}, | |
body: JSON.stringify({ | |
model: model, | |
messages: messages, | |
stream: true, | |
}), | |
}); | |
if (!response.ok) { | |
Alpine.store("chat").add( | |
"assistant", | |
`<span class='error'>Error: POST /v1/chat/completions ${response.status}</span>`, | |
); | |
return; | |
} | |
const reader = response.body | |
?.pipeThrough(new TextDecoderStream()) | |
.getReader(); | |
if (!reader) { | |
Alpine.store("chat").add( | |
"assistant", | |
`<span class='error'>Error: Failed to decode API response</span>`, | |
); | |
return; | |
} | |
// Function to add content to the chat and handle DOM updates efficiently | |
const addToChat = (token) => { | |
const chatStore = Alpine.store("chat"); | |
chatStore.add("assistant", token); | |
// Efficiently scroll into view without triggering multiple reflows | |
const messages = document.getElementById('messages'); | |
messages.scrollTop = messages.scrollHeight; | |
}; | |
let buffer = ""; | |
let contentBuffer = []; | |
try { | |
while (true) { | |
const { value, done } = await reader.read(); | |
if (done) break; | |
buffer += value; | |
let lines = buffer.split("\n"); | |
buffer = lines.pop(); // Retain any incomplete line in the buffer | |
lines.forEach((line) => { | |
if (line.length === 0 || line.startsWith(":")) return; | |
if (line === "data: [DONE]") { | |
return; | |
} | |
if (line.startsWith("data: ")) { | |
try { | |
const jsonData = JSON.parse(line.substring(6)); | |
const token = jsonData.choices[0].delta.content; | |
if (token) { | |
contentBuffer.push(token); | |
} | |
} catch (error) { | |
console.error("Failed to parse line:", line, error); | |
} | |
} | |
}); | |
// Efficiently update the chat in batch | |
if (contentBuffer.length > 0) { | |
addToChat(contentBuffer.join("")); | |
contentBuffer = []; | |
} | |
} | |
// Final content flush if any data remains | |
if (contentBuffer.length > 0) { | |
addToChat(contentBuffer.join("")); | |
} | |
// Highlight all code blocks once at the end | |
hljs.highlightAll(); | |
} catch (error) { | |
console.error("An error occurred while reading the stream:", error); | |
Alpine.store("chat").add( | |
"assistant", | |
`<span class='error'>Error: Failed to process stream</span>`, | |
); | |
} finally { | |
// Perform any cleanup if necessary | |
reader.releaseLock(); | |
} | |
// Remove class "loader" from the element with "loader" id | |
//document.getElementById("loader").classList.remove("loader"); | |
document.getElementById("loader").style.display = "none"; | |
// enable input | |
document.getElementById("input").disabled = false; | |
// scroll to the bottom of the chat | |
document.getElementById('messages').scrollIntoView(false) | |
// set focus to the input | |
document.getElementById("input").focus(); | |
} | |
document.getElementById("key").addEventListener("submit", submitKey); | |
document.getElementById("system_prompt").addEventListener("submit", submitSystemPrompt); | |
document.getElementById("prompt").addEventListener("submit", submitPrompt); | |
document.getElementById("input").focus(); | |
document.getElementById("input_image").addEventListener("change", readInputImage); | |
storeKey = localStorage.getItem("key"); | |
if (storeKey) { | |
document.getElementById("apiKey").value = storeKey; | |
} else { | |
document.getElementById("apiKey").value = null; | |
} | |
storesystemPrompt = localStorage.getItem("system_prompt"); | |
if (storesystemPrompt) { | |
document.getElementById("systemPrompt").value = storesystemPrompt; | |
} else { | |
document.getElementById("systemPrompt").value = null; | |
} | |
marked.setOptions({ | |
highlight: function (code) { | |
return hljs.highlightAuto(code).value; | |
}, | |
}); | |