UNPKG

@assistant-ui/react

Version:

Open-source TypeScript/React library for building production-grade AI chat experiences

130 lines (113 loc) • 4.31 kB
import { useEffectEvent, useMemo, useState } from "react"; import { resource } from "@assistant-ui/tap"; import type { Unstable_TriggerAdapter, Unstable_TriggerCategory, Unstable_TriggerItem, } from "@assistant-ui/core"; import { matchesTriggerItemQuery } from "./matchesTriggerItemQuery"; export type TriggerNavigationResourceOutput = { /** Filtered categories visible in the list (empty in search mode). */ readonly categories: readonly Unstable_TriggerCategory[]; /** Filtered items visible in the list. */ readonly items: readonly Unstable_TriggerItem[]; /** `true` when the current list is search results rather than categories. */ readonly isSearchMode: boolean; /** Currently drilled-into category id (or `null` for the top level). */ readonly activeCategoryId: string | null; /** Flat list used for keyboard navigation (categories or items). */ readonly navigableList: readonly ( | Unstable_TriggerCategory | Unstable_TriggerItem )[]; /** Drill into a category. */ selectCategory(categoryId: string): void; /** Return to the top-level category list. */ goBack(): void; }; /** * Computes categories, items, search results, and navigation state from the * adapter + current query. Pure derivation — no side effects on the composer. */ const useTriggerNavigationResource = ({ adapter, query, open, }: { adapter: Unstable_TriggerAdapter | undefined; query: string; open: boolean; }): TriggerNavigationResourceOutput => { const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null); const [wasOpen, setWasOpen] = useState(open); if (wasOpen !== open) { setWasOpen(open); if (!open) setActiveCategoryId(null); } const categories = useMemo<readonly Unstable_TriggerCategory[]>(() => { if (!open || !adapter) return []; return adapter.categories(); }, [open, adapter]); const effectiveActiveCategoryId = open ? activeCategoryId : null; const allItems = useMemo<readonly Unstable_TriggerItem[]>(() => { if (!effectiveActiveCategoryId || !adapter) return []; return adapter.categoryItems(effectiveActiveCategoryId); }, [effectiveActiveCategoryId, adapter]); const searchResults = useMemo<readonly Unstable_TriggerItem[] | null>(() => { if (!open || !adapter || effectiveActiveCategoryId) return null; // If categories exist and query is empty, show categories first (not search) if (!query && categories.length > 0) return null; if (adapter.search) return adapter.search(query); // fallback: no adapter.search const all: Unstable_TriggerItem[] = []; const lower = query.toLowerCase(); for (const cat of categories) { for (const item of adapter.categoryItems(cat.id)) { if (matchesTriggerItemQuery(item, lower)) { all.push(item); } } } return all; }, [open, adapter, query, effectiveActiveCategoryId, categories]); const isSearchMode = searchResults !== null; const filteredCategories = useMemo(() => { if (isSearchMode) return []; if (!query) return categories; const lower = query.toLowerCase(); return categories.filter((cat) => cat.label.toLowerCase().includes(lower)); }, [categories, query, isSearchMode]); const filteredItems = useMemo(() => { if (isSearchMode) return searchResults ?? []; if (!query) return allItems; const lower = query.toLowerCase(); return allItems.filter((item) => matchesTriggerItemQuery(item, lower)); }, [allItems, query, isSearchMode, searchResults]); const navigableList = useMemo(() => { if (isSearchMode) return searchResults ?? []; if (effectiveActiveCategoryId) return filteredItems; return filteredCategories; }, [ isSearchMode, searchResults, effectiveActiveCategoryId, filteredItems, filteredCategories, ]); const selectCategory = useEffectEvent((categoryId: string) => { setActiveCategoryId(categoryId); }); const goBack = useEffectEvent(() => { setActiveCategoryId(null); }); return { categories: filteredCategories, items: filteredItems, isSearchMode, activeCategoryId: effectiveActiveCategoryId, navigableList, selectCategory, goBack, }; }; export const TriggerNavigationResource = resource(useTriggerNavigationResource);