2025-08-05 03:56:23 +02:00
|
|
|
(() => {
|
2025-08-11 01:38:16 +02:00
|
|
|
const $version = document.getElementById("version"),
|
|
|
|
$messages = document.getElementById("messages"),
|
2025-08-05 03:56:23 +02:00
|
|
|
$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"),
|
2025-08-10 22:32:40 +02:00
|
|
|
$reasoningEffort = document.getElementById("reasoning-effort"),
|
|
|
|
$reasoningTokens = document.getElementById("reasoning-tokens"),
|
2025-08-11 00:15:58 +02:00
|
|
|
$json = document.getElementById("json"),
|
|
|
|
$search = document.getElementById("search"),
|
2025-08-05 03:56:23 +02:00
|
|
|
$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-10 22:32:40 +02:00
|
|
|
const messages = [],
|
|
|
|
models = {};
|
2025-08-07 22:09:08 +02:00
|
|
|
|
2025-08-10 15:53:30 +02:00
|
|
|
let autoScrolling = false,
|
2025-08-11 00:15:58 +02:00
|
|
|
jsonMode = false,
|
|
|
|
searchTool = false,
|
2025-08-10 15:53:30 +02:00
|
|
|
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
|
|
|
|
2025-08-10 15:53:30 +02:00
|
|
|
setTimeout(() => {
|
|
|
|
$messages.scroll({
|
2025-08-11 00:15:58 +02:00
|
|
|
top: $messages.scrollHeight,
|
2025-08-10 15:53:30 +02:00
|
|
|
behavior: "smooth",
|
|
|
|
});
|
|
|
|
}, 0);
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class Message {
|
|
|
|
#id;
|
|
|
|
#role;
|
|
|
|
#reasoning;
|
|
|
|
#text;
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
#tags = [];
|
|
|
|
#error = false;
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
#editing = false;
|
|
|
|
#expanded = false;
|
|
|
|
#state = false;
|
|
|
|
|
2025-08-10 15:53:30 +02:00
|
|
|
#_diff;
|
|
|
|
#pending = {};
|
|
|
|
#patching = {};
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
#_message;
|
2025-08-11 00:15:58 +02:00
|
|
|
#_tags;
|
2025-08-07 22:09:08 +02:00
|
|
|
#_reasoning;
|
|
|
|
#_text;
|
|
|
|
#_edit;
|
|
|
|
|
|
|
|
constructor(role, reasoning, text) {
|
|
|
|
this.#id = uid();
|
|
|
|
this.#role = role;
|
|
|
|
this.#reasoning = reasoning || "";
|
|
|
|
this.#text = text || "";
|
|
|
|
|
2025-08-10 15:53:30 +02:00
|
|
|
this.#_diff = document.createElement("div");
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
this.#build();
|
|
|
|
this.#render();
|
|
|
|
|
|
|
|
messages.push(this);
|
2025-08-10 15:53:30 +02:00
|
|
|
|
|
|
|
if (this.#reasoning || this.#text) {
|
|
|
|
this.#save();
|
|
|
|
}
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#build() {
|
|
|
|
// main message div
|
|
|
|
this.#_message = make("div", "message", this.#role);
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
// message role (wrapper)
|
|
|
|
const _wrapper = make("div", "role", this.#role);
|
|
|
|
|
|
|
|
this.#_message.appendChild(_wrapper);
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
// message role
|
2025-08-11 00:15:58 +02:00
|
|
|
const _role = make("div");
|
|
|
|
|
|
|
|
_role.textContent = this.#role;
|
|
|
|
|
|
|
|
_wrapper.appendChild(_role);
|
2025-08-07 22:09:08 +02:00
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
// message tags
|
|
|
|
this.#_tags = make("div", "tags");
|
|
|
|
|
|
|
|
_wrapper.appendChild(this.#_tags);
|
2025-08-07 22:09:08 +02:00
|
|
|
|
|
|
|
// 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) {
|
2025-08-11 01:45:40 +02:00
|
|
|
this.#updateReasoningHeight();
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
this.#_message.classList.add("expanded");
|
|
|
|
} else {
|
|
|
|
this.#_message.classList.remove("expanded");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-08-11 01:45:40 +02:00
|
|
|
// message reasoning (height wrapper)
|
|
|
|
const _height = make("div", "reasoning-wrapper");
|
|
|
|
|
|
|
|
_reasoning.appendChild(_height);
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
// 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
|
|
|
|
2025-08-11 01:45:40 +02:00
|
|
|
_height.appendChild(this.#_reasoning);
|
2025-08-07 22:09:08 +02:00
|
|
|
|
|
|
|
// 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-10 15:53:30 +02:00
|
|
|
#handleImages(element) {
|
|
|
|
element.querySelectorAll("img:not(.image)").forEach((img) => {
|
|
|
|
img.classList.add("image");
|
|
|
|
|
|
|
|
img.addEventListener("load", () => {
|
|
|
|
scroll(!interacted);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-08-11 01:45:40 +02:00
|
|
|
#updateReasoningHeight() {
|
|
|
|
this.#_reasoning.parentNode.style.setProperty(
|
|
|
|
"--height",
|
|
|
|
`${this.#_reasoning.scrollHeight}px`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-08-10 15:53:30 +02:00
|
|
|
#patch(name, element, md, after = false) {
|
|
|
|
if (!element.firstChild) {
|
|
|
|
element.innerHTML = render(md);
|
|
|
|
|
|
|
|
this.#handleImages(element);
|
|
|
|
|
|
|
|
after?.();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#pending[name] = md;
|
|
|
|
|
|
|
|
if (this.#patching[name]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#patching[name] = true;
|
|
|
|
|
|
|
|
schedule(() => {
|
|
|
|
const html = render(this.#pending[name]);
|
|
|
|
|
|
|
|
this.#patching[name] = false;
|
|
|
|
|
|
|
|
this.#_diff.innerHTML = html;
|
|
|
|
|
|
|
|
morphdom(element, this.#_diff, {
|
|
|
|
childrenOnly: true,
|
|
|
|
onBeforeElUpdated: (fromEl, toEl) => {
|
|
|
|
return !fromEl.isEqualNode || !fromEl.isEqualNode(toEl);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
this.#_diff.innerHTML = "";
|
|
|
|
|
|
|
|
this.#handleImages(element);
|
|
|
|
|
|
|
|
after?.();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
#render(only = false, noScroll = false) {
|
2025-08-11 00:15:58 +02:00
|
|
|
if (!only || only === "tags") {
|
|
|
|
this.#_tags.innerHTML = this.#tags
|
|
|
|
.map((tag) => `<div class="tag-${tag}" title="${tag}"></div>`)
|
|
|
|
.join("");
|
|
|
|
|
|
|
|
this.#_message.classList.toggle("has-tags", this.#tags.length > 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.#error) {
|
|
|
|
return;
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!only || only === "reasoning") {
|
2025-08-10 15:53:30 +02:00
|
|
|
this.#patch("reasoning", this.#_reasoning, this.#reasoning, () => {
|
2025-08-11 01:45:40 +02:00
|
|
|
this.#updateReasoningHeight();
|
2025-08-07 22:09:08 +02:00
|
|
|
|
2025-08-10 15:53:30 +02:00
|
|
|
noScroll || scroll();
|
|
|
|
});
|
2025-08-09 21:16:24 +02:00
|
|
|
|
2025-08-10 15:53:30 +02:00
|
|
|
this.#_message.classList.toggle("has-reasoning", !!this.#reasoning);
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!only || only === "text") {
|
2025-08-11 00:15:58 +02:00
|
|
|
let text = this.#text;
|
|
|
|
|
2025-08-11 01:17:12 +02:00
|
|
|
if (text && this.#tags.includes("json")) {
|
2025-08-11 00:15:58 +02:00
|
|
|
text = `\`\`\`json\n${text}\n\`\`\``;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#patch("text", this.#_text, text, () => {
|
2025-08-10 15:53:30 +02:00
|
|
|
noScroll || scroll();
|
|
|
|
});
|
2025-08-07 22:09:08 +02:00
|
|
|
|
2025-08-10 15:53:30 +02:00
|
|
|
this.#_message.classList.toggle("has-text", !!this.#text);
|
2025-08-09 21:16:24 +02:00
|
|
|
}
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#save() {
|
2025-08-09 21:16:24 +02:00
|
|
|
storeValue(
|
|
|
|
"messages",
|
2025-08-11 00:15:58 +02:00
|
|
|
messages.map((message) => message.getData(true)).filter(Boolean),
|
2025-08-09 21:16:24 +02:00
|
|
|
);
|
2025-08-07 22:09:08 +02:00
|
|
|
}
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
getData(full = false) {
|
2025-08-07 22:09:08 +02:00
|
|
|
const data = {
|
|
|
|
role: this.#role,
|
|
|
|
text: this.#text,
|
|
|
|
};
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
if (this.#reasoning && full) {
|
2025-08-07 22:09:08 +02:00
|
|
|
data.reasoning = this.#reasoning;
|
|
|
|
}
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
if (this.#error && full) {
|
|
|
|
data.error = this.#error;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.#tags.length && full) {
|
|
|
|
data.tags = this.#tags;
|
|
|
|
}
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
addTag(tag) {
|
|
|
|
if (this.#tags.includes(tag)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#tags.push(tag);
|
|
|
|
|
|
|
|
this.#render("tags");
|
|
|
|
|
|
|
|
if (tag === "json") {
|
|
|
|
this.#render("text");
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#save();
|
|
|
|
}
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
showError(error) {
|
|
|
|
this.#error = error;
|
|
|
|
|
|
|
|
this.#_message.classList.add("errored");
|
|
|
|
|
|
|
|
const _err = make("div", "error");
|
|
|
|
|
|
|
|
_err.textContent = this.#error;
|
|
|
|
|
|
|
|
this.#_text.appendChild(_err);
|
|
|
|
|
|
|
|
this.#save();
|
|
|
|
}
|
|
|
|
|
2025-08-07 22:09:08 +02:00
|
|
|
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) {
|
2025-08-11 00:15:58 +02:00
|
|
|
const err = await response.json();
|
|
|
|
|
|
|
|
throw new Error(err?.error || response.statusText);
|
2025-08-05 03:56:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-08-11 01:38:16 +02:00
|
|
|
async function loadData() {
|
|
|
|
const data = await json("/-/data");
|
2025-08-05 03:56:23 +02:00
|
|
|
|
2025-08-11 01:38:16 +02:00
|
|
|
if (!data) {
|
|
|
|
alert("Failed to load data.");
|
2025-08-05 03:56:23 +02:00
|
|
|
|
2025-08-11 01:38:16 +02:00
|
|
|
return false;
|
2025-08-05 03:56:23 +02:00
|
|
|
}
|
|
|
|
|
2025-08-11 01:38:16 +02:00
|
|
|
// render version
|
2025-08-11 01:38:46 +02:00
|
|
|
if (data.version === "dev") {
|
|
|
|
$version.remove();
|
|
|
|
} else {
|
|
|
|
$version.innerHTML = `<a href="https://github.com/coalaura/whiskr" target="_blank">whiskr</a> <a href="https://github.com/coalaura/whiskr/releases/tag/${data.version}" target="_blank">${data.version}</a>`;
|
|
|
|
}
|
2025-08-11 01:38:16 +02:00
|
|
|
|
|
|
|
// render models
|
2025-08-05 03:56:23 +02:00
|
|
|
$model.innerHTML = "";
|
|
|
|
|
2025-08-11 01:38:16 +02:00
|
|
|
for (const model of data.models) {
|
2025-08-05 03:56:23 +02:00
|
|
|
const el = document.createElement("option");
|
|
|
|
|
|
|
|
el.value = model.id;
|
2025-08-10 16:38:02 +02:00
|
|
|
el.title = model.description;
|
2025-08-05 03:56:23 +02:00
|
|
|
el.textContent = model.name;
|
|
|
|
|
2025-08-10 16:38:02 +02:00
|
|
|
el.dataset.tags = (model.tags || []).join(",");
|
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
$model.appendChild(el);
|
2025-08-10 22:32:40 +02:00
|
|
|
|
|
|
|
models[model.id] = model;
|
2025-08-05 03:56:23 +02:00
|
|
|
}
|
2025-08-09 21:16:24 +02:00
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
dropdown($model, 4);
|
2025-08-09 21:16:24 +02:00
|
|
|
|
2025-08-11 01:38:16 +02:00
|
|
|
return data;
|
2025-08-05 03:56:23 +02:00
|
|
|
}
|
|
|
|
|
2025-08-10 22:32:40 +02:00
|
|
|
function restore(modelList) {
|
2025-08-09 21:16:24 +02:00
|
|
|
$role.value = loadValue("role", "user");
|
2025-08-10 22:32:40 +02:00
|
|
|
$model.value = loadValue("model", modelList[0].id);
|
2025-08-09 21:16:24 +02:00
|
|
|
$prompt.value = loadValue("prompt", "normal");
|
|
|
|
$temperature.value = loadValue("temperature", 0.85);
|
2025-08-10 22:32:40 +02:00
|
|
|
$reasoningEffort.value = loadValue("reasoning-effort", "medium");
|
|
|
|
$reasoningTokens.value = loadValue("reasoning-tokens", 1024);
|
2025-08-05 03:56:23 +02:00
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
if (loadValue("json")) {
|
|
|
|
$json.click();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (loadValue("search")) {
|
|
|
|
$search.click();
|
|
|
|
}
|
|
|
|
|
2025-08-09 21:16:24 +02:00
|
|
|
if (loadValue("scrolling")) {
|
|
|
|
$scrolling.click();
|
|
|
|
}
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
loadValue("messages", []).forEach((message) => {
|
|
|
|
const obj = new Message(message.role, message.reasoning, message.text);
|
|
|
|
|
|
|
|
if (message.error) {
|
|
|
|
obj.showError(message.error);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (message.tags) {
|
|
|
|
message.tags.forEach((tag) => obj.addTag(tag));
|
|
|
|
}
|
|
|
|
});
|
2025-08-09 21:16:24 +02:00
|
|
|
|
|
|
|
scroll(true);
|
2025-08-10 22:36:15 +02:00
|
|
|
|
|
|
|
// small fix, sometimes when hard reloading we don't scroll all the way
|
|
|
|
setTimeout(scroll, 250, 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-10 22:32:40 +02:00
|
|
|
const model = $model.value,
|
|
|
|
data = model ? models[model] : null;
|
|
|
|
|
|
|
|
storeValue("model", model);
|
|
|
|
|
|
|
|
if (data?.tags.includes("reasoning")) {
|
|
|
|
$reasoningEffort.parentNode.classList.remove("none");
|
|
|
|
$reasoningTokens.parentNode.classList.toggle(
|
|
|
|
"none",
|
|
|
|
!!$reasoningEffort.value,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$reasoningEffort.parentNode.classList.add("none");
|
|
|
|
$reasoningTokens.parentNode.classList.add("none");
|
|
|
|
}
|
2025-08-11 00:15:58 +02:00
|
|
|
|
|
|
|
$json.classList.toggle("none", !data?.tags.includes("json"));
|
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-11 00:15:58 +02:00
|
|
|
const value = $temperature.value,
|
|
|
|
temperature = parseFloat(value);
|
|
|
|
|
|
|
|
storeValue("temperature", value);
|
|
|
|
|
|
|
|
$temperature.classList.toggle(
|
|
|
|
"invalid",
|
|
|
|
Number.isNaN(temperature) || temperature < 0 || temperature > 2,
|
|
|
|
);
|
2025-08-05 03:56:23 +02:00
|
|
|
});
|
|
|
|
|
2025-08-10 22:32:40 +02:00
|
|
|
$reasoningEffort.addEventListener("change", () => {
|
|
|
|
const effort = $reasoningEffort.value;
|
|
|
|
|
|
|
|
storeValue("reasoning-effort", effort);
|
|
|
|
|
|
|
|
$reasoningTokens.parentNode.classList.toggle("none", !!effort);
|
|
|
|
});
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
$reasoningTokens.addEventListener("input", () => {
|
|
|
|
const value = $reasoningTokens.value,
|
|
|
|
tokens = parseInt(value);
|
|
|
|
|
|
|
|
storeValue("reasoning-tokens", value);
|
|
|
|
|
|
|
|
$reasoningTokens.classList.toggle(
|
|
|
|
"invalid",
|
|
|
|
Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
$json.addEventListener("click", () => {
|
|
|
|
jsonMode = !jsonMode;
|
|
|
|
|
|
|
|
storeValue("json", jsonMode);
|
|
|
|
|
|
|
|
$json.classList.toggle("on", jsonMode);
|
|
|
|
});
|
|
|
|
|
|
|
|
$search.addEventListener("click", () => {
|
|
|
|
searchTool = !searchTool;
|
|
|
|
|
|
|
|
storeValue("search", searchTool);
|
|
|
|
|
|
|
|
$search.classList.toggle("on", searchTool);
|
2025-08-10 22:32:40 +02:00
|
|
|
});
|
|
|
|
|
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");
|
2025-08-11 00:15:58 +02:00
|
|
|
|
|
|
|
scroll();
|
2025-08-09 21:16:24 +02:00
|
|
|
} 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;
|
|
|
|
}
|
|
|
|
|
2025-08-10 22:32:40 +02:00
|
|
|
if (!$temperature.value) {
|
|
|
|
$temperature.value = 0.85;
|
|
|
|
}
|
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
const temperature = parseFloat($temperature.value);
|
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
if (Number.isNaN(temperature) || temperature < 0 || temperature > 2) {
|
2025-08-05 03:56:23 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-08-10 22:32:40 +02:00
|
|
|
const effort = $reasoningEffort.value,
|
|
|
|
tokens = parseInt($reasoningTokens.value);
|
|
|
|
|
|
|
|
if (
|
|
|
|
!effort &&
|
|
|
|
(Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-08-05 03:56:23 +02:00
|
|
|
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-10 22:32:40 +02:00
|
|
|
reasoning: {
|
|
|
|
effort: effort,
|
|
|
|
tokens: tokens || 0,
|
|
|
|
},
|
2025-08-11 00:15:58 +02:00
|
|
|
json: jsonMode,
|
|
|
|
search: searchTool,
|
2025-08-11 01:22:31 +02:00
|
|
|
messages: messages
|
|
|
|
.map((message) => message.getData())
|
|
|
|
.filter((data) => data?.text),
|
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
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
if (jsonMode) {
|
|
|
|
message.addTag("json");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (searchTool) {
|
|
|
|
message.addTag("search");
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
2025-08-11 00:15:58 +02:00
|
|
|
break;
|
|
|
|
case "error":
|
|
|
|
message.showError(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;
|
|
|
|
});
|
|
|
|
|
2025-08-09 22:14:35 +02:00
|
|
|
dropdown($role);
|
|
|
|
dropdown($prompt);
|
2025-08-10 22:32:40 +02:00
|
|
|
dropdown($reasoningEffort);
|
2025-08-09 22:14:35 +02:00
|
|
|
|
2025-08-11 01:38:16 +02:00
|
|
|
loadData().then((data) => {
|
|
|
|
restore(data?.models || []);
|
|
|
|
|
|
|
|
document.body.classList.remove("loading");
|
|
|
|
});
|
2025-08-05 03:56:23 +02:00
|
|
|
})();
|