Skip to main content

priv/inspector/assets/js/pending.js

/**
 * pending.js - Pending tab: sampling and elicitation requests
 */

import { el, renderContent } from "./render.js";
import { buildForm } from "./schema-form.js";

let ctx = null;
const pendingCards = new Map(); // request_id -> card element
let domInited = false;

// session() is a function returning the current session id
function sid() { return ctx?.session(); }

export function init(context) {
  ctx = context;

  if (!domInited) {
    domInited = true;
    const panel = document.getElementById("tab-pending");

    panel.appendChild(el("h2", {}, "Pending Requests"));

    const container = el("div", { id: "pending-list" });
    panel.appendChild(container);

    const emptyMsg = el("div", {
      id: "pending-empty",
      style: { color: "var(--text-muted)", padding: "20px", textAlign: "center" }
    }, "No pending requests");
    panel.appendChild(emptyMsg);

    ctx.events.on("pending_request", (data) => {
      addPendingRequest(data);
    });

    ctx.events.on("pending_resolved", (data) => {
      removePendingRequest(data.request_id);
    });

    // Reset on session change
    ctx.events.on("session_changed", () => {
      pendingCards.clear();
      const container = document.getElementById("pending-list");
      if (container) container.innerHTML = "";
      updateBadge();
    });
  }
}

function updateBadge() {
  const badge = document.getElementById("pending-badge");
  const count = pendingCards.size;
  if (badge) {
    badge.textContent = String(count);
    if (count > 0) {
      badge.classList.remove("hidden");
      badge.classList.add("pulse");
    } else {
      badge.classList.add("hidden");
      badge.classList.remove("pulse");
    }
  }

  const emptyMsg = document.getElementById("pending-empty");
  if (emptyMsg) {
    emptyMsg.style.display = count > 0 ? "none" : "";
  }
}

function addPendingRequest(data) {
  const container = document.getElementById("pending-list");
  if (!container) return;

  const requestId = data.request_id;
  const kind = data.kind; // "sampling" or "elicitation"
  const params = data.params || {};

  let card;
  if (kind === "sampling") {
    card = buildSamplingCard(requestId, params);
  } else if (kind === "elicitation") {
    card = buildElicitationCard(requestId, params);
  } else {
    card = buildGenericPendingCard(requestId, kind, params);
  }

  pendingCards.set(requestId, card);
  container.appendChild(card);
  updateBadge();
}

function removePendingRequest(requestId) {
  const card = pendingCards.get(requestId);
  if (card) {
    card.remove();
    pendingCards.delete(requestId);
  }
  updateBadge();
}

function buildSamplingCard(requestId, params) {
  const card = el("div", { class: "card" });

  card.appendChild(el("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, [
    el("span", { class: "card-title" }, "Sampling Request"),
    el("span", { class: "badge-pill badge-blue" }, requestId)
  ]));

  // Render messages
  if (params.messages && params.messages.length > 0) {
    const messagesArea = el("div", { style: { marginTop: "12px" } });
    for (const msg of params.messages) {
      const msgBlock = el("div", { class: "message-block" });
      const roleClass = `role-badge role-${msg.role || "user"}`;
      msgBlock.appendChild(el("span", { class: roleClass }, msg.role || "unknown"));

      const contentEl = el("div", { style: { marginTop: "6px" } });
      const content = msg.content;
      if (typeof content === "string") {
        contentEl.appendChild(el("pre", { style: { whiteSpace: "pre-wrap" } }, content));
      } else if (content) {
        renderContent(Array.isArray(content) ? content : [content], contentEl);
      }
      msgBlock.appendChild(contentEl);
      messagesArea.appendChild(msgBlock);
    }
    card.appendChild(messagesArea);
  }

  // Model preferences / maxTokens
  const meta = el("div", { style: { marginTop: "8px", fontSize: "12px", color: "var(--text-muted)" } });
  if (params.modelPreferences) {
    meta.appendChild(el("div", {}, `Model preferences: ${JSON.stringify(params.modelPreferences)}`));
  }
  if (params.maxTokens !== undefined) {
    meta.appendChild(el("div", {}, `Max tokens: ${params.maxTokens}`));
  }
  if (params.systemPrompt) {
    meta.appendChild(el("div", { style: { marginTop: "4px" } }, [
      el("strong", {}, "System: "),
      el("span", {}, params.systemPrompt)
    ]));
  }
  card.appendChild(meta);

  // Response form
  const responseArea = el("div", { style: { marginTop: "12px" } });
  responseArea.appendChild(el("label", {}, "Response"));
  const textarea = el("textarea", { rows: "4", placeholder: "Enter response text..." });
  responseArea.appendChild(textarea);

  const modelGroup = el("div", { class: "form-group", style: { marginTop: "8px" } }, [
    el("label", {}, "Model (optional)"),
    el("input", { type: "text", placeholder: "e.g. claude-3-opus", id: `sampling-model-${requestId}` })
  ]);
  responseArea.appendChild(modelGroup);

  card.appendChild(responseArea);

  // Buttons
  const btnRow = el("div", { class: "btn-group", style: { marginTop: "12px" } });

  const respondBtn = el("button", { class: "btn-primary btn-sm" }, "Respond");
  respondBtn.addEventListener("click", async () => {
    const modelInput = document.getElementById(`sampling-model-${requestId}`);
    const body = {
      role: "assistant",
      content: { type: "text", text: textarea.value },
    };
    if (modelInput?.value) {
      body.model = modelInput.value;
    }

    try {
      await ctx.api.apiPost(`/api/session/${sid()}/respond/${requestId}`, body);
      removePendingRequest(requestId);
    } catch (err) {
      ctx.api.showToast(`Respond failed: ${err.message}`);
    }
  });

  const rejectBtn = el("button", { class: "btn-danger btn-sm" }, "Reject");
  rejectBtn.addEventListener("click", async () => {
    try {
      await ctx.api.apiPost(`/api/session/${sid()}/respond/${requestId}`, {
        error: "rejected by user"
      });
      removePendingRequest(requestId);
    } catch (err) {
      ctx.api.showToast(`Reject failed: ${err.message}`);
    }
  });

  btnRow.appendChild(respondBtn);
  btnRow.appendChild(rejectBtn);
  card.appendChild(btnRow);

  return card;
}

function buildElicitationCard(requestId, params) {
  const card = el("div", { class: "card" });

  card.appendChild(el("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, [
    el("span", { class: "card-title" }, "Elicitation Request"),
    el("span", { class: "badge-pill badge-yellow" }, requestId)
  ]));

  // Message
  if (params.message) {
    card.appendChild(el("div", { style: { marginTop: "8px" } }, params.message));
  }

  // Build form from requestedSchema
  const formContainer = el("div", { style: { marginTop: "12px" } });
  let form = null;
  if (params.requestedSchema) {
    form = buildForm(params.requestedSchema, formContainer);
  }
  card.appendChild(formContainer);

  // Buttons: Accept, Decline, Cancel
  const btnRow = el("div", { class: "btn-group", style: { marginTop: "12px" } });

  const acceptBtn = el("button", { class: "btn-primary btn-sm" }, "Accept");
  acceptBtn.addEventListener("click", async () => {
    const value = form ? form.getValue() : {};
    try {
      await ctx.api.apiPost(`/api/session/${sid()}/respond/${requestId}`, {
        action: "accept",
        content: value
      });
      removePendingRequest(requestId);
    } catch (err) {
      ctx.api.showToast(`Accept failed: ${err.message}`);
    }
  });

  const declineBtn = el("button", { class: "btn-sm" }, "Decline");
  declineBtn.addEventListener("click", async () => {
    try {
      await ctx.api.apiPost(`/api/session/${sid()}/respond/${requestId}`, {
        action: "decline"
      });
      removePendingRequest(requestId);
    } catch (err) {
      ctx.api.showToast(`Decline failed: ${err.message}`);
    }
  });

  const cancelBtn = el("button", { class: "btn-danger btn-sm" }, "Cancel");
  cancelBtn.addEventListener("click", async () => {
    try {
      await ctx.api.apiPost(`/api/session/${sid()}/respond/${requestId}`, {
        action: "cancel"
      });
      removePendingRequest(requestId);
    } catch (err) {
      ctx.api.showToast(`Cancel failed: ${err.message}`);
    }
  });

  btnRow.appendChild(acceptBtn);
  btnRow.appendChild(declineBtn);
  btnRow.appendChild(cancelBtn);
  card.appendChild(btnRow);

  return card;
}

function buildGenericPendingCard(requestId, kind, params) {
  const card = el("div", { class: "card" });

  card.appendChild(el("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, [
    el("span", { class: "card-title" }, `${kind} Request`),
    el("span", { class: "badge-pill badge-accent" }, requestId)
  ]));

  card.appendChild(el("pre", { style: { marginTop: "8px" } }, JSON.stringify(params, null, 2)));

  const textarea = el("textarea", { rows: "4", placeholder: "Enter response JSON...", style: { marginTop: "8px" } });
  card.appendChild(textarea);

  const respondBtn = el("button", { class: "btn-primary btn-sm", style: { marginTop: "8px" } }, "Respond");
  respondBtn.addEventListener("click", async () => {
    let body;
    try {
      body = JSON.parse(textarea.value);
    } catch {
      body = { text: textarea.value };
    }
    try {
      await ctx.api.apiPost(`/api/session/${sid()}/respond/${requestId}`, body);
      removePendingRequest(requestId);
    } catch (err) {
      ctx.api.showToast(`Respond failed: ${err.message}`);
    }
  });
  card.appendChild(respondBtn);

  return card;
}