diff --git a/chat.html b/chat.html
index 67bbbf0..0f8cf04 100644
--- a/chat.html
+++ b/chat.html
@@ -98,8 +98,7 @@
}
.system-message pre,
- .system-message code,
- .system-message pre {
+ .system-message code:not(pre code) {
font-family: menlo, consolas, monospace;
background: var(--bg-code);
border-radius: 0.4rem;
@@ -195,10 +194,10 @@
}
#model-selector {
+ user-select: none;
position: absolute;
- position-anchor: --submit;
- bottom: anchor(top);
- right: anchor(right);
+ top: 1rem;
+ left: 1rem;
opacity: 0.3;
padding: 0.5rem;
}
@@ -206,6 +205,64 @@
opacity: 1;
}
+ /* History panel (mirrors model selector behavior) */
+ #history-panel {
+ position: absolute;
+ user-select: none;
+ top: 1rem;
+ right: 1rem;
+ opacity: 0.3;
+ padding: 0.5rem;
+ }
+ #history-panel:is(:hover, [open]) {
+ opacity: 1;
+ }
+
+ #history-panel summary {
+ font-weight: bold;
+ }
+
+ .history-item {
+ cursor: pointer;
+ padding: 0.25rem 0.2rem;
+ border-radius: 0.2rem;
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ }
+
+ .history-item:hover {
+ background: rgba(0, 0, 0, 0.05);
+ }
+
+ /* History list classes */
+ .history-list {
+ min-width: 220px;
+ max-width: 320px;
+ padding-top: 0.5rem;
+ }
+
+ .history-none {
+ opacity: 0.7;
+ }
+
+ .history-time {
+ font-weight: 700;
+ }
+ .history-time::after {
+ content: " - ";
+ }
+
+ .history-excerpt {
+ /* margin-left: 6px; */
+ /* display: inline-block; */
+ max-width: 220px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ vertical-align: middle;
+ }
+
#model-selector summary {
font-weight: bold;
}
@@ -290,6 +347,14 @@
const $prompt = document.querySelector("#prompt");
const $submit = document.querySelector("#submit");
const $modelSelector = document.querySelector("#model-selector");
+ const $historyPanel = (function createHistoryPanel() {
+ const details = document.createElement("details");
+ details.id = "history-panel";
+ details.innerHTML = `History`;
+ return details;
+ })();
+ // insert history panel next to model selector
+ $modelSelector.parentNode.appendChild($historyPanel);
// grow the prompt textarea as needed
const minRows = +$prompt.rows;
@@ -305,10 +370,14 @@
// populate model selector
$modelSelector.append(
...models.map((model) => {
- return Object.assign(document.createElement("label"), {
- className: "model-opt__label",
- innerHTML: `
${model.name}
`,
- });
+ const $label = document.createElement("label");
+ $label.className = "model-opt__label";
+ const $input = document.createElement("input");
+ $input.type = "radio";
+ $input.name = "model";
+ $input.value = model.name;
+ $label.append($input, model.name);
+ return $label;
})
);
$modelSelector.addEventListener("mouseenter", (e) => {
@@ -349,6 +418,143 @@
}
$prompt.focus();
+ const STORAGE_KEY = "chat_conversations";
+
+ function todayKey(date = new Date()) {
+ return date.toISOString().slice(0, 10); // YYYY-MM-DD
+ }
+
+ function loadConversations() {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return {};
+ return JSON.parse(raw);
+ } catch (e) {
+ console.error("Failed to load conversations", e);
+ return {};
+ }
+ }
+
+ function saveConversations(obj) {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
+ } catch (e) {
+ console.error("Failed to save conversations", e);
+ }
+ }
+
+ function clearOldConversations() {
+ const all = loadConversations();
+ const today = todayKey();
+ Object.keys(all).forEach((k) => {
+ if (k !== today) delete all[k];
+ });
+ saveConversations(all);
+ }
+
+ function saveConversationNow(messages) {
+ // messages: array of {role, content}
+ const all = loadConversations();
+ const today = todayKey();
+ all[today] ||= [];
+ const ts = new Date();
+ const entry = { time: ts.toISOString(), messages };
+ all[today].unshift(entry);
+ // keep only last 5
+ if (all[today].length > 5) all[today] = all[today].slice(0, 5);
+ saveConversations(all);
+ renderHistoryList();
+ }
+
+ function formatTime(iso) {
+ const d = new Date(iso);
+ return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
+ }
+
+ function renderHistoryList() {
+ // remove existing list
+ const today = todayKey();
+ const all = loadConversations();
+ const $list = document.createElement("div");
+ $list.className = "history-list";
+
+ const items = (all[today] || []).slice(0, 5);
+ if (items.length === 0) {
+ const $none = document.createElement("div");
+ $none.textContent = "No recent conversations today";
+ $none.className = "history-none";
+ $list.appendChild($none);
+ } else {
+ items.forEach((entry, idx) => {
+ const firstUser = entry.messages.find((m) => m.role === "user")?.content || "";
+ const preview = firstUser.trim().replace(/\n/g, " ").slice(0, 30);
+
+ const $line = document.createElement("div");
+ $line.className = "history-item";
+ $line.dataset.idx = idx;
+
+ const $timeSpan = document.createElement("span");
+ $timeSpan.className = "history-time";
+ $timeSpan.textContent = formatTime(entry.time);
+
+ const $previewSpan = document.createElement("span");
+ $previewSpan.className = "history-excerpt";
+ $previewSpan.innerText = preview;
+
+ $line.appendChild($timeSpan);
+ $line.appendChild($previewSpan);
+ $line.addEventListener("click", () => {
+ restoreConversation(entry.messages);
+ $historyPanel.removeAttribute("open");
+ });
+ $list.appendChild($line);
+ });
+ }
+
+ // clear previous children and append
+ $historyPanel.querySelectorAll(".history-list").forEach((n) => n.remove());
+ const wrapper = document.createElement("div");
+ wrapper.className = "history-list";
+ wrapper.appendChild($list);
+ $historyPanel.appendChild(wrapper);
+ }
+
+ function escapeHtml(str) {
+ return str.replace(/[&<>"']/g, function (m) {
+ return {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
+ }[m];
+ });
+ }
+
+ function restoreConversation(messagesArr) {
+ // Clear current messages UI and re-populate
+ messages.length = 0; // clear array
+ // push system prompt back if exists
+ messagesArr.forEach((m) => messages.push(m));
+
+ // re-render messages UI
+ $messages.innerHTML = "";
+ messages
+ .filter((m) => m.role !== "system")
+ .forEach((m) => {
+ const el = createMessageElement(m.content, m.role === "user");
+ if (!(m.role === "user")) {
+ el.innerHTML = renderMarkdown(m.content);
+ }
+ $messages.appendChild(el);
+ });
+ scrollToBottom($messages);
+ }
+
+ // initialize
+ clearOldConversations();
+ renderHistoryList();
+
async function handleSubmit() {
const prompt = $prompt.value.trim();
if (!prompt) return;
@@ -407,6 +613,14 @@
}
} finally {
md.finish();
+ // Save conversation after assistant finishes responding
+ try {
+ // clone messages to avoid mutation issues
+ const clone = messages.map((m) => ({ role: m.role, content: m.content }));
+ saveConversationNow(clone);
+ } catch (e) {
+ console.error("Failed to save conversation", e);
+ }
$submit.textContent = "Send";
$submit.classList.remove("busy");
abortController = null;
@@ -416,9 +630,14 @@
function createMessageElement(content, isUser) {
const $el = document.createElement("div");
$el.className = `message ${isUser ? "user-message" : "system-message"}`;
- if (isUser) {
- $el.innerText = content;
- }
+ $el.innerText = content;
+ // if (isUser) {
+ // } else {
+ // // For system messages we create a pre element so streaming markdown can render into it
+ // const $pre = document.createElement("pre");
+ // $pre.textContent = content;
+ // $el.appendChild($pre);
+ // }
return $el;
}
@@ -618,6 +837,15 @@
}
}
+ function renderMarkdown(markdown) {
+ const $container = document.createElement("div");
+ const renderer = smd.default_renderer($container);
+ const parser = smd.parser(renderer);
+ smd.parser_write(parser, markdown);
+ smd.parser_end(parser);
+ return $container.innerHTML;
+ }
+
class StreamingMarkdown {
constructor($el) {
this.$el = $el;
@@ -682,12 +910,12 @@
$btn.textContent = "Copy";
$btn.addEventListener("click", (e) => {
try {
- const textArea = document.createElement("textarea");
- textArea.value = $code.innerText;
- document.body.appendChild(textArea);
- textArea.select();
+ const $textArea = document.createElement("textarea");
+ $textArea.value = $code.innerText;
+ document.body.appendChild($textArea);
+ $textArea.select();
document.execCommand("copy");
- document.body.removeChild(textArea);
+ document.body.removeChild($textArea);
$btn.textContent = "Copied!";
setTimeout(() => ($btn.textContent = "Copy"), 1200);