next: ai part 2

This commit is contained in:
nym21
2026-07-22 16:50:27 +02:00
parent 227a31f64e
commit 52e4db5ea6
18 changed files with 1318 additions and 1281 deletions
+79
View File
@@ -0,0 +1,79 @@
import {
createAskMessage,
setMessageContent,
} from "./message/index.js";
/** @typedef {import("../storage.js").StoredMessage} StoredMessage */
/**
* @typedef {Object} AskConversationOptions
* @property {HTMLElement} hero
* @property {() => void} onScroll
*/
/** @param {AskConversationOptions} options */
export function createAskConversation(options) {
const conversation = document.createElement("section");
const transcript = document.createElement("ol");
let scrollFrame = 0;
conversation.dataset.askConversation = "";
conversation.setAttribute("aria-label", "Conversation");
transcript.ariaLive = "polite";
transcript.hidden = true;
conversation.append(options.hero, transcript);
conversation.addEventListener("scroll", options.onScroll);
function sync() {
const hasMessages = transcript.childElementCount > 0;
options.hero.hidden = hasMessages;
transcript.hidden = !hasMessages;
}
return {
element: conversation,
sync,
isNearBottom() {
const { clientHeight, scrollHeight, scrollTop } = conversation;
return scrollHeight - clientHeight - scrollTop < 96;
},
cancelScroll() {
cancelAnimationFrame(scrollFrame);
},
scrollToBottom() {
cancelAnimationFrame(scrollFrame);
scrollFrame = requestAnimationFrame(() => {
conversation.scrollTop = conversation.scrollHeight;
});
},
/**
* @param {"user" | "assistant"} role
* @param {string} text
*/
append(role, text) {
const message = createAskMessage(role, text);
transcript.append(message.item);
sync();
return message;
},
/**
* @param {HTMLElement} content
* @param {"user" | "assistant"} role
* @param {string} text
*/
setContent(content, role, text) {
setMessageContent(content, role, text);
},
/** @param {StoredMessage[]} messages */
render(messages) {
transcript.replaceChildren(
...messages.map((message) =>
createAskMessage(message.role, message.content).item
),
);
sync();
},
};
}
@@ -0,0 +1,30 @@
import { renderMarkdown } from "../../markdown.js";
/** @typedef {"user" | "assistant"} MessageRole */
/**
* @param {HTMLElement} content
* @param {MessageRole} role
* @param {string} text
*/
export function setMessageContent(content, role, text) {
if (role === "assistant") renderMarkdown(content, text);
else content.textContent = text;
}
/**
* @param {MessageRole} role
* @param {string} text
*/
export function createAskMessage(role, text) {
const item = document.createElement("li");
const label = document.createElement("strong");
const content = document.createElement("div");
item.dataset.role = role;
label.append(role === "user" ? "You" : "Assistant");
setMessageContent(content, role, text);
item.append(label, content);
return { item, content };
}
@@ -0,0 +1,119 @@
main[data-page="ask"] [data-ask-conversation] > ol > li {
display: grid;
gap: 0.35rem;
max-width: 100%;
&[data-role="user"] {
justify-self: end;
max-width: min(82%, 34rem);
padding: 0.5rem 0.75rem;
border-radius: 0.65rem 0.65rem 0.15rem 0.65rem;
color: var(--black);
background: var(--white);
}
> strong {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
> div {
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
white-space: pre-wrap;
overflow-wrap: anywhere;
> * {
white-space: normal;
}
> * + * {
margin-top: 0.75rem;
}
h1,
h2,
h3,
h4 {
color: var(--white);
font-weight: var(--font-weight-regular);
line-height: 1.1;
}
h1,
h2 {
font-size: var(--font-size-lg);
}
h3,
h4 {
font-size: var(--font-size-base);
}
:is(ul, ol) {
display: grid;
gap: 0.4rem;
padding-left: 1.35rem;
}
li::marker {
color: var(--orange);
}
strong {
font-weight: 600;
}
a {
color: var(--white);
text-decoration-color: var(--orange);
text-decoration-thickness: 1px;
text-underline-offset: 0.15em;
}
:not(pre) > code {
border: 1px solid var(--ask-border);
border-radius: 0.2rem;
padding: 0.08em 0.28em;
color: color-mix(in oklch, var(--orange) 72%, var(--white));
background: var(--ask-panel);
font-size: 0.9em;
}
pre {
max-width: 100%;
border: 1px solid var(--ask-border);
border-radius: var(--control-radius);
padding: 0.85rem 1rem;
background: var(--ask-panel);
overflow-x: auto;
white-space: pre;
code {
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
}
}
blockquote {
border-left: 1px solid var(--orange);
padding-left: 0.85rem;
color: color-mix(in oklch, var(--white) 72%, transparent);
}
}
}
@media (max-width: 48rem) {
main[data-page="ask"]
[data-ask-conversation]
> ol
> li[data-role="user"] {
max-width: 90%;
}
}
+35
View File
@@ -0,0 +1,35 @@
main[data-page="ask"] [data-ask-conversation] {
height: 100%;
overflow-y: auto;
overscroll-behavior: contain;
scroll-padding-block: 1.5rem 7rem;
scrollbar-gutter: stable;
mask-image: linear-gradient(
to bottom,
transparent,
black 0.85rem,
black calc(100% - 6rem),
transparent
);
> ol {
display: grid;
align-content: start;
gap: 1.5rem;
width: min(100% - 3rem, var(--ask-content-width));
margin: 0 auto;
padding: 1.5rem 0 7.5rem;
list-style: none;
}
}
@media (max-width: 48rem) {
main[data-page="ask"] [data-ask-conversation] {
scroll-padding-top: 5.75rem;
> ol {
width: min(100% - 2rem, var(--ask-content-width));
padding-top: 5.75rem;
}
}
}