From 0fca66b9fcb1aaf439ee95607bbec80f00be4023 Mon Sep 17 00:00:00 2001 From: Simon Farshid Date: Tue, 30 Jul 2024 12:32:03 -0700 Subject: [PATCH] feat: Smooth status (#615) --- packages/react-markdown/package.json | 1 + .../src/primitives/MarkdownText.tsx | 117 +++++++++++------- .../src/styles/tailwindcss/markdown.css | 85 ++++++------- .../react-markdown/src/ui/markdown-text.tsx | 23 ++-- .../context/providers/ContentPartProvider.tsx | 6 +- .../react/src/context/stores/ContentPart.ts | 6 +- packages/react/src/internal.ts | 2 +- .../contentPart/ContentPartText.tsx | 29 +++-- .../react/src/styles/tailwindcss/thread.css | 2 +- packages/react/src/types/AssistantTypes.ts | 2 +- .../src/types/ContentPartComponentTypes.tsx | 4 +- packages/react/src/ui/content-part.tsx | 17 ++- .../react/src/utils/smooth/SmoothContext.tsx | 76 ++++++++++++ packages/react/src/utils/smooth/index.ts | 3 + .../src/utils/{hooks => smooth}/useSmooth.tsx | 51 +++++++- pnpm-lock.yaml | 3 + 16 files changed, 291 insertions(+), 136 deletions(-) create mode 100644 packages/react/src/utils/smooth/SmoothContext.tsx create mode 100644 packages/react/src/utils/smooth/index.ts rename packages/react/src/utils/{hooks => smooth}/useSmooth.tsx (61%) diff --git a/packages/react-markdown/package.json b/packages/react-markdown/package.json index cb910efb8..fde1749b4 100644 --- a/packages/react-markdown/package.json +++ b/packages/react-markdown/package.json @@ -40,6 +40,7 @@ "build": "tsx scripts/build.mts" }, "dependencies": { + "@radix-ui/react-primitive": "^2.0.0", "@radix-ui/react-use-callback-ref": "^1.1.0", "classnames": "^2.5.1", "lucide-react": "^0.416.0", diff --git a/packages/react-markdown/src/primitives/MarkdownText.tsx b/packages/react-markdown/src/primitives/MarkdownText.tsx index 3dfc9d925..4e027e51f 100644 --- a/packages/react-markdown/src/primitives/MarkdownText.tsx +++ b/packages/react-markdown/src/primitives/MarkdownText.tsx @@ -1,8 +1,13 @@ "use client"; import { useContentPartText } from "@assistant-ui/react"; -import { useSmooth } from "@assistant-ui/react/internal"; -import type { ComponentType, FC } from "react"; +import { + ElementRef, + ElementType, + forwardRef, + type ComponentPropsWithoutRef, + type ComponentType, +} from "react"; import ReactMarkdown, { type Options } from "react-markdown"; import { SyntaxHighlighterProps, CodeHeaderProps } from "../overrides/types"; import { PreOverride } from "../overrides/PreOverride"; @@ -14,11 +19,19 @@ import { } from "../overrides/defaultComponents"; import { useCallbackRef } from "@radix-ui/react-use-callback-ref"; import { CodeOverride } from "../overrides/CodeOverride"; +import { useSmooth } from "@assistant-ui/react/internal"; +import { Primitive } from "@radix-ui/react-primitive"; +import classNames from "classnames"; + +type MarkdownTextPrimitiveElement = ElementRef; +type PrimitiveDivProps = ComponentPropsWithoutRef; export type MarkdownTextPrimitiveProps = Omit< Options, - "components" | "children" + "components" | "children" | "asChild" > & { + containerProps?: Omit; + containerComponent?: ElementType; components?: NonNullable & { SyntaxHighlighter?: ComponentType; CodeHeader?: ComponentType; @@ -32,44 +45,64 @@ export type MarkdownTextPrimitiveProps = Omit< }; smooth?: boolean; }; -export const MarkdownTextPrimitive: FC = ({ - smooth = true, - components: userComponents, - ...rest -}) => { - const { - part: { text }, - } = useContentPartText(); - const smoothText = useSmooth(text, smooth); // TODO loading indicator disappears before smooth animation ends +export const MarkdownTextPrimitive = forwardRef< + MarkdownTextPrimitiveElement, + MarkdownTextPrimitiveProps +>( + ( + { + components: userComponents, + className, + containerProps, + containerComponent: Container = "div", + ...rest + }, + forwardedRef, + smooth = true, + ) => { + const { + part: { text }, + status, + } = useSmooth(useContentPartText(), smooth); - const { - pre = DefaultPre, - code = DefaultCode, - SyntaxHighlighter = DefaultCodeBlockContent, - CodeHeader = DefaultCodeHeader, - by_language, - ...componentsRest - } = userComponents ?? {}; - const components: typeof userComponents = { - ...componentsRest, - pre: PreOverride, - code: useCallbackRef((props) => ( - - )), - }; + const { + pre = DefaultPre, + code = DefaultCode, + SyntaxHighlighter = DefaultCodeBlockContent, + CodeHeader = DefaultCodeHeader, + by_language, + ...componentsRest + } = userComponents ?? {}; + const components: typeof userComponents = { + ...componentsRest, + pre: PreOverride, + code: useCallbackRef((props) => ( + + )), + }; - return ( - - {smoothText} - - ); -}; + return ( + + + {text} + + + ); + }, +); + +MarkdownTextPrimitive.displayName = "MarkdownTextPrimitive"; diff --git a/packages/react-markdown/src/styles/tailwindcss/markdown.css b/packages/react-markdown/src/styles/tailwindcss/markdown.css index 922e7e001..fe6dc3a67 100644 --- a/packages/react-markdown/src/styles/tailwindcss/markdown.css +++ b/packages/react-markdown/src/styles/tailwindcss/markdown.css @@ -1,107 +1,100 @@ -/* in progress indicator */ -:where(.aui-md-in-progress:empty)::after, -:where(.aui-md-in-progress > :not(ol):not(ul):not(pre):last-child)::after, -:where(.aui-md-in-progress > pre:last-child code)::after, -:where( - .aui-md-in-progress - > :is(ol, ul):last-child - > li:last-child:not(:has(* > li)) - )::after, -:where( - .aui-md-in-progress - > :is(ol, ul):last-child - > li:last-child - > :is(ol, ul):last-child - > li:last-child:not(:has(* > li)) - )::after, -:where( - .aui-md-in-progress - > :is(ol, ul):last-child - > li:last-child - > :is(ol, ul):last-child - > li:last-child - > :is(ol, ul):last-child - > li:last-child - )::after { +/* running indicator */ +:where(.aui-md-running):empty::after, +:where(.aui-md-running) > :where(:not(ol):not(ul):not(pre)):last-child::after, +:where(.aui-md-running) > pre:last-child code::after, +:where(.aui-md-running) + > :where(:is(ol, ul):last-child) + > :where(li:last-child:not(:has(* > li)))::after, +:where(.aui-md-running) + > :where(:is(ol, ul):last-child) + > :where(li:last-child) + > :where(:is(ol, ul):last-child) + > :where(li:last-child:not(:has(* > li)))::after, +:where(.aui-md-running) + > :where(:is(ol, ul):last-child) + > :where(li:last-child) + > :where(:is(ol, ul):last-child) + > :where(li:last-child) + > :where(:is(ol, ul):last-child) + > :where(li:last-child)::after { @apply animate-pulse font-sans content-['\25CF'] ltr:ml-1 rtl:mr-1; } - /* typography */ -:where(.aui-md-root) h1 { +.aui-md-root h1 { @apply mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0; } -:where(.aui-md-root) h2 { +.aui-md-root h2 { @apply mb-4 mt-8 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0; } -:where(.aui-md-root) h3 { +.aui-md-root h3 { @apply mb-4 mt-6 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0; } -:where(.aui-md-root) h4 { +.aui-md-root h4 { @apply mb-4 mt-6 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0; } -:where(.aui-md-root) h5 { +.aui-md-root h5 { @apply my-4 text-lg font-semibold first:mt-0 last:mb-0; } -:where(.aui-md-root) h6 { +.aui-md-root h6 { @apply my-4 font-semibold first:mt-0 last:mb-0; } -:where(.aui-md-root) p { +.aui-md-root p { @apply mb-5 mt-5 leading-7 first:mt-0 last:mb-0; } -:where(.aui-md-root) a { +.aui-md-root a { @apply text-aui-primary font-medium underline underline-offset-4; } -:where(.aui-md-root) blockquote { +.aui-md-root blockquote { @apply border-l-2 pl-6 italic; } -:where(.aui-md-root) ul { +.aui-md-root ul { @apply my-5 ml-6 list-disc [&>li]:mt-2; } -:where(.aui-md-root) ol { +.aui-md-root ol { @apply my-5 ml-6 list-decimal [&>li]:mt-2; } -:where(.aui-md-root) hr { +.aui-md-root hr { @apply my-5 border-b; } -:where(.aui-md-root) table { +.aui-md-root table { @apply my-5 w-full border-separate border-spacing-0 overflow-y-auto; } -:where(.aui-md-root) th { +.aui-md-root th { @apply bg-aui-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [&[align=center]]:text-center [&[align=right]]:text-right; } -:where(.aui-md-root) td { +.aui-md-root td { @apply border-b border-l px-4 py-2 text-left last:border-r [&[align=center]]:text-center [&[align=right]]:text-right; } -:where(.aui-md-root) tr { +.aui-md-root tr { @apply m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg; } -:where(.aui-md-root) sup { +.aui-md-root sup { @apply [&>a]:text-xs [&>a]:no-underline; } -:where(.aui-md-root) pre { +.aui-md-root pre { @apply overflow-x-auto rounded-b-lg bg-black p-4 text-white; } -:where(.aui-md-root) > code, -:where(.aui-md-root) :not(:where(pre)) code { +.aui-md-root > code, +.aui-md-root :not(:where(pre)) code { @apply bg-aui-muted rounded border font-semibold; } diff --git a/packages/react-markdown/src/ui/markdown-text.tsx b/packages/react-markdown/src/ui/markdown-text.tsx index 15700a689..c8591964b 100644 --- a/packages/react-markdown/src/ui/markdown-text.tsx +++ b/packages/react-markdown/src/ui/markdown-text.tsx @@ -1,4 +1,3 @@ -import { TextContentPartProps } from "@assistant-ui/react"; import { FC, memo } from "react"; import { CodeHeader } from "./code-header"; import classNames from "classnames"; @@ -6,6 +5,10 @@ import { MarkdownTextPrimitive, MarkdownTextPrimitiveProps, } from "../primitives/MarkdownText"; +import { + withSmoothContextProvider, + useSmoothStatus, +} from "@assistant-ui/react/internal"; export type MakeMarkdownTextProps = MarkdownTextPrimitiveProps; @@ -19,23 +22,21 @@ export const makeMarkdownText = ({ CodeHeader: userComponents?.CodeHeader ?? CodeHeader, }; - const MarkdownTextImpl: FC = ({ status }) => { + const MarkdownTextImpl: FC = () => { + const status = useSmoothStatus(); return ( -
- -
+ /> ); }; MarkdownTextImpl.displayName = "MarkdownText"; - return memo( - MarkdownTextImpl, - (prev, next) => prev.status.type === next.status.type, - ); + return memo(withSmoothContextProvider(MarkdownTextImpl), () => true); }; diff --git a/packages/react/src/context/providers/ContentPartProvider.tsx b/packages/react/src/context/providers/ContentPartProvider.tsx index 511ddac4b..03b4f5fb6 100644 --- a/packages/react/src/context/providers/ContentPartProvider.tsx +++ b/packages/react/src/context/providers/ContentPartProvider.tsx @@ -12,7 +12,7 @@ import { ThreadAssistantContentPart, ThreadMessage, ThreadUserContentPart, - ToolContentPartStatus, + ToolCallContentPartStatus, } from "../../types/AssistantTypes"; type ContentPartProviderProps = PropsWithChildren<{ @@ -27,7 +27,7 @@ const toContentPartStatus = ( message: ThreadMessage, partIndex: number, part: ThreadUserContentPart | ThreadAssistantContentPart, -): ToolContentPartStatus => { +): ToolCallContentPartStatus => { if (message.role !== "assistant") return COMPLETE_STATUS; const isLastPart = partIndex === Math.max(0, message.content.length - 1); @@ -48,7 +48,7 @@ const toContentPartStatus = ( return COMPLETE_STATUS; } - return message.status as ToolContentPartStatus; + return message.status as ToolCallContentPartStatus; }; export const EMPTY_CONTENT = Object.freeze({ type: "text", text: "" }); diff --git a/packages/react/src/context/stores/ContentPart.ts b/packages/react/src/context/stores/ContentPart.ts index f302964d4..540a17d07 100644 --- a/packages/react/src/context/stores/ContentPart.ts +++ b/packages/react/src/context/stores/ContentPart.ts @@ -3,7 +3,7 @@ import type { ImageContentPart, TextContentPart, ToolCallContentPart, - ToolContentPartStatus, + ToolCallContentPartStatus, UIContentPart, } from "../../types/AssistantTypes"; @@ -23,12 +23,12 @@ export type UIContentPartState = Readonly<{ }>; export type ToolCallContentPartState = Readonly<{ - status: ToolContentPartStatus; + status: ToolCallContentPartStatus; part: ToolCallContentPart; }>; export type ContentPartState = Readonly<{ - status: ToolContentPartStatus; + status: ContentPartStatus | ToolCallContentPartStatus; part: | TextContentPart | ImageContentPart diff --git a/packages/react/src/internal.ts b/packages/react/src/internal.ts index 3646cd79e..927b9348e 100644 --- a/packages/react/src/internal.ts +++ b/packages/react/src/internal.ts @@ -1,6 +1,6 @@ export { ProxyConfigProvider } from "./utils/ProxyConfigProvider"; export { MessageRepository } from "./runtimes/utils/MessageRepository"; export { BaseAssistantRuntime } from "./runtimes/core/BaseAssistantRuntime"; -export { useSmooth } from "./utils/hooks/useSmooth"; +export * from "./utils/smooth"; export { TooltipIconButton } from "./ui/base/tooltip-icon-button"; export { generateId } from "./utils/idUtils"; diff --git a/packages/react/src/primitives/contentPart/ContentPartText.tsx b/packages/react/src/primitives/contentPart/ContentPartText.tsx index 1820cc12e..1b381b2fe 100644 --- a/packages/react/src/primitives/contentPart/ContentPartText.tsx +++ b/packages/react/src/primitives/contentPart/ContentPartText.tsx @@ -1,32 +1,39 @@ "use client"; import { Primitive } from "@radix-ui/react-primitive"; -import { type ElementRef, forwardRef, ComponentPropsWithoutRef } from "react"; +import { + type ElementRef, + forwardRef, + ComponentPropsWithoutRef, + ElementType, +} from "react"; import { useContentPartText } from "../../primitive-hooks/contentPart/useContentPartText"; -import { useSmooth } from "../../utils/hooks/useSmooth"; +import { useSmooth } from "../../utils/smooth/useSmooth"; type ContentPartPrimitiveTextElement = ElementRef; type PrimitiveSpanProps = ComponentPropsWithoutRef; export type ContentPartPrimitiveTextProps = Omit< PrimitiveSpanProps, - "children" -> & { smooth?: boolean }; + "children" | "asChild" +> & { + smooth?: boolean; + component?: ElementType; +}; export const ContentPartPrimitiveText = forwardRef< ContentPartPrimitiveTextElement, ContentPartPrimitiveTextProps ->(({ smooth = true, ...rest }, forwardedRef) => { +>(({ smooth = true, component: Component = "span", ...rest }, forwardedRef) => { const { - status, part: { text }, - } = useContentPartText(); - const smoothText = useSmooth(text, smooth); + status, + } = useSmooth(useContentPartText(), smooth); return ( - - {smoothText} - + + {text} + ); }); diff --git a/packages/react/src/styles/tailwindcss/thread.css b/packages/react/src/styles/tailwindcss/thread.css index 3b1284662..82068bbdc 100644 --- a/packages/react/src/styles/tailwindcss/thread.css +++ b/packages/react/src/styles/tailwindcss/thread.css @@ -154,6 +154,6 @@ @apply whitespace-pre-line; } -.aui-text-in-progress::after { +.aui-text-running::after { @apply animate-pulse font-sans content-['\25CF'] ltr:ml-1 rtl:mr-1; } diff --git a/packages/react/src/types/AssistantTypes.ts b/packages/react/src/types/AssistantTypes.ts index f8d9cea48..6051cdc7a 100644 --- a/packages/react/src/types/AssistantTypes.ts +++ b/packages/react/src/types/AssistantTypes.ts @@ -73,7 +73,7 @@ export type ContentPartStatus = error?: unknown; }; -export type ToolContentPartStatus = +export type ToolCallContentPartStatus = | { type: "requires-action"; reason: "tool-calls"; diff --git a/packages/react/src/types/ContentPartComponentTypes.tsx b/packages/react/src/types/ContentPartComponentTypes.tsx index 353a08e70..b931331b0 100644 --- a/packages/react/src/types/ContentPartComponentTypes.tsx +++ b/packages/react/src/types/ContentPartComponentTypes.tsx @@ -5,7 +5,7 @@ import type { ImageContentPart, TextContentPart, ToolCallContentPart, - ToolContentPartStatus, + ToolCallContentPartStatus, UIContentPart, } from "./AssistantTypes"; @@ -32,7 +32,7 @@ export type ToolCallContentPartProps< TResult = unknown, > = { part: ToolCallContentPart; - status: ToolContentPartStatus; + status: ToolCallContentPartStatus; addResult: (result: any) => void; }; diff --git a/packages/react/src/ui/content-part.tsx b/packages/react/src/ui/content-part.tsx index a63db5dea..e69c1878c 100644 --- a/packages/react/src/ui/content-part.tsx +++ b/packages/react/src/ui/content-part.tsx @@ -1,22 +1,21 @@ import { FC } from "react"; - import { ContentPartPrimitive } from "../primitives"; -import { TextContentPartProps } from "../types"; +import { useSmoothStatus, withSmoothContextProvider } from "../utils/smooth"; import classNames from "classnames"; -const Text: FC = ({ status }) => { +export const Text: FC = () => { + const status = useSmoothStatus(); return ( -

- -

+ component="p" + /> ); }; -const exports = { Text }; +const exports = { Text: withSmoothContextProvider(Text) }; export default exports; diff --git a/packages/react/src/utils/smooth/SmoothContext.tsx b/packages/react/src/utils/smooth/SmoothContext.tsx new file mode 100644 index 000000000..16c94f183 --- /dev/null +++ b/packages/react/src/utils/smooth/SmoothContext.tsx @@ -0,0 +1,76 @@ +import { + createContext, + FC, + forwardRef, + PropsWithChildren, + useContext, + useState, +} from "react"; +import { useContentPartContext } from "../../context"; +import { ReadonlyStore } from "../../context/ReadonlyStore"; +import { create } from "zustand"; +import { + ContentPartStatus, + ToolCallContentPartStatus, +} from "../../types/AssistantTypes"; + +type SmoothContextValue = { + useSmoothStatus: ReadonlyStore; +}; + +const SmoothContext = createContext(null); + +const makeSmoothContext = ( + initialState: ContentPartStatus | ToolCallContentPartStatus, +) => { + const useSmoothStatus = create(() => initialState); + return { useSmoothStatus }; +}; + +export const SmoothContextProvider: FC = ({ children }) => { + const outer = useSmoothContext({ optional: true }); + const { useContentPart } = useContentPartContext(); + + const [context] = useState(() => + makeSmoothContext(useContentPart.getState().status), + ); + + // do not wrap if there is an outer SmoothContextProvider + if (outer) return children; + + return ( + {children} + ); +}; + +export const withSmoothContextProvider = >( + Component: C, +): C => { + const Wrapped = forwardRef((props, ref) => { + return ( + + + + ); + }); + Wrapped.displayName = Component.displayName; + return Wrapped as any; +}; + +export function useSmoothContext(): SmoothContextValue; +export function useSmoothContext(options: { + optional: true; +}): SmoothContextValue | null; +export function useSmoothContext(options?: { optional: true }) { + const context = useContext(SmoothContext); + if (!options?.optional && !context) + throw new Error( + "This component must be used within a SmoothContextProvider.", + ); + return context; +} + +export const useSmoothStatus = () => { + const { useSmoothStatus } = useSmoothContext(); + return useSmoothStatus(); +}; diff --git a/packages/react/src/utils/smooth/index.ts b/packages/react/src/utils/smooth/index.ts new file mode 100644 index 000000000..8b32076ae --- /dev/null +++ b/packages/react/src/utils/smooth/index.ts @@ -0,0 +1,3 @@ +export { useSmooth } from "./useSmooth"; +export { useSmoothStatus } from "./SmoothContext"; +export { withSmoothContextProvider } from "./SmoothContext"; diff --git a/packages/react/src/utils/hooks/useSmooth.tsx b/packages/react/src/utils/smooth/useSmooth.tsx similarity index 61% rename from packages/react/src/utils/hooks/useSmooth.tsx rename to packages/react/src/utils/smooth/useSmooth.tsx index daf59d4c8..dd0d0bb9a 100644 --- a/packages/react/src/utils/hooks/useSmooth.tsx +++ b/packages/react/src/utils/smooth/useSmooth.tsx @@ -1,7 +1,15 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useMessageContext } from "../../context"; +import { + ContentPartStatus, + ToolCallContentPartStatus, +} from "../../types/AssistantTypes"; +import { TextContentPartState } from "../../context/stores/ContentPart"; +import { useSmoothContext } from "./SmoothContext"; +import { StoreApi } from "zustand"; +import { useCallbackRef } from "@radix-ui/react-use-callback-ref"; class TextStreamAnimator { private animationFrameId: number | null = null; @@ -57,14 +65,36 @@ class TextStreamAnimator { }; } -export const useSmooth = (text: string, smooth: boolean = false) => { +const SMOOTH_STATUS: ContentPartStatus = Object.freeze({ + type: "running", +}); + +export const useSmooth = ( + state: TextContentPartState, + smooth: boolean = false, +): TextContentPartState => { + const { useSmoothStatus } = useSmoothContext({ optional: true }) ?? {}; + + const { + part: { text }, + } = state; const { useMessage } = useMessageContext(); const id = useMessage((m) => m.message.id); const idRef = useRef(id); const [displayedText, setDisplayedText] = useState(text); + + const setText = useCallbackRef((text: string) => { + setDisplayedText(text); + ( + useSmoothStatus as unknown as + | StoreApi + | undefined + )?.setState(text !== state.part.text ? SMOOTH_STATUS : state.status); + }); + const [animatorRef] = useState( - new TextStreamAnimator(text, setDisplayedText), + new TextStreamAnimator(text, setText), ); useEffect(() => { @@ -75,7 +105,7 @@ export const useSmooth = (text: string, smooth: boolean = false) => { if (idRef.current !== id || !text.startsWith(animatorRef.targetText)) { idRef.current = id; - setDisplayedText(text); + setText(text); animatorRef.currentText = text; animatorRef.targetText = text; @@ -86,7 +116,7 @@ export const useSmooth = (text: string, smooth: boolean = false) => { animatorRef.targetText = text; animatorRef.start(); - }, [animatorRef, id, smooth, text]); + }, [setText, animatorRef, id, smooth, text]); useEffect(() => { return () => { @@ -94,5 +124,14 @@ export const useSmooth = (text: string, smooth: boolean = false) => { }; }, [animatorRef]); - return smooth ? displayedText : text; + return useMemo( + () => + smooth + ? { + part: { type: "text", text: displayedText }, + status: text === displayedText ? state.status : SMOOTH_STATUS, + } + : state, + [smooth, displayedText, state, text], + ); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 078c14f90..eb49cd859 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -844,6 +844,9 @@ importers: '@assistant-ui/react': specifier: ^0.5.9 version: link:../react + '@radix-ui/react-primitive': + specifier: ^2.0.0 + version: 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.3)(react@18.3.1)