@assistant-ui/react
Version:
Open-source TypeScript/React library for building production-grade AI chat experiences
80 lines (79 loc) • 2.89 kB
JavaScript
"use client";
import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect.js";
import { matchesTriggerItemQuery } from "../primitives/composer/trigger/matchesTriggerItemQuery.js";
import { useMemo, useRef } from "@assistant-ui/tap/react-shim";
// src/unstable/useSlashCommandAdapter.ts
/**
* Bundles slash command definitions (with inline `execute` callbacks) into
* `{adapter, action}` that plug directly into `ComposerTriggerPopover`.
* `execute` stays in the hook closure and is never attached to the returned
* `TriggerItem`, keeping items serializable.
*
* @deprecated Experimental since 2026-04-06. Not scheduled for removal; the API may change in any release.
*
* @example
* ```tsx
* const slash = unstable_useSlashCommandAdapter({
* commands: [
* { id: "summarize", execute: () => runSummarize(), icon: "FileText" },
* { id: "translate", execute: () => runTranslate(), icon: "Languages" },
* ],
* });
*
* <ComposerTriggerPopover char="/" {...slash} />
* ```
*/
function unstable_useSlashCommandAdapter(options) {
const { commands, removeOnExecute } = options;
const commandsRef = useRef(commands);
const committedItemsRef = useRef(void 0);
const nextItems = commands.map(toItem);
const items = areTriggerItemsEqual(committedItemsRef.current, nextItems) ? committedItemsRef.current : nextItems;
useIsomorphicLayoutEffect(() => {
commandsRef.current = commands;
committedItemsRef.current = items;
}, [commands, items]);
const adapter = useMemo(() => ({
categories: () => [],
categoryItems: () => [],
search: (query) => {
const lower = query.toLowerCase();
return items.filter((item) => matchesTriggerItemQuery(item, lower));
}
}), [items]);
const action = useMemo(() => ({
onExecute: (item) => {
commandsRef.current.find((c) => c.id === item.id)?.execute();
},
...removeOnExecute !== void 0 ? { removeOnExecute } : {}
}), [removeOnExecute]);
return useMemo(() => ({
adapter,
action,
...options.iconMap ? { iconMap: options.iconMap } : {},
...options.fallbackIcon ? { fallbackIcon: options.fallbackIcon } : {}
}), [
adapter,
action,
options.iconMap,
options.fallbackIcon
]);
}
function toItem(cmd) {
return {
id: cmd.id,
type: "command",
label: cmd.label ?? `/${cmd.id}`,
...cmd.description !== void 0 ? { description: cmd.description } : {},
...cmd.icon !== void 0 ? { metadata: { icon: cmd.icon } } : {}
};
}
function areTriggerItemsEqual(previous, next) {
if (!previous || previous.length !== next.length) return false;
return previous.every((item, index) => {
const nextItem = next[index];
return nextItem !== void 0 && item.id === nextItem.id && item.label === nextItem.label && item.description === nextItem.description && item.metadata?.icon === nextItem.metadata?.icon;
});
}
export { unstable_useSlashCommandAdapter };
//# sourceMappingURL=useSlashCommandAdapter.js.map