Add inline approval controls to chat
This commit is contained in:
parent
72abeae2e8
commit
2d3a047548
|
|
@ -151,19 +151,87 @@ function updateToolTerminal(article, eventPayload) {
|
|||
|
||||
function appendApprovalTerminal(article, eventPayload) {
|
||||
const payload = eventPayload.payload || eventPayload;
|
||||
const action = payload.action || {};
|
||||
appendToolTerminal(article, {
|
||||
payload: {
|
||||
index: payload.index,
|
||||
tool: payload.tool,
|
||||
args: payload.action?.args || {},
|
||||
args: action.args || {},
|
||||
},
|
||||
});
|
||||
const terminal = article?.querySelector(`.tool-terminal[data-tool-index="${payload.index || ""}"]`);
|
||||
const body = terminal?.querySelector(".tool-terminal-body");
|
||||
const status = terminal?.querySelector(".tool-terminal-status");
|
||||
const command = formatToolCommand(action.tool || payload.tool, action.args || {});
|
||||
terminal?.classList.add("is-waiting");
|
||||
if (terminal && payload.approval_id) terminal.dataset.approvalId = payload.approval_id;
|
||||
if (status) status.textContent = "approval";
|
||||
if (body) body.textContent += `\n\napproval required\n${payload.reason || ""}`;
|
||||
if (body) {
|
||||
body.textContent = [
|
||||
body.textContent,
|
||||
"",
|
||||
"approval required",
|
||||
command,
|
||||
payload.reason || "",
|
||||
].filter(Boolean).join("\n");
|
||||
}
|
||||
terminal?.append(createInlineApprovalActions(payload.approval_id, command));
|
||||
}
|
||||
|
||||
function createInlineApprovalActions(approvalId, command) {
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "tool-approval-actions";
|
||||
actions.dataset.command = command || "tool action";
|
||||
actions.append(
|
||||
inlineApprovalButton("Allow once", "allow_once"),
|
||||
inlineApprovalButton("Allow forever", "allow_forever"),
|
||||
inlineApprovalButton("Deny", "deny", "danger"),
|
||||
);
|
||||
if (!approvalId) {
|
||||
actions.querySelectorAll("button").forEach((button) => {
|
||||
button.disabled = true;
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
function inlineApprovalButton(label, action, tone = "") {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = label;
|
||||
button.dataset.inlineApprovalAction = action;
|
||||
if (tone) button.dataset.tone = tone;
|
||||
return button;
|
||||
}
|
||||
|
||||
async function resolveInlineApproval(button) {
|
||||
const terminal = button.closest(".tool-terminal");
|
||||
const approvalId = terminal?.dataset.approvalId;
|
||||
const action = button.dataset.inlineApprovalAction;
|
||||
if (!terminal || !approvalId || !action) return;
|
||||
|
||||
terminal.querySelectorAll("[data-inline-approval-action]").forEach((item) => {
|
||||
item.disabled = true;
|
||||
});
|
||||
await jsonFetch(`/v1/approvals/${approvalId}/${action}`, {method: "POST"});
|
||||
|
||||
const status = terminal.querySelector(".tool-terminal-status");
|
||||
const body = terminal.querySelector(".tool-terminal-body");
|
||||
const actions = terminal.querySelector(".tool-approval-actions");
|
||||
const command = actions?.dataset.command || terminal.querySelector(".tool-terminal-title")?.textContent || "tool action";
|
||||
const decision = action === "deny" ? "denied" : "allowed";
|
||||
|
||||
terminal.classList.toggle("is-error", action === "deny");
|
||||
terminal.classList.remove("is-waiting");
|
||||
if (status) status.textContent = decision;
|
||||
if (body) body.textContent = `${command}\n\n${decision}: ${humanApprovalDecision(action)}`;
|
||||
actions?.remove();
|
||||
}
|
||||
|
||||
function humanApprovalDecision(action) {
|
||||
if (action === "allow_once") return "разрешено один раз";
|
||||
if (action === "allow_forever") return "разрешено навсегда";
|
||||
return "запрещено";
|
||||
}
|
||||
|
||||
function setMessagePending(article, text) {
|
||||
|
|
@ -403,6 +471,11 @@ function bindChat() {
|
|||
setStatus("#task-status", "none");
|
||||
});
|
||||
document.querySelector("#messages")?.addEventListener("click", (event) => {
|
||||
const approvalButton = event.target.closest("[data-inline-approval-action]");
|
||||
if (approvalButton) {
|
||||
resolveInlineApproval(approvalButton).catch(console.error);
|
||||
return;
|
||||
}
|
||||
const button = event.target.closest(".message-reasoning-toggle");
|
||||
if (button) toggleInlineReasoning(button);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -538,6 +538,34 @@ dd {
|
|||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.tool-approval-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 12px;
|
||||
border-top: 1px solid #1e293b;
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.tool-approval-actions button {
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
padding: 8px 10px;
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.tool-approval-actions button[data-tone="danger"] {
|
||||
background: #b42318;
|
||||
}
|
||||
|
||||
.tool-approval-actions button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
Loading…
Reference in New Issue