@assistant-ui/react
Version:
Open-source TypeScript/React library for building production-grade AI chat experiences
147 lines (133 loc) • 4.48 kB
text/typescript
import { useEffectEvent } from "react";
import { useResource, resource } from "@assistant-ui/tap";
import type {
Unstable_TriggerAdapter,
Unstable_TriggerCategory,
Unstable_TriggerItem,
} from "@assistant-ui/core";
import type { AssistantClient } from "@assistant-ui/store";
import { TriggerDetectionResource } from "./triggerDetectionResource";
import { TriggerKeyboardResource } from "./triggerKeyboardResource";
import { TriggerNavigationResource } from "./triggerNavigationResource";
import {
TriggerSelectionResource,
type SelectItemOverride,
type TriggerBehavior,
} from "./triggerSelectionResource";
import type { TriggerMatcher } from "./detectTrigger";
export type { SelectItemOverride, TriggerBehavior };
export type { TriggerPopoverKeyEvent } from "./triggerKeyboardResource";
/** @deprecated Use `TriggerBehavior`. */
export type OnSelectBehavior = TriggerBehavior;
export type TriggerPopoverResourceOutput = {
readonly open: boolean;
readonly query: string;
readonly activeCategoryId: string | null;
readonly categories: readonly Unstable_TriggerCategory[];
readonly items: readonly Unstable_TriggerItem[];
readonly highlightedIndex: number;
readonly isSearchMode: boolean;
/** Whether the adapter is currently resolving items (async sources). */
readonly isLoading: boolean;
/** Stable ID prefix for generating accessible element IDs. */
readonly popoverId: string;
/** ID of the currently highlighted item (for aria-activedescendant). */
readonly highlightedItemId: string | undefined;
selectCategory(categoryId: string): void;
goBack(): void;
selectItem(item: Unstable_TriggerItem): void;
close(): void;
/** Move the highlight to an entry index (e.g. from pointer hover). Out-of-range values are ignored. */
highlightIndex(index: number): void;
handleKeyDown(e: {
readonly key: string;
readonly shiftKey: boolean;
preventDefault(): void;
}): boolean;
setCursorPosition(pos: number): void;
registerSelectItemOverride(fn: SelectItemOverride): () => void;
};
/** Composes detection, navigation, keyboard, and selection sub-resources. */
const useTriggerPopoverResource = ({
adapter,
text,
triggerChar,
matcher,
behavior,
aui,
popoverId,
isLoading,
}: {
adapter: Unstable_TriggerAdapter | undefined;
text: string;
triggerChar: string;
matcher: TriggerMatcher | undefined;
behavior: TriggerBehavior | undefined;
aui: AssistantClient;
/** Stable ID for accessible element IDs (pass React's useId() from component layer). */
popoverId: string;
isLoading: boolean;
}): TriggerPopoverResourceOutput => {
const detection = useResource(
TriggerDetectionResource({ text, triggerChar, matcher }),
);
const open =
detection.trigger !== null &&
adapter !== undefined &&
behavior !== undefined;
const navigation = useResource(
TriggerNavigationResource({
adapter,
query: detection.query,
open,
}),
);
const onSelected = useEffectEvent(() => {
navigation.goBack();
});
const selection = useResource(
TriggerSelectionResource({
behavior,
trigger: detection.trigger,
aui,
triggerChar,
setCursorPosition: detection.setCursorPosition,
onSelected,
}),
);
const keyboard = useResource(
TriggerKeyboardResource({
navigableList: navigation.navigableList,
isSearchMode: navigation.isSearchMode,
activeCategoryId: navigation.activeCategoryId,
query: detection.query,
popoverId,
open,
selectItem: selection.selectItem,
selectCategory: navigation.selectCategory,
goBack: navigation.goBack,
close: selection.close,
}),
);
return {
open,
query: detection.query,
activeCategoryId: navigation.activeCategoryId,
categories: navigation.categories,
items: navigation.items,
highlightedIndex: keyboard.highlightedIndex,
isSearchMode: navigation.isSearchMode,
isLoading,
popoverId,
highlightedItemId: keyboard.highlightedItemId,
selectCategory: navigation.selectCategory,
goBack: navigation.goBack,
selectItem: selection.selectItem,
close: selection.close,
highlightIndex: keyboard.highlightIndex,
handleKeyDown: keyboard.handleKeyDown,
setCursorPosition: detection.setCursorPosition,
registerSelectItemOverride: selection.registerSelectItemOverride,
};
};
export const TriggerPopoverResource = resource(useTriggerPopoverResource);