@assistant-ui/react 0.14.21 → 0.14.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +2 -4
- package/dist/internal.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts +4 -2
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.js +125 -119
- package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +3 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js +22 -20
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
- package/dist/primitives/composer/trigger/index.d.ts +1 -0
- package/dist/unstable/useLiveCompletionAdapter.d.ts +47 -0
- package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -0
- package/dist/unstable/useLiveCompletionAdapter.js +116 -0
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -0
- package/dist/utils/smooth/useSmooth.d.ts +8 -0
- package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +11 -2
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +7 -0
- package/src/internal.ts +0 -2
- package/src/primitives/composer/trigger/TriggerPopover.tsx +11 -1
- package/src/primitives/composer/trigger/TriggerPopoverResource.ts +5 -0
- package/src/tests/ExternalStoreThreadRuntimeCore.test.ts +113 -0
- package/src/unstable/useLiveCompletionAdapter.test.tsx +176 -0
- package/src/unstable/useLiveCompletionAdapter.ts +145 -0
- package/src/utils/smooth/useSmooth.test.tsx +106 -2
- package/src/utils/smooth/useSmooth.ts +20 -2
|
@@ -13,7 +13,8 @@ type TriggerPopoverResourceOutput = {
|
|
|
13
13
|
readonly categories: readonly Unstable_TriggerCategory[];
|
|
14
14
|
readonly items: readonly Unstable_TriggerItem[];
|
|
15
15
|
readonly highlightedIndex: number;
|
|
16
|
-
readonly isSearchMode: boolean; /**
|
|
16
|
+
readonly isSearchMode: boolean; /** Whether the adapter is currently resolving items (async sources). */
|
|
17
|
+
readonly isLoading: boolean; /** Stable ID prefix for generating accessible element IDs. */
|
|
17
18
|
readonly popoverId: string; /** ID of the currently highlighted item (for aria-activedescendant). */
|
|
18
19
|
readonly highlightedItemId: string | undefined;
|
|
19
20
|
selectCategory(categoryId: string): void;
|
|
@@ -36,6 +37,7 @@ declare const TriggerPopoverResource: import("@assistant-ui/tap").Resource<Trigg
|
|
|
36
37
|
behavior: TriggerBehavior | undefined;
|
|
37
38
|
aui: AssistantClient; /** Stable ID for accessible element IDs (pass React's useId() from component layer). */
|
|
38
39
|
popoverId: string;
|
|
40
|
+
isLoading: boolean;
|
|
39
41
|
}]>;
|
|
40
42
|
//#endregion
|
|
41
43
|
export { OnSelectBehavior, type SelectItemOverride, type TriggerBehavior, type TriggerPopoverKeyEvent, TriggerPopoverResource, TriggerPopoverResourceOutput };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TriggerPopoverResource.d.ts","names":[],"sources":["../../../../src/primitives/composer/trigger/TriggerPopoverResource.ts"],"mappings":";;;;;;;KAqBY,gBAAA,GAAmB,eAAe;AAAA,KAElC,4BAAA;EAAA,SACD,IAAA;EAAA,SACA,KAAA;EAAA,SACA,gBAAA;EAAA,SACA,UAAA,WAAqB,wBAAA;EAAA,SACrB,KAAA,WAAgB,oBAAA;EAAA,SAChB,gBAAA;EAAA,SACA,YAAA,WAFgB;EAAA,SAIhB,SAAA,
|
|
1
|
+
{"version":3,"file":"TriggerPopoverResource.d.ts","names":[],"sources":["../../../../src/primitives/composer/trigger/TriggerPopoverResource.ts"],"mappings":";;;;;;;KAqBY,gBAAA,GAAmB,eAAe;AAAA,KAElC,4BAAA;EAAA,SACD,IAAA;EAAA,SACA,KAAA;EAAA,SACA,gBAAA;EAAA,SACA,UAAA,WAAqB,wBAAA;EAAA,SACrB,KAAA,WAAgB,oBAAA;EAAA,SAChB,gBAAA;EAAA,SACA,YAAA,WAFgB;EAAA,SAIhB,SAAA,WAmBsB;EAAA,SAjBtB,SAAA,UAiBwC;EAAA,SAfxC,iBAAA;EAET,cAAA,CAAe,UAAA;EACf,MAAA;EACA,UAAA,CAAW,IAAA,EAAM,oBAAA;EACjB,KAAA,UAbS;EAeT,cAAA,CAAe,KAAA;EACf,aAAA,CAAc,CAAA;IAAA,SACH,GAAA;IAAA,SACA,QAAA;IACT,cAAA;EAAA;EAGF,iBAAA,CAAkB,GAAA;EAClB,0BAAA,CAA2B,EAAA,EAAI,kBAAA;AAAA;AAAA,cA2FpB,sBAAA,8BAAsB,QAAA,CAAA,4BAAA;WA9ExB,uBAAA;;;YAGC,eAAA;OACL,eAAA,EAxBL"}
|
|
@@ -8,8 +8,8 @@ import { resource, useResource } from "@assistant-ui/tap";
|
|
|
8
8
|
//#region src/primitives/composer/trigger/TriggerPopoverResource.ts
|
|
9
9
|
/** Composes detection, navigation, keyboard, and selection sub-resources. */
|
|
10
10
|
const useTriggerPopoverResource = (t0) => {
|
|
11
|
-
const $ = c(
|
|
12
|
-
const { adapter, text, triggerChar, behavior, aui, popoverId } = t0;
|
|
11
|
+
const $ = c(46);
|
|
12
|
+
const { adapter, text, triggerChar, behavior, aui, popoverId, isLoading } = t0;
|
|
13
13
|
let t1;
|
|
14
14
|
if ($[0] !== text || $[1] !== triggerChar) {
|
|
15
15
|
t1 = TriggerDetectionResource({
|
|
@@ -91,7 +91,7 @@ const useTriggerPopoverResource = (t0) => {
|
|
|
91
91
|
} else t5 = $[26];
|
|
92
92
|
const keyboard = useResource(t5);
|
|
93
93
|
let t6;
|
|
94
|
-
if ($[27] !== detection.query || $[28] !== detection.setCursorPosition || $[29] !== keyboard.handleKeyDown || $[
|
|
94
|
+
if ($[27] !== detection.query || $[28] !== detection.setCursorPosition || $[29] !== isLoading || $[30] !== keyboard.handleKeyDown || $[31] !== keyboard.highlightIndex || $[32] !== keyboard.highlightedIndex || $[33] !== keyboard.highlightedItemId || $[34] !== navigation.activeCategoryId || $[35] !== navigation.categories || $[36] !== navigation.goBack || $[37] !== navigation.isSearchMode || $[38] !== navigation.items || $[39] !== navigation.selectCategory || $[40] !== open || $[41] !== popoverId || $[42] !== selection.close || $[43] !== selection.registerSelectItemOverride || $[44] !== selection.selectItem) {
|
|
95
95
|
t6 = {
|
|
96
96
|
open,
|
|
97
97
|
query: detection.query,
|
|
@@ -100,6 +100,7 @@ const useTriggerPopoverResource = (t0) => {
|
|
|
100
100
|
items: navigation.items,
|
|
101
101
|
highlightedIndex: keyboard.highlightedIndex,
|
|
102
102
|
isSearchMode: navigation.isSearchMode,
|
|
103
|
+
isLoading,
|
|
103
104
|
popoverId,
|
|
104
105
|
highlightedItemId: keyboard.highlightedItemId,
|
|
105
106
|
selectCategory: navigation.selectCategory,
|
|
@@ -113,23 +114,24 @@ const useTriggerPopoverResource = (t0) => {
|
|
|
113
114
|
};
|
|
114
115
|
$[27] = detection.query;
|
|
115
116
|
$[28] = detection.setCursorPosition;
|
|
116
|
-
$[29] =
|
|
117
|
-
$[30] = keyboard.
|
|
118
|
-
$[31] = keyboard.
|
|
119
|
-
$[32] = keyboard.
|
|
120
|
-
$[33] =
|
|
121
|
-
$[34] = navigation.
|
|
122
|
-
$[35] = navigation.
|
|
123
|
-
$[36] = navigation.
|
|
124
|
-
$[37] = navigation.
|
|
125
|
-
$[38] = navigation.
|
|
126
|
-
$[39] =
|
|
127
|
-
$[40] =
|
|
128
|
-
$[41] =
|
|
129
|
-
$[42] = selection.
|
|
130
|
-
$[43] = selection.
|
|
131
|
-
$[44] =
|
|
132
|
-
|
|
117
|
+
$[29] = isLoading;
|
|
118
|
+
$[30] = keyboard.handleKeyDown;
|
|
119
|
+
$[31] = keyboard.highlightIndex;
|
|
120
|
+
$[32] = keyboard.highlightedIndex;
|
|
121
|
+
$[33] = keyboard.highlightedItemId;
|
|
122
|
+
$[34] = navigation.activeCategoryId;
|
|
123
|
+
$[35] = navigation.categories;
|
|
124
|
+
$[36] = navigation.goBack;
|
|
125
|
+
$[37] = navigation.isSearchMode;
|
|
126
|
+
$[38] = navigation.items;
|
|
127
|
+
$[39] = navigation.selectCategory;
|
|
128
|
+
$[40] = open;
|
|
129
|
+
$[41] = popoverId;
|
|
130
|
+
$[42] = selection.close;
|
|
131
|
+
$[43] = selection.registerSelectItemOverride;
|
|
132
|
+
$[44] = selection.selectItem;
|
|
133
|
+
$[45] = t6;
|
|
134
|
+
} else t6 = $[45];
|
|
133
135
|
return t6;
|
|
134
136
|
};
|
|
135
137
|
const TriggerPopoverResource = resource(useTriggerPopoverResource);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TriggerPopoverResource.js","names":["useEffectEvent","useResource","resource","Unstable_TriggerAdapter","Unstable_TriggerCategory","Unstable_TriggerItem","AssistantClient","TriggerDetectionResource","TriggerKeyboardResource","TriggerNavigationResource","TriggerSelectionResource","SelectItemOverride","TriggerBehavior","TriggerPopoverKeyEvent","OnSelectBehavior","TriggerPopoverResourceOutput","open","query","activeCategoryId","categories","items","highlightedIndex","isSearchMode","popoverId","highlightedItemId","selectCategory","categoryId","goBack","selectItem","item","close","highlightIndex","index","handleKeyDown","e","key","shiftKey","preventDefault","setCursorPosition","pos","registerSelectItemOverride","fn","useTriggerPopoverResource","t0","$","_c","adapter","text","triggerChar","behavior","aui","t1","detection","trigger","undefined","t2","navigation","t3","onSelected","t4","selection","t5","navigableList","keyboard","t6","TriggerPopoverResource"],"sources":["../../../../src/primitives/composer/trigger/TriggerPopoverResource.ts"],"sourcesContent":["import { useEffectEvent } from \"react\";\nimport { useResource, resource } from \"@assistant-ui/tap\";\nimport type {\n Unstable_TriggerAdapter,\n Unstable_TriggerCategory,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\nimport type { AssistantClient } from \"@assistant-ui/store\";\nimport { TriggerDetectionResource } from \"./triggerDetectionResource\";\nimport { TriggerKeyboardResource } from \"./triggerKeyboardResource\";\nimport { TriggerNavigationResource } from \"./triggerNavigationResource\";\nimport {\n TriggerSelectionResource,\n type SelectItemOverride,\n type TriggerBehavior,\n} from \"./triggerSelectionResource\";\n\nexport type { SelectItemOverride, TriggerBehavior };\nexport type { TriggerPopoverKeyEvent } from \"./triggerKeyboardResource\";\n\n/** @deprecated Use `TriggerBehavior`. */\nexport type OnSelectBehavior = TriggerBehavior;\n\nexport type TriggerPopoverResourceOutput = {\n readonly open: boolean;\n readonly query: string;\n readonly activeCategoryId: string | null;\n readonly categories: readonly Unstable_TriggerCategory[];\n readonly items: readonly Unstable_TriggerItem[];\n readonly highlightedIndex: number;\n readonly isSearchMode: boolean;\n /** Stable ID prefix for generating accessible element IDs. */\n readonly popoverId: string;\n /** ID of the currently highlighted item (for aria-activedescendant). */\n readonly highlightedItemId: string | undefined;\n\n selectCategory(categoryId: string): void;\n goBack(): void;\n selectItem(item: Unstable_TriggerItem): void;\n close(): void;\n /** Move the highlight to an entry index (e.g. from pointer hover). Out-of-range values are ignored. */\n highlightIndex(index: number): void;\n handleKeyDown(e: {\n readonly key: string;\n readonly shiftKey: boolean;\n preventDefault(): void;\n }): boolean;\n\n setCursorPosition(pos: number): void;\n registerSelectItemOverride(fn: SelectItemOverride): () => void;\n};\n\n/** Composes detection, navigation, keyboard, and selection sub-resources. */\nconst useTriggerPopoverResource = ({\n adapter,\n text,\n triggerChar,\n behavior,\n aui,\n popoverId,\n}: {\n adapter: Unstable_TriggerAdapter | undefined;\n text: string;\n triggerChar: string;\n behavior: TriggerBehavior | undefined;\n aui: AssistantClient;\n /** Stable ID for accessible element IDs (pass React's useId() from component layer). */\n popoverId: string;\n}): TriggerPopoverResourceOutput => {\n const detection = useResource(\n TriggerDetectionResource({ text, triggerChar }),\n );\n\n const open =\n detection.trigger !== null &&\n adapter !== undefined &&\n behavior !== undefined;\n\n const navigation = useResource(\n TriggerNavigationResource({\n adapter,\n query: detection.query,\n open,\n }),\n );\n\n const onSelected = useEffectEvent(() => {\n navigation.goBack();\n });\n\n const selection = useResource(\n TriggerSelectionResource({\n behavior,\n trigger: detection.trigger,\n aui,\n triggerChar,\n setCursorPosition: detection.setCursorPosition,\n onSelected,\n }),\n );\n\n const keyboard = useResource(\n TriggerKeyboardResource({\n navigableList: navigation.navigableList,\n isSearchMode: navigation.isSearchMode,\n activeCategoryId: navigation.activeCategoryId,\n query: detection.query,\n popoverId,\n open,\n selectItem: selection.selectItem,\n selectCategory: navigation.selectCategory,\n goBack: navigation.goBack,\n close: selection.close,\n }),\n );\n\n return {\n open,\n query: detection.query,\n activeCategoryId: navigation.activeCategoryId,\n categories: navigation.categories,\n items: navigation.items,\n highlightedIndex: keyboard.highlightedIndex,\n isSearchMode: navigation.isSearchMode,\n popoverId,\n highlightedItemId: keyboard.highlightedItemId,\n selectCategory: navigation.selectCategory,\n goBack: navigation.goBack,\n selectItem: selection.selectItem,\n close: selection.close,\n highlightIndex: keyboard.highlightIndex,\n handleKeyDown: keyboard.handleKeyDown,\n setCursorPosition: detection.setCursorPosition,\n registerSelectItemOverride: selection.registerSelectItemOverride,\n };\n};\n\nexport const TriggerPopoverResource = resource(useTriggerPopoverResource);\n"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"TriggerPopoverResource.js","names":["useEffectEvent","useResource","resource","Unstable_TriggerAdapter","Unstable_TriggerCategory","Unstable_TriggerItem","AssistantClient","TriggerDetectionResource","TriggerKeyboardResource","TriggerNavigationResource","TriggerSelectionResource","SelectItemOverride","TriggerBehavior","TriggerPopoverKeyEvent","OnSelectBehavior","TriggerPopoverResourceOutput","open","query","activeCategoryId","categories","items","highlightedIndex","isSearchMode","isLoading","popoverId","highlightedItemId","selectCategory","categoryId","goBack","selectItem","item","close","highlightIndex","index","handleKeyDown","e","key","shiftKey","preventDefault","setCursorPosition","pos","registerSelectItemOverride","fn","useTriggerPopoverResource","t0","$","_c","adapter","text","triggerChar","behavior","aui","t1","detection","trigger","undefined","t2","navigation","t3","onSelected","t4","selection","t5","navigableList","keyboard","t6","TriggerPopoverResource"],"sources":["../../../../src/primitives/composer/trigger/TriggerPopoverResource.ts"],"sourcesContent":["import { useEffectEvent } from \"react\";\nimport { useResource, resource } from \"@assistant-ui/tap\";\nimport type {\n Unstable_TriggerAdapter,\n Unstable_TriggerCategory,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\nimport type { AssistantClient } from \"@assistant-ui/store\";\nimport { TriggerDetectionResource } from \"./triggerDetectionResource\";\nimport { TriggerKeyboardResource } from \"./triggerKeyboardResource\";\nimport { TriggerNavigationResource } from \"./triggerNavigationResource\";\nimport {\n TriggerSelectionResource,\n type SelectItemOverride,\n type TriggerBehavior,\n} from \"./triggerSelectionResource\";\n\nexport type { SelectItemOverride, TriggerBehavior };\nexport type { TriggerPopoverKeyEvent } from \"./triggerKeyboardResource\";\n\n/** @deprecated Use `TriggerBehavior`. */\nexport type OnSelectBehavior = TriggerBehavior;\n\nexport type TriggerPopoverResourceOutput = {\n readonly open: boolean;\n readonly query: string;\n readonly activeCategoryId: string | null;\n readonly categories: readonly Unstable_TriggerCategory[];\n readonly items: readonly Unstable_TriggerItem[];\n readonly highlightedIndex: number;\n readonly isSearchMode: boolean;\n /** Whether the adapter is currently resolving items (async sources). */\n readonly isLoading: boolean;\n /** Stable ID prefix for generating accessible element IDs. */\n readonly popoverId: string;\n /** ID of the currently highlighted item (for aria-activedescendant). */\n readonly highlightedItemId: string | undefined;\n\n selectCategory(categoryId: string): void;\n goBack(): void;\n selectItem(item: Unstable_TriggerItem): void;\n close(): void;\n /** Move the highlight to an entry index (e.g. from pointer hover). Out-of-range values are ignored. */\n highlightIndex(index: number): void;\n handleKeyDown(e: {\n readonly key: string;\n readonly shiftKey: boolean;\n preventDefault(): void;\n }): boolean;\n\n setCursorPosition(pos: number): void;\n registerSelectItemOverride(fn: SelectItemOverride): () => void;\n};\n\n/** Composes detection, navigation, keyboard, and selection sub-resources. */\nconst useTriggerPopoverResource = ({\n adapter,\n text,\n triggerChar,\n behavior,\n aui,\n popoverId,\n isLoading,\n}: {\n adapter: Unstable_TriggerAdapter | undefined;\n text: string;\n triggerChar: string;\n behavior: TriggerBehavior | undefined;\n aui: AssistantClient;\n /** Stable ID for accessible element IDs (pass React's useId() from component layer). */\n popoverId: string;\n isLoading: boolean;\n}): TriggerPopoverResourceOutput => {\n const detection = useResource(\n TriggerDetectionResource({ text, triggerChar }),\n );\n\n const open =\n detection.trigger !== null &&\n adapter !== undefined &&\n behavior !== undefined;\n\n const navigation = useResource(\n TriggerNavigationResource({\n adapter,\n query: detection.query,\n open,\n }),\n );\n\n const onSelected = useEffectEvent(() => {\n navigation.goBack();\n });\n\n const selection = useResource(\n TriggerSelectionResource({\n behavior,\n trigger: detection.trigger,\n aui,\n triggerChar,\n setCursorPosition: detection.setCursorPosition,\n onSelected,\n }),\n );\n\n const keyboard = useResource(\n TriggerKeyboardResource({\n navigableList: navigation.navigableList,\n isSearchMode: navigation.isSearchMode,\n activeCategoryId: navigation.activeCategoryId,\n query: detection.query,\n popoverId,\n open,\n selectItem: selection.selectItem,\n selectCategory: navigation.selectCategory,\n goBack: navigation.goBack,\n close: selection.close,\n }),\n );\n\n return {\n open,\n query: detection.query,\n activeCategoryId: navigation.activeCategoryId,\n categories: navigation.categories,\n items: navigation.items,\n highlightedIndex: keyboard.highlightedIndex,\n isSearchMode: navigation.isSearchMode,\n isLoading,\n popoverId,\n highlightedItemId: keyboard.highlightedItemId,\n selectCategory: navigation.selectCategory,\n goBack: navigation.goBack,\n selectItem: selection.selectItem,\n close: selection.close,\n highlightIndex: keyboard.highlightIndex,\n handleKeyDown: keyboard.handleKeyDown,\n setCursorPosition: detection.setCursorPosition,\n registerSelectItemOverride: selection.registerSelectItemOverride,\n };\n};\n\nexport const TriggerPopoverResource = resource(useTriggerPopoverResource);\n"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@ declare const ComposerPrimitiveTriggerPopover: import("react").ForwardRefExoticC
|
|
|
15
15
|
} & import("react").RefAttributes<HTMLDivElement>, "ref">, "onSelect"> & {
|
|
16
16
|
readonly char: string;
|
|
17
17
|
readonly adapter?: import("@assistant-ui/core").Unstable_TriggerAdapter | undefined;
|
|
18
|
+
readonly isLoading?: boolean | undefined;
|
|
18
19
|
} & import("react").RefAttributes<HTMLDivElement>> & {
|
|
19
20
|
Directive: import("react").FC<ComposerPrimitiveTriggerPopoverDirective.Props>;
|
|
20
21
|
Action: import("react").FC<ComposerPrimitiveTriggerPopoverAction.Props>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Unstable_TriggerAdapter, Unstable_TriggerItem } from "@assistant-ui/core";
|
|
2
|
+
|
|
3
|
+
//#region src/unstable/useLiveCompletionAdapter.d.ts
|
|
4
|
+
type Unstable_UseLiveCompletionAdapterOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Fetches the items for a query from an async source. Called debounced; the
|
|
7
|
+
* resolved items are cached and returned synchronously to the popover on the
|
|
8
|
+
* next render.
|
|
9
|
+
*/
|
|
10
|
+
readonly fetcher: (query: string) => Promise<readonly Unstable_TriggerItem[]>; /** Debounce applied before a fetch fires, in milliseconds. @default 60 */
|
|
11
|
+
readonly debounceMs?: number | undefined; /** When `false`, no fetch is scheduled and the adapter stays empty. @default true */
|
|
12
|
+
readonly enabled?: boolean | undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated Under active development and may change without notice.
|
|
16
|
+
*
|
|
17
|
+
* Bridges an async completion source (a server search, a gateway RPC) into the
|
|
18
|
+
* synchronous `Unstable_TriggerAdapter` that `ComposerTriggerPopover` consumes.
|
|
19
|
+
* `search(query)` returns the last fetched items synchronously and schedules a
|
|
20
|
+
* debounced fetch when the query changes; when results arrive the returned
|
|
21
|
+
* `adapter` identity changes, which re-runs the popover's lookup so the fresh
|
|
22
|
+
* items render. This is a search-only adapter (`categories` are empty).
|
|
23
|
+
*
|
|
24
|
+
* `isLoading` is `true` while a fetch is in flight. Pass it to the popover's
|
|
25
|
+
* `isLoading` prop to render a loading state.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const mentions = unstable_useLiveCompletionAdapter({
|
|
30
|
+
* fetcher: (query) => searchUsers(query),
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* <ComposerTriggerPopover
|
|
34
|
+
* char="@"
|
|
35
|
+
* adapter={mentions.adapter}
|
|
36
|
+
* isLoading={mentions.isLoading}
|
|
37
|
+
* directive={{ onInserted }}
|
|
38
|
+
* />
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare function unstable_useLiveCompletionAdapter(options: Unstable_UseLiveCompletionAdapterOptions): {
|
|
42
|
+
adapter: Unstable_TriggerAdapter;
|
|
43
|
+
isLoading: boolean;
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { Unstable_UseLiveCompletionAdapterOptions, unstable_useLiveCompletionAdapter };
|
|
47
|
+
//# sourceMappingURL=useLiveCompletionAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLiveCompletionAdapter.d.ts","names":[],"sources":["../../src/unstable/useLiveCompletionAdapter.ts"],"mappings":";;;KAQY,wCAAA;;AAAZ;;;;WAMW,OAAA,GAAU,KAAA,aAAkB,OAAO,UAAU,oBAAA,KAAnC;EAAA,SAEV,UAAA,uBAF6C;EAAA,SAI7C,OAAA;AAAA;;AAAO;AAiClB;;;;;;;;;;AAEgD;;;;;;;;;;;;;;;iBAFhC,iCAAA,CACd,OAAA,EAAS,wCAAA;EACN,OAAA,EAAS,uBAAuB;EAAE,SAAA;AAAA"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "@assistant-ui/tap/react-shim";
|
|
3
|
+
//#region src/unstable/useLiveCompletionAdapter.ts
|
|
4
|
+
/** Sentinel that no real query (including the empty string) equals, so the first query always fetches. */
|
|
5
|
+
const NO_QUERY = "\0";
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Under active development and may change without notice.
|
|
8
|
+
*
|
|
9
|
+
* Bridges an async completion source (a server search, a gateway RPC) into the
|
|
10
|
+
* synchronous `Unstable_TriggerAdapter` that `ComposerTriggerPopover` consumes.
|
|
11
|
+
* `search(query)` returns the last fetched items synchronously and schedules a
|
|
12
|
+
* debounced fetch when the query changes; when results arrive the returned
|
|
13
|
+
* `adapter` identity changes, which re-runs the popover's lookup so the fresh
|
|
14
|
+
* items render. This is a search-only adapter (`categories` are empty).
|
|
15
|
+
*
|
|
16
|
+
* `isLoading` is `true` while a fetch is in flight. Pass it to the popover's
|
|
17
|
+
* `isLoading` prop to render a loading state.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const mentions = unstable_useLiveCompletionAdapter({
|
|
22
|
+
* fetcher: (query) => searchUsers(query),
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* <ComposerTriggerPopover
|
|
26
|
+
* char="@"
|
|
27
|
+
* adapter={mentions.adapter}
|
|
28
|
+
* isLoading={mentions.isLoading}
|
|
29
|
+
* directive={{ onInserted }}
|
|
30
|
+
* />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function unstable_useLiveCompletionAdapter(options) {
|
|
34
|
+
const { fetcher, debounceMs = 60, enabled = true } = options;
|
|
35
|
+
const [state, setState] = useState({
|
|
36
|
+
query: NO_QUERY,
|
|
37
|
+
items: []
|
|
38
|
+
});
|
|
39
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
40
|
+
const fetcherRef = useRef(fetcher);
|
|
41
|
+
fetcherRef.current = fetcher;
|
|
42
|
+
const timerRef = useRef(null);
|
|
43
|
+
const tokenRef = useRef(0);
|
|
44
|
+
const pendingQueryRef = useRef(null);
|
|
45
|
+
const cancelTimer = useCallback(() => {
|
|
46
|
+
if (timerRef.current !== null) {
|
|
47
|
+
clearTimeout(timerRef.current);
|
|
48
|
+
timerRef.current = null;
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
const scheduleFetch = useCallback((query) => {
|
|
52
|
+
if (!enabled) return;
|
|
53
|
+
if (pendingQueryRef.current === query) return;
|
|
54
|
+
pendingQueryRef.current = query;
|
|
55
|
+
cancelTimer();
|
|
56
|
+
const token = ++tokenRef.current;
|
|
57
|
+
setIsLoading(true);
|
|
58
|
+
timerRef.current = setTimeout(() => {
|
|
59
|
+
timerRef.current = null;
|
|
60
|
+
fetcherRef.current(query).then((items) => {
|
|
61
|
+
if (token !== tokenRef.current) return;
|
|
62
|
+
setState({
|
|
63
|
+
query,
|
|
64
|
+
items
|
|
65
|
+
});
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
}, () => {
|
|
68
|
+
if (token !== tokenRef.current) return;
|
|
69
|
+
setState({
|
|
70
|
+
query,
|
|
71
|
+
items: []
|
|
72
|
+
});
|
|
73
|
+
setIsLoading(false);
|
|
74
|
+
});
|
|
75
|
+
}, debounceMs);
|
|
76
|
+
}, [
|
|
77
|
+
enabled,
|
|
78
|
+
debounceMs,
|
|
79
|
+
cancelTimer
|
|
80
|
+
]);
|
|
81
|
+
const invalidatePending = useCallback(() => {
|
|
82
|
+
cancelTimer();
|
|
83
|
+
pendingQueryRef.current = null;
|
|
84
|
+
tokenRef.current += 1;
|
|
85
|
+
setIsLoading(false);
|
|
86
|
+
}, [cancelTimer]);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (enabled) return;
|
|
89
|
+
invalidatePending();
|
|
90
|
+
setState((s) => s.query === NO_QUERY ? s : {
|
|
91
|
+
query: NO_QUERY,
|
|
92
|
+
items: []
|
|
93
|
+
});
|
|
94
|
+
}, [enabled, invalidatePending]);
|
|
95
|
+
useEffect(() => cancelTimer, [cancelTimer]);
|
|
96
|
+
return {
|
|
97
|
+
adapter: useMemo(() => ({
|
|
98
|
+
categories: () => [],
|
|
99
|
+
categoryItems: () => [],
|
|
100
|
+
search: (query) => {
|
|
101
|
+
if (query !== state.query) queueMicrotask(() => scheduleFetch(query));
|
|
102
|
+
else if (pendingQueryRef.current !== null && pendingQueryRef.current !== query) queueMicrotask(invalidatePending);
|
|
103
|
+
return state.items;
|
|
104
|
+
}
|
|
105
|
+
}), [
|
|
106
|
+
state,
|
|
107
|
+
scheduleFetch,
|
|
108
|
+
invalidatePending
|
|
109
|
+
]),
|
|
110
|
+
isLoading
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { unstable_useLiveCompletionAdapter };
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=useLiveCompletionAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLiveCompletionAdapter.js","names":["useCallback","useEffect","useMemo","useRef","useState","Unstable_TriggerAdapter","Unstable_TriggerItem","Unstable_UseLiveCompletionAdapterOptions","fetcher","query","Promise","debounceMs","enabled","NO_QUERY","unstable_useLiveCompletionAdapter","options","adapter","isLoading","state","setState","items","setIsLoading","fetcherRef","current","timerRef","ReturnType","setTimeout","tokenRef","pendingQueryRef","cancelTimer","clearTimeout","scheduleFetch","token","then","invalidatePending","s","categories","categoryItems","search","queueMicrotask"],"sources":["../../src/unstable/useLiveCompletionAdapter.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n Unstable_TriggerAdapter,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\n\nexport type Unstable_UseLiveCompletionAdapterOptions = {\n /**\n * Fetches the items for a query from an async source. Called debounced; the\n * resolved items are cached and returned synchronously to the popover on the\n * next render.\n */\n readonly fetcher: (query: string) => Promise<readonly Unstable_TriggerItem[]>;\n /** Debounce applied before a fetch fires, in milliseconds. @default 60 */\n readonly debounceMs?: number | undefined;\n /** When `false`, no fetch is scheduled and the adapter stays empty. @default true */\n readonly enabled?: boolean | undefined;\n};\n\n/** Sentinel that no real query (including the empty string) equals, so the first query always fetches. */\nconst NO_QUERY = \"\\u0000\";\n\n/**\n * @deprecated Under active development and may change without notice.\n *\n * Bridges an async completion source (a server search, a gateway RPC) into the\n * synchronous `Unstable_TriggerAdapter` that `ComposerTriggerPopover` consumes.\n * `search(query)` returns the last fetched items synchronously and schedules a\n * debounced fetch when the query changes; when results arrive the returned\n * `adapter` identity changes, which re-runs the popover's lookup so the fresh\n * items render. This is a search-only adapter (`categories` are empty).\n *\n * `isLoading` is `true` while a fetch is in flight. Pass it to the popover's\n * `isLoading` prop to render a loading state.\n *\n * @example\n * ```tsx\n * const mentions = unstable_useLiveCompletionAdapter({\n * fetcher: (query) => searchUsers(query),\n * });\n *\n * <ComposerTriggerPopover\n * char=\"@\"\n * adapter={mentions.adapter}\n * isLoading={mentions.isLoading}\n * directive={{ onInserted }}\n * />\n * ```\n */\nexport function unstable_useLiveCompletionAdapter(\n options: Unstable_UseLiveCompletionAdapterOptions,\n): { adapter: Unstable_TriggerAdapter; isLoading: boolean } {\n const { fetcher, debounceMs = 60, enabled = true } = options;\n\n const [state, setState] = useState<{\n query: string;\n items: readonly Unstable_TriggerItem[];\n }>({ query: NO_QUERY, items: [] });\n const [isLoading, setIsLoading] = useState(false);\n\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const tokenRef = useRef(0);\n const pendingQueryRef = useRef<string | null>(null);\n\n const cancelTimer = useCallback(() => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }, []);\n\n const scheduleFetch = useCallback(\n (query: string) => {\n if (!enabled) return;\n if (pendingQueryRef.current === query) return;\n pendingQueryRef.current = query;\n cancelTimer();\n const token = ++tokenRef.current;\n setIsLoading(true);\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n fetcherRef.current(query).then(\n (items) => {\n if (token !== tokenRef.current) return;\n setState({ query, items });\n setIsLoading(false);\n },\n () => {\n if (token !== tokenRef.current) return;\n setState({ query, items: [] });\n setIsLoading(false);\n },\n );\n }, debounceMs);\n },\n [enabled, debounceMs, cancelTimer],\n );\n\n const invalidatePending = useCallback(() => {\n cancelTimer();\n pendingQueryRef.current = null;\n tokenRef.current += 1;\n setIsLoading(false);\n }, [cancelTimer]);\n\n useEffect(() => {\n if (enabled) return;\n invalidatePending();\n setState((s) =>\n s.query === NO_QUERY ? s : { query: NO_QUERY, items: [] },\n );\n }, [enabled, invalidatePending]);\n\n useEffect(() => cancelTimer, [cancelTimer]);\n\n const adapter = useMemo<Unstable_TriggerAdapter>(\n () => ({\n categories: () => [],\n categoryItems: () => [],\n search: (query: string) => {\n // search() runs inside the popover's render; defer state updates with\n // queueMicrotask so they are not dispatched while another component renders.\n if (query !== state.query) {\n queueMicrotask(() => scheduleFetch(query));\n } else if (\n pendingQueryRef.current !== null &&\n pendingQueryRef.current !== query\n ) {\n // the query returned to a cached value while a fetch for a different\n // query is in flight; drop it so its result cannot overwrite the cache\n queueMicrotask(invalidatePending);\n }\n return state.items;\n },\n }),\n [state, scheduleFetch, invalidatePending],\n );\n\n return { adapter, isLoading };\n}\n"],"mappings":";;;;AAsBA,MAAMa,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BjB,SAAgBC,kCACdC,SAC0D;CAC1D,MAAM,EAAEP,SAASG,aAAa,IAAIC,UAAU,SAASG;CAErD,MAAM,CAACG,OAAOC,YAAYf,SAGvB;EAAEK,OAAOI;EAAUO,OAAO,CAAA;CAAG,CAAC;CACjC,MAAM,CAACH,WAAWI,gBAAgBjB,SAAS,KAAK;CAEhD,MAAMkB,aAAanB,OAAOK,OAAO;CACjCc,WAAWC,UAAUf;CAErB,MAAMgB,WAAWrB,OAA6C,IAAI;CAClE,MAAMwB,WAAWxB,OAAO,CAAC;CACzB,MAAMyB,kBAAkBzB,OAAsB,IAAI;CAElD,MAAM0B,cAAc7B,kBAAkB;EACpC,IAAIwB,SAASD,YAAY,MAAM;GAC7BO,aAAaN,SAASD,OAAO;GAC7BC,SAASD,UAAU;EACrB;CACF,GAAG,CAAA,CAAE;CAEL,MAAMQ,gBAAgB/B,aACnBS,UAAkB;EACjB,IAAI,CAACG,SAAS;EACd,IAAIgB,gBAAgBL,YAAYd,OAAO;EACvCmB,gBAAgBL,UAAUd;EAC1BoB,YAAY;EACZ,MAAMG,QAAQ,EAAEL,SAASJ;EACzBF,aAAa,IAAI;EACjBG,SAASD,UAAUG,iBAAiB;GAClCF,SAASD,UAAU;GACnBD,WAAWC,QAAQd,KAAK,CAAC,CAACwB,MACvBb,UAAU;IACT,IAAIY,UAAUL,SAASJ,SAAS;IAChCJ,SAAS;KAAEV;KAAOW;IAAM,CAAC;IACzBC,aAAa,KAAK;GACpB,SACM;IACJ,IAAIW,UAAUL,SAASJ,SAAS;IAChCJ,SAAS;KAAEV;KAAOW,OAAO,CAAA;IAAG,CAAC;IAC7BC,aAAa,KAAK;GACpB,CACF;EACF,GAAGV,UAAU;CACf,GACA;EAACC;EAASD;EAAYkB;CAAW,CACnC;CAEA,MAAMK,oBAAoBlC,kBAAkB;EAC1C6B,YAAY;EACZD,gBAAgBL,UAAU;EAC1BI,SAASJ,WAAW;EACpBF,aAAa,KAAK;CACpB,GAAG,CAACQ,WAAW,CAAC;CAEhB5B,gBAAgB;EACd,IAAIW,SAAS;EACbsB,kBAAkB;EAClBf,UAAUgB,MACRA,EAAE1B,UAAUI,WAAWsB,IAAI;GAAE1B,OAAOI;GAAUO,OAAO,CAAA;EAAG,CAC1D;CACF,GAAG,CAACR,SAASsB,iBAAiB,CAAC;CAE/BjC,gBAAgB4B,aAAa,CAACA,WAAW,CAAC;CAyB1C,OAAO;EAAEb,SAvBOd,eACP;GACLkC,kBAAkB,CAAA;GAClBC,qBAAqB,CAAA;GACrBC,SAAS7B,UAAkB;IAGzB,IAAIA,UAAUS,MAAMT,OAClB8B,qBAAqBR,cAActB,KAAK,CAAC;SACpC,IACLmB,gBAAgBL,YAAY,QAC5BK,gBAAgBL,YAAYd,OAI5B8B,eAAeL,iBAAiB;IAElC,OAAOhB,MAAME;GACf;EACF,IACA;GAACF;GAAOa;GAAeG;EAAiB,CAGjClB;EAASC;CAAU;AAC9B"}
|
|
@@ -22,6 +22,14 @@ type SmoothOptions = {
|
|
|
22
22
|
* @default Infinity
|
|
23
23
|
*/
|
|
24
24
|
maxCharsPerFrame?: number | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Minimum time in milliseconds between committed updates. The reveal keeps
|
|
27
|
+
* advancing every frame, but the visible text (and the downstream re-render
|
|
28
|
+
* and markdown re-parse it triggers) is committed at most once per interval.
|
|
29
|
+
* The final frame always commits. `0` commits every frame.
|
|
30
|
+
* @default 0
|
|
31
|
+
*/
|
|
32
|
+
minCommitMs?: number | undefined;
|
|
25
33
|
};
|
|
26
34
|
/**
|
|
27
35
|
* Animates streamed message part text with a typewriter-style reveal.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;;;AAiBA;KAAY,aAAA;;;;;;EAMV,OAAA;
|
|
1
|
+
{"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;;;AAiBA;KAAY,aAAA;;;;;;EAMV,OAAA;EAmBW;AAAA;AAsGb;;;EAnHE,iBAAA;EAoH2B;;;;EA/G3B,gBAAA;EAiHuC;;;;;;;EAzGvC,WAAA;AAAA;;;;;AAyG2D;;;;;;;;;;;;cAHhD,SAAA,GACX,KAAA,EAAO,gBAAA,IAAoB,eAAA,GAAkB,oBAAA,GAC7C,MAAA,aAAkB,aAAA,KACjB,gBAAA,IAAoB,eAAA,GAAkB,oBAAA"}
|
|
@@ -12,10 +12,12 @@ var TextStreamAnimator = class {
|
|
|
12
12
|
setText;
|
|
13
13
|
animationFrameId = null;
|
|
14
14
|
lastUpdateTime = Date.now();
|
|
15
|
+
lastCommitTime = 0;
|
|
15
16
|
targetText = "";
|
|
16
17
|
drainMs = DEFAULT_DRAIN_MS;
|
|
17
18
|
maxCharIntervalMs = DEFAULT_MAX_CHAR_INTERVAL_MS;
|
|
18
19
|
maxCharsPerFrame = Infinity;
|
|
20
|
+
minCommitMs = 0;
|
|
19
21
|
constructor(currentText, setText) {
|
|
20
22
|
this.currentText = currentText;
|
|
21
23
|
this.setText = setText;
|
|
@@ -48,7 +50,10 @@ var TextStreamAnimator = class {
|
|
|
48
50
|
if (charsToAdd === 0) return;
|
|
49
51
|
this.currentText = this.targetText.slice(0, this.currentText.length + charsToAdd);
|
|
50
52
|
this.lastUpdateTime = currentTime - timeToConsume;
|
|
51
|
-
this.
|
|
53
|
+
if (charsToAdd === remainingChars || currentTime - this.lastCommitTime >= this.minCommitMs) {
|
|
54
|
+
this.lastCommitTime = currentTime;
|
|
55
|
+
this.setText(this.currentText);
|
|
56
|
+
}
|
|
52
57
|
};
|
|
53
58
|
};
|
|
54
59
|
const SMOOTH_STATUS = Object.freeze({ type: "running" });
|
|
@@ -76,6 +81,7 @@ const useSmooth = (state, smooth = false) => {
|
|
|
76
81
|
const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);
|
|
77
82
|
const maxCharIntervalMs = positiveOr(options?.maxCharIntervalMs, DEFAULT_MAX_CHAR_INTERVAL_MS);
|
|
78
83
|
const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);
|
|
84
|
+
const minCommitMs = positiveOr(options?.minCommitMs, 0);
|
|
79
85
|
const [displayedText, setDisplayedText] = useState(state.status.type === "running" ? "" : text);
|
|
80
86
|
const aui = useAui();
|
|
81
87
|
const part = useAuiState(() => aui.part());
|
|
@@ -109,11 +115,13 @@ const useSmooth = (state, smooth = false) => {
|
|
|
109
115
|
animatorRef.drainMs = drainMs;
|
|
110
116
|
animatorRef.maxCharIntervalMs = maxCharIntervalMs;
|
|
111
117
|
animatorRef.maxCharsPerFrame = maxCharsPerFrame;
|
|
118
|
+
animatorRef.minCommitMs = minCommitMs;
|
|
112
119
|
}, [
|
|
113
120
|
animatorRef,
|
|
114
121
|
drainMs,
|
|
115
122
|
maxCharIntervalMs,
|
|
116
|
-
maxCharsPerFrame
|
|
123
|
+
maxCharsPerFrame,
|
|
124
|
+
minCommitMs
|
|
117
125
|
]);
|
|
118
126
|
const animatorPartRef = useRef(part);
|
|
119
127
|
useEffect(() => {
|
|
@@ -127,6 +135,7 @@ const useSmooth = (state, smooth = false) => {
|
|
|
127
135
|
if (state.status.type === "running") {
|
|
128
136
|
animatorRef.currentText = "";
|
|
129
137
|
animatorRef.targetText = text;
|
|
138
|
+
animatorRef.lastCommitTime = 0;
|
|
130
139
|
animatorRef.start();
|
|
131
140
|
} else {
|
|
132
141
|
animatorRef.currentText = text;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","Date","now","targetText","Infinity","constructor","currentText","setText","newText","start","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"@radix-ui/react-use-callback-ref\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\n/**\n * Tuning options for the smooth text streaming animation.\n */\nexport type SmoothOptions = {\n /**\n * Target time in milliseconds to drain the backlog of unrevealed\n * characters. Larger values reveal long backlogs more gradually.\n * @default 250\n */\n drainMs?: number | undefined;\n /**\n * Maximum time in milliseconds between revealed characters, i.e. the\n * slowest reveal rate when the backlog is short.\n * @default 5\n */\n maxCharIntervalMs?: number | undefined;\n /**\n * Maximum number of characters revealed per animation frame.\n * @default Infinity\n */\n maxCharsPerFrame?: number | undefined;\n};\n\nconst DEFAULT_DRAIN_MS = 250;\nconst DEFAULT_MAX_CHAR_INTERVAL_MS = 5;\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = Date.now();\n\n public targetText: string = \"\";\n public drainMs: number = DEFAULT_DRAIN_MS;\n public maxCharIntervalMs: number = DEFAULT_MAX_CHAR_INTERVAL_MS;\n public maxCharsPerFrame: number = Infinity;\n\n constructor(\n public currentText: string,\n private setText: (newText: string) => void,\n ) {}\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = Date.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = Date.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(\n this.maxCharIntervalMs,\n this.drainMs / remainingChars,\n );\n\n const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n // A cap-limited frame must not bank its surplus time, or the next\n // frame would burst past the cap.\n if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) {\n timeToConsume = 0;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n this.setText(this.currentText);\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nconst positiveOr = (value: number | undefined, fallback: number): number =>\n value !== undefined && value > 0 ? value : fallback;\n\n/**\n * Animates streamed message part text with a typewriter-style reveal.\n *\n * Takes the current part state and a `smooth` argument: `false` disables,\n * `true` uses the default rate, and a {@link SmoothOptions} object tunes\n * the reveal. Returns the part state with `text` replaced by the revealed\n * prefix and `status` reporting `running` until the reveal catches up.\n *\n * @example\n * ```tsx\n * const { text, status } = useSmooth(useMessagePartText(), {\n * drainMs: 500,\n * maxCharsPerFrame: 30,\n * });\n * ```\n */\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean | SmoothOptions = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null;\n const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);\n const maxCharIntervalMs = positiveOr(\n options?.maxCharIntervalMs,\n DEFAULT_MAX_CHAR_INTERVAL_MS,\n );\n const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part());\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n enabled && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, enabled, text, displayedText, state.status]);\n\n const [animatorRef] = useState<TextStreamAnimator>(\n new TextStreamAnimator(displayedText, setText),\n );\n\n useEffect(() => {\n animatorRef.drainMs = drainMs;\n animatorRef.maxCharIntervalMs = maxCharIntervalMs;\n animatorRef.maxCharsPerFrame = maxCharsPerFrame;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n // Discontinuity: part flipped, or new text breaks continuation\n // of the animator's current target. Either case requires\n // resetting the cursor — without the part check, a new part\n // whose text happens to share a prefix with the previous target\n // would keep the stale cursor and flicker.\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n }\n return;\n }\n\n animatorRef.targetText = text;\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n enabled\n ? {\n ...state,\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [enabled, displayedText, state, text],\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","minCommitMs","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","Date","now","lastCommitTime","targetText","Infinity","constructor","currentText","setText","newText","start","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","isComplete","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"@radix-ui/react-use-callback-ref\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\n/**\n * Tuning options for the smooth text streaming animation.\n */\nexport type SmoothOptions = {\n /**\n * Target time in milliseconds to drain the backlog of unrevealed\n * characters. Larger values reveal long backlogs more gradually.\n * @default 250\n */\n drainMs?: number | undefined;\n /**\n * Maximum time in milliseconds between revealed characters, i.e. the\n * slowest reveal rate when the backlog is short.\n * @default 5\n */\n maxCharIntervalMs?: number | undefined;\n /**\n * Maximum number of characters revealed per animation frame.\n * @default Infinity\n */\n maxCharsPerFrame?: number | undefined;\n /**\n * Minimum time in milliseconds between committed updates. The reveal keeps\n * advancing every frame, but the visible text (and the downstream re-render\n * and markdown re-parse it triggers) is committed at most once per interval.\n * The final frame always commits. `0` commits every frame.\n * @default 0\n */\n minCommitMs?: number | undefined;\n};\n\nconst DEFAULT_DRAIN_MS = 250;\nconst DEFAULT_MAX_CHAR_INTERVAL_MS = 5;\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = Date.now();\n public lastCommitTime: number = 0;\n\n public targetText: string = \"\";\n public drainMs: number = DEFAULT_DRAIN_MS;\n public maxCharIntervalMs: number = DEFAULT_MAX_CHAR_INTERVAL_MS;\n public maxCharsPerFrame: number = Infinity;\n public minCommitMs: number = 0;\n\n constructor(\n public currentText: string,\n private setText: (newText: string) => void,\n ) {}\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = Date.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = Date.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(\n this.maxCharIntervalMs,\n this.drainMs / remainingChars,\n );\n\n const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n // A cap-limited frame must not bank its surplus time, or the next\n // frame would burst past the cap.\n if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) {\n timeToConsume = 0;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n\n const isComplete = charsToAdd === remainingChars;\n if (isComplete || currentTime - this.lastCommitTime >= this.minCommitMs) {\n this.lastCommitTime = currentTime;\n this.setText(this.currentText);\n }\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nconst positiveOr = (value: number | undefined, fallback: number): number =>\n value !== undefined && value > 0 ? value : fallback;\n\n/**\n * Animates streamed message part text with a typewriter-style reveal.\n *\n * Takes the current part state and a `smooth` argument: `false` disables,\n * `true` uses the default rate, and a {@link SmoothOptions} object tunes\n * the reveal. Returns the part state with `text` replaced by the revealed\n * prefix and `status` reporting `running` until the reveal catches up.\n *\n * @example\n * ```tsx\n * const { text, status } = useSmooth(useMessagePartText(), {\n * drainMs: 500,\n * maxCharsPerFrame: 30,\n * });\n * ```\n */\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean | SmoothOptions = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null;\n const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);\n const maxCharIntervalMs = positiveOr(\n options?.maxCharIntervalMs,\n DEFAULT_MAX_CHAR_INTERVAL_MS,\n );\n const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);\n const minCommitMs = positiveOr(options?.minCommitMs, 0);\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part());\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n enabled && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, enabled, text, displayedText, state.status]);\n\n const [animatorRef] = useState<TextStreamAnimator>(\n new TextStreamAnimator(displayedText, setText),\n );\n\n useEffect(() => {\n animatorRef.drainMs = drainMs;\n animatorRef.maxCharIntervalMs = maxCharIntervalMs;\n animatorRef.maxCharsPerFrame = maxCharsPerFrame;\n animatorRef.minCommitMs = minCommitMs;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame, minCommitMs]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n // Discontinuity: part flipped, or new text breaks continuation\n // of the animator's current target. Either case requires\n // resetting the cursor — without the part check, a new part\n // whose text happens to share a prefix with the previous target\n // would keep the stale cursor and flicker.\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.lastCommitTime = 0;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n }\n return;\n }\n\n animatorRef.targetText = text;\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n enabled\n ? {\n ...state,\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [enabled, displayedText, state, text],\n );\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.22",
|
|
4
4
|
"description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
],
|
|
56
56
|
"sideEffects": false,
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@assistant-ui/core": "^0.2.
|
|
58
|
+
"@assistant-ui/core": "^0.2.17",
|
|
59
59
|
"@assistant-ui/store": "^0.2.18",
|
|
60
60
|
"@assistant-ui/tap": "^0.9.2",
|
|
61
61
|
"@radix-ui/primitive": "^1.1.4",
|
package/src/index.ts
CHANGED
|
@@ -124,6 +124,7 @@ export type {
|
|
|
124
124
|
|
|
125
125
|
// --- external-store ---
|
|
126
126
|
export type { ThreadMessageLike } from "@assistant-ui/core";
|
|
127
|
+
export { fromThreadMessageLike, generateId } from "@assistant-ui/core";
|
|
127
128
|
export {
|
|
128
129
|
getExternalStoreMessages,
|
|
129
130
|
bindExternalStoreMessage,
|
|
@@ -414,6 +415,12 @@ export {
|
|
|
414
415
|
type Unstable_UseSlashCommandAdapterOptions,
|
|
415
416
|
} from "./unstable/useSlashCommandAdapter";
|
|
416
417
|
|
|
418
|
+
// Unstable - live (async) completion adapter helper
|
|
419
|
+
export {
|
|
420
|
+
unstable_useLiveCompletionAdapter,
|
|
421
|
+
type Unstable_UseLiveCompletionAdapterOptions,
|
|
422
|
+
} from "./unstable/useLiveCompletionAdapter";
|
|
423
|
+
|
|
417
424
|
export type { ToolExecutionStatus } from "./internal";
|
|
418
425
|
|
|
419
426
|
// Unstable - trigger popover (unified root for @ mentions, / slash commands, etc.)
|
package/src/internal.ts
CHANGED
|
@@ -72,6 +72,8 @@ export namespace ComposerPrimitiveTriggerPopover {
|
|
|
72
72
|
readonly char: string;
|
|
73
73
|
/** Adapter providing categories and items. */
|
|
74
74
|
readonly adapter?: Unstable_TriggerAdapter | undefined;
|
|
75
|
+
/** Whether the adapter is resolving items, surfaced to the popover scope for async sources. @default false */
|
|
76
|
+
readonly isLoading?: boolean | undefined;
|
|
75
77
|
};
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -107,7 +109,14 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
|
|
|
107
109
|
ComposerPrimitiveTriggerPopover.Props
|
|
108
110
|
>(
|
|
109
111
|
(
|
|
110
|
-
{
|
|
112
|
+
{
|
|
113
|
+
char,
|
|
114
|
+
adapter,
|
|
115
|
+
isLoading = false,
|
|
116
|
+
"aria-label": ariaLabel,
|
|
117
|
+
children,
|
|
118
|
+
...props
|
|
119
|
+
},
|
|
111
120
|
forwardedRef,
|
|
112
121
|
) => {
|
|
113
122
|
const aui = useAui();
|
|
@@ -159,6 +168,7 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
|
|
|
159
168
|
behavior: behavior ?? undefined,
|
|
160
169
|
aui,
|
|
161
170
|
popoverId,
|
|
171
|
+
isLoading,
|
|
162
172
|
}),
|
|
163
173
|
);
|
|
164
174
|
|
|
@@ -29,6 +29,8 @@ export type TriggerPopoverResourceOutput = {
|
|
|
29
29
|
readonly items: readonly Unstable_TriggerItem[];
|
|
30
30
|
readonly highlightedIndex: number;
|
|
31
31
|
readonly isSearchMode: boolean;
|
|
32
|
+
/** Whether the adapter is currently resolving items (async sources). */
|
|
33
|
+
readonly isLoading: boolean;
|
|
32
34
|
/** Stable ID prefix for generating accessible element IDs. */
|
|
33
35
|
readonly popoverId: string;
|
|
34
36
|
/** ID of the currently highlighted item (for aria-activedescendant). */
|
|
@@ -58,6 +60,7 @@ const useTriggerPopoverResource = ({
|
|
|
58
60
|
behavior,
|
|
59
61
|
aui,
|
|
60
62
|
popoverId,
|
|
63
|
+
isLoading,
|
|
61
64
|
}: {
|
|
62
65
|
adapter: Unstable_TriggerAdapter | undefined;
|
|
63
66
|
text: string;
|
|
@@ -66,6 +69,7 @@ const useTriggerPopoverResource = ({
|
|
|
66
69
|
aui: AssistantClient;
|
|
67
70
|
/** Stable ID for accessible element IDs (pass React's useId() from component layer). */
|
|
68
71
|
popoverId: string;
|
|
72
|
+
isLoading: boolean;
|
|
69
73
|
}): TriggerPopoverResourceOutput => {
|
|
70
74
|
const detection = useResource(
|
|
71
75
|
TriggerDetectionResource({ text, triggerChar }),
|
|
@@ -122,6 +126,7 @@ const useTriggerPopoverResource = ({
|
|
|
122
126
|
items: navigation.items,
|
|
123
127
|
highlightedIndex: keyboard.highlightedIndex,
|
|
124
128
|
isSearchMode: navigation.isSearchMode,
|
|
129
|
+
isLoading,
|
|
125
130
|
popoverId,
|
|
126
131
|
highlightedItemId: keyboard.highlightedItemId,
|
|
127
132
|
selectCategory: navigation.selectCategory,
|