@assistant-ui/react 0.11.41 → 0.11.44
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/client/AssistantClient.d.ts.map +1 -1
- package/dist/client/AssistantClient.js.map +1 -1
- package/dist/client/ModelContext.d.ts +1 -1
- package/dist/client/ModelContext.d.ts.map +1 -1
- package/dist/client/ModelContext.js.map +1 -1
- package/dist/client/ModelContextClient.d.ts +1 -1
- package/dist/client/ThreadMessageClient.d.ts +1 -0
- package/dist/client/ThreadMessageClient.d.ts.map +1 -1
- package/dist/client/ThreadMessageClient.js +3 -1
- package/dist/client/ThreadMessageClient.js.map +1 -1
- package/dist/client/types/Message.d.ts +2 -0
- package/dist/client/types/Message.d.ts.map +1 -1
- package/dist/client/types/ModelContext.d.ts +1 -1
- package/dist/client/types/ModelContext.d.ts.map +1 -1
- package/dist/client/types/Tools.d.ts +1 -2
- package/dist/client/types/Tools.d.ts.map +1 -1
- package/dist/context/providers/ThreadViewportProvider.d.ts +5 -1
- package/dist/context/providers/ThreadViewportProvider.d.ts.map +1 -1
- package/dist/context/providers/ThreadViewportProvider.js +17 -6
- package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
- package/dist/context/react/AssistantApiContext.d.ts +1 -1
- package/dist/context/react/AssistantApiContext.d.ts.map +1 -1
- package/dist/context/react/AssistantApiContext.js +1 -2
- package/dist/context/react/AssistantApiContext.js.map +1 -1
- package/dist/context/stores/ThreadViewport.d.ts +33 -3
- package/dist/context/stores/ThreadViewport.d.ts.map +1 -1
- package/dist/context/stores/ThreadViewport.js +67 -5
- package/dist/context/stores/ThreadViewport.js.map +1 -1
- package/dist/devtools/DevToolsHooks.d.ts +1 -1
- package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
- package/dist/devtools/DevToolsHooks.js.map +1 -1
- package/dist/legacy-runtime/AssistantRuntimeProvider.d.ts.map +1 -1
- package/dist/legacy-runtime/AssistantRuntimeProvider.js +2 -1
- package/dist/legacy-runtime/AssistantRuntimeProvider.js.map +1 -1
- package/dist/legacy-runtime/client/ComposerRuntimeClient.d.ts +3 -3
- package/dist/legacy-runtime/client/ComposerRuntimeClient.d.ts.map +1 -1
- package/dist/legacy-runtime/client/ComposerRuntimeClient.js.map +1 -1
- package/dist/legacy-runtime/client/EventManagerRuntimeClient.d.ts +1 -1
- package/dist/legacy-runtime/client/ThreadRuntimeClient.js.map +1 -1
- package/dist/legacy-runtime/runtime/MessageRuntime.d.ts +3 -0
- package/dist/legacy-runtime/runtime/MessageRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime/MessageRuntime.js.map +1 -1
- package/dist/legacy-runtime/runtime/RuntimeBindings.d.ts +2 -0
- package/dist/legacy-runtime/runtime/RuntimeBindings.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime/ThreadRuntime.d.ts +1 -0
- package/dist/legacy-runtime/runtime/ThreadRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime/ThreadRuntime.js +6 -3
- package/dist/legacy-runtime/runtime/ThreadRuntime.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js +5 -5
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/core/BaseThreadRuntimeCore.d.ts +1 -0
- package/dist/legacy-runtime/runtime-cores/core/BaseThreadRuntimeCore.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/core/ThreadRuntimeCore.d.ts +1 -0
- package/dist/legacy-runtime/runtime-cores/core/ThreadRuntimeCore.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts +2 -0
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts +2 -0
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/utils/MessageRepository.d.ts +1 -0
- package/dist/legacy-runtime/runtime-cores/utils/MessageRepository.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/utils/MessageRepository.js +2 -1
- package/dist/legacy-runtime/runtime-cores/utils/MessageRepository.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts +2 -2
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +31 -11
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/index.d.ts +1 -0
- package/dist/primitives/composer/index.d.ts.map +1 -1
- package/dist/primitives/composer/index.js +2 -0
- package/dist/primitives/composer/index.js.map +1 -1
- package/dist/primitives/message/MessageRoot.d.ts +3 -0
- package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
- package/dist/primitives/message/MessageRoot.js +24 -2
- package/dist/primitives/message/MessageRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadScrollToBottom.d.ts +7 -2
- package/dist/primitives/thread/ThreadScrollToBottom.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadScrollToBottom.js +7 -4
- package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
- package/dist/primitives/thread/ThreadViewport.d.ts +17 -3
- package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +19 -5
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/ThreadViewportFooter.d.ts +31 -0
- package/dist/primitives/thread/ThreadViewportFooter.d.ts.map +1 -0
- package/dist/primitives/thread/ThreadViewportFooter.js +27 -0
- package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -0
- package/dist/primitives/thread/ThreadViewportSlack.d.ts +20 -0
- package/dist/primitives/thread/ThreadViewportSlack.d.ts.map +1 -0
- package/dist/primitives/thread/ThreadViewportSlack.js +77 -0
- package/dist/primitives/thread/ThreadViewportSlack.js.map +1 -0
- package/dist/primitives/thread/index.d.ts +3 -0
- package/dist/primitives/thread/index.d.ts.map +1 -1
- package/dist/primitives/thread/index.js +7 -1
- package/dist/primitives/thread/index.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts +6 -0
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +17 -8
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/utils/hooks/useOnScrollToBottom.d.ts +3 -1
- package/dist/utils/hooks/useOnScrollToBottom.d.ts.map +1 -1
- package/dist/utils/hooks/useOnScrollToBottom.js.map +1 -1
- package/dist/utils/hooks/useSizeHandle.d.ts +11 -0
- package/dist/utils/hooks/useSizeHandle.d.ts.map +1 -0
- package/dist/utils/hooks/useSizeHandle.js +30 -0
- package/dist/utils/hooks/useSizeHandle.js.map +1 -0
- package/dist/utils/tap-store/derived-scopes.d.ts +2 -1
- package/dist/utils/tap-store/derived-scopes.d.ts.map +1 -1
- package/dist/utils/tap-store/derived-scopes.js.map +1 -1
- package/dist/utils/tap-store/store.d.ts +2 -1
- package/dist/utils/tap-store/store.d.ts.map +1 -1
- package/dist/utils/tap-store/store.js.map +1 -1
- package/package.json +3 -4
- package/src/client/AssistantClient.ts +1 -1
- package/src/client/ModelContext.ts +1 -1
- package/src/client/ThreadMessageClient.tsx +4 -1
- package/src/client/types/Message.ts +3 -0
- package/src/client/types/ModelContext.ts +1 -1
- package/src/client/types/Tools.ts +1 -2
- package/src/context/providers/ThreadViewportProvider.tsx +27 -5
- package/src/context/react/AssistantApiContext.tsx +2 -5
- package/src/context/stores/ThreadViewport.tsx +125 -7
- package/src/devtools/DevToolsHooks.ts +1 -1
- package/src/legacy-runtime/AssistantRuntimeProvider.tsx +6 -1
- package/src/legacy-runtime/client/ComposerRuntimeClient.ts +3 -3
- package/src/legacy-runtime/client/ThreadRuntimeClient.ts +2 -2
- package/src/legacy-runtime/runtime/MessageRuntime.ts +2 -0
- package/src/legacy-runtime/runtime/RuntimeBindings.ts +2 -0
- package/src/legacy-runtime/runtime/ThreadRuntime.ts +6 -3
- package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.ts +5 -5
- package/src/legacy-runtime/runtime-cores/assistant-transport/utils.ts +1 -1
- package/src/legacy-runtime/runtime-cores/core/ThreadRuntimeCore.tsx +1 -0
- package/src/legacy-runtime/runtime-cores/utils/MessageRepository.tsx +1 -0
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +35 -12
- package/src/primitives/composer/index.ts +1 -0
- package/src/primitives/message/MessageRoot.tsx +45 -2
- package/src/primitives/thread/ThreadScrollToBottom.tsx +12 -3
- package/src/primitives/thread/ThreadViewport.tsx +35 -9
- package/src/primitives/thread/ThreadViewportFooter.tsx +57 -0
- package/src/primitives/thread/ThreadViewportSlack.tsx +109 -0
- package/src/primitives/thread/index.ts +3 -0
- package/src/primitives/thread/useThreadViewportAutoScroll.tsx +24 -12
- package/src/utils/hooks/useOnScrollToBottom.tsx +3 -1
- package/src/utils/hooks/useSizeHandle.ts +43 -0
- package/src/utils/tap-store/derived-scopes.ts +2 -1
- package/src/utils/tap-store/store.ts +1 -1
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
} from "react";
|
|
10
10
|
import { useAssistantApi, useAssistantState } from "../../context";
|
|
11
11
|
import { useManagedRef } from "../../utils/hooks/useManagedRef";
|
|
12
|
+
import { useSizeHandle } from "../../utils/hooks/useSizeHandle";
|
|
12
13
|
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
14
|
+
import { useThreadViewport } from "../../context/react/ThreadViewportContext";
|
|
15
|
+
import { ThreadPrimitiveViewportSlack } from "../thread/ThreadViewportSlack";
|
|
13
16
|
|
|
14
17
|
const useIsHoveringRef = () => {
|
|
15
18
|
const api = useAssistantApi();
|
|
@@ -41,6 +44,34 @@ const useIsHoveringRef = () => {
|
|
|
41
44
|
return useManagedRef(callbackRef);
|
|
42
45
|
};
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Hook that registers the anchor user message as a content inset.
|
|
49
|
+
* Only registers if: user message, at index messages.length-2, and last message is assistant.
|
|
50
|
+
*/
|
|
51
|
+
const useMessageViewportRef = () => {
|
|
52
|
+
const turnAnchor = useThreadViewport((s) => s.turnAnchor);
|
|
53
|
+
const registerUserHeight = useThreadViewport(
|
|
54
|
+
(s) => s.registerUserMessageHeight,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// inset rules:
|
|
58
|
+
// - the previous user message before the last assistant message registers its full height
|
|
59
|
+
const shouldRegisterAsInset = useAssistantState(
|
|
60
|
+
({ thread, message }) =>
|
|
61
|
+
turnAnchor === "top" &&
|
|
62
|
+
message.role === "user" &&
|
|
63
|
+
message.index === thread.messages.length - 2 &&
|
|
64
|
+
thread.messages.at(-1)?.role === "assistant",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const getHeight = useCallback((el: HTMLElement) => el.offsetHeight, []);
|
|
68
|
+
|
|
69
|
+
return useSizeHandle(
|
|
70
|
+
shouldRegisterAsInset ? registerUserHeight : null,
|
|
71
|
+
getHeight,
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
44
75
|
export namespace MessagePrimitiveRoot {
|
|
45
76
|
export type Element = ComponentRef<typeof Primitive.div>;
|
|
46
77
|
/**
|
|
@@ -57,6 +88,9 @@ export namespace MessagePrimitiveRoot {
|
|
|
57
88
|
* hover state management for the message. It automatically tracks when the user
|
|
58
89
|
* is hovering over the message, which can be used by child components like action bars.
|
|
59
90
|
*
|
|
91
|
+
* When `turnAnchor="top"` is set on the viewport, this component
|
|
92
|
+
* registers itself as the scroll anchor if it's the last user message.
|
|
93
|
+
*
|
|
60
94
|
* @example
|
|
61
95
|
* ```tsx
|
|
62
96
|
* <MessagePrimitive.Root>
|
|
@@ -73,9 +107,18 @@ export const MessagePrimitiveRoot = forwardRef<
|
|
|
73
107
|
MessagePrimitiveRoot.Props
|
|
74
108
|
>((props, forwardRef) => {
|
|
75
109
|
const isHoveringRef = useIsHoveringRef();
|
|
76
|
-
const
|
|
110
|
+
const anchorUserMessageRef = useMessageViewportRef();
|
|
111
|
+
const ref = useComposedRefs<HTMLDivElement>(
|
|
112
|
+
forwardRef,
|
|
113
|
+
isHoveringRef,
|
|
114
|
+
anchorUserMessageRef,
|
|
115
|
+
);
|
|
77
116
|
|
|
78
|
-
return
|
|
117
|
+
return (
|
|
118
|
+
<ThreadPrimitiveViewportSlack>
|
|
119
|
+
<Primitive.div {...props} ref={ref} />
|
|
120
|
+
</ThreadPrimitiveViewportSlack>
|
|
121
|
+
);
|
|
79
122
|
});
|
|
80
123
|
|
|
81
124
|
MessagePrimitiveRoot.displayName = "MessagePrimitive.Root";
|
|
@@ -11,14 +11,22 @@ import {
|
|
|
11
11
|
useThreadViewportStore,
|
|
12
12
|
} from "../../context/react/ThreadViewportContext";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
export namespace useThreadScrollToBottom {
|
|
15
|
+
export type Options = {
|
|
16
|
+
behavior?: ScrollBehavior | undefined;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const useThreadScrollToBottom = ({
|
|
21
|
+
behavior,
|
|
22
|
+
}: useThreadScrollToBottom.Options = {}) => {
|
|
15
23
|
const isAtBottom = useThreadViewport((s) => s.isAtBottom);
|
|
16
24
|
|
|
17
25
|
const threadViewportStore = useThreadViewportStore();
|
|
18
26
|
|
|
19
27
|
const handleScrollToBottom = useCallback(() => {
|
|
20
|
-
threadViewportStore.getState().scrollToBottom();
|
|
21
|
-
}, [threadViewportStore]);
|
|
28
|
+
threadViewportStore.getState().scrollToBottom({ behavior });
|
|
29
|
+
}, [threadViewportStore, behavior]);
|
|
22
30
|
|
|
23
31
|
if (isAtBottom) return null;
|
|
24
32
|
return handleScrollToBottom;
|
|
@@ -32,4 +40,5 @@ export namespace ThreadPrimitiveScrollToBottom {
|
|
|
32
40
|
export const ThreadPrimitiveScrollToBottom = createActionButton(
|
|
33
41
|
"ThreadPrimitive.ScrollToBottom",
|
|
34
42
|
useThreadScrollToBottom,
|
|
43
|
+
["behavior"],
|
|
35
44
|
);
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
4
4
|
import { Primitive } from "@radix-ui/react-primitive";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type ComponentRef,
|
|
7
|
+
forwardRef,
|
|
8
|
+
ComponentPropsWithoutRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
} from "react";
|
|
6
11
|
import { useThreadViewportAutoScroll } from "./useThreadViewportAutoScroll";
|
|
7
|
-
import {
|
|
12
|
+
import { ThreadPrimitiveViewportProvider } from "../../context/providers/ThreadViewportProvider";
|
|
13
|
+
import { useSizeHandle } from "../../utils/hooks/useSizeHandle";
|
|
14
|
+
import { useThreadViewport } from "../../context/react/ThreadViewportContext";
|
|
8
15
|
|
|
9
16
|
export namespace ThreadPrimitiveViewport {
|
|
10
17
|
export type Element = ComponentRef<typeof Primitive.div>;
|
|
@@ -12,12 +19,31 @@ export namespace ThreadPrimitiveViewport {
|
|
|
12
19
|
/**
|
|
13
20
|
* Whether to automatically scroll to the bottom when new messages are added.
|
|
14
21
|
* When enabled, the viewport will automatically scroll to show the latest content.
|
|
15
|
-
*
|
|
22
|
+
*
|
|
23
|
+
* Default false if `turnAnchor` is "top", otherwise defaults to true.
|
|
16
24
|
*/
|
|
17
25
|
autoScroll?: boolean | undefined;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Controls scroll anchoring behavior for new messages.
|
|
29
|
+
* - "bottom" (default): Messages anchor at the bottom, classic chat behavior.
|
|
30
|
+
* - "top": New user messages anchor at the top of the viewport for a focused reading experience.
|
|
31
|
+
*/
|
|
32
|
+
turnAnchor?: "top" | "bottom" | undefined;
|
|
18
33
|
};
|
|
19
34
|
}
|
|
20
35
|
|
|
36
|
+
const useViewportSizeRef = () => {
|
|
37
|
+
const register = useThreadViewport((s) => s.registerViewport);
|
|
38
|
+
const getHeight = useCallback(
|
|
39
|
+
(el: HTMLElement) =>
|
|
40
|
+
el.clientHeight - parseFloat(getComputedStyle(el).paddingTop),
|
|
41
|
+
[],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return useSizeHandle(register, getHeight);
|
|
45
|
+
};
|
|
46
|
+
|
|
21
47
|
const ThreadPrimitiveViewportScrollable = forwardRef<
|
|
22
48
|
ThreadPrimitiveViewport.Element,
|
|
23
49
|
ThreadPrimitiveViewport.Props
|
|
@@ -25,8 +51,8 @@ const ThreadPrimitiveViewportScrollable = forwardRef<
|
|
|
25
51
|
const autoScrollRef = useThreadViewportAutoScroll<HTMLDivElement>({
|
|
26
52
|
autoScroll,
|
|
27
53
|
});
|
|
28
|
-
|
|
29
|
-
const ref = useComposedRefs(forwardedRef, autoScrollRef);
|
|
54
|
+
const viewportSizeRef = useViewportSizeRef();
|
|
55
|
+
const ref = useComposedRefs(forwardedRef, autoScrollRef, viewportSizeRef);
|
|
30
56
|
|
|
31
57
|
return (
|
|
32
58
|
<Primitive.div {...rest} ref={ref}>
|
|
@@ -47,7 +73,7 @@ ThreadPrimitiveViewportScrollable.displayName =
|
|
|
47
73
|
*
|
|
48
74
|
* @example
|
|
49
75
|
* ```tsx
|
|
50
|
-
* <ThreadPrimitive.Viewport
|
|
76
|
+
* <ThreadPrimitive.Viewport turnAnchor="top">
|
|
51
77
|
* <ThreadPrimitive.Messages components={{ Message: MyMessage }} />
|
|
52
78
|
* </ThreadPrimitive.Viewport>
|
|
53
79
|
* ```
|
|
@@ -55,11 +81,11 @@ ThreadPrimitiveViewportScrollable.displayName =
|
|
|
55
81
|
export const ThreadPrimitiveViewport = forwardRef<
|
|
56
82
|
ThreadPrimitiveViewport.Element,
|
|
57
83
|
ThreadPrimitiveViewport.Props
|
|
58
|
-
>((props, ref) => {
|
|
84
|
+
>(({ turnAnchor, ...props }, ref) => {
|
|
59
85
|
return (
|
|
60
|
-
<
|
|
86
|
+
<ThreadPrimitiveViewportProvider options={{ turnAnchor }}>
|
|
61
87
|
<ThreadPrimitiveViewportScrollable {...props} ref={ref} />
|
|
62
|
-
</
|
|
88
|
+
</ThreadPrimitiveViewportProvider>
|
|
63
89
|
);
|
|
64
90
|
});
|
|
65
91
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
4
|
+
import { Primitive } from "@radix-ui/react-primitive";
|
|
5
|
+
import {
|
|
6
|
+
type ComponentRef,
|
|
7
|
+
forwardRef,
|
|
8
|
+
ComponentPropsWithoutRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { useSizeHandle } from "../../utils/hooks/useSizeHandle";
|
|
12
|
+
import { useThreadViewport } from "../../context/react/ThreadViewportContext";
|
|
13
|
+
|
|
14
|
+
export namespace ThreadPrimitiveViewportFooter {
|
|
15
|
+
export type Element = ComponentRef<typeof Primitive.div>;
|
|
16
|
+
export type Props = ComponentPropsWithoutRef<typeof Primitive.div>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A footer container that measures its height for scroll calculations.
|
|
21
|
+
*
|
|
22
|
+
* This component measures its height and provides it to the viewport context
|
|
23
|
+
* for use in scroll calculations (e.g., ViewportSlack min-height).
|
|
24
|
+
*
|
|
25
|
+
* Multiple ViewportFooter components can be used - their heights are summed.
|
|
26
|
+
*
|
|
27
|
+
* Typically used with `className="sticky bottom-0"` to keep the footer
|
|
28
|
+
* visible at the bottom of the viewport while scrolling.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <ThreadPrimitive.Viewport>
|
|
33
|
+
* <ThreadPrimitive.Messages components={{ ... }} />
|
|
34
|
+
* <ThreadPrimitive.ViewportFooter className="sticky bottom-0">
|
|
35
|
+
* <Composer />
|
|
36
|
+
* </ThreadPrimitive.ViewportFooter>
|
|
37
|
+
* </ThreadPrimitive.Viewport>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const ThreadPrimitiveViewportFooter = forwardRef<
|
|
41
|
+
ThreadPrimitiveViewportFooter.Element,
|
|
42
|
+
ThreadPrimitiveViewportFooter.Props
|
|
43
|
+
>((props, forwardedRef) => {
|
|
44
|
+
const register = useThreadViewport((s) => s.registerContentInset);
|
|
45
|
+
const getHeight = useCallback((el: HTMLElement) => {
|
|
46
|
+
const marginTop = parseFloat(getComputedStyle(el).marginTop) || 0;
|
|
47
|
+
return el.offsetHeight + marginTop;
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const resizeRef = useSizeHandle(register, getHeight);
|
|
51
|
+
|
|
52
|
+
const ref = useComposedRefs(forwardedRef, resizeRef);
|
|
53
|
+
|
|
54
|
+
return <Primitive.div {...props} ref={ref} />;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
ThreadPrimitiveViewportFooter.displayName = "ThreadPrimitive.ViewportFooter";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
type FC,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { useThreadViewportStore } from "../../context/react/ThreadViewportContext";
|
|
12
|
+
import { useAssistantState } from "../../context";
|
|
13
|
+
import { useManagedRef } from "../../utils/hooks/useManagedRef";
|
|
14
|
+
|
|
15
|
+
const SlackNestingContext = createContext(false);
|
|
16
|
+
|
|
17
|
+
const parseCssLength = (value: string, element: HTMLElement): number => {
|
|
18
|
+
const match = value.match(/^([\d.]+)(em|px|rem)$/);
|
|
19
|
+
if (!match) return 0;
|
|
20
|
+
|
|
21
|
+
const num = parseFloat(match[1]!);
|
|
22
|
+
const unit = match[2];
|
|
23
|
+
|
|
24
|
+
if (unit === "px") return num;
|
|
25
|
+
if (unit === "em") {
|
|
26
|
+
const fontSize = parseFloat(getComputedStyle(element).fontSize) || 16;
|
|
27
|
+
return num * fontSize;
|
|
28
|
+
}
|
|
29
|
+
if (unit === "rem") {
|
|
30
|
+
const rootFontSize =
|
|
31
|
+
parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
32
|
+
return num * rootFontSize;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ThreadViewportSlackProps = {
|
|
38
|
+
/** Threshold at which the user message height clamps to the offset */
|
|
39
|
+
fillClampThreshold?: string;
|
|
40
|
+
/** Offset used when clamping large user messages */
|
|
41
|
+
fillClampOffset?: string;
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A slot component that provides minimum height to enable scroll anchoring.
|
|
47
|
+
*
|
|
48
|
+
* When using `turnAnchor="top"`, this component ensures there is
|
|
49
|
+
* enough scroll room below the anchor point (last user message) for it to scroll
|
|
50
|
+
* to the top of the viewport. The min-height is applied only to the last
|
|
51
|
+
* assistant message.
|
|
52
|
+
*
|
|
53
|
+
* This component is used internally by MessagePrimitive.Root.
|
|
54
|
+
*/
|
|
55
|
+
export const ThreadPrimitiveViewportSlack: FC<ThreadViewportSlackProps> = ({
|
|
56
|
+
children,
|
|
57
|
+
fillClampThreshold = "10em",
|
|
58
|
+
fillClampOffset = "6em",
|
|
59
|
+
}) => {
|
|
60
|
+
const isLast = useAssistantState(({ message }) => message.isLast);
|
|
61
|
+
const threadViewportStore = useThreadViewportStore({ optional: true });
|
|
62
|
+
const isNested = useContext(SlackNestingContext);
|
|
63
|
+
|
|
64
|
+
const callback = useCallback(
|
|
65
|
+
(el: HTMLElement) => {
|
|
66
|
+
if (!threadViewportStore || isNested) return;
|
|
67
|
+
|
|
68
|
+
const updateMinHeight = () => {
|
|
69
|
+
const state = threadViewportStore.getState();
|
|
70
|
+
if (state.turnAnchor === "top" && isLast) {
|
|
71
|
+
const { viewport, inset, userMessage } = state.height;
|
|
72
|
+
const threshold = parseCssLength(fillClampThreshold, el);
|
|
73
|
+
const offset = parseCssLength(fillClampOffset, el);
|
|
74
|
+
const clampAdjustment =
|
|
75
|
+
userMessage <= threshold ? userMessage : offset;
|
|
76
|
+
|
|
77
|
+
const minHeight = Math.max(0, viewport - inset - clampAdjustment);
|
|
78
|
+
el.style.minHeight = `${minHeight}px`;
|
|
79
|
+
el.style.flexShrink = "0";
|
|
80
|
+
el.style.transition = "min-height 0s";
|
|
81
|
+
} else {
|
|
82
|
+
el.style.minHeight = "";
|
|
83
|
+
el.style.flexShrink = "";
|
|
84
|
+
el.style.transition = "";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
updateMinHeight();
|
|
89
|
+
return threadViewportStore.subscribe(updateMinHeight);
|
|
90
|
+
},
|
|
91
|
+
[
|
|
92
|
+
threadViewportStore,
|
|
93
|
+
isLast,
|
|
94
|
+
isNested,
|
|
95
|
+
fillClampThreshold,
|
|
96
|
+
fillClampOffset,
|
|
97
|
+
],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const ref = useManagedRef<HTMLElement>(callback);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<SlackNestingContext.Provider value={true}>
|
|
104
|
+
<Slot ref={ref}>{children}</Slot>
|
|
105
|
+
</SlackNestingContext.Provider>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
ThreadPrimitiveViewportSlack.displayName = "ThreadPrimitive.ViewportSlack";
|
|
@@ -2,6 +2,9 @@ export { ThreadPrimitiveRoot as Root } from "./ThreadRoot";
|
|
|
2
2
|
export { ThreadPrimitiveEmpty as Empty } from "./ThreadEmpty";
|
|
3
3
|
export { ThreadPrimitiveIf as If } from "./ThreadIf";
|
|
4
4
|
export { ThreadPrimitiveViewport as Viewport } from "./ThreadViewport";
|
|
5
|
+
export { ThreadPrimitiveViewportProvider as ViewportProvider } from "../../context/providers/ThreadViewportProvider";
|
|
6
|
+
export { ThreadPrimitiveViewportFooter as ViewportFooter } from "./ThreadViewportFooter";
|
|
7
|
+
export { ThreadPrimitiveViewportSlack as ViewportSlack } from "./ThreadViewportSlack";
|
|
5
8
|
export { ThreadPrimitiveMessages as Messages } from "./ThreadMessages";
|
|
6
9
|
export { ThreadPrimitiveMessageByIndex as MessageByIndex } from "./ThreadMessages";
|
|
7
10
|
export { ThreadPrimitiveScrollToBottom as ScrollToBottom } from "./ThreadScrollToBottom";
|
|
@@ -11,28 +11,38 @@ import { useThreadViewportStore } from "../../context/react/ThreadViewportContex
|
|
|
11
11
|
|
|
12
12
|
export namespace useThreadViewportAutoScroll {
|
|
13
13
|
export type Options = {
|
|
14
|
+
/**
|
|
15
|
+
* Whether to automatically scroll to the bottom when new messages are added.
|
|
16
|
+
* When enabled, the viewport will automatically scroll to show the latest content.
|
|
17
|
+
*
|
|
18
|
+
* Default false if `turnAnchor` is "top", otherwise defaults to true.
|
|
19
|
+
*/
|
|
14
20
|
autoScroll?: boolean | undefined;
|
|
15
21
|
};
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
19
|
-
autoScroll
|
|
25
|
+
autoScroll,
|
|
20
26
|
}: useThreadViewportAutoScroll.Options): RefCallback<TElement> => {
|
|
21
27
|
const divRef = useRef<TElement>(null);
|
|
22
28
|
|
|
23
29
|
const threadViewportStore = useThreadViewportStore();
|
|
30
|
+
if (autoScroll === undefined) {
|
|
31
|
+
autoScroll = threadViewportStore.getState().turnAnchor !== "top";
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
const lastScrollTop = useRef<number>(0);
|
|
26
35
|
|
|
27
36
|
// bug: when ScrollToBottom's button changes its disabled state, the scroll stops
|
|
28
37
|
// fix: delay the state change until the scroll is done
|
|
29
|
-
|
|
38
|
+
// stores the scroll behavior to reuse during content resize, or null if not scrolling
|
|
39
|
+
const scrollingToBottomBehaviorRef = useRef<ScrollBehavior | null>(null);
|
|
30
40
|
|
|
31
41
|
const scrollToBottom = useCallback((behavior: ScrollBehavior) => {
|
|
32
42
|
const div = divRef.current;
|
|
33
43
|
if (!div) return;
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
scrollingToBottomBehaviorRef.current = behavior;
|
|
36
46
|
div.scrollTo({ top: div.scrollHeight, behavior });
|
|
37
47
|
}, []);
|
|
38
48
|
|
|
@@ -49,7 +59,7 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
49
59
|
// ignore scroll down
|
|
50
60
|
} else {
|
|
51
61
|
if (newIsAtBottom) {
|
|
52
|
-
|
|
62
|
+
scrollingToBottomBehaviorRef.current = null;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
if (newIsAtBottom !== isAtBottom) {
|
|
@@ -63,11 +73,10 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
63
73
|
};
|
|
64
74
|
|
|
65
75
|
const resizeRef = useOnResizeContent(() => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(
|
|
69
|
-
|
|
70
|
-
) {
|
|
76
|
+
const scrollBehavior = scrollingToBottomBehaviorRef.current;
|
|
77
|
+
if (scrollBehavior) {
|
|
78
|
+
scrollToBottom(scrollBehavior);
|
|
79
|
+
} else if (autoScroll && threadViewportStore.getState().isAtBottom) {
|
|
71
80
|
scrollToBottom("instant");
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -81,13 +90,16 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
|
|
|
81
90
|
};
|
|
82
91
|
});
|
|
83
92
|
|
|
84
|
-
useOnScrollToBottom(() => {
|
|
85
|
-
scrollToBottom(
|
|
93
|
+
useOnScrollToBottom(({ behavior }) => {
|
|
94
|
+
scrollToBottom(behavior);
|
|
86
95
|
});
|
|
87
96
|
|
|
88
97
|
// autoscroll on run start
|
|
89
98
|
useAssistantEvent("thread.run-start", () => {
|
|
90
|
-
|
|
99
|
+
scrollingToBottomBehaviorRef.current = "auto";
|
|
100
|
+
requestAnimationFrame(() => {
|
|
101
|
+
scrollToBottom("auto");
|
|
102
|
+
});
|
|
91
103
|
});
|
|
92
104
|
|
|
93
105
|
const autoScrollRef = useComposedRefs<TElement>(resizeRef, scrollRef, divRef);
|
|
@@ -4,7 +4,9 @@ import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
|
|
|
4
4
|
import { useEffect } from "react";
|
|
5
5
|
import { useThreadViewport } from "../../context/react/ThreadViewportContext";
|
|
6
6
|
|
|
7
|
-
export const useOnScrollToBottom = (
|
|
7
|
+
export const useOnScrollToBottom = (
|
|
8
|
+
callback: (config: { behavior: ScrollBehavior }) => void,
|
|
9
|
+
) => {
|
|
8
10
|
const callbackRef = useCallbackRef(callback);
|
|
9
11
|
const onScrollToBottom = useThreadViewport((vp) => vp.onScrollToBottom);
|
|
10
12
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useManagedRef } from "./useManagedRef";
|
|
5
|
+
import type { SizeHandle } from "../../context/stores/ThreadViewport";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook that creates a ref for tracking element size via a SizeHandle.
|
|
9
|
+
* Automatically sets up ResizeObserver and reports height changes.
|
|
10
|
+
*
|
|
11
|
+
* @param register - Function that returns a SizeHandle (e.g., registerContentInset)
|
|
12
|
+
* @param getHeight - Optional function to compute height (defaults to el.offsetHeight)
|
|
13
|
+
* @returns A ref callback to attach to the element
|
|
14
|
+
*/
|
|
15
|
+
export const useSizeHandle = (
|
|
16
|
+
register: (() => SizeHandle) | null | undefined,
|
|
17
|
+
getHeight?: (el: HTMLElement) => number,
|
|
18
|
+
) => {
|
|
19
|
+
const callbackRef = useCallback(
|
|
20
|
+
(el: HTMLElement) => {
|
|
21
|
+
if (!register) return;
|
|
22
|
+
|
|
23
|
+
const sizeHandle = register();
|
|
24
|
+
|
|
25
|
+
const updateHeight = () => {
|
|
26
|
+
const height = getHeight ? getHeight(el) : el.offsetHeight;
|
|
27
|
+
sizeHandle.setHeight(height);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ro = new ResizeObserver(updateHeight);
|
|
31
|
+
ro.observe(el);
|
|
32
|
+
updateHeight();
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
ro.disconnect();
|
|
36
|
+
sizeHandle.unregister();
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
[register, getHeight],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return useManagedRef(callbackRef);
|
|
43
|
+
};
|
|
@@ -10,7 +10,8 @@ import type {
|
|
|
10
10
|
AssistantEventCallback,
|
|
11
11
|
AssistantEventSelector,
|
|
12
12
|
} from "../../types/EventTypes";
|
|
13
|
-
import type { ResourceElement
|
|
13
|
+
import type { ResourceElement } from "@assistant-ui/tap";
|
|
14
|
+
import { Unsubscribe } from "../../types";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Extract the API return type from an AssistantApiField
|