Drafts survive reload
This commit is contained in:
Vendored
+2
@@ -1,5 +1,6 @@
|
||||
import { get, put, subscribe } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { loadDrafts } from "./drafts.js";
|
||||
import { leadPane, taskPane } from "./panes.js";
|
||||
import { handleEvent, loadFocus, loadInbox, loadLeadSessions, loadMe, loadProjects, notify, on, selectTask, setFocus, startLeadStream, state, tabs } from "./state.js";
|
||||
import { bindShortcuts } from "./switcher.js";
|
||||
@@ -51,6 +52,7 @@ async function main() {
|
||||
const banner = h("div", { class: "banner", hidden: true });
|
||||
const status = await get("/api/admin/status");
|
||||
await Promise.all([loadMe(), loadProjects(), loadInbox(), loadLeadSessions()]);
|
||||
loadDrafts();
|
||||
const linked = location.hash;
|
||||
await loadFocus();
|
||||
if (/^#\/(task|project)\/\w+(\/\w+)?$/.test(linked)) {
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvK,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,SAAS,cAAc,CAAC,IAAiB;IACvC,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;YACzC,OAAO,CAAC,IAAI,CAAC,CAAC;YACd,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,CAAC,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7F,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACtF,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,qCAAqC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QAC9I,IAAI,CAAC,QAAQ,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE;YAC1B,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,CAAC;gBACH,KAAK,CAAC,EAAE,GAAG,MAAM,GAAG,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC5F,MAAM,EAAE,CAAC;YACX,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,KAAK,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;YACjC,CAAC;QACH,CAAC,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACtB,CAAC,CAAC;IACF,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACjB,MAAM,EAAE,CAAC;AACX,CAAC;AAED,SAAS,KAAK;IACZ,MAAM,KAAK,GAAG,wCAAwC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5D,MAAM,SAAS,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAClE,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,MAAM,KAAK,KAAK,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACvE,QAAQ,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;SAAM,IAAI,KAAK,CAAC,KAAK,CAAC,OAAO,IAAI,SAAS,KAAK,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;QACvE,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;IACtH,CAAC;IACD,IAAI,MAAM,KAAK,KAAK,CAAC,cAAc,EAAE,CAAC;QACpC,UAAU,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3D,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC9C,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC;IAC/E,UAAU,EAAE,CAAC;IACb,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC7B,MAAM,SAAS,EAAE,CAAC;IAClB,IAAI,kCAAkC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACpD,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC;IACzB,CAAC;IACD,eAAe,EAAE,CAAC;IAElB,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IACrF,aAAa,EAAE,CAAC;IAChB,cAAc,CAAC,MAAM,CAAC,CAAC;IAEvB,gBAAgB,CACd,SAAS,EACT,CAAC,CAAC,EAAE,EAAE;QACJ,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QACD,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,QAAQ,CAAC,aAAa,CAAoB,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC;IACrE,CAAC,EACD,EAAE,OAAO,EAAE,IAAI,EAAE,CAClB,CAAC;IAEF,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,SAAS,EAAE,EAAE;QAC9D,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC;QAC5B,MAAM,CAAC,YAAY,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,gBAAgB,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACtC,KAAK,EAAE,CAAC;AACV,CAAC;AAED,KAAK,UAAU,KAAK;IAClB,IAAI,eAAe,IAAI,SAAS,EAAE,CAAC;QACjC,KAAK,MAAM,GAAG,IAAI,MAAM,SAAS,CAAC,aAAa,CAAC,gBAAgB,EAAE,EAAE,CAAC;YACnE,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC;QACzB,CAAC;QACD,IAAI,SAAS,CAAC,aAAa,CAAC,UAAU,EAAE,CAAC;YACvC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;IACH,CAAC;IACD,MAAM,IAAI,EAAE,CAAC;AACf,CAAC;AAED,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC7B,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,yBAAyB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAClG,CAAC,CAAC,CAAC"}
|
||||
Vendored
+187
@@ -0,0 +1,187 @@
|
||||
import { findTask, onEach, state } from "./state.js";
|
||||
const composerTexts = new Map();
|
||||
const composerImages = new Map();
|
||||
const sending = new Set();
|
||||
const lostImages = new Map();
|
||||
const checked = new Map();
|
||||
const texts = new Map();
|
||||
let storeKey;
|
||||
let timer = 0;
|
||||
export function loadDrafts() {
|
||||
storeKey = `byre.drafts.${state.me.login}`;
|
||||
const saved = readSaved(storeKey);
|
||||
for (const [key, d] of Object.entries(saved.composer)) {
|
||||
if (d.text) {
|
||||
composerTexts.set(key, d.text);
|
||||
}
|
||||
if (d.images > 0) {
|
||||
lostImages.set(key, d.images);
|
||||
}
|
||||
}
|
||||
for (const [id, opts] of Object.entries(saved.checked)) {
|
||||
checked.set(id, opts);
|
||||
}
|
||||
for (const [id, text] of Object.entries(saved.texts)) {
|
||||
texts.set(id, text);
|
||||
}
|
||||
prune();
|
||||
onEach(["tasks", "inbox"], prune);
|
||||
addEventListener("pagehide", flushDrafts);
|
||||
}
|
||||
function readSaved(key) {
|
||||
const out = { composer: {}, checked: {}, texts: {} };
|
||||
let raw;
|
||||
try {
|
||||
raw = JSON.parse(localStorage.getItem(key) ?? "{}");
|
||||
}
|
||||
catch (err) {
|
||||
console.error("read drafts", err);
|
||||
return out;
|
||||
}
|
||||
if (!isRecord(raw)) {
|
||||
return out;
|
||||
}
|
||||
for (const [k, v] of Object.entries(isRecord(raw["composer"]) ? raw["composer"] : {})) {
|
||||
if (isRecord(v) && typeof v["text"] === "string" && typeof v["images"] === "number") {
|
||||
out.composer[k] = { text: v["text"], images: v["images"] };
|
||||
}
|
||||
}
|
||||
for (const [k, v] of Object.entries(isRecord(raw["checked"]) ? raw["checked"] : {})) {
|
||||
if (Array.isArray(v) && v.every((o) => typeof o === "string")) {
|
||||
out.checked[k] = v;
|
||||
}
|
||||
}
|
||||
for (const [k, v] of Object.entries(isRecord(raw["texts"]) ? raw["texts"] : {})) {
|
||||
if (typeof v === "string") {
|
||||
out.texts[k] = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function isRecord(v) {
|
||||
return typeof v === "object" && v !== null && !Array.isArray(v);
|
||||
}
|
||||
function prune() {
|
||||
let changed = false;
|
||||
for (const map of [composerTexts, composerImages, lostImages]) {
|
||||
for (const key of map.keys()) {
|
||||
const taskId = key.replace(/^message:/, "");
|
||||
if (taskId !== "lead" && findTask(taskId)?.status !== "active") {
|
||||
map.delete(key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const map of [checked, texts]) {
|
||||
for (const id of map.keys()) {
|
||||
if (!state.inbox.some((i) => i.id === id)) {
|
||||
map.delete(id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(flushDrafts, 400);
|
||||
}
|
||||
export function flushDrafts() {
|
||||
clearTimeout(timer);
|
||||
timer = 0;
|
||||
if (!storeKey) {
|
||||
return;
|
||||
}
|
||||
const out = { composer: {}, checked: Object.fromEntries(checked), texts: Object.fromEntries(texts) };
|
||||
const keys = new Set([...composerTexts.keys(), ...composerImages.keys(), ...[...sending].map((s) => s.key), ...lostImages.keys()]);
|
||||
for (const key of keys) {
|
||||
const inFlight = [...sending].filter((s) => s.key === key);
|
||||
const text = [...inFlight.map((s) => s.text), composerTexts.get(key) ?? ""].filter((t) => t).join("\n\n");
|
||||
const images = (composerImages.get(key) ?? 0) + inFlight.reduce((n, s) => n + s.images, 0) + (lostImages.get(key) ?? 0);
|
||||
if (text || images > 0) {
|
||||
out.composer[key] = { text, images };
|
||||
}
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(storeKey, JSON.stringify(out));
|
||||
}
|
||||
catch (err) {
|
||||
console.error("save drafts", err);
|
||||
}
|
||||
}
|
||||
export function composerText(key) {
|
||||
return composerTexts.get(key) ?? "";
|
||||
}
|
||||
export function setComposerText(key, text) {
|
||||
if (text) {
|
||||
composerTexts.set(key, text);
|
||||
}
|
||||
else {
|
||||
composerTexts.delete(key);
|
||||
}
|
||||
save();
|
||||
}
|
||||
export function setComposerImages(key, n) {
|
||||
if (n > 0) {
|
||||
composerImages.set(key, n);
|
||||
}
|
||||
else {
|
||||
composerImages.delete(key);
|
||||
}
|
||||
save();
|
||||
}
|
||||
export function lostImageCount(key) {
|
||||
return lostImages.get(key) ?? 0;
|
||||
}
|
||||
export function forgetLostImages(key) {
|
||||
lostImages.delete(key);
|
||||
save();
|
||||
}
|
||||
export function startSend(key, text, images) {
|
||||
const send = { key, text, images };
|
||||
sending.add(send);
|
||||
composerTexts.delete(key);
|
||||
return send;
|
||||
}
|
||||
export function endSend(send, ok) {
|
||||
sending.delete(send);
|
||||
if (ok) {
|
||||
lostImages.delete(send.key);
|
||||
}
|
||||
else {
|
||||
const text = [send.text, composerTexts.get(send.key) ?? ""].filter((t) => t).join("\n\n");
|
||||
if (text) {
|
||||
composerTexts.set(send.key, text);
|
||||
}
|
||||
}
|
||||
flushDrafts();
|
||||
}
|
||||
export function checkedAnswer(id) {
|
||||
return checked.get(id) ?? [];
|
||||
}
|
||||
export function setCheckedAnswer(id, options) {
|
||||
checked.set(id, options);
|
||||
save();
|
||||
}
|
||||
export function hasTextAnswer(id) {
|
||||
return texts.has(id);
|
||||
}
|
||||
export function textAnswer(id) {
|
||||
return texts.get(id);
|
||||
}
|
||||
export function setTextAnswer(id, text) {
|
||||
texts.set(id, text);
|
||||
save();
|
||||
}
|
||||
export function dropTextAnswer(id) {
|
||||
texts.delete(id);
|
||||
save();
|
||||
}
|
||||
export function clearAnswer(id) {
|
||||
checked.delete(id);
|
||||
texts.delete(id);
|
||||
flushDrafts();
|
||||
}
|
||||
//# sourceMappingURL=drafts.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+16
-24
@@ -2,12 +2,11 @@ import { post } from "./api.js";
|
||||
import { mountChanges } from "./changes.js";
|
||||
import { ago, errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { image, inlineMarkdown, markdown } from "./markdown.js";
|
||||
import { checkedAnswer, clearAnswer, dropTextAnswer, flushDrafts, hasTextAnswer, setCheckedAnswer, setTextAnswer, textAnswer } from "./drafts.js";
|
||||
import { diagram } from "./mermaid.js";
|
||||
import { reviewCard } from "./reviews.js";
|
||||
import { focusTab, onEach, state } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
const checked = new Map();
|
||||
const texts = new Map();
|
||||
export function mountInbox(root) {
|
||||
const list = h("div", { class: "inbox-list" });
|
||||
const cards = new Map();
|
||||
@@ -23,13 +22,6 @@ export function mountInbox(root) {
|
||||
cards.delete(id);
|
||||
}
|
||||
}
|
||||
for (const draft of [checked, texts]) {
|
||||
for (const id of draft.keys()) {
|
||||
if (!state.inbox.some((i) => i.id === id)) {
|
||||
draft.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
const cardFor = (item) => {
|
||||
const sig = JSON.stringify(item);
|
||||
const cached = cards.get(item.id);
|
||||
@@ -105,8 +97,7 @@ function card(item) {
|
||||
lock(true);
|
||||
try {
|
||||
await post("/api/inbox/responses", { body: { responses: [response] } });
|
||||
checked.delete(item.id);
|
||||
texts.delete(item.id);
|
||||
clearAnswer(item.id);
|
||||
const others = state.inbox.some((i) => i.task_id === item.task_id && i.id !== item.id);
|
||||
if (!others && state.focus.tab === "inbox" && state.selectedTaskId === item.task_id) {
|
||||
focusTab("transcript");
|
||||
@@ -136,24 +127,24 @@ function card(item) {
|
||||
void submit({ item_id: item.id, type: "option", options: [text] });
|
||||
});
|
||||
}
|
||||
const input = h("input", { type: "checkbox", checked: (checked.get(item.id) ?? []).includes(text) });
|
||||
const input = h("input", { type: "checkbox", checked: checkedAnswer(item.id).includes(text) });
|
||||
input.onchange = () => {
|
||||
const now = (checked.get(item.id) ?? []).filter((o) => o !== text);
|
||||
const now = checkedAnswer(item.id).filter((o) => o !== text);
|
||||
if (input.checked) {
|
||||
now.push(text);
|
||||
}
|
||||
checked.set(item.id, options.filter((o) => now.includes(o)));
|
||||
setCheckedAnswer(item.id, options.filter((o) => now.includes(o)));
|
||||
renderActions();
|
||||
};
|
||||
return h("label", { class: rec ? "option choice-box rec" : "option choice-box" }, h("div", { class: "choice-head" }, input, name, tag));
|
||||
};
|
||||
const textEntry = (label, placeholder, shared) => {
|
||||
const open = texts.has(item.id);
|
||||
const text = h("textarea", { rows: 3, placeholder, value: texts.get(item.id) ?? "" });
|
||||
const open = hasTextAnswer(item.id);
|
||||
const text = h("textarea", { rows: 3, placeholder, value: textAnswer(item.id) ?? "" });
|
||||
const send = h("button", { type: "button", class: "primary", disabled: !text.value.trim() }, "Submit");
|
||||
const editor = h("div", { class: "text-editor", hidden: !open }, text, !shared && h("div", { class: "submit-row" }, send));
|
||||
const block = clickable(h("div", { class: "pick other", role: "button", tabIndex: 0, hidden: open }, label), () => {
|
||||
texts.set(item.id, "");
|
||||
setTextAnswer(item.id, "");
|
||||
block.hidden = true;
|
||||
editor.hidden = false;
|
||||
text.focus();
|
||||
@@ -169,17 +160,18 @@ function card(item) {
|
||||
};
|
||||
send.onclick = go;
|
||||
text.oninput = () => {
|
||||
texts.set(item.id, text.value);
|
||||
setTextAnswer(item.id, text.value);
|
||||
send.disabled = !text.value.trim();
|
||||
shared?.onInput();
|
||||
};
|
||||
text.onblur = () => {
|
||||
if (!text.value.trim() && !busy()) {
|
||||
texts.delete(item.id);
|
||||
dropTextAnswer(item.id);
|
||||
editor.hidden = true;
|
||||
block.hidden = false;
|
||||
shared?.onInput();
|
||||
}
|
||||
flushDrafts();
|
||||
};
|
||||
text.onkeydown = (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
||||
@@ -198,18 +190,18 @@ function card(item) {
|
||||
return;
|
||||
}
|
||||
const send = h("button", { type: "button", class: "primary" }, "Submit");
|
||||
const typed = () => (texts.get(item.id) ?? "").trim();
|
||||
const typed = () => (textAnswer(item.id) ?? "").trim();
|
||||
const refresh = () => {
|
||||
send.disabled = !typed() && (checked.get(item.id) ?? []).length === 0;
|
||||
send.disabled = !typed() && checkedAnswer(item.id).length === 0;
|
||||
};
|
||||
const go = () => {
|
||||
const options = checked.get(item.id) ?? [];
|
||||
const options = checkedAnswer(item.id);
|
||||
if (options.length > 0) {
|
||||
void submit({ item_id: item.id, type: "option", options, text: typed() ? texts.get(item.id) : undefined });
|
||||
void submit({ item_id: item.id, type: "option", options, text: typed() ? textAnswer(item.id) : undefined });
|
||||
return;
|
||||
}
|
||||
if (typed()) {
|
||||
void submit({ item_id: item.id, type: "other", text: texts.get(item.id) });
|
||||
void submit({ item_id: item.id, type: "other", text: textAnswer(item.id) });
|
||||
}
|
||||
};
|
||||
const other = textEntry("Other…", placeholder, { onInput: refresh, onSubmit: go });
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+47
-19
@@ -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 } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
import { fillTodoLinks } from "./todolink.js";
|
||||
@@ -213,6 +214,10 @@ export function mountChat(root, src, makeHead) {
|
||||
let focusStart = false;
|
||||
const renderComposer = () => {
|
||||
const key = src.key();
|
||||
const old = composer.querySelector("textarea");
|
||||
if (old) {
|
||||
releaseInput(old);
|
||||
}
|
||||
if (!key) {
|
||||
replace(composer);
|
||||
return;
|
||||
@@ -221,7 +226,6 @@ export function mountChat(root, src, makeHead) {
|
||||
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");
|
||||
@@ -473,20 +477,39 @@ export function mountChat(root, src, makeHead) {
|
||||
resizes.disconnect();
|
||||
mutations.disconnect();
|
||||
dropStream();
|
||||
const input = composer.querySelector("textarea");
|
||||
if (input) {
|
||||
releaseInput(input);
|
||||
}
|
||||
flushDrafts();
|
||||
};
|
||||
}
|
||||
const composerDrafts = new Map();
|
||||
const composerInputs = new Map();
|
||||
function releaseInput(input) {
|
||||
for (const [key, el] of composerInputs) {
|
||||
if (el === input) {
|
||||
composerInputs.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
function keepDraft(input, key) {
|
||||
input.value = composerDrafts.get(key) ?? "";
|
||||
input.addEventListener("input", () => {
|
||||
if (input.value) {
|
||||
composerDrafts.set(key, input.value);
|
||||
}
|
||||
else {
|
||||
composerDrafts.delete(key);
|
||||
}
|
||||
});
|
||||
return () => composerDrafts.delete(key);
|
||||
composerInputs.set(key, input);
|
||||
input.value = composerText(key);
|
||||
input.addEventListener("input", () => setComposerText(key, input.value));
|
||||
input.addEventListener("blur", flushDrafts);
|
||||
}
|
||||
function lostImagesNote(key) {
|
||||
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;
|
||||
}
|
||||
const imageDrafts = new Map();
|
||||
const imageTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
||||
@@ -495,6 +518,7 @@ function imageTray(form, input, key) {
|
||||
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, ...imgs.map((img) => {
|
||||
const remove = h("button", { type: "button", class: "remove", title: "Remove image" }, "×");
|
||||
@@ -607,7 +631,8 @@ function modelSelect(key, current, vendor) {
|
||||
}
|
||||
function composeForm(src, key, sessionId, sessionState, currentModel, vendor, showPending) {
|
||||
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 () => {
|
||||
@@ -624,27 +649,30 @@ function composeForm(src, key, sessionId, sessionState, currentModel, vendor, sh
|
||||
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));
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user