Files
tinygrad/examples/tinychat/index.js
2024-06-08 12:05:45 -07:00

278 lines
7.2 KiB
JavaScript

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`,
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";
// 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: "ai", content: "" });
gottenFirstChunk = true;
}
// add chunk to the last message
this.cstate.messages[this.cstate.messages.length - 1].content += chunk;
}
// 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();
}
},
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");
}
for await (
const event of response.body.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream())
) {
if (event.type === "event") {
const json = JSON.parse(event.data);
if (json.choices) {
const choice = json.choices[0];
// see if the completion is done
if (choice.finish_reason === "stop") break;
// yield the completion
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);
}