1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-09-09 01:09:54 +00:00

morphdom & better images

This commit is contained in:
Laura
2025-08-10 15:53:30 +02:00
parent 8996173b3f
commit 960736e161
7 changed files with 96 additions and 78 deletions

View File

@@ -52,7 +52,7 @@ body {
margin: auto; margin: auto;
margin-top: 30px; margin-top: 30px;
width: 100%; width: 100%;
max-width: 1100px; max-width: 1200px;
height: calc(100% - 30px); height: calc(100% - 30px);
border-top-left-radius: 6px; border-top-left-radius: 6px;
border-top-right-radius: 6px; border-top-right-radius: 6px;

View File

@@ -70,14 +70,10 @@
text-decoration-color: rgba(183, 189, 248, 0.6); text-decoration-color: rgba(183, 189, 248, 0.6);
} }
.markdown .image { .markdown img {
max-width: 100%; max-width: 100%;
border-radius: 6px; border-radius: 6px;
margin: 12px auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
background-position: center;
background-size: cover;
background-repeat: no-repeat;
} }
.markdown blockquote { .markdown blockquote {

View File

@@ -62,6 +62,7 @@
<script src="lib/highlight.min.js"></script> <script src="lib/highlight.min.js"></script>
<script src="lib/marked.min.js"></script> <script src="lib/marked.min.js"></script>
<script src="lib/morphdom.min.js"></script>
<script src="js/lib.js"></script> <script src="js/lib.js"></script>
<script src="js/markdown.js"></script> <script src="js/markdown.js"></script>
<script src="js/dropdown.js"></script> <script src="js/dropdown.js"></script>

View File

@@ -14,17 +14,20 @@
const messages = []; const messages = [];
let autoScrolling = false, interacted = false; let autoScrolling = false,
interacted = false;
function scroll(force = false) { function scroll(force = false) {
if (!autoScrolling && !force) { if (!autoScrolling && !force) {
return; return;
} }
$messages.scroll({ setTimeout(() => {
top: $messages.scrollHeight + 200, $messages.scroll({
behavior: "smooth", top: $messages.scrollHeight + 200,
}); behavior: "smooth",
});
}, 0);
} }
class Message { class Message {
@@ -37,6 +40,10 @@
#expanded = false; #expanded = false;
#state = false; #state = false;
#_diff;
#pending = {};
#patching = {};
#_message; #_message;
#_role; #_role;
#_reasoning; #_reasoning;
@@ -49,10 +56,16 @@
this.#reasoning = reasoning || ""; this.#reasoning = reasoning || "";
this.#text = text || ""; this.#text = text || "";
this.#_diff = document.createElement("div");
this.#build(); this.#build();
this.#render(); this.#render();
messages.push(this); messages.push(this);
if (this.#reasoning || this.#text) {
this.#save();
}
} }
#build() { #build() {
@@ -165,38 +178,81 @@
scroll(); scroll();
} }
#handleImages(element) {
element.querySelectorAll("img:not(.image)").forEach((img) => {
img.classList.add("image");
img.addEventListener("load", () => {
scroll(!interacted);
});
});
}
#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?.();
});
}
#render(only = false, noScroll = false) { #render(only = false, noScroll = false) {
if (!only || only === "role") { if (!only || only === "role") {
this.#_role.textContent = this.#role; this.#_role.textContent = this.#role;
} }
if (!only || only === "reasoning") { if (!only || only === "reasoning") {
this.#_reasoning.innerHTML = render(this.#reasoning); this.#patch("reasoning", this.#_reasoning, this.#reasoning, () => {
this.#_reasoning.style.setProperty(
"--height",
`${this.#_reasoning.scrollHeight}px`,
);
if (this.#reasoning) { noScroll || scroll();
this.#_message.classList.add("has-reasoning"); });
} else {
this.#_message.classList.remove("has-reasoning");
}
this.#_reasoning.style.setProperty( this.#_message.classList.toggle("has-reasoning", !!this.#reasoning);
"--height",
`${this.#_reasoning.scrollHeight}px`,
);
} }
if (!only || only === "text") { if (!only || only === "text") {
this.#_text.innerHTML = render(this.#text); this.#patch("text", this.#_text, this.#text, () => {
noScroll || scroll();
});
if (this.#text) { this.#_message.classList.toggle("has-text", !!this.#text);
this.#_message.classList.add("has-text");
} else {
this.#_message.classList.remove("has-text");
}
}
if (!noScroll) {
scroll();
} }
} }
@@ -555,8 +611,6 @@
return; return;
} }
console.debug(chunk);
switch (chunk.type) { switch (chunk.type) {
case "reason": case "reason":
message.setState("reasoning"); message.setState("reasoning");
@@ -585,10 +639,6 @@
interacted = true; interacted = true;
}); });
addEventListener("image-loaded", () => {
scroll(!interacted);
});
dropdown($role); dropdown($role);
dropdown($prompt); dropdown($prompt);

View File

@@ -28,6 +28,16 @@ function loadValue(key, fallback = false) {
return fallback; return fallback;
} }
function schedule(cb) {
if (document.visibilityState === "visible") {
requestAnimationFrame(cb);
return;
}
setTimeout(cb, 80);
}
function uid() { function uid() {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
} }

View File

@@ -1,6 +1,5 @@
(() => { (() => {
const timeouts = new WeakMap(), const timeouts = new WeakMap();
images = {};
marked.use({ marked.use({
async: false, async: false,
@@ -36,48 +35,9 @@
return `<pre>${header}<code>${code.text}</code></pre>`; return `<pre>${header}<code>${code.text}</code></pre>`;
}, },
image(image) {
const { href } = image;
const id = `i_${btoa(href).replace(/=/g, "")}`,
style = prepareImage(id, href) || "";
return `<div class="image ${id}" style="${style}"></div>`;
},
}, },
}); });
function prepareImage(id, href) {
if (href in images) {
return images[href];
}
images[href] = false;
const image = new Image();
image.addEventListener("load", () => {
const style = `aspect-ratio:${image.naturalWidth}/${image.naturalHeight};width:${image.naturalWidth}px;background-image:url(${href})`;
images[href] = style;
document.querySelectorAll(`.image.${id}`).forEach((img) => {
img.setAttribute("style", style);
});
window.dispatchEvent(new Event("image-loaded"));
});
image.addEventListener("error", () => {
console.error(`Failed to load image: ${href}`);
});
image.src = href;
return false;
}
document.body.addEventListener("click", (event) => { document.body.addEventListener("click", (event) => {
const button = event.target, const button = event.target,
header = button.closest(".pre-header"), header = button.closest(".pre-header"),

1
static/lib/morphdom.min.js vendored Normal file

File diff suppressed because one or more lines are too long