mirror of
https://github.com/coalaura/whiskr.git
synced 2025-09-08 00:29:54 +00:00
file attachments
This commit is contained in:
@@ -121,6 +121,7 @@ body.loading #version {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 14px 12px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#messages:empty::before {
|
||||
@@ -307,11 +308,10 @@ body.loading #version {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message .tool,
|
||||
.message:not(.has-tool):not(.has-text) .reasoning,
|
||||
.message:not(.has-tool) .text {
|
||||
.message .body {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message.has-reasoning .text {
|
||||
@@ -535,6 +535,64 @@ body.loading #version {
|
||||
background: #24273a;
|
||||
}
|
||||
|
||||
#chat:has(.has-files) {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
#attachments {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.files {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.files:not(.has-files) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message .files {
|
||||
background: #181926;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.files .file {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
background: #24273a;
|
||||
box-shadow: 0px 0px 10px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 8px 10px;
|
||||
padding-right: 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #363a4f;
|
||||
}
|
||||
|
||||
.files .file::before {
|
||||
content: "";
|
||||
background-image: url(icons/file.svg);
|
||||
}
|
||||
|
||||
.files .file button.remove {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-image: url(icons/remove.svg);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
opacity: 0;
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
.files .file:hover button.remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#message {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
@@ -609,6 +667,8 @@ body.loading #version,
|
||||
.reasoning .toggle::before,
|
||||
.reasoning .toggle::after,
|
||||
#bottom,
|
||||
.files .file::before,
|
||||
.files .file .remove,
|
||||
.message .role::before,
|
||||
.message .tag-json,
|
||||
.message .tag-search,
|
||||
@@ -629,6 +689,7 @@ body.loading #version,
|
||||
#import,
|
||||
#export,
|
||||
#clear,
|
||||
#upload,
|
||||
#add,
|
||||
#send,
|
||||
#chat .option label {
|
||||
@@ -725,14 +786,14 @@ label[for="reasoning-tokens"] {
|
||||
|
||||
#bottom {
|
||||
top: -38px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
right: 20px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-image: url(icons/down.svg);
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
#upload,
|
||||
#add,
|
||||
#send {
|
||||
bottom: 4px;
|
||||
@@ -744,11 +805,15 @@ label[for="reasoning-tokens"] {
|
||||
}
|
||||
|
||||
#add {
|
||||
bottom: 4px;
|
||||
right: 52px;
|
||||
background-image: url(icons/add.svg);
|
||||
}
|
||||
|
||||
#upload {
|
||||
right: 84px;
|
||||
background-image: url(icons/attach.svg);
|
||||
}
|
||||
|
||||
#json,
|
||||
#search,
|
||||
#scrolling,
|
||||
@@ -794,6 +859,7 @@ label[for="reasoning-tokens"] {
|
||||
background-image: url(icons/trash.svg);
|
||||
}
|
||||
|
||||
.completing #upload,
|
||||
.completing #add {
|
||||
display: none;
|
||||
}
|
||||
|
7
static/css/icons/attach.svg
Normal file
7
static/css/icons/attach.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 1.1 KiB |
7
static/css/icons/file.svg
Normal file
7
static/css/icons/file.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 872 B |
7
static/css/icons/remove.svg
Normal file
7
static/css/icons/remove.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 578 B |
@@ -23,8 +23,11 @@
|
||||
<div id="chat">
|
||||
<button id="bottom" class="hidden" title="Scroll to bottom"></button>
|
||||
|
||||
<div id="attachments" class="files"></div>
|
||||
|
||||
<textarea id="message" placeholder="Type something..." autocomplete="off"></textarea>
|
||||
|
||||
<button id="upload" title="Add files to message"></button>
|
||||
<button id="add" title="Add message to chat"></button>
|
||||
<button id="send" title="Add message to chat and start completion"></button>
|
||||
|
||||
|
@@ -4,6 +4,7 @@
|
||||
$chat = document.getElementById("chat"),
|
||||
$message = document.getElementById("message"),
|
||||
$bottom = document.getElementById("bottom"),
|
||||
$attachments = document.getElementById("attachments"),
|
||||
$role = document.getElementById("role"),
|
||||
$model = document.getElementById("model"),
|
||||
$prompt = document.getElementById("prompt"),
|
||||
@@ -12,6 +13,7 @@
|
||||
$reasoningTokens = document.getElementById("reasoning-tokens"),
|
||||
$json = document.getElementById("json"),
|
||||
$search = document.getElementById("search"),
|
||||
$upload = document.getElementById("upload"),
|
||||
$add = document.getElementById("add"),
|
||||
$send = document.getElementById("send"),
|
||||
$scrolling = document.getElementById("scrolling"),
|
||||
@@ -29,10 +31,12 @@
|
||||
modelList = [];
|
||||
|
||||
let autoScrolling = false,
|
||||
searchAvailable = false,
|
||||
jsonMode = false,
|
||||
searchTool = false;
|
||||
|
||||
let searchAvailable = false,
|
||||
activeMessage;
|
||||
|
||||
function scroll(force = false) {
|
||||
if (!autoScrolling && !force) {
|
||||
return;
|
||||
@@ -57,6 +61,7 @@
|
||||
#role;
|
||||
#reasoning;
|
||||
#text;
|
||||
#files = [];
|
||||
|
||||
#tool;
|
||||
#tags = [];
|
||||
@@ -73,13 +78,14 @@
|
||||
|
||||
#_message;
|
||||
#_tags;
|
||||
#_files;
|
||||
#_reasoning;
|
||||
#_text;
|
||||
#_edit;
|
||||
#_tool;
|
||||
#_statistics;
|
||||
|
||||
constructor(role, reasoning, text) {
|
||||
constructor(role, reasoning, text, files = []) {
|
||||
this.#id = uid();
|
||||
this.#role = role;
|
||||
this.#reasoning = reasoning || "";
|
||||
@@ -90,6 +96,10 @@
|
||||
this.#build();
|
||||
this.#render();
|
||||
|
||||
for (const file of files) {
|
||||
this.addFile(file);
|
||||
}
|
||||
|
||||
messages.push(this);
|
||||
|
||||
if (this.#reasoning || this.#text) {
|
||||
@@ -118,10 +128,19 @@
|
||||
|
||||
_wrapper.appendChild(this.#_tags);
|
||||
|
||||
const _body = make("div", "body");
|
||||
|
||||
this.#_message.appendChild(_body);
|
||||
|
||||
// message files
|
||||
this.#_files = make("div", "files");
|
||||
|
||||
_body.appendChild(this.#_files);
|
||||
|
||||
// message reasoning (wrapper)
|
||||
const _reasoning = make("div", "reasoning");
|
||||
|
||||
this.#_message.appendChild(_reasoning);
|
||||
_body.appendChild(_reasoning);
|
||||
|
||||
// message reasoning (toggle)
|
||||
const _toggle = make("button", "toggle");
|
||||
@@ -155,14 +174,14 @@
|
||||
// message content
|
||||
this.#_text = make("div", "text", "markdown");
|
||||
|
||||
this.#_message.appendChild(this.#_text);
|
||||
_body.appendChild(this.#_text);
|
||||
|
||||
// message edit textarea
|
||||
this.#_edit = make("textarea", "text");
|
||||
|
||||
this.#_message.appendChild(this.#_edit);
|
||||
_body.appendChild(this.#_edit);
|
||||
|
||||
this.#_edit.addEventListener("keydown", (event) => {
|
||||
this.#_edit.addEventListener("keydown", event => {
|
||||
if (event.ctrlKey && event.key === "Enter") {
|
||||
this.toggleEdit();
|
||||
} else if (event.key === "Escape") {
|
||||
@@ -175,7 +194,7 @@
|
||||
// message tool
|
||||
this.#_tool = make("div", "tool");
|
||||
|
||||
this.#_message.appendChild(this.#_tool);
|
||||
_body.appendChild(this.#_tool);
|
||||
|
||||
// tool call
|
||||
const _call = make("div", "call");
|
||||
@@ -229,9 +248,7 @@
|
||||
|
||||
// retry option
|
||||
const _assistant = this.#role === "assistant",
|
||||
_retryLabel = _assistant
|
||||
? "Delete message and messages after this one and try again"
|
||||
: "Delete messages after this one and try again";
|
||||
_retryLabel = _assistant ? "Delete message and messages after this one and try again" : "Delete messages after this one and try again";
|
||||
|
||||
const _optRetry = make("button", "retry");
|
||||
|
||||
@@ -299,7 +316,7 @@
|
||||
}
|
||||
|
||||
#handleImages(element) {
|
||||
element.querySelectorAll("img:not(.image)").forEach((img) => {
|
||||
element.querySelectorAll("img:not(.image)").forEach(img => {
|
||||
img.classList.add("image");
|
||||
|
||||
img.addEventListener("load", () => {
|
||||
@@ -309,10 +326,7 @@
|
||||
}
|
||||
|
||||
#updateReasoningHeight() {
|
||||
this.#_reasoning.parentNode.style.setProperty(
|
||||
"--height",
|
||||
`${this.#_reasoning.scrollHeight}px`,
|
||||
);
|
||||
this.#_reasoning.parentNode.style.setProperty("--height", `${this.#_reasoning.scrollHeight}px`);
|
||||
}
|
||||
|
||||
#updateToolHeight() {
|
||||
@@ -368,9 +382,7 @@
|
||||
|
||||
#render(only = false, noScroll = false) {
|
||||
if (!only || only === "tags") {
|
||||
const tags = this.#tags.map(
|
||||
(tag) => `<div class="tag-${tag}" title="${tag}"></div>`,
|
||||
);
|
||||
const tags = this.#tags.map(tag => `<div class="tag-${tag}" title="${tag}"></div>`);
|
||||
|
||||
this.#_tags.innerHTML = tags.join("");
|
||||
|
||||
@@ -409,12 +421,12 @@
|
||||
let html = "";
|
||||
|
||||
if (this.#statistics) {
|
||||
const { provider, ttft, time, input, output } = this.#statistics;
|
||||
const { provider, model, ttft, time, input, output } = this.#statistics;
|
||||
|
||||
const tps = output / (time / 1000);
|
||||
|
||||
html = [
|
||||
provider ? `<div class="provider">${provider}</div>` : "",
|
||||
provider ? `<div class="provider">${provider} (${model.split("/").pop()})</div>` : "",
|
||||
`<div class="ttft">${formatMilliseconds(ttft)}</div>`,
|
||||
`<div class="tps">${fixed(tps, 2)} t/s</div>`,
|
||||
`<div class="tokens">
|
||||
@@ -462,14 +474,15 @@
|
||||
}
|
||||
|
||||
#save() {
|
||||
storeValue(
|
||||
"messages",
|
||||
messages.map((message) => message.getData(true)).filter(Boolean),
|
||||
);
|
||||
storeValue("messages", messages.map(message => message.getData(true)).filter(Boolean));
|
||||
}
|
||||
|
||||
isUser() {
|
||||
return this.#role === "user";
|
||||
}
|
||||
|
||||
index(offset = 0) {
|
||||
const index = messages.findIndex((message) => message.#id === this.#id);
|
||||
const index = messages.findIndex(message => message.#id === this.#id);
|
||||
|
||||
if (index === -1) {
|
||||
return false;
|
||||
@@ -488,6 +501,10 @@
|
||||
text: this.#text,
|
||||
};
|
||||
|
||||
if (this.#files.length) {
|
||||
data.files = this.#files;
|
||||
}
|
||||
|
||||
if (this.#tool) {
|
||||
data.tool = this.#tool;
|
||||
}
|
||||
@@ -557,6 +574,32 @@
|
||||
this.#save();
|
||||
}
|
||||
|
||||
addFile(file) {
|
||||
this.#files.push(file);
|
||||
|
||||
this.#_files.appendChild(
|
||||
buildFileElement(file, el => {
|
||||
const index = this.#files.findIndex(attachment => attachment.id === file.id);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#files.splice(index, 1);
|
||||
|
||||
el.remove();
|
||||
|
||||
this.#_files.classList.toggle("has-files", !!this.#files.length);
|
||||
|
||||
this.#save();
|
||||
})
|
||||
);
|
||||
|
||||
this.#_files.classList.add("has-files");
|
||||
|
||||
this.#save();
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
if (this.#state === state) {
|
||||
return;
|
||||
@@ -626,6 +669,8 @@
|
||||
this.#editing = !this.#editing;
|
||||
|
||||
if (this.#editing) {
|
||||
activeMessage = this;
|
||||
|
||||
this.#_edit.value = this.#text;
|
||||
|
||||
this.#_edit.style.height = `${this.#_text.offsetHeight}px`;
|
||||
@@ -635,6 +680,8 @@
|
||||
|
||||
this.#_edit.focus();
|
||||
} else {
|
||||
activeMessage = null;
|
||||
|
||||
this.#text = this.#_edit.value;
|
||||
|
||||
this.setState(false);
|
||||
@@ -645,7 +692,7 @@
|
||||
}
|
||||
|
||||
delete() {
|
||||
const index = messages.findIndex((msg) => msg.#id === this.#id);
|
||||
const index = messages.findIndex(msg => msg.#id === this.#id);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
@@ -769,10 +816,7 @@
|
||||
const effort = $reasoningEffort.value,
|
||||
tokens = parseInt($reasoningTokens.value);
|
||||
|
||||
if (
|
||||
!effort &&
|
||||
(Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024)
|
||||
) {
|
||||
if (!effort && (Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -792,7 +836,7 @@
|
||||
},
|
||||
json: jsonMode,
|
||||
search: searchTool,
|
||||
messages: messages.map((message) => message.getData()).filter(Boolean),
|
||||
messages: messages.map(message => message.getData()).filter(Boolean),
|
||||
};
|
||||
|
||||
let message, generationID;
|
||||
@@ -836,7 +880,7 @@
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
},
|
||||
(chunk) => {
|
||||
chunk => {
|
||||
if (!chunk) {
|
||||
controller = null;
|
||||
|
||||
@@ -884,7 +928,7 @@
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -905,7 +949,7 @@
|
||||
username: username,
|
||||
password: password,
|
||||
}),
|
||||
}).then((response) => response.json());
|
||||
}).then(response => response.json());
|
||||
|
||||
if (!data?.authenticated) {
|
||||
throw new Error(data.error || "authentication failed");
|
||||
@@ -982,6 +1026,12 @@
|
||||
$reasoningEffort.value = loadValue("reasoning-effort", "medium");
|
||||
$reasoningTokens.value = loadValue("reasoning-tokens", 1024);
|
||||
|
||||
const files = loadValue("attachments", []);
|
||||
|
||||
for (const file of files) {
|
||||
pushAttachment(file);
|
||||
}
|
||||
|
||||
if (loadValue("json")) {
|
||||
$json.click();
|
||||
}
|
||||
@@ -994,15 +1044,15 @@
|
||||
$scrolling.click();
|
||||
}
|
||||
|
||||
loadValue("messages", []).forEach((message) => {
|
||||
const obj = new Message(message.role, message.reasoning, message.text);
|
||||
loadValue("messages", []).forEach(message => {
|
||||
const obj = new Message(message.role, message.reasoning, message.text, message.files || []);
|
||||
|
||||
if (message.error) {
|
||||
obj.showError(message.error);
|
||||
}
|
||||
|
||||
if (message.tags) {
|
||||
message.tags.forEach((tag) => obj.addTag(tag));
|
||||
message.tags.forEach(tag => obj.addTag(tag));
|
||||
}
|
||||
|
||||
if (message.tool) {
|
||||
@@ -1020,6 +1070,75 @@
|
||||
setTimeout(scroll, 250);
|
||||
}
|
||||
|
||||
let attachments = [];
|
||||
|
||||
function buildFileElement(file, callback) {
|
||||
// file wrapper
|
||||
const _file = make("div", "file");
|
||||
|
||||
// file name
|
||||
const _name = make("div", "name");
|
||||
|
||||
_name.title = `FILE ${JSON.stringify(file.name)} LINES ${lines(file.content)}`;
|
||||
_name.textContent = file.name;
|
||||
|
||||
_file.appendChild(_name);
|
||||
|
||||
// remove button
|
||||
const _remove = make("button", "remove");
|
||||
|
||||
_remove.title = "Remove attachment";
|
||||
|
||||
_file.appendChild(_remove);
|
||||
|
||||
_remove.addEventListener("click", () => {
|
||||
callback(_file);
|
||||
});
|
||||
|
||||
return _file;
|
||||
}
|
||||
|
||||
function pushAttachment(file) {
|
||||
file.id = uid();
|
||||
|
||||
if (activeMessage?.isUser()) {
|
||||
activeMessage.addFile(file);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
attachments.push(file);
|
||||
|
||||
storeValue("attachments", attachments);
|
||||
|
||||
$attachments.appendChild(
|
||||
buildFileElement(file, el => {
|
||||
const index = attachments.findIndex(attachment => attachment.id === file.id);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
attachments.splice(index, 1);
|
||||
|
||||
el.remove();
|
||||
|
||||
$attachments.classList.toggle("has-files", !!attachments.length);
|
||||
})
|
||||
);
|
||||
|
||||
$attachments.classList.add("has-files");
|
||||
}
|
||||
|
||||
function clearAttachments() {
|
||||
attachments = [];
|
||||
|
||||
$attachments.innerHTML = "";
|
||||
$attachments.classList.remove("has-files");
|
||||
|
||||
storeValue("attachments", []);
|
||||
}
|
||||
|
||||
function pushMessage() {
|
||||
const text = $message.value.trim();
|
||||
|
||||
@@ -1030,12 +1149,15 @@
|
||||
$message.value = "";
|
||||
storeValue("message", "");
|
||||
|
||||
return new Message($role.value, "", text);
|
||||
const message = new Message($role.value, "", text, attachments);
|
||||
|
||||
clearAttachments();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
$messages.addEventListener("scroll", () => {
|
||||
const bottom =
|
||||
$messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight);
|
||||
const bottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight);
|
||||
|
||||
if (bottom >= 80) {
|
||||
$bottom.classList.remove("hidden");
|
||||
@@ -1061,10 +1183,7 @@
|
||||
|
||||
if (tags.includes("reasoning")) {
|
||||
$reasoningEffort.parentNode.classList.remove("none");
|
||||
$reasoningTokens.parentNode.classList.toggle(
|
||||
"none",
|
||||
!!$reasoningEffort.value,
|
||||
);
|
||||
$reasoningTokens.parentNode.classList.toggle("none", !!$reasoningEffort.value);
|
||||
} else {
|
||||
$reasoningEffort.parentNode.classList.add("none");
|
||||
$reasoningTokens.parentNode.classList.add("none");
|
||||
@@ -1089,10 +1208,7 @@
|
||||
|
||||
storeValue("temperature", value);
|
||||
|
||||
$temperature.classList.toggle(
|
||||
"invalid",
|
||||
Number.isNaN(temperature) || temperature < 0 || temperature > 2,
|
||||
);
|
||||
$temperature.classList.toggle("invalid", Number.isNaN(temperature) || temperature < 0 || temperature > 2);
|
||||
});
|
||||
|
||||
$reasoningEffort.addEventListener("change", () => {
|
||||
@@ -1109,10 +1225,7 @@
|
||||
|
||||
storeValue("reasoning-tokens", value);
|
||||
|
||||
$reasoningTokens.classList.toggle(
|
||||
"invalid",
|
||||
Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024,
|
||||
);
|
||||
$reasoningTokens.classList.toggle("invalid", Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024);
|
||||
});
|
||||
|
||||
$json.addEventListener("click", () => {
|
||||
@@ -1135,6 +1248,38 @@
|
||||
storeValue("message", $message.value);
|
||||
});
|
||||
|
||||
$upload.addEventListener("click", async () => {
|
||||
const file = await selectFile(
|
||||
// the ultimate list
|
||||
".adoc,.bash,.bashrc,.bat,.c,.cc,.cfg,.cjs,.cmd,.conf,.cpp,.cs,.css,.csv,.cxx,.dockerfile,.dockerignore,.editorconfig,.env,.fish,.fs,.fsx,.gitattributes,.gitignore,.go,.gradle,.groovy,.h,.hh,.hpp,.htm,.html,.ini,.ipynb,.java,.jl,.js,.json,.jsonc,.jsx,.kt,.kts,.less,.log,.lua,.m,.makefile,.markdown,.md,.mjs,.mk,.mm,.php,.phtml,.pl,.pm,.profile,.properties,.ps1,.psql,.py,.pyw,.r,.rb,.rs,.rst,.sass,.scala,.scss,.sh,.sql,.svelte,.swift,.t,.toml,.ts,.tsv,.tsx,.txt,.vb,.vue,.xhtml,.xml,.xsd,.xsl,.xslt,.yaml,.yml,.zig,.zsh",
|
||||
false
|
||||
);
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!file.name) {
|
||||
file.name = "unknown.txt";
|
||||
} else if (file.name.length > 512) {
|
||||
throw new Error("File name too long (max 512 characters)");
|
||||
}
|
||||
|
||||
if (typeof file.content !== "string") {
|
||||
throw new Error("File is not a text file");
|
||||
} else if (!file.content) {
|
||||
throw new Error("File is empty");
|
||||
} else if (file.content.length > 4 * 1024 * 1024) {
|
||||
throw new Error("File is too big (max 4MB)");
|
||||
}
|
||||
|
||||
pushAttachment(file);
|
||||
} catch(err) {
|
||||
alert(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
$add.addEventListener("click", () => {
|
||||
pushMessage();
|
||||
});
|
||||
@@ -1150,6 +1295,7 @@
|
||||
$export.addEventListener("click", () => {
|
||||
const data = JSON.stringify({
|
||||
message: $message.value,
|
||||
attachments: attachments,
|
||||
role: $role.value,
|
||||
model: $model.value,
|
||||
prompt: $prompt.value,
|
||||
@@ -1160,7 +1306,7 @@
|
||||
},
|
||||
json: jsonMode,
|
||||
search: searchTool,
|
||||
messages: messages.map((message) => message.getData()).filter(Boolean),
|
||||
messages: messages.map(message => message.getData()).filter(Boolean),
|
||||
});
|
||||
|
||||
download("chat.json", "application/json", data);
|
||||
@@ -1171,7 +1317,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await selectFile("application/json");
|
||||
const file = await selectFile("application/json", true),
|
||||
data = file?.content;
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
@@ -1180,6 +1327,7 @@
|
||||
clearMessages();
|
||||
|
||||
storeValue("message", data.message);
|
||||
storeValue("attachments", data.attachments);
|
||||
storeValue("role", data.role);
|
||||
storeValue("model", data.model);
|
||||
storeValue("prompt", data.prompt);
|
||||
@@ -1221,8 +1369,8 @@
|
||||
await login();
|
||||
|
||||
$authentication.classList.remove("open");
|
||||
} catch(err) {
|
||||
$authError.textContent =`Error: ${err.message}`;
|
||||
} catch (err) {
|
||||
$authError.textContent = `Error: ${err.message}`;
|
||||
$authentication.classList.add("errored");
|
||||
|
||||
$password.value = "";
|
||||
@@ -1239,7 +1387,7 @@
|
||||
$authentication.classList.remove("errored");
|
||||
});
|
||||
|
||||
$message.addEventListener("keydown", (event) => {
|
||||
$message.addEventListener("keydown", event => {
|
||||
if (!event.ctrlKey || event.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
|
@@ -55,10 +55,7 @@ function make(tag, ...classes) {
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function formatMilliseconds(ms) {
|
||||
@@ -101,8 +98,26 @@ function download(name, type, data) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function selectFile(accept) {
|
||||
return new Promise((resolve) => {
|
||||
function lines(text) {
|
||||
let count = 0,
|
||||
index = 0;
|
||||
|
||||
while (index < text.length) {
|
||||
index = text.indexOf("\n", index);
|
||||
|
||||
if (index === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
count++;
|
||||
index++;
|
||||
}
|
||||
|
||||
return count + 1;
|
||||
}
|
||||
|
||||
function selectFile(accept, asJson = false) {
|
||||
return new Promise(resolve => {
|
||||
const input = make("input");
|
||||
|
||||
input.type = "file";
|
||||
@@ -120,13 +135,22 @@ function selectFile(accept) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const data = JSON.parse(reader.result);
|
||||
let content = reader.result;
|
||||
|
||||
resolve(data);
|
||||
} catch {
|
||||
resolve(false);
|
||||
if (asJson) {
|
||||
try {
|
||||
content = JSON.parse(content);
|
||||
} catch {
|
||||
resolve(false);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
name: file.name,
|
||||
content: content,
|
||||
});
|
||||
};
|
||||
|
||||
reader.onerror = () => resolve(false);
|
||||
|
Reference in New Issue
Block a user