2025-08-05 03:56:23 +02:00
|
|
|
(() => {
|
|
|
|
const $messages = document.getElementById("messages"),
|
|
|
|
$chat = document.getElementById("chat"),
|
|
|
|
$message = document.getElementById("message"),
|
|
|
|
$bottom = document.getElementById("bottom"),
|
|
|
|
$role = document.getElementById("role"),
|
|
|
|
$model = document.getElementById("model"),
|
2025-08-07 22:09:08 +02:00
|
|
|
$prompt = document.getElementById("prompt"),
|
2025-08-05 03:56:23 +02:00
|
|
|
$temperature = document.getElementById("temperature"),
|
|
|
|
$add = document.getElementById("add"),
|
|
|
|
$send = document.getElementById("send"),
|
2025-08-09 21:16:24 +02:00
|
|
|
$scrolling = document.getElementById("scrolling"),
|
2025-08-05 03:56:23 +02:00
|
|
|
$clear = document.getElementById("clear");
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
const messages = [];
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
let autoScrolling = false, interacted = false;
|
2025-08-07 22:09:08 +02:00
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
function scroll(force = false) {
|
|
|
|
if (!autoScrolling && !force) {
|
|
|
|
return;
|
|
|
|
}
|
2025-08-07 22:09:08 +02:00
|
|
|
|
|
|
|
$messages.scroll({
|
|
|
|
top: $messages.scrollHeight + 200,
|
|
|
|
behavior: "smooth",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
class Message {
|
|
|
|
#id;
|
|
|
|
#role;
|
|
|
|
#reasoning;
|
|
|
|
#text;
|
|
|
|
|
|
|
|
#editing = false;
|
|
|
|
#expanded = false;
|
|
|
|
#state = false;
|
|
|
|
|
|
|
|
#_message;
|
|
|
|
#_role;
|
|
|
|
#_reasoning;
|
|
|
|
#_text;
|
|
|
|
#_edit;
|
|
|
|
|
|
|
|
constructor(role, reasoning, text) {
|
|
|
|
this.#id = uid();
|
|
|
|
this.#role = role;
|
|
|
|
this.#reasoning = reasoning || "";
|
|
|
|
this.#text = text || "";
|
|
|
|
|
|
|
|
this.#build();
|
|
|
|
this.#render();
|
|
|
|
|
|
|
|
messages.push(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
#build() {
|
|
|
|
// main message div
|
|
|
|
this.#_message = make("div", "message", this.#role);
|
|
|
|
|
|
|
|
// message role
|
|
|
|
this.#_role = make("div", "role", this.#role);
|
|
|
|
|
|
|
|
this.#_message.appendChild(this.#_role);
|
|
|
|
|
|
|
|
// message reasoning (wrapper)
|
2025-08-09 22:58:17 +02:00
|
|
|
const _reasoning = make("div", "reasoning");
|
2025-08-07 22:09:08 +02:00
|
|
|
|
|
|
|
this.#_message.appendChild(_reasoning);
|
|
|
|
|
|
|
|
// message reasoning (toggle)
|
|
|
|
const _toggle = make("button", "toggle");
|
|
|
|
|
|
|
|
_toggle.textContent = "Reasoning";
|
|
|
|
|
|
|
|
_reasoning.appendChild(_toggle);
|
|
|
|
|
|
|
|
_toggle.addEventListener("click", () => {
|
|
|
|
this.#expanded = !this.#expanded;
|
|
|
|
|
|
|
|
if (this.#expanded) {
|
|
|
|
this.#_message.classList.add("expanded");
|
|
|
|
} else {
|
|
|
|
this.#_message.classList.remove("expanded");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// message reasoning (content)
|
2025-08-09 22:58:17 +02:00
|
|
|
this.#_reasoning = make("div", "reasoning-text", "markdown");
|
2025-08-07 22:09:08 +02:00
|
|
|
|
|
|
|
_reasoning.appendChild(this.#_reasoning);
|
|
|
|
|
|
|
|
// message content
|
|
|
|
this.#_text = make("div", "text", "markdown");
|
|
|
|
|
|
|
|
this.#_message.appendChild(this.#_text);
|
|
|
|
|
|
|
|
// message edit textarea
|
|
|
|
this.#_edit = make("textarea", "text");
|
|
|
|
|
|
|
|
this.#_message.appendChild(this.#_edit);
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
this.#_edit.addEventListener("keydown", (event) => {
|
|
|
|
if (event.ctrlKey && event.key === "Enter") {
|
|
|
|
this.toggleEdit();
|
|
|
|
} else if (event.key === "Escape") {
|
|
|
|
this.#_edit.value = this.#text;
|
|
|
|
|
|
|
|
this.toggleEdit();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
// message options
|
|
|
|
const _opts = make("div", "options");
|
|
|
|
|
|
|
|
this.#_message.appendChild(_opts);
|
|
|
|
|
|
|
|
// copy option
|
|
|
|
const _optCopy = make("button", "copy");
|
|
|
|
|
|
|
|
_optCopy.title = "Copy message content";
|
|
|
|
|
|
|
|
_opts.appendChild(_optCopy);
|
|
|
|
|
|
|
|
let timeout;
|
|
|
|
|
|
|
|
_optCopy.addEventListener("click", () => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
|
|
navigator.clipboard.writeText(this.#text);
|
|
|
|
|
|
|
|
_optCopy.classList.add("copied");
|
|
|
|
|
|
|
|
timeout = setTimeout(() => {
|
|
|
|
_optCopy.classList.remove("copied");
|
|
|
|
}, 1000);
|
|
|
|
});
|
|
|
|
|
|
|
|
// edit option
|
|
|
|
const _optEdit = make("button", "edit");
|
|
|
|
|
|
|
|
_optEdit.title = "Edit message content";
|
|
|
|
|
|
|
|
_opts.appendChild(_optEdit);
|
|
|
|
|
|
|
|
_optEdit.addEventListener("click", () => {
|
|
|
|
this.toggleEdit();
|
|
|
|
});
|
|
|
|
|
|
|
|
// delete option
|
|
|
|
const _optDelete = make("button", "delete");
|
|
|
|
|
|
|
|
_optDelete.title = "Delete message";
|
|
|
|
|
|
|
|
_opts.appendChild(_optDelete);
|
|
|
|
|
|
|
|
_optDelete.addEventListener("click", () => {
|
|
|
|
this.delete();
|
|
|
|
});
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
// add to dom
|
2025-08-07 22:09:08 +02:00
|
|
|
$messages.appendChild(this.#_message);
|
|
|
|
|
|
|
|
scroll();
|
|
|
|
}
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
#render(only = false, noScroll = false) {
|
2025-08-07 22:09:08 +02:00
|
|
|
if (!only || only === "role") {
|
|
|
|
this.#_role.textContent = this.#role;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!only || only === "reasoning") {
|
|
|
|
this.#_reasoning.innerHTML = render(this.#reasoning);
|
|
|
|
|
|
|
|
if (this.#reasoning) {
|
|
|
|
this.#_message.classList.add("has-reasoning");
|
|
|
|
} else {
|
|
|
|
this.#_message.classList.remove("has-reasoning");
|
|
|
|
}
|
2025-08-09 21:16:24 +02:00
|
|
|
|
|
|
|
this.#_reasoning.style.setProperty(
|
|
|
|
"--height",
|
|
|
|
`${this.#_reasoning.scrollHeight}px`,
|
|
|
|
);
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!only || only === "text") {
|
|
|
|
this.#_text.innerHTML = render(this.#text);
|
2025-08-09 22:58:17 +02:00
|
|
|
|
|
|
|
if (this.#text) {
|
|
|
|
this.#_message.classList.add("has-text");
|
|
|
|
} else {
|
|
|
|
this.#_message.classList.remove("has-text");
|
|
|
|
}
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
if (!noScroll) {
|
|
|
|
scroll();
|
|
|
|
}
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#save() {
|
2025-08-09 21:16:24 +02:00
|
|
|
storeValue(
|
|
|
|
"messages",
|
|
|
|
messages.map((message) => message.getData(true)),
|
|
|
|
);
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
getData(includeReasoning = false) {
|
|
|
|
const data = {
|
|
|
|
role: this.#role,
|
|
|
|
text: this.#text,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (this.#reasoning && includeReasoning) {
|
|
|
|
data.reasoning = this.#reasoning;
|
|
|
|
}
|
|
|
|
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
setState(state) {
|
|
|
|
if (this.#state === state) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.#state) {
|
|
|
|
this.#_message.classList.remove(this.#state);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state) {
|
|
|
|
this.#_message.classList.add(state);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#state = state;
|
|
|
|
}
|
|
|
|
|
|
|
|
addReasoning(chunk) {
|
|
|
|
this.#reasoning += chunk;
|
|
|
|
|
|
|
|
this.#render("reasoning");
|
|
|
|
this.#save();
|
|
|
|
}
|
|
|
|
|
|
|
|
addText(text) {
|
|
|
|
this.#text += text;
|
|
|
|
|
|
|
|
this.#render("text");
|
|
|
|
this.#save();
|
|
|
|
}
|
|
|
|
|
|
|
|
stopEdit() {
|
|
|
|
if (!this.#editing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.toggleEdit();
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleEdit() {
|
|
|
|
this.#editing = !this.#editing;
|
|
|
|
|
|
|
|
if (this.#editing) {
|
|
|
|
this.#_edit.value = this.#text;
|
|
|
|
|
|
|
|
this.#_edit.style.height = `${this.#_text.offsetHeight}px`;
|
|
|
|
this.#_edit.style.width = `${this.#_text.offsetWidth}px`;
|
|
|
|
|
|
|
|
this.setState("editing");
|
|
|
|
|
|
|
|
this.#_edit.focus();
|
|
|
|
} else {
|
|
|
|
this.#text = this.#_edit.value;
|
|
|
|
|
|
|
|
this.setState(false);
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
this.#render(false, true);
|
2025-08-07 22:09:08 +02:00
|
|
|
this.#save();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
delete() {
|
|
|
|
const index = messages.findIndex((msg) => msg.#id === this.#id);
|
|
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
this.#_message.remove();
|
2025-08-07 22:09:08 +02:00
|
|
|
|
|
|
|
messages.splice(index, 1);
|
|
|
|
|
|
|
|
this.#save();
|
2025-08-09 21:16:24 +02:00
|
|
|
|
|
|
|
$messages.dispatchEvent(new Event("scroll"));
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
let controller;
|
|
|
|
|
|
|
|
async function json(url) {
|
|
|
|
try {
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(response.statusText);
|
|
|
|
}
|
|
|
|
|
|
|
|
return await response.json();
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function stream(url, options, callback) {
|
|
|
|
try {
|
|
|
|
const response = await fetch(url, options);
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(response.statusText);
|
|
|
|
}
|
|
|
|
|
|
|
|
const reader = response.body.getReader(),
|
|
|
|
decoder = new TextDecoder();
|
|
|
|
|
|
|
|
let buffer = "";
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const { value, done } = await reader.read();
|
|
|
|
|
|
|
|
if (done) break;
|
|
|
|
|
|
|
|
buffer += decoder.decode(value, {
|
|
|
|
stream: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const idx = buffer.indexOf("\n\n");
|
|
|
|
|
|
|
|
if (idx === -1) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const frame = buffer.slice(0, idx).trim();
|
|
|
|
buffer = buffer.slice(idx + 2);
|
|
|
|
|
|
|
|
if (!frame) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const chunk = JSON.parse(frame);
|
|
|
|
|
|
|
|
if (!chunk) {
|
|
|
|
throw new Error("invalid chunk");
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(chunk);
|
|
|
|
} catch (err) {
|
|
|
|
console.warn("bad frame", frame);
|
|
|
|
console.warn(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
if (err.name !== "AbortError") {
|
|
|
|
callback({
|
|
|
|
type: "error",
|
|
|
|
text: err.message,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
callback(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function loadModels() {
|
|
|
|
const models = await json("/-/models");
|
|
|
|
|
|
|
|
if (!models) {
|
|
|
|
alert("Failed to load models.");
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
return [];
|
2025-08-05 03:56:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$model.innerHTML = "";
|
|
|
|
|
|
|
|
for (const model of models) {
|
|
|
|
const el = document.createElement("option");
|
|
|
|
|
|
|
|
el.value = model.id;
|
|
|
|
el.textContent = model.name;
|
|
|
|
|
|
|
|
$model.appendChild(el);
|
|
|
|
}
|
2025-08-09 21:16:24 +02:00
|
|
|
|
|
|
|
dropdown($model);
|
|
|
|
|
|
|
|
return models;
|
2025-08-05 03:56:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function restore(models) {
|
2025-08-09 21:16:24 +02:00
|
|
|
$role.value = loadValue("role", "user");
|
|
|
|
$model.value = loadValue("model", models[0].id);
|
|
|
|
$prompt.value = loadValue("prompt", "normal");
|
|
|
|
$temperature.value = loadValue("temperature", 0.85);
|
2025-08-05 03:56:23 +02:00
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
if (loadValue("scrolling")) {
|
|
|
|
$scrolling.click();
|
|
|
|
}
|
|
|
|
|
|
|
|
loadValue("messages", []).forEach(
|
|
|
|
(message) => new Message(message.role, message.reasoning, message.text),
|
|
|
|
);
|
|
|
|
|
|
|
|
scroll(true);
|
2025-08-05 03:56:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function pushMessage() {
|
|
|
|
const text = $message.value.trim();
|
|
|
|
|
|
|
|
if (!text) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$message.value = "";
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
return new Message($role.value, "", text);
|
2025-08-05 03:56:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$messages.addEventListener("scroll", () => {
|
|
|
|
const bottom =
|
|
|
|
$messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight);
|
|
|
|
|
|
|
|
if (bottom >= 80) {
|
|
|
|
$bottom.classList.remove("hidden");
|
|
|
|
} else {
|
|
|
|
$bottom.classList.add("hidden");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
$bottom.addEventListener("click", () => {
|
2025-08-09 21:16:24 +02:00
|
|
|
interacted = true;
|
|
|
|
|
|
|
|
scroll(true);
|
2025-08-05 03:56:23 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
$role.addEventListener("change", () => {
|
2025-08-09 22:58:17 +02:00
|
|
|
storeValue("role", $role.value);
|
2025-08-05 03:56:23 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
$model.addEventListener("change", () => {
|
2025-08-09 22:58:17 +02:00
|
|
|
storeValue("model", $model.value);
|
2025-08-05 03:56:23 +02:00
|
|
|
});
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
$prompt.addEventListener("change", () => {
|
2025-08-09 22:58:17 +02:00
|
|
|
storeValue("prompt", $prompt.value);
|
2025-08-07 22:09:08 +02:00
|
|
|
});
|
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
$temperature.addEventListener("input", () => {
|
2025-08-09 22:58:17 +02:00
|
|
|
storeValue("temperature", $temperature.value);
|
2025-08-05 03:56:23 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
$message.addEventListener("input", () => {
|
2025-08-09 22:58:17 +02:00
|
|
|
storeValue("message", $message.value);
|
2025-08-05 03:56:23 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
$add.addEventListener("click", () => {
|
2025-08-09 21:16:24 +02:00
|
|
|
interacted = true;
|
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
pushMessage();
|
|
|
|
});
|
|
|
|
|
|
|
|
$clear.addEventListener("click", () => {
|
|
|
|
if (!confirm("Are you sure you want to delete all messages?")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
interacted = true;
|
|
|
|
|
|
|
|
for (let x = messages.length - 1; x >= 0; x--) {
|
|
|
|
messages[x].delete();
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
2025-08-05 03:56:23 +02:00
|
|
|
});
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
$scrolling.addEventListener("click", () => {
|
|
|
|
interacted = true;
|
|
|
|
|
|
|
|
autoScrolling = !autoScrolling;
|
|
|
|
|
|
|
|
if (autoScrolling) {
|
|
|
|
$scrolling.title = "Turn off auto-scrolling";
|
|
|
|
$scrolling.classList.add("on");
|
|
|
|
} else {
|
|
|
|
$scrolling.title = "Turn on auto-scrolling";
|
|
|
|
$scrolling.classList.remove("on");
|
|
|
|
}
|
|
|
|
|
|
|
|
storeValue("scrolling", autoScrolling);
|
|
|
|
});
|
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
$send.addEventListener("click", () => {
|
2025-08-09 21:16:24 +02:00
|
|
|
interacted = true;
|
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
if (controller) {
|
|
|
|
controller.abort();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const temperature = parseFloat($temperature.value);
|
|
|
|
|
|
|
|
if (Number.isNaN(temperature) || temperature < 0 || temperature > 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
pushMessage();
|
|
|
|
|
|
|
|
controller = new AbortController();
|
|
|
|
|
|
|
|
$chat.classList.add("completing");
|
|
|
|
|
|
|
|
const body = {
|
2025-08-07 22:09:08 +02:00
|
|
|
prompt: $prompt.value,
|
2025-08-05 03:56:23 +02:00
|
|
|
model: $model.value,
|
|
|
|
temperature: temperature,
|
2025-08-07 22:09:08 +02:00
|
|
|
messages: messages.map((message) => message.getData()),
|
2025-08-05 03:56:23 +02:00
|
|
|
};
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
const message = new Message("assistant", "", "");
|
2025-08-05 03:56:23 +02:00
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
message.setState("waiting");
|
2025-08-05 03:56:23 +02:00
|
|
|
|
|
|
|
stream(
|
|
|
|
"/-/chat",
|
|
|
|
{
|
|
|
|
method: "POST",
|
|
|
|
headers: {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
signal: controller.signal,
|
|
|
|
},
|
|
|
|
(chunk) => {
|
|
|
|
if (!chunk) {
|
|
|
|
controller = null;
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
message.setState(false);
|
2025-08-05 03:56:23 +02:00
|
|
|
|
|
|
|
$chat.classList.remove("completing");
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
console.debug(chunk);
|
2025-08-07 22:09:08 +02:00
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
switch (chunk.type) {
|
|
|
|
case "reason":
|
2025-08-07 22:09:08 +02:00
|
|
|
message.setState("reasoning");
|
|
|
|
message.addReasoning(chunk.text);
|
2025-08-05 03:56:23 +02:00
|
|
|
|
|
|
|
break;
|
|
|
|
case "text":
|
2025-08-07 22:09:08 +02:00
|
|
|
message.setState("receiving");
|
|
|
|
message.addText(chunk.text);
|
2025-08-05 03:56:23 +02:00
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
$message.addEventListener("keydown", (event) => {
|
|
|
|
if (!event.ctrlKey || event.key !== "Enter") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$send.click();
|
|
|
|
});
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
addEventListener("wheel", () => {
|
|
|
|
interacted = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
addEventListener("image-loaded", () => {
|
|
|
|
scroll(!interacted);
|
|
|
|
});
|
|
|
|
|
2025-08-09 22:14:35 +02:00
|
|
|
dropdown($role);
|
|
|
|
dropdown($prompt);
|
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
loadModels().then(restore);
|
|
|
|
})();
|