Render Markdown, add ask_human context, simplify inbox responses, group inbox by task
This commit is contained in:
+88
-34
@@ -1,10 +1,11 @@
|
||||
import { post, type Attachment, type InboxItem, type InboxResponseCreate } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { inlineMarkdown, markdown } from "./markdown.js";
|
||||
import { findTask, onEach, state } from "./state.js";
|
||||
import { reportCard } from "./task.js";
|
||||
|
||||
type Scope = "all" | "project" | "task";
|
||||
type Mode = InboxResponseCreate["type"];
|
||||
type Mode = "pick" | "reply" | "snooze" | "dismiss";
|
||||
|
||||
const drafts = new Map<string, InboxResponseCreate>();
|
||||
const modes = new Map<string, Mode>();
|
||||
@@ -83,7 +84,7 @@ export function mountInbox(root: HTMLElement): () => void {
|
||||
}
|
||||
}
|
||||
|
||||
const els = items.map((item) => {
|
||||
const cardFor = (item: InboxItem) => {
|
||||
const sig = JSON.stringify(item);
|
||||
const cached = cards.get(item.id);
|
||||
if (cached && cached.sig === sig) {
|
||||
@@ -94,6 +95,17 @@ export function mountInbox(root: HTMLElement): () => void {
|
||||
});
|
||||
cards.set(item.id, { sig, el });
|
||||
return el;
|
||||
};
|
||||
|
||||
const els = groupByTask(items).map((g) => {
|
||||
const task = findTask(g.taskId);
|
||||
const project = state.projects.find((p) => p.id === g.projectId);
|
||||
return h(
|
||||
"section",
|
||||
{ class: `task-group ${g.blocked ? "blocked" : ""}` },
|
||||
h("h3", {}, h("span", { class: "project-name" }, project?.name ?? g.projectId, " › "), h("a", { href: `#/task/${g.taskId}` }, task?.title ?? g.taskId)),
|
||||
...g.items.map(cardFor),
|
||||
);
|
||||
});
|
||||
|
||||
replace(list, ...(els.length > 0 ? els : [h("div", { class: "empty" }, "Nothing waiting on you.")]));
|
||||
@@ -106,8 +118,29 @@ export function mountInbox(root: HTMLElement): () => void {
|
||||
return off;
|
||||
}
|
||||
|
||||
type Group = { taskId: string; projectId: string; blocked: boolean; items: InboxItem[] };
|
||||
|
||||
function groupByTask(items: InboxItem[]): Group[] {
|
||||
const groups = new Map<string, Group>();
|
||||
for (const item of [...items].sort((a, b) => a.created_at.localeCompare(b.created_at))) {
|
||||
let g = groups.get(item.task_id);
|
||||
if (!g) {
|
||||
g = { taskId: item.task_id, projectId: item.project_id, blocked: false, items: [] };
|
||||
groups.set(item.task_id, g);
|
||||
}
|
||||
g.items.push(item);
|
||||
g.blocked ||= item.priority === "blocked";
|
||||
}
|
||||
const projectName = (id: string) => state.projects.find((p) => p.id === id)?.name ?? id;
|
||||
return [...groups.values()].sort(
|
||||
(a, b) =>
|
||||
Number(b.blocked) - Number(a.blocked) ||
|
||||
projectName(a.projectId).localeCompare(projectName(b.projectId)) ||
|
||||
a.items[0]!.created_at.localeCompare(b.items[0]!.created_at),
|
||||
);
|
||||
}
|
||||
|
||||
function card(item: InboxItem, changed: () => void): HTMLElement {
|
||||
const task = findTask(item.task_id);
|
||||
const p = item.payload as Record<string, unknown>;
|
||||
const body = h("div", { class: "card-body" });
|
||||
const actions = h("div", { class: "card-actions" });
|
||||
@@ -119,7 +152,6 @@ function card(item: InboxItem, changed: () => void): HTMLElement {
|
||||
{},
|
||||
h("span", { class: `priority ${item.priority}` }, item.priority),
|
||||
h("span", { class: "kind" }, item.kind.replace("_", " ")),
|
||||
h("a", { href: `#/task/${item.task_id}`, class: "card-task" }, task?.title ?? item.task_id),
|
||||
h("time", { dateTime: item.created_at, title: item.created_at }, ago(item.created_at)),
|
||||
),
|
||||
body,
|
||||
@@ -127,18 +159,29 @@ function card(item: InboxItem, changed: () => void): HTMLElement {
|
||||
actions,
|
||||
);
|
||||
|
||||
const allowed: Mode[] =
|
||||
item.kind === "question" ? ["option", "free_form", "question", "redirect", "snooze"] : ["dismiss", "redirect", "snooze"];
|
||||
const allowed: Mode[] = item.kind === "question" ? ["pick", "snooze"] : ["dismiss", "reply", "snooze"];
|
||||
const labels: Record<Mode, string> = {
|
||||
option: "Pick",
|
||||
free_form: "Other answer",
|
||||
question: "Ask back",
|
||||
redirect: "Redirect",
|
||||
pick: "Pick",
|
||||
reply: "Reply",
|
||||
snooze: "Snooze",
|
||||
dismiss: "Dismiss",
|
||||
};
|
||||
|
||||
const renderActions = () => {
|
||||
if (item.kind === "report") {
|
||||
const ok = h("button", { type: "button", class: "primary" }, "OK");
|
||||
ok.onclick = async () => {
|
||||
ok.disabled = true;
|
||||
try {
|
||||
await post("/api/inbox/responses", { body: { responses: [{ item_id: item.id, type: "dismiss" }] } });
|
||||
} catch (err) {
|
||||
alert(errorText(err));
|
||||
ok.disabled = false;
|
||||
}
|
||||
};
|
||||
replace(actions, h("div", { class: "ack" }, ok));
|
||||
return;
|
||||
}
|
||||
const mode = modes.get(item.id) ?? allowed[0]!;
|
||||
const tabs = h(
|
||||
"div",
|
||||
@@ -164,8 +207,9 @@ function card(item: InboxItem, changed: () => void): HTMLElement {
|
||||
case "question":
|
||||
replace(
|
||||
body,
|
||||
h("p", { class: "question" }, String(p["question"])),
|
||||
h("p", { class: "why" }, h("strong", {}, "Blocked on: "), String(p["why_blocked"])),
|
||||
p["context"] ? markdown(String(p["context"]), "md context") : null,
|
||||
markdown(String(p["question"]), "md question"),
|
||||
h("p", { class: "why" }, h("strong", {}, "Blocked on: "), inlineMarkdown(String(p["why_blocked"]))),
|
||||
attachments((p["attachments"] as Attachment[] | undefined) ?? []),
|
||||
);
|
||||
break;
|
||||
@@ -199,39 +243,49 @@ function modeInput(item: InboxItem, mode: Mode, changed: () => void): HTMLElemen
|
||||
};
|
||||
|
||||
switch (mode) {
|
||||
case "option": {
|
||||
case "pick": {
|
||||
const p = item.payload as Record<string, unknown>;
|
||||
const options = (p["options"] as string[]) ?? [];
|
||||
const note = h("input", { placeholder: "Optional note", value: draft?.text ?? "" });
|
||||
const choose = (option: string) => set({ item_id: item.id, type: "option", option, text: note.value || undefined });
|
||||
note.oninput = () => {
|
||||
const current = drafts.get(item.id);
|
||||
if (current?.type === "option" && current.option) {
|
||||
choose(current.option);
|
||||
}
|
||||
const text = h("textarea", {
|
||||
rows: 3,
|
||||
placeholder: "Your answer, a question back, or new instructions",
|
||||
value: draft?.type === "other" ? (draft.text ?? "") : "",
|
||||
hidden: draft?.type !== "other",
|
||||
});
|
||||
const setOther = () => set(text.value.trim() ? { item_id: item.id, type: "other", text: text.value } : undefined);
|
||||
text.oninput = setOther;
|
||||
const other = h("input", { type: "radio", name: `opt-${item.id}`, checked: draft?.type === "other" });
|
||||
other.onchange = () => {
|
||||
text.hidden = false;
|
||||
text.focus();
|
||||
setOther();
|
||||
};
|
||||
return h(
|
||||
"div",
|
||||
{ class: "options" },
|
||||
...options.map((option) => {
|
||||
const radio = h("input", { type: "radio", name: `opt-${item.id}`, checked: drafts.get(item.id)?.option === option });
|
||||
radio.onchange = () => choose(option);
|
||||
return h("label", { class: "option" }, radio, " ", option, option === p["recommended_option"] && h("span", { class: "recommended" }, "recommended"));
|
||||
const radio = h("input", { type: "radio", name: `opt-${item.id}`, checked: draft?.type === "option" && draft.option === option });
|
||||
radio.onchange = () => {
|
||||
text.hidden = true;
|
||||
set({ item_id: item.id, type: "option", option });
|
||||
};
|
||||
return h(
|
||||
"label",
|
||||
{ class: "option" },
|
||||
radio,
|
||||
" ",
|
||||
inlineMarkdown(option),
|
||||
option === p["recommended_option"] && h("span", { class: "recommended" }, "recommended"),
|
||||
);
|
||||
}),
|
||||
note,
|
||||
h("label", { class: "option" }, other, " Other"),
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
case "free_form":
|
||||
case "question":
|
||||
case "redirect": {
|
||||
const placeholder = {
|
||||
free_form: "Your answer",
|
||||
question: "Your question back to the agent",
|
||||
redirect: "New instructions for the agent",
|
||||
}[mode];
|
||||
const text = h("textarea", { rows: 3, placeholder, value: draft?.type === mode ? (draft.text ?? "") : "" });
|
||||
text.oninput = () => set(text.value.trim() ? { item_id: item.id, type: mode, text: text.value } : undefined);
|
||||
case "reply": {
|
||||
const text = h("textarea", { rows: 3, placeholder: "Reply to the agent", value: draft?.type === "other" ? (draft.text ?? "") : "" });
|
||||
text.oninput = () => set(text.value.trim() ? { item_id: item.id, type: "other", text: text.value } : undefined);
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user