Drafts survive reload
This commit is contained in:
+49
-18
@@ -3,6 +3,7 @@ import { ago, copyable, errorText, h, replace, showAlert, showConfirm } from "./
|
||||
import { markdown } from "./markdown.js";
|
||||
import { previewURL } from "./preview.js";
|
||||
import { findingCount, reviewCounts } from "./reviews.js";
|
||||
import { composerText, endSend, flushDrafts, forgetLostImages, lostImageCount, setComposerImages, setComposerText, startSend } from "./drafts.js";
|
||||
import { currentSession, findTask, focusTab, focusTask, onEach, pinSnapshot, setFocus, showReview, state, type Topic } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
import { fillTodoLinks } from "./todolink.js";
|
||||
@@ -284,6 +285,10 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
let focusStart = false;
|
||||
const renderComposer = () => {
|
||||
const key = src.key();
|
||||
const old = composer.querySelector("textarea");
|
||||
if (old) {
|
||||
releaseInput(old);
|
||||
}
|
||||
if (!key) {
|
||||
replace(composer);
|
||||
return;
|
||||
@@ -292,7 +297,6 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
const live = sess && sess.state !== "completed" ? sess : undefined;
|
||||
composerModel = live ? live.model || context.get(live.id)?.model || "" : "";
|
||||
const form = composeForm(src, key, live?.id, live?.state ?? "idle", composerModel, live?.vendor, showPending);
|
||||
const old = composer.querySelector("textarea");
|
||||
const caret = old !== null && document.activeElement === old ? [old.selectionStart, old.selectionEnd] : undefined;
|
||||
replace(composer, form);
|
||||
const text = form.querySelector("textarea")!;
|
||||
@@ -557,21 +561,43 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
resizes.disconnect();
|
||||
mutations.disconnect();
|
||||
dropStream();
|
||||
const input = composer.querySelector("textarea");
|
||||
if (input) {
|
||||
releaseInput(input);
|
||||
}
|
||||
flushDrafts();
|
||||
};
|
||||
}
|
||||
|
||||
const composerDrafts = new Map<string, string>();
|
||||
const composerInputs = new Map<string, HTMLTextAreaElement>();
|
||||
|
||||
function keepDraft(input: HTMLTextAreaElement, key: string): () => void {
|
||||
input.value = composerDrafts.get(key) ?? "";
|
||||
input.addEventListener("input", () => {
|
||||
if (input.value) {
|
||||
composerDrafts.set(key, input.value);
|
||||
} else {
|
||||
composerDrafts.delete(key);
|
||||
function releaseInput(input: HTMLTextAreaElement): void {
|
||||
for (const [key, el] of composerInputs) {
|
||||
if (el === input) {
|
||||
composerInputs.delete(key);
|
||||
}
|
||||
});
|
||||
return () => composerDrafts.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function keepDraft(input: HTMLTextAreaElement, key: string): void {
|
||||
composerInputs.set(key, input);
|
||||
input.value = composerText(key);
|
||||
input.addEventListener("input", () => setComposerText(key, input.value));
|
||||
input.addEventListener("blur", flushDrafts);
|
||||
}
|
||||
|
||||
function lostImagesNote(key: string): HTMLElement | null {
|
||||
const n = lostImageCount(key);
|
||||
if (n === 0) {
|
||||
return null;
|
||||
}
|
||||
const dismiss = h("button", { type: "button", class: "remove", title: "Dismiss" }, "×");
|
||||
const note = h("div", { class: "compose-note" }, `${n === 1 ? "1 image" : `${n} images`} from before the reload need re-attaching.`, dismiss);
|
||||
dismiss.onclick = () => {
|
||||
forgetLostImages(key);
|
||||
note.remove();
|
||||
};
|
||||
return note;
|
||||
}
|
||||
|
||||
type PendingImage = { file: Blob; url: string };
|
||||
@@ -585,6 +611,7 @@ function imageTray(form: HTMLFormElement, input: HTMLTextAreaElement, key: strin
|
||||
const el = h("div", { class: "compose-images" });
|
||||
const render = () => {
|
||||
const imgs = imageDrafts.get(key) ?? [];
|
||||
setComposerImages(key, imgs.length);
|
||||
el.hidden = imgs.length === 0;
|
||||
replace(
|
||||
el,
|
||||
@@ -715,7 +742,8 @@ function composeForm(
|
||||
showPending: (text: string, imgs: PendingImage[]) => () => void,
|
||||
): HTMLElement {
|
||||
const text = h("textarea", { placeholder: src.placeholder, rows: 3 });
|
||||
const clearDraft = keepDraft(text, `message:${key}`);
|
||||
const draftKey = `message:${key}`;
|
||||
keepDraft(text, draftKey);
|
||||
const send = h("button", { type: "submit", disabled: !src.canSend(sessionState) }, "Send");
|
||||
const interrupt = h("button", { type: "button", class: "danger", disabled: sessionState !== "running" }, "Interrupt");
|
||||
interrupt.onclick = async () => {
|
||||
@@ -732,26 +760,29 @@ function composeForm(
|
||||
const modelKey = `message:${sessionId ?? key}`;
|
||||
const model = modelSelect(modelKey, currentModel, vendor);
|
||||
const form = h("form", { class: "compose" });
|
||||
const tray = imageTray(form, text, `message:${key}`);
|
||||
form.append(tray.el, text, h("div", { class: "compose-actions" }, model, interrupt, send));
|
||||
const tray = imageTray(form, text, draftKey);
|
||||
form.append(lostImagesNote(draftKey) ?? "", tray.el, text, h("div", { class: "compose-actions" }, model, interrupt, send));
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const value = text.value;
|
||||
if (!value.trim() && !imageDrafts.has(`message:${key}`)) {
|
||||
if (!value.trim() && !imageDrafts.has(draftKey)) {
|
||||
return;
|
||||
}
|
||||
const imgs = tray.take();
|
||||
text.value = "";
|
||||
clearDraft();
|
||||
const sent = startSend(draftKey, value, imgs.length);
|
||||
const cancel = showPending(value, imgs);
|
||||
try {
|
||||
const images = await uploadImages(imgs);
|
||||
await src.send({ key, sessionId, text: value, images, model: model.value !== currentModel ? model.value : undefined, firstModel: model.value });
|
||||
modelChoices.delete(modelKey);
|
||||
endSend(sent, true);
|
||||
form.querySelector(".compose-note")?.remove();
|
||||
} catch (err) {
|
||||
cancel();
|
||||
composerDrafts.set(`message:${key}`, value);
|
||||
text.value = value;
|
||||
endSend(sent, false);
|
||||
const live = composerInputs.get(draftKey) ?? text;
|
||||
live.value = composerText(draftKey);
|
||||
tray.restore(imgs);
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user