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) {
|
function appendApprovalTerminal(article, eventPayload) {
|
||||||
const payload = eventPayload.payload || eventPayload;
|
const payload = eventPayload.payload || eventPayload;
|
||||||
|
const action = payload.action || {};
|
||||||
appendToolTerminal(article, {
|
appendToolTerminal(article, {
|
||||||
payload: {
|
payload: {
|
||||||
index: payload.index,
|
index: payload.index,
|
||||||
tool: payload.tool,
|
tool: payload.tool,
|
||||||
args: payload.action?.args || {},
|
args: action.args || {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const terminal = article?.querySelector(`.tool-terminal[data-tool-index="${payload.index || ""}"]`);
|
const terminal = article?.querySelector(`.tool-terminal[data-tool-index="${payload.index || ""}"]`);
|
||||||
const body = terminal?.querySelector(".tool-terminal-body");
|
const body = terminal?.querySelector(".tool-terminal-body");
|
||||||
const status = terminal?.querySelector(".tool-terminal-status");
|
const status = terminal?.querySelector(".tool-terminal-status");
|
||||||
|
const command = formatToolCommand(action.tool || payload.tool, action.args || {});
|
||||||
terminal?.classList.add("is-waiting");
|
terminal?.classList.add("is-waiting");
|
||||||
|
if (terminal && payload.approval_id) terminal.dataset.approvalId = payload.approval_id;
|
||||||
if (status) status.textContent = "approval";
|
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) {
|
function setMessagePending(article, text) {
|
||||||
|
|
@ -403,6 +471,11 @@ function bindChat() {
|
||||||
setStatus("#task-status", "none");
|
setStatus("#task-status", "none");
|
||||||
});
|
});
|
||||||
document.querySelector("#messages")?.addEventListener("click", (event) => {
|
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");
|
const button = event.target.closest(".message-reasoning-toggle");
|
||||||
if (button) toggleInlineReasoning(button);
|
if (button) toggleInlineReasoning(button);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -538,6 +538,34 @@ dd {
|
||||||
overflow-wrap: anywhere;
|
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 {
|
.message-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue