mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
268b39e3e0
Add capability-gated surface methods across the bot packages so one handler degrades gracefully per platform: - thread.react / thread.unreact — emoji reactions (Slack, Discord, Telegram) - thread.postEphemeral — only-you messages with a required DM fallback - openModal (interaction/command-scoped) — structured forms (Slack rich, Discord text-only), omitted on surfaces with no live interaction trigger Spans @copilotkit/bot-ui, bot, bot-slack, bot-discord, bot-telegram with per-adapter capability flags. Discord: open modals from slash-command interactions, and never clear registered commands on an empty publish.
121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
import type { BotNode } from "@copilotkit/bot-ui";
|
|
import { ModalRenderError } from "@copilotkit/bot-ui";
|
|
import type { View } from "@slack/web-api";
|
|
|
|
const plain = (text: string) => ({ type: "plain_text" as const, text });
|
|
|
|
function optionFrom(node: BotNode) {
|
|
return {
|
|
text: plain(String(node.props.label ?? "")),
|
|
value: String(node.props.value ?? ""),
|
|
};
|
|
}
|
|
|
|
function childOptions(node: BotNode): BotNode[] {
|
|
const kids = node.props.children;
|
|
return Array.isArray(kids)
|
|
? (kids as BotNode[]).filter((k) => k.type === "modal_select_option")
|
|
: [];
|
|
}
|
|
|
|
/**
|
|
* Render a modal IR tree (a `<Modal>` root with `modal_text_input` /
|
|
* `modal_select` / `modal_radio` children) into a Slack `views.open` `View`.
|
|
*
|
|
* Each field's `id` becomes both the block `block_id` and the element
|
|
* `action_id`, so a `view_submission`'s `state.values[id][id]` maps straight
|
|
* back to the field. Throws {@link ModalRenderError} when there is no modal
|
|
* root or a child uses an unsupported element type.
|
|
*/
|
|
export function renderSlackModal(ir: BotNode[]): View {
|
|
const root = ir.find((n) => n.type === "modal");
|
|
if (!root)
|
|
throw new ModalRenderError("renderSlackModal: no <Modal> root in IR");
|
|
const p = root.props as Record<string, unknown>;
|
|
const kids = Array.isArray(p.children) ? (p.children as BotNode[]) : [];
|
|
const blocks = kids.map((node): unknown => {
|
|
const fp = node.props as Record<string, unknown>;
|
|
const id = String(fp.id ?? "");
|
|
const label = plain(String(fp.label ?? ""));
|
|
const optional = fp.optional === true;
|
|
switch (node.type) {
|
|
case "modal_text_input":
|
|
return {
|
|
type: "input",
|
|
block_id: id,
|
|
label,
|
|
optional,
|
|
element: {
|
|
type: "plain_text_input",
|
|
action_id: id,
|
|
...(fp.multiline ? { multiline: true } : {}),
|
|
...(fp.placeholder
|
|
? { placeholder: plain(String(fp.placeholder)) }
|
|
: {}),
|
|
...(fp.initialValue
|
|
? { initial_value: String(fp.initialValue) }
|
|
: {}),
|
|
...(fp.maxLength ? { max_length: Number(fp.maxLength) } : {}),
|
|
},
|
|
};
|
|
case "modal_select": {
|
|
const options = childOptions(node).map(optionFrom);
|
|
const init = options.find(
|
|
(o) => o.value === String(fp.initialOption ?? ""),
|
|
);
|
|
return {
|
|
type: "input",
|
|
block_id: id,
|
|
label,
|
|
optional,
|
|
element: {
|
|
type: "static_select",
|
|
action_id: id,
|
|
options,
|
|
...(fp.placeholder
|
|
? { placeholder: plain(String(fp.placeholder)) }
|
|
: {}),
|
|
...(init ? { initial_option: init } : {}),
|
|
},
|
|
};
|
|
}
|
|
case "modal_radio": {
|
|
const options = childOptions(node).map(optionFrom);
|
|
const init = options.find(
|
|
(o) => o.value === String(fp.initialOption ?? ""),
|
|
);
|
|
return {
|
|
type: "input",
|
|
block_id: id,
|
|
label,
|
|
optional,
|
|
element: {
|
|
type: "radio_buttons",
|
|
action_id: id,
|
|
options,
|
|
...(init ? { initial_option: init } : {}),
|
|
},
|
|
};
|
|
}
|
|
default:
|
|
throw new ModalRenderError(
|
|
`renderSlackModal: unsupported modal element "${String(node.type)}"`,
|
|
);
|
|
}
|
|
});
|
|
return {
|
|
type: "modal",
|
|
callback_id: String(p.callbackId ?? ""),
|
|
title: plain(String(p.title ?? "")),
|
|
...(p.submitLabel
|
|
? { submit: plain(String(p.submitLabel)) }
|
|
: { submit: plain("Submit") }),
|
|
...(p.closeLabel ? { close: plain(String(p.closeLabel)) } : {}),
|
|
...(p.notifyOnClose ? { notify_on_close: true } : {}),
|
|
...(p.privateMetadata
|
|
? { private_metadata: String(p.privateMetadata) }
|
|
: {}),
|
|
blocks: blocks as View["blocks"],
|
|
} as View;
|
|
}
|