llm-py-web/client/main.tsx

147 lines
3.8 KiB
TypeScript

import { Signal } from "@char/aftercare";
import { ChatResponse } from "./response.tsx";
const main = document.querySelector("main")!;
async function nav() {
const nav = <nav />;
const conversations = await fetch("/api/conversation").then(r => r.json());
for (const conversation of conversations) {
const button = <button type="button">{conversation.name}</button>;
button.addEventListener("click", e => {
e.preventDefault();
main.append(conversationUI(conversation.id));
nav.remove();
});
nav.append(button);
}
nav.append(
<button
type="button"
_tap={b =>
b.addEventListener("click", e => {
e.preventDefault();
main.append(conversationUI("new"));
nav.remove();
})
}
>
new
</button>,
);
return nav;
}
function conversationUI(id: string) {
window.location.hash = `#${id}`;
let socket: WebSocket;
let connected = false;
const connect = () => {
socket = new WebSocket(
`/api/conversation/${id}/connect` + (connected ? "?continue=1" : ""),
);
socket.addEventListener("open", () => (connected = true));
socket.addEventListener("close", () => (socket = connect()));
socket.addEventListener("error", ev => {
console.warn(ev);
// TODO: handle errors
});
return socket;
};
socket = connect();
const header = (
<header>
<h1 />
<button tabIndex={-1} type="button" className="danger">
delete
</button>
</header>
);
const name = new Signal("");
name.subscribeImmediate(it => (header.querySelector("h1")!.textContent = it));
header.querySelector("button")!.addEventListener("click", async e => {
e.preventDefault();
await fetch(`/api/conversation/${id}`, { method: "DELETE" });
window.location.hash = "";
});
const chatlog = <section className="chatlog" />;
const inFlightMessages = new Map<string, ChatResponse>();
socket.addEventListener("message", event => {
if (typeof event.data !== "string") return;
const message = JSON.parse(event.data);
const scrolledToBottom =
chatlog.scrollTop + 16 >= chatlog.scrollHeight - chatlog.clientHeight;
if ("i" in message) {
window.history.replaceState(null, "", "#" + message.i);
id = message.i;
} else if ("u" in message) {
chatlog.append(<article className="user">{message.u}</article>);
} else if ("f" in message) {
const response = new ChatResponse();
response.append(message.f);
response.finalize();
chatlog.append(response.element);
} else if ("s" in message) {
const response = new ChatResponse();
inFlightMessages.set(message.s, response);
chatlog.append(response.element);
} else if ("r" in message && "c" in message) {
const article = inFlightMessages.get(message.r)!;
article.append(message.c);
} else if ("d" in message) {
const response = inFlightMessages.get(message.d);
if (response) response.finalize();
inFlightMessages.delete(message.d);
} else if ("n" in message) {
name.set(message.n);
}
if (scrolledToBottom) chatlog.scrollTop = chatlog.scrollHeight - chatlog.clientHeight;
});
const form = (
<form>
<input type="text" placeholder="Enter a prompt…" required />
</form>
);
const input = form.querySelector("input")!;
form.addEventListener("submit", e => {
e.preventDefault();
socket.send(input.value);
input.value = "";
});
return (
<section className="conversation">
{header}
{chatlog}
{form}
</section>
);
}
const showUI = async () => {
main.innerHTML = "";
if (window.location.hash) {
main.append(conversationUI(window.location.hash.substring(1)));
} else {
main.append(await nav());
}
};
await showUI();
window.addEventListener("hashchange", async () => {
await showUI();
});