document.addEventListener("alpine:init", () => { Alpine.data("state", () => ({ // current state cstate: { time: null, messages: [], }, // historical state histories: JSON.parse(localStorage.getItem("histories")) || [], home: 0, generating: false, endpoint: `${window.location.origin}/v1`, // performance tracking time_till_first: 0, tokens_per_second: 0, total_tokens: 0, removeHistory(cstate) { const index = this.histories.findIndex((state) => { return state.time === cstate.time; }); if (index !== -1) { this.histories.splice(index, 1); localStorage.setItem("histories", JSON.stringify(this.histories)); } }, async handleSend() { const el = document.getElementById("input-form"); const value = el.value.trim(); if (!value) return; if (this.generating) return; this.generating = true; if (this.home === 0) this.home = 1; // ensure that going back in history will go back to home window.history.pushState({}, "", "/"); // add message to list this.cstate.messages.push({ role: "user", content: value }); // clear textarea el.value = ""; el.style.height = "auto"; el.style.height = el.scrollHeight + "px"; // reset performance tracking const prefill_start = Date.now(); let start_time = 0; let tokens = 0; this.tokens_per_second = 0; // start receiving server sent events let gottenFirstChunk = false; for await ( const chunk of this.openaiChatCompletion(this.cstate.messages) ) { if (!gottenFirstChunk) { this.cstate.messages.push({ role: "assistant", content: "" }); gottenFirstChunk = true; } // add chunk to the last message this.cstate.messages[this.cstate.messages.length - 1].content += chunk; // calculate performance tracking tokens += 1; this.total_tokens += 1; if (start_time === 0) { start_time = Date.now(); this.time_till_first = start_time - prefill_start; } else { const diff = Date.now() - start_time; if (diff > 0) { this.tokens_per_second = tokens / (diff / 1000); } } } // update the state in histories or add it if it doesn't exist const index = this.histories.findIndex((cstate) => { return cstate.time === this.cstate.time; }); this.cstate.time = Date.now(); if (index !== -1) { // update the time this.histories[index] = this.cstate; } else { this.histories.push(this.cstate); } // update in local storage localStorage.setItem("histories", JSON.stringify(this.histories)); this.generating = false; }, async handleEnter(event) { // if shift is not pressed if (!event.shiftKey) { event.preventDefault(); await this.handleSend(); } }, updateTotalTokens(messages) { fetch(`${this.endpoint}/chat/token/encode`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages }), }).then((response) => response.json()).then((data) => { this.total_tokens = data.length; }).catch(console.error); }, async *openaiChatCompletion(messages) { // stream response const response = await fetch(`${this.endpoint}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ "messages": messages, "stream": true, }), }); if (!response.ok) { throw new Error("Failed to fetch"); } const reader = response.body.pipeThrough(new TextDecoderStream()) .pipeThrough(new EventSourceParserStream()).getReader(); while (true) { const { done, value } = await reader.read(); if (done) { break; } if (value.type === "event") { const json = JSON.parse(value.data); if (json.choices) { const choice = json.choices[0]; if (choice.finish_reason === "stop") { break; } yield choice.delta.content; } } } }, })); }); const { markedHighlight } = globalThis.markedHighlight; marked.use(markedHighlight({ langPrefix: "hljs language-", highlight(code, lang, _info) { const language = hljs.getLanguage(lang) ? lang : "plaintext"; return hljs.highlight(code, { language }).value; }, })); // **** eventsource-parser **** class EventSourceParserStream extends TransformStream { constructor() { let parser; super({ start(controller) { parser = createParser((event) => { if (event.type === "event") { controller.enqueue(event); } }); }, transform(chunk) { parser.feed(chunk); }, }); } } function createParser(onParse) { let isFirstChunk; let buffer; let startingPosition; let startingFieldLength; let eventId; let eventName; let data; reset(); return { feed, reset, }; function reset() { isFirstChunk = true; buffer = ""; startingPosition = 0; startingFieldLength = -1; eventId = void 0; eventName = void 0; data = ""; } function feed(chunk) { buffer = buffer ? buffer + chunk : chunk; if (isFirstChunk && hasBom(buffer)) { buffer = buffer.slice(BOM.length); } isFirstChunk = false; const length = buffer.length; let position = 0; let discardTrailingNewline = false; while (position < length) { if (discardTrailingNewline) { if (buffer[position] === "\n") { ++position; } discardTrailingNewline = false; } let lineLength = -1; let fieldLength = startingFieldLength; let character; for ( let index = startingPosition; lineLength < 0 && index < length; ++index ) { character = buffer[index]; if (character === ":" && fieldLength < 0) { fieldLength = index - position; } else if (character === "\r") { discardTrailingNewline = true; lineLength = index - position; } else if (character === "\n") { lineLength = index - position; } } if (lineLength < 0) { startingPosition = length - position; startingFieldLength = fieldLength; break; } else { startingPosition = 0; startingFieldLength = -1; } parseEventStreamLine(buffer, position, fieldLength, lineLength); position += lineLength + 1; } if (position === length) { buffer = ""; } else if (position > 0) { buffer = buffer.slice(position); } } function parseEventStreamLine(lineBuffer, index, fieldLength, lineLength) { if (lineLength === 0) { if (data.length > 0) { onParse({ type: "event", id: eventId, event: eventName || void 0, data: data.slice(0, -1), // remove trailing newline }); data = ""; eventId = void 0; } eventName = void 0; return; } const noValue = fieldLength < 0; const field = lineBuffer.slice( index, index + (noValue ? lineLength : fieldLength), ); let step = 0; if (noValue) { step = lineLength; } else if (lineBuffer[index + fieldLength + 1] === " ") { step = fieldLength + 2; } else { step = fieldLength + 1; } const position = index + step; const valueLength = lineLength - step; const value = lineBuffer.slice(position, position + valueLength).toString(); if (field === "data") { data += value ? "".concat(value, "\n") : "\n"; } else if (field === "event") { eventName = value; } else if (field === "id" && !value.includes("\0")) { eventId = value; } else if (field === "retry") { const retry = parseInt(value, 10); if (!Number.isNaN(retry)) { onParse({ type: "reconnect-interval", value: retry, }); } } } } const BOM = [239, 187, 191]; function hasBom(buffer) { return BOM.every((charCode, index) => buffer.charCodeAt(index) === charCode); }