From 44537aed9fd282ac30c84171b6789d9c8f0d1aba Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 25 Mar 2026 11:53:46 +0530 Subject: [PATCH 1/4] added missing export --- .../react-ui/src/components/Charts/index.ts | 31 +++++++++++++++---- packages/react-ui/src/index.ts | 2 ++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/react-ui/src/components/Charts/index.ts b/packages/react-ui/src/components/Charts/index.ts index f9f64af1a..1ed4a30f7 100644 --- a/packages/react-ui/src/components/Charts/index.ts +++ b/packages/react-ui/src/components/Charts/index.ts @@ -1,13 +1,32 @@ -export { AreaChart as ScrollableAreaChart } from "./AreaChart"; +export { + AreaChart as ScrollableAreaChart, + type AreaChartProps as ScrollableAreaChartProps, +} from "./AreaChart"; +export { + type AreaChartData as ScrollableAreaChartData, + type AreaChartVariant as ScrollableAreaChartVariant, +} from "./AreaChart/types"; export * from "./AreaChartCondensed"; -export { AreaChartCondensed as AreaChart } from "./AreaChartCondensed"; -export { BarChart as ScrollableBarChart } from "./BarChart"; +export { + BarChart as ScrollableBarChart, + type BarChartProps as ScrollableBarChartProps, +} from "./BarChart"; +export { + type BarChartData as ScrollableBarChartData, + type BarChartVariant as ScrollableBarChartVariant, +} from "./BarChart/types"; export * from "./BarChartCondensed"; -export { BarChartCondensed as BarChart } from "./BarChartCondensed"; export * from "./HorizontalBarChart"; -export { LineChart as ScrollableLineChart } from "./LineChart"; +export { + LineChart as ScrollableLineChart, + type LineChartProps as ScrollableLineChartProps, +} from "./LineChart"; +export { + type LineChartData as ScrollableLineChartData, + type LineChartVariant as ScrollableLineChartVariant, +} from "./LineChart/types"; export * from "./LineChartCondensed"; -export { LineChartCondensed as LineChart } from "./LineChartCondensed"; + export * from "./MiniAreaChart"; export * from "./MiniBarChart"; export * from "./MiniLineChart"; diff --git a/packages/react-ui/src/index.ts b/packages/react-ui/src/index.ts index fac42b290..fc82c883f 100644 --- a/packages/react-ui/src/index.ts +++ b/packages/react-ui/src/index.ts @@ -68,6 +68,8 @@ export { defaultLightTheme, swatchTokens, } from "./components/ThemeProvider"; +export type { ThemeProps } from "./components/ThemeProvider/ThemeProvider"; +export type { ThemeMode } from "./components/ThemeProvider/types"; export * from "./components/ToolCall"; export * from "./components/ToolResult"; From 8552f3394301f54f35dca3ba055f089213128723 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sat, 28 Mar 2026 15:55:39 +0530 Subject: [PATCH 2/4] Add Share Thread feature and fix Charts exports for c1 migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit prepares the OpenUI react-ui package for c1's migration from @crayonai/react-ui. It adds the Share Thread feature natively to OpenUI and fixes chart export naming. Share Thread (new feature): - Add ShareThread component, ShareThreadModal (Radix Dialog), and supporting hooks (useShareThread, useShareMessages) - Add generateShareLink prop to SharedChatUIProps — when provided, a share button appears in the chat header for all three variants - Wire into FullScreen (MobileHeader + ThreadHeader), Copilot (Header), and BottomTray (Header) - Add @radix-ui/react-dialog dependency - Add storybook stories for all three variants with share link Charts exports fix: - Remove Scrollable* aliases from Charts/index.ts — export AreaChart, BarChart, LineChart under their original names (matching subpath exports and consumer expectations) - Fix genui-lib chart condensed imports to use correct paths Shell Thread fixes: - Export AssistantMessageComponent type from _shared/types - Minor fixes to Shell, CopilotShell, BottomTray Thread components Checkpoint: openui-comp side of c1 migration is complete. The c1 repo (composition) has separate commits for Phases 1-6 covering the full migration from @crayonai to @openuidev. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/react-ui/package.json | 5 +- .../src/components/BottomTray/Thread.tsx | 7 +- .../react-ui/src/components/Charts/index.ts | 28 +-- .../src/components/CopilotShell/Thread.tsx | 7 +- .../OpenUIChat/ComposedBottomTray.tsx | 16 +- .../components/OpenUIChat/ComposedCopilot.tsx | 15 +- .../OpenUIChat/ComposedStandalone.tsx | 22 +- .../src/components/OpenUIChat/ShareThread.tsx | 91 +++++++++ .../OpenUIChat/ShareThreadModal.tsx | 190 ++++++++++++++++++ .../src/components/OpenUIChat/index.ts | 2 + .../src/components/OpenUIChat/openUIChat.scss | 1 + .../components/OpenUIChat/shareThread.scss | 111 ++++++++++ .../OpenUIChat/stories/OpenUIChat.stories.tsx | 42 ++++ .../src/components/OpenUIChat/types.ts | 6 + .../components/OpenUIChat/useShareMessages.ts | 128 ++++++++++++ .../components/OpenUIChat/useShareThread.ts | 38 ++++ .../react-ui/src/components/Shell/Thread.tsx | 8 +- .../src/components/_shared/types/index.ts | 1 + .../genui-lib/Charts/AreaChartCondensed.ts | 2 +- .../src/genui-lib/Charts/BarChartCondensed.ts | 2 +- .../genui-lib/Charts/LineChartCondensed.ts | 2 +- pnpm-lock.yaml | 71 ++++--- 22 files changed, 719 insertions(+), 76 deletions(-) create mode 100644 packages/react-ui/src/components/OpenUIChat/ShareThread.tsx create mode 100644 packages/react-ui/src/components/OpenUIChat/ShareThreadModal.tsx create mode 100644 packages/react-ui/src/components/OpenUIChat/shareThread.scss create mode 100644 packages/react-ui/src/components/OpenUIChat/useShareMessages.ts create mode 100644 packages/react-ui/src/components/OpenUIChat/useShareThread.ts diff --git a/packages/react-ui/package.json b/packages/react-ui/package.json index 55d747712..df27c52f3 100644 --- a/packages/react-ui/package.json +++ b/packages/react-ui/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@openuidev/react-ui", "license": "MIT", - "version": "0.9.19", + "version": "0.9.20", "description": "Component library for Generative UI SDK", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -66,8 +66,8 @@ "ci": "pnpm run lint:check && pnpm run format:check" }, "peerDependencies": { - "@openuidev/react-lang": "workspace:^", "@openuidev/react-headless": "workspace:^", + "@openuidev/react-lang": "workspace:^", "react": ">=19.0.0", "react-dom": ">=19.0.0", "zustand": "^4.5.5" @@ -77,6 +77,7 @@ "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-aspect-ratio": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.3", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-radio-group": "^1.2.2", diff --git a/packages/react-ui/src/components/BottomTray/Thread.tsx b/packages/react-ui/src/components/BottomTray/Thread.tsx index 68f67925d..712e9f190 100644 --- a/packages/react-ui/src/components/BottomTray/Thread.tsx +++ b/packages/react-ui/src/components/BottomTray/Thread.tsx @@ -191,12 +191,14 @@ export const RenderMessage = memo( allMessages, assistantMessage: CustomAssistantMessage, userMessage: CustomUserMessage, + isStreaming, }: { message: Message; className?: string; allMessages: Message[]; assistantMessage?: AssistantMessageComponent; userMessage?: UserMessageComponent; + isStreaming: boolean; }) => { if (message.role === "tool") { return null; @@ -204,7 +206,7 @@ export const RenderMessage = memo( if (message.role === "assistant") { if (CustomAssistantMessage) { - return ; + return ; } return ( @@ -252,7 +254,7 @@ export const Messages = ({ return (
- {messages.map((message) => { + {messages.map((message, i) => { return ( ); diff --git a/packages/react-ui/src/components/Charts/index.ts b/packages/react-ui/src/components/Charts/index.ts index 1ed4a30f7..914a4e0a2 100644 --- a/packages/react-ui/src/components/Charts/index.ts +++ b/packages/react-ui/src/components/Charts/index.ts @@ -1,32 +1,10 @@ -export { - AreaChart as ScrollableAreaChart, - type AreaChartProps as ScrollableAreaChartProps, -} from "./AreaChart"; -export { - type AreaChartData as ScrollableAreaChartData, - type AreaChartVariant as ScrollableAreaChartVariant, -} from "./AreaChart/types"; +export * from "./AreaChart"; export * from "./AreaChartCondensed"; -export { - BarChart as ScrollableBarChart, - type BarChartProps as ScrollableBarChartProps, -} from "./BarChart"; -export { - type BarChartData as ScrollableBarChartData, - type BarChartVariant as ScrollableBarChartVariant, -} from "./BarChart/types"; +export * from "./BarChart"; export * from "./BarChartCondensed"; export * from "./HorizontalBarChart"; -export { - LineChart as ScrollableLineChart, - type LineChartProps as ScrollableLineChartProps, -} from "./LineChart"; -export { - type LineChartData as ScrollableLineChartData, - type LineChartVariant as ScrollableLineChartVariant, -} from "./LineChart/types"; +export * from "./LineChart"; export * from "./LineChartCondensed"; - export * from "./MiniAreaChart"; export * from "./MiniBarChart"; export * from "./MiniLineChart"; diff --git a/packages/react-ui/src/components/CopilotShell/Thread.tsx b/packages/react-ui/src/components/CopilotShell/Thread.tsx index 99e70bdc3..28f0bdcc6 100644 --- a/packages/react-ui/src/components/CopilotShell/Thread.tsx +++ b/packages/react-ui/src/components/CopilotShell/Thread.tsx @@ -189,12 +189,14 @@ export const RenderMessage = memo( allMessages, assistantMessage: CustomAssistantMessage, userMessage: CustomUserMessage, + isStreaming, }: { message: Message; className?: string; allMessages: Message[]; assistantMessage?: AssistantMessageComponent; userMessage?: UserMessageComponent; + isStreaming: boolean; }) => { if (message.role === "tool") { return null; @@ -202,7 +204,7 @@ export const RenderMessage = memo( if (message.role === "assistant") { if (CustomAssistantMessage) { - return ; + return ; } return ( @@ -250,7 +252,7 @@ export const Messages = ({ return (
- {messages.map((message) => { + {messages.map((message, i) => { return ( ); diff --git a/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx b/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx index 0200ee24d..148fb6fc0 100644 --- a/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx +++ b/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx @@ -13,6 +13,7 @@ import { WelcomeScreen, } from "../BottomTray"; import { CustomComposerAdapter } from "./CustomComposerAdapter"; +import { ShareThread } from "./ShareThread"; import type { SharedChatUIProps } from "./types"; import { isChatEmpty, isWelcomeComponent } from "./utils"; import { withChatProvider } from "./withChatProvider"; @@ -82,6 +83,7 @@ const BottomTrayInner = ({ userMessage, composer: ComposerComponent, headerActions, + generateShareLink, }: BottomTraySpecificProps) => { const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState(defaultOpen); @@ -94,6 +96,10 @@ const BottomTrayInner = ({ onOpenChange?.(newIsOpen); }; + const shareButton = generateShareLink ? ( + + ) : null; + return ( <> handleOpenChange(!isOpen)} isOpen={isOpen}> @@ -104,7 +110,15 @@ const BottomTrayInner = ({ -
handleOpenChange(false)} rightChildren={headerActions} /> +
handleOpenChange(false)} + rightChildren={ + <> + {shareButton} + {headerActions} + + } + /> { + const shareButton = generateShareLink ? ( + + ) : null; + return ( -
+
+ {shareButton} + {headerActions} + + } + /> { + const shareButton = generateShareLink ? ( + + ) : null; + return ( @@ -100,8 +106,20 @@ const FullScreenInner = ({ - - {threadHeader && {threadHeader}} + + {shareButton} + {mobileHeaderActions} + + } + /> + {(threadHeader || shareButton) && ( + + {threadHeader} + {shareButton} + + )} Promise; + /** Title for the share modal. Defaults to `"Share chat"`. */ + modalTitle?: string; + /** Custom trigger element. When omitted, a default share button is rendered. */ + customTrigger?: ReactNode; +} + +/** + * Share button that opens a modal for generating and copying a shareable link. + * Renders nothing when there are no messages to share. + * + * @category Components + */ +export const ShareThread = ({ generateShareLink, modalTitle, customTrigger }: ShareThreadProps) => { + const { layout } = useLayoutContext() || {}; + const isMobile = layout === "mobile"; + const { portalThemeClassName } = useTheme(); + + const { selectedMessages, getShareThreadLink, shouldDisableShareButton } = useShareThread({ + generateShareLink, + }); + + if (selectedMessages.length === 0) return null; + + return ( + + ) + } + generateLink={getShareThreadLink} + themeClassName={portalThemeClassName} + /> + ); +}; + +ShareThread.displayName = "ShareThread"; + +type DefaultShareButtonProps = { + isMobile: boolean; + shouldDisableShareButton?: boolean; +} & React.ButtonHTMLAttributes; + +const DefaultShareButton = React.forwardRef( + ({ isMobile, shouldDisableShareButton, ...props }, ref) => { + return isMobile ? ( + } + size="medium" + icon={} + variant="secondary" + disabled={shouldDisableShareButton} + {...props} + /> + ) : ( + + ); + }, +); + +DefaultShareButton.displayName = "DefaultShareButton"; diff --git a/packages/react-ui/src/components/OpenUIChat/ShareThreadModal.tsx b/packages/react-ui/src/components/OpenUIChat/ShareThreadModal.tsx new file mode 100644 index 000000000..7444aadae --- /dev/null +++ b/packages/react-ui/src/components/OpenUIChat/ShareThreadModal.tsx @@ -0,0 +1,190 @@ +import * as Dialog from "@radix-ui/react-dialog"; +import clsx from "clsx"; +import { Check, Copy, Link, Loader2, X } from "lucide-react"; +import { forwardRef, type ReactNode, useCallback, useState } from "react"; +import { useLayoutContext } from "../../context/LayoutContext"; +import { Button } from "../Button"; +import { IconButton } from "../IconButton"; +import { Input } from "../Input"; +import { useTheme } from "../ThemeProvider/ThemeProvider"; +import "./shareThread.scss"; + +/** + * Props for {@link ShareThreadModal}. + * + * @category Components + */ +export interface ShareThreadModalProps { + /** Modal title. Defaults to `"Share chat"`. */ + title?: string; + /** The trigger element that opens the modal. */ + trigger: ReactNode; + /** Async function that returns a shareable URL. */ + generateLink: () => Promise; + /** Theme class name for portal targeting. */ + themeClassName?: string; +} + +const getErrorMessage = (error: Error) => { + if (error instanceof DOMException && error.name === "NotAllowedError") { + return "Clipboard access denied. Please allow clipboard access in your browser settings, or copy the link manually from the text area above."; + } else if (error instanceof DOMException && error.name === "NotSupportedError") { + return "Clipboard not supported. Please copy the link manually from the text area above."; + } else { + return "Failed to copy to clipboard. Please copy the link manually from the text area above."; + } +}; + +/** + * Modal dialog for generating and copying a shareable link. + * + * @category Components + */ +export const ShareThreadModal = forwardRef( + ({ title, trigger, generateLink, themeClassName }, _ref) => { + const { portalThemeClassName } = useTheme(); + const { layout } = useLayoutContext() || {}; + const isMobile = layout === "mobile"; + + const [isOpen, setIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [generatedLink, setGeneratedLink] = useState(null); + const [hasCopied, setHasCopied] = useState(false); + const [clipboardError, setClipboardError] = useState(null); + + const handleGenerateLink = useCallback(async () => { + setIsLoading(true); + try { + const link = await generateLink(); + setGeneratedLink(link); + } catch (_error) { + // Consumer handles errors in their generateLink callback + } finally { + setIsLoading(false); + } + }, [generateLink]); + + const handleCopy = useCallback(async () => { + if (!generatedLink) return; + setClipboardError(null); + + if (!navigator.clipboard) { + setClipboardError( + "Clipboard access not available. Please copy the link manually from the text area above.", + ); + return; + } + + try { + await navigator.clipboard.writeText(generatedLink); + setHasCopied(true); + setTimeout(() => setHasCopied(false), 2000); + } catch (error) { + console.warn("Copy to clipboard failed:", error); + setClipboardError(getErrorMessage(error as Error)); + } + }, [generatedLink]); + + const handleOnOpenChange = useCallback((open: boolean) => { + setIsOpen(open); + if (!open) { + setTimeout(() => { + setIsLoading(false); + setGeneratedLink(null); + setHasCopied(false); + setClipboardError(null); + }, 300); + } + }, []); + + const renderActionButton = () => { + if (isLoading) { + return ( + + ); + } + + if (generatedLink) { + return ( + + ); + } + + return ( + + ); + }; + + return ( + + {trigger} + + + +
+ + {title ?? "Share chat"} + + } + variant="tertiary" + size="small" + onClick={() => handleOnOpenChange(false)} + className="openui-share-thread-modal__close-button" + /> +
+ +
+

+ This conversation may include personal information. Take a moment to check the + content before sharing the link. +

+ +
+
+ +
+ {renderActionButton()} +
+
+ + {clipboardError && ( +

{clipboardError}

+ )} +
+
+
+
+
+ ); + }, +); + +ShareThreadModal.displayName = "ShareThreadModal"; diff --git a/packages/react-ui/src/components/OpenUIChat/index.ts b/packages/react-ui/src/components/OpenUIChat/index.ts index 2afe1c22f..7a9c8d7ca 100644 --- a/packages/react-ui/src/components/OpenUIChat/index.ts +++ b/packages/react-ui/src/components/OpenUIChat/index.ts @@ -2,6 +2,8 @@ export { BottomTray } from "./ComposedBottomTray"; export { Copilot } from "./ComposedCopilot"; export { FullScreen } from "./ComposedStandalone"; export { GenUIUserMessage } from "./GenUIUserMessage"; +export { ShareThread } from "./ShareThread"; +export type { ShareThreadProps } from "./ShareThread"; export type { AssistantMessageComponent, ComposerComponent, diff --git a/packages/react-ui/src/components/OpenUIChat/openUIChat.scss b/packages/react-ui/src/components/OpenUIChat/openUIChat.scss index adb45f588..d3b49825c 100644 --- a/packages/react-ui/src/components/OpenUIChat/openUIChat.scss +++ b/packages/react-ui/src/components/OpenUIChat/openUIChat.scss @@ -1 +1,2 @@ @forward "./composedBottomTray.scss"; +@forward "./shareThread.scss"; diff --git a/packages/react-ui/src/components/OpenUIChat/shareThread.scss b/packages/react-ui/src/components/OpenUIChat/shareThread.scss new file mode 100644 index 000000000..8328a1047 --- /dev/null +++ b/packages/react-ui/src/components/OpenUIChat/shareThread.scss @@ -0,0 +1,111 @@ +@use "../../cssUtils" as cssUtils; + +.openui-share-thread-modal { + &__overlay { + position: absolute; + inset: 0; + z-index: 9999; + background-color: cssUtils.$overlay; + } + + &__content { + position: absolute; + left: 50%; + top: 50%; + width: 100%; + max-width: 32rem; + transform: translate(-50%, -50%); + border-radius: cssUtils.$radius-2xl; + background-color: cssUtils.$elevated; + padding: cssUtils.$space-l; + box-shadow: cssUtils.$shadow-m; + border: 1px solid cssUtils.$border-interactive; + z-index: 10001; + + &--mobile { + max-width: 20rem; + padding: cssUtils.$space-l; + } + } + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: cssUtils.$space-m; + } + + &__title { + color: cssUtils.$text-neutral-primary; + @include cssUtils.typography(heading, small); + margin: 0; + } + + &__body { + color: cssUtils.$text-neutral-primary; + display: flex; + flex-direction: column; + gap: cssUtils.$space-m; + } + + &__description { + @include cssUtils.typography(body, small); + color: cssUtils.$text-neutral-secondary; + margin: 0; + } + + &__input-section { + display: flex; + flex-direction: column; + gap: cssUtils.$space-s; + } + + &__input-wrapper { + display: flex; + gap: cssUtils.$space-s; + padding: cssUtils.$space-xs; + border: 1px solid cssUtils.$border-default; + border-radius: cssUtils.$radius-l; + background-color: cssUtils.$elevated-strong; + color: cssUtils.$text-neutral-primary; + + &:focus-within { + border-color: cssUtils.$border-interactive; + outline: 2px solid cssUtils.$border-interactive; + outline-offset: -1px; + } + } + + &__input { + padding: 0 cssUtils.$space-xs; + border: none; + background: transparent; + @include cssUtils.typography(body, default); + } + + &__button-container { + display: flex; + align-items: flex-start; + flex-shrink: 0; + } + + &__loading-icon { + height: 1rem; + width: 1rem; + animation: openui-share-spin 1s linear infinite; + } + + &__error-message { + @include cssUtils.typography(body, small); + color: cssUtils.$text-danger-primary; + } +} + +@keyframes openui-share-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/packages/react-ui/src/components/OpenUIChat/stories/OpenUIChat.stories.tsx b/packages/react-ui/src/components/OpenUIChat/stories/OpenUIChat.stories.tsx index a1dce08c2..e48090f92 100644 --- a/packages/react-ui/src/components/OpenUIChat/stories/OpenUIChat.stories.tsx +++ b/packages/react-ui/src/components/OpenUIChat/stories/OpenUIChat.stories.tsx @@ -75,6 +75,11 @@ const LONG_STARTERS: ConversationStartersConfig = { ], }; +const mockGenerateShareLink = async (_messages: Message[]) => { + await new Promise((r) => setTimeout(r, 1000)); + return `https://example.com/shared/${crypto.randomUUID().slice(0, 8)}`; +}; + const mockProcessMessage = async ({ messages }: { messages: Message[] }) => { const lastMsg = messages[messages.length - 1]; const content = @@ -145,6 +150,19 @@ export const StandaloneLongStarters = { ), }; +export const StandaloneWithShareLink = { + render: () => ( +
+ +
+ ), +}; + export const StandaloneWithThreadHeader = { render: () => (
@@ -201,6 +219,19 @@ export const CopilotWithWelcome = { ), }; +export const CopilotWithShareLink = { + render: () => ( +
+ +
+ ), +}; + export const CopilotWithHeaderActions = { render: () => (
@@ -283,6 +314,17 @@ export const BottomTrayLongStarters = { ), }; +export const BottomTrayWithShareLink = { + render: () => ( + + ), +}; + export const BottomTrayWithHeaderActions = { render: () => ( Promise; } diff --git a/packages/react-ui/src/components/OpenUIChat/useShareMessages.ts b/packages/react-ui/src/components/OpenUIChat/useShareMessages.ts new file mode 100644 index 000000000..8c0185b99 --- /dev/null +++ b/packages/react-ui/src/components/OpenUIChat/useShareMessages.ts @@ -0,0 +1,128 @@ +import type { Message } from "@openuidev/react-headless"; +import { useCallback, useState } from "react"; + +interface UseShareMessagesOptions { + initialMessages?: Message[]; + shareMode?: boolean; +} + +/** + * Hook for managing a list of messages that can be selected for sharing. + * + * @category Hooks + */ +export const useShareMessages = ({ + initialMessages = [], + shareMode = false, +}: UseShareMessagesOptions = {}) => { + const [messages, setMessages] = useState(initialMessages); + const [selectedMessageIds, setSelectedMessageIds] = useState>( + new Set(initialMessages.map((message) => message.id)), + ); + + const addMessage = useCallback( + (message: Message) => { + if (!shareMode) return; + + setSelectedMessageIds((prevIds) => { + if (prevIds.has(message.id)) return prevIds; + const newIds = new Set(prevIds); + newIds.add(message.id); + return newIds; + }); + + setMessages((prevMessages) => { + if (prevMessages.some((m) => m.id === message.id)) return prevMessages; + return [...prevMessages, message]; + }); + }, + [shareMode], + ); + + const removeMessage = useCallback( + (messageToRemove: Message) => { + if (!shareMode) return; + + setSelectedMessageIds((prevIds) => { + if (!prevIds.has(messageToRemove.id)) return prevIds; + const newIds = new Set(prevIds); + newIds.delete(messageToRemove.id); + return newIds; + }); + + setMessages((prevMessages) => + prevMessages.filter((message) => message.id !== messageToRemove.id), + ); + }, + [shareMode], + ); + + const toggleMessageSelection = useCallback( + (message: Message) => { + if (!shareMode) return; + + setSelectedMessageIds((prevIds) => { + if (prevIds.has(message.id)) { + setMessages((prevMessages) => prevMessages.filter((m) => m.id !== message.id)); + const newIds = new Set(prevIds); + newIds.delete(message.id); + return newIds; + } else { + setMessages((prevMessages) => { + if (prevMessages.some((m) => m.id === message.id)) return prevMessages; + return [...prevMessages, message]; + }); + const newIds = new Set(prevIds); + newIds.add(message.id); + return newIds; + } + }); + }, + [shareMode], + ); + + const updateSelectedMessages = useCallback( + (messagesToUpdate: Message[], mode: "append" | "replace" = "replace") => { + if (!shareMode) return; + + if (mode === "append") { + setMessages((prevMessages) => { + const existingIds = new Set(prevMessages.map((m) => m.id)); + const newMessages = messagesToUpdate.filter((message) => !existingIds.has(message.id)); + return [...prevMessages, ...newMessages]; + }); + + setSelectedMessageIds((prevIds) => { + const newIds = new Set(prevIds); + messagesToUpdate.forEach((message) => newIds.add(message.id)); + return newIds; + }); + } else { + const filteredMessages = messagesToUpdate.filter( + (message, index, self) => index === self.findIndex((m) => m.id === message.id), + ); + + setMessages(filteredMessages); + setSelectedMessageIds(new Set(filteredMessages.map((message) => message.id))); + } + }, + [shareMode], + ); + + const isMessageSelected = useCallback( + (message: Message) => { + return selectedMessageIds.has(message.id); + }, + [selectedMessageIds], + ); + + return { + addMessage, + isMessageSelected, + removeMessage, + selectedMessages: messages, + shareMode, + toggleMessageSelection, + updateSelectedMessages, + }; +}; diff --git a/packages/react-ui/src/components/OpenUIChat/useShareThread.ts b/packages/react-ui/src/components/OpenUIChat/useShareThread.ts new file mode 100644 index 000000000..859fdb73b --- /dev/null +++ b/packages/react-ui/src/components/OpenUIChat/useShareThread.ts @@ -0,0 +1,38 @@ +import type { Message } from "@openuidev/react-headless"; +import { useThread } from "@openuidev/react-headless"; +import { useCallback, useEffect } from "react"; +import { useShareMessages } from "./useShareMessages"; + +/** + * Hook for sharing entire conversation threads. + * Auto-selects all messages and provides a function to generate a shareable link. + * + * @category Hooks + */ +export const useShareThread = ({ + generateShareLink, +}: { + generateShareLink: (messages: Message[]) => Promise; +}) => { + const { isLoadingMessages, messages, isRunning } = useThread(); + + const { selectedMessages, updateSelectedMessages } = useShareMessages({ + shareMode: true, + }); + + useEffect(() => { + if (!isLoadingMessages && messages.length > 0) { + updateSelectedMessages(messages, "replace"); + } + }, [isLoadingMessages, messages, updateSelectedMessages]); + + const getShareThreadLink = useCallback(async () => { + return generateShareLink(selectedMessages); + }, [generateShareLink, selectedMessages]); + + return { + shouldDisableShareButton: isRunning || isLoadingMessages, + selectedMessages, + getShareThreadLink, + }; +}; diff --git a/packages/react-ui/src/components/Shell/Thread.tsx b/packages/react-ui/src/components/Shell/Thread.tsx index bbc6c2e25..33635b72d 100644 --- a/packages/react-ui/src/components/Shell/Thread.tsx +++ b/packages/react-ui/src/components/Shell/Thread.tsx @@ -256,12 +256,14 @@ export const RenderMessage = memo( allMessages, assistantMessage: CustomAssistantMessage, userMessage: CustomUserMessage, + isStreaming, }: { message: Message; className?: string; allMessages: Message[]; assistantMessage?: AssistantMessageComponent; userMessage?: UserMessageComponent; + isStreaming: boolean; }) => { if (message.role === "tool") { // Tool messages are rendered inline with their parent assistant message @@ -270,7 +272,7 @@ export const RenderMessage = memo( if (message.role === "assistant") { if (CustomAssistantMessage) { - return ; + return ; } return ( @@ -335,7 +337,7 @@ export const Messages = ({ return (
- {messages.map((message) => { + {messages.map((message, i) => { return ( ); diff --git a/packages/react-ui/src/components/_shared/types/index.ts b/packages/react-ui/src/components/_shared/types/index.ts index 9fd648af9..869f4ea5f 100644 --- a/packages/react-ui/src/components/_shared/types/index.ts +++ b/packages/react-ui/src/components/_shared/types/index.ts @@ -14,6 +14,7 @@ import type { AssistantMessage, UserMessage } from "@openuidev/react-headless"; */ export type AssistantMessageComponent = React.ComponentType<{ message: AssistantMessage; + isStreaming: boolean; }>; /** diff --git a/packages/react-ui/src/genui-lib/Charts/AreaChartCondensed.ts b/packages/react-ui/src/genui-lib/Charts/AreaChartCondensed.ts index 394c23b99..4f5010898 100644 --- a/packages/react-ui/src/genui-lib/Charts/AreaChartCondensed.ts +++ b/packages/react-ui/src/genui-lib/Charts/AreaChartCondensed.ts @@ -3,7 +3,7 @@ import { defineComponent } from "@openuidev/react-lang"; import React from "react"; import { z } from "zod"; -import { AreaChart as AreaChartCondensedComponent } from "../../components/Charts"; +import { AreaChartCondensed as AreaChartCondensedComponent } from "../../components/Charts"; import { buildChartData, hasAllProps } from "../helpers"; import { SeriesSchema } from "./Series"; diff --git a/packages/react-ui/src/genui-lib/Charts/BarChartCondensed.ts b/packages/react-ui/src/genui-lib/Charts/BarChartCondensed.ts index c9b4db761..92af8b6e8 100644 --- a/packages/react-ui/src/genui-lib/Charts/BarChartCondensed.ts +++ b/packages/react-ui/src/genui-lib/Charts/BarChartCondensed.ts @@ -3,7 +3,7 @@ import { defineComponent } from "@openuidev/react-lang"; import React from "react"; import { z } from "zod"; -import { BarChart as BarChartCondensedComponent } from "../../components/Charts"; +import { BarChartCondensed as BarChartCondensedComponent } from "../../components/Charts"; import { buildChartData, hasAllProps } from "../helpers"; import { SeriesSchema } from "./Series"; diff --git a/packages/react-ui/src/genui-lib/Charts/LineChartCondensed.ts b/packages/react-ui/src/genui-lib/Charts/LineChartCondensed.ts index 431ba3d34..9e7cd61f6 100644 --- a/packages/react-ui/src/genui-lib/Charts/LineChartCondensed.ts +++ b/packages/react-ui/src/genui-lib/Charts/LineChartCondensed.ts @@ -3,7 +3,7 @@ import { defineComponent } from "@openuidev/react-lang"; import React from "react"; import { z } from "zod"; -import { LineChart as LineChartCondensedComponent } from "../../components/Charts"; +import { LineChartCondensed as LineChartCondensedComponent } from "../../components/Charts"; import { buildChartData, hasAllProps } from "../helpers"; import { SeriesSchema } from "./Series"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d18e02ed..78e5ea8c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,13 +61,13 @@ importers: version: 0.68.17 fumadocs-core: specifier: 16.6.5 - version: 16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) + version: 16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) fumadocs-mdx: specifier: 14.2.8 - version: 14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.89.2)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.89.2)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0)) fumadocs-ui: specifier: 16.6.5 - version: 16.6.5(@takumi-rs/image-response@0.68.17)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1) + version: 16.6.5(@takumi-rs/image-response@0.68.17)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1) gpt-tokenizer: specifier: ^3.4.0 version: 3.4.0 @@ -79,7 +79,7 @@ importers: version: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) + version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -149,7 +149,7 @@ importers: version: 0.575.0(react@19.2.3) next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) + version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) openai: specifier: ^6.22.0 version: 6.22.0(ws@8.18.2)(zod@4.3.6) @@ -207,7 +207,7 @@ importers: version: 0.575.0(react@19.2.3) next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) + version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) openai: specifier: ^6.22.0 version: 6.22.0(ws@8.18.2)(zod@4.3.6) @@ -252,7 +252,7 @@ importers: dependencies: next: specifier: ^15.2.3 - version: 15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) + version: 15.5.12(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) openai: specifier: ^4.90.0 version: 4.104.0(ws@8.18.2)(zod@4.3.6) @@ -347,7 +347,7 @@ importers: version: 0.562.0(react@19.2.3) next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) + version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) openai: specifier: ^6.22.0 version: 6.22.0(ws@8.18.2)(zod@4.3.6) @@ -480,7 +480,7 @@ importers: version: 0.575.0(react@19.2.3) next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) + version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) openai: specifier: ^6.22.0 version: 6.22.0(ws@8.18.2)(zod@4.3.6) @@ -571,7 +571,7 @@ importers: version: 0.575.0(react@19.2.3) next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) + version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) react: specifier: 19.2.3 version: 19.2.3 @@ -709,6 +709,9 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.1.3 version: 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-dropdown-menu': specifier: ^2.1.7 version: 2.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -13359,7 +13362,7 @@ snapshots: aria-hidden: 1.2.6 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.1(@types/react@19.2.14)(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) @@ -18065,7 +18068,7 @@ snapshots: '@next/eslint-plugin-next': 16.1.6 eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.6.1)) @@ -18092,7 +18095,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -18107,14 +18110,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -18129,7 +18132,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -18615,7 +18618,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6): + fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6): dependencies: '@formatjs/intl-localematcher': 0.8.1 '@orama/orama': 3.1.18 @@ -18647,21 +18650,21 @@ snapshots: '@types/mdast': 4.0.4 '@types/react': 19.2.14 lucide-react: 0.570.0(react@19.2.4) - next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) + next: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) zod: 4.3.6 transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.89.2)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0)): + fumadocs-mdx@14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.89.2)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.27.3 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) + fumadocs-core: 16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) js-yaml: 4.1.1 mdast-util-mdx: 3.0.0 mdast-util-to-markdown: 2.1.2 @@ -18678,13 +18681,13 @@ snapshots: '@types/mdast': 4.0.4 '@types/mdx': 2.0.13 '@types/react': 19.2.14 - next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) + next: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) react: 19.2.4 vite: 7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.89.2)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - fumadocs-ui@16.6.5(@takumi-rs/image-response@0.68.17)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1): + fumadocs-ui@16.6.5(@takumi-rs/image-response@0.68.17)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1): dependencies: '@fumadocs/tailwind': 0.0.2(tailwindcss@4.2.1) '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -18698,7 +18701,7 @@ snapshots: '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) class-variance-authority: 0.7.1 - fumadocs-core: 16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) + fumadocs-core: 16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) lucide-react: 0.570.0(react@19.2.4) motion: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -18713,7 +18716,7 @@ snapshots: optionalDependencies: '@takumi-rs/image-response': 0.68.17 '@types/react': 19.2.14 - next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) + next: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2) transitivePeerDependencies: - '@emotion/is-prop-valid' - '@types/react-dom' @@ -20611,7 +20614,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2): + next@15.5.12(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2): dependencies: '@next/env': 15.5.12 '@swc/helpers': 0.5.15 @@ -20619,7 +20622,7 @@ snapshots: postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) + styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: '@next/swc-darwin-arm64': 15.5.12 '@next/swc-darwin-x64': 15.5.12 @@ -20637,7 +20640,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2): + next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2): dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 @@ -20646,7 +20649,7 @@ snapshots: postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3) + styled-jsx: 5.1.6(react@19.2.3) optionalDependencies: '@next/swc-darwin-arm64': 16.1.6 '@next/swc-darwin-x64': 16.1.6 @@ -20664,7 +20667,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2): + next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2): dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 @@ -20673,7 +20676,7 @@ snapshots: postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) + styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: '@next/swc-darwin-arm64': 16.1.6 '@next/swc-darwin-x64': 16.1.6 @@ -22390,19 +22393,15 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3): + styled-jsx@5.1.6(react@19.2.3): dependencies: client-only: 0.0.1 react: 19.2.3 - optionalDependencies: - '@babel/core': 7.29.0 - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): + styled-jsx@5.1.6(react@19.2.4): dependencies: client-only: 0.0.1 react: 19.2.4 - optionalDependencies: - '@babel/core': 7.29.0 styleq@0.1.3: {} From f2472a0f1996d7c56adf8362981d0f9bfb45a146 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Sun, 29 Mar 2026 03:41:15 +0530 Subject: [PATCH 3/4] Simplify Share Thread API: switch from messages to threadId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace generateShareLink((messages) => url) with generateShareLink((threadId) => url). The consumer's backend looks up messages by threadId — no need to pass the full message array client-side. Changes: - Delete useShareMessages.ts (message selection no longer needed) - Rewrite useShareThread.ts to get selectedThreadId from store - Update ShareThread props and types to (threadId: string) - Remove Message type import from SharedChatUIProps This is a cleaner API that avoids message format conversion concerns and simplifies the hook from ~130 lines to ~28 lines. Checkpoint: Share Thread feature complete with threadId-based API. Builds on 8552f33 which added the initial messages-based version. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/OpenUIChat/ShareThread.tsx | 9 +- .../OpenUIChat/stories/OpenUIChat.stories.tsx | 4 +- .../src/components/OpenUIChat/types.ts | 5 +- .../components/OpenUIChat/useShareMessages.ts | 128 ------------------ .../components/OpenUIChat/useShareThread.ts | 34 ++--- 5 files changed, 20 insertions(+), 160 deletions(-) delete mode 100644 packages/react-ui/src/components/OpenUIChat/useShareMessages.ts diff --git a/packages/react-ui/src/components/OpenUIChat/ShareThread.tsx b/packages/react-ui/src/components/OpenUIChat/ShareThread.tsx index 124bd53cd..5ff6df93e 100644 --- a/packages/react-ui/src/components/OpenUIChat/ShareThread.tsx +++ b/packages/react-ui/src/components/OpenUIChat/ShareThread.tsx @@ -1,4 +1,3 @@ -import type { Message } from "@openuidev/react-headless"; import { Share2 } from "lucide-react"; import React, { type ReactNode } from "react"; import { useLayoutContext } from "../../context/LayoutContext"; @@ -14,8 +13,8 @@ import { useShareThread } from "./useShareThread"; * @category Components */ export interface ShareThreadProps { - /** Async function that receives the current messages and returns a shareable URL. */ - generateShareLink: (messages: Message[]) => Promise; + /** Async function that receives the threadId and returns a shareable URL. */ + generateShareLink: (threadId: string) => Promise; /** Title for the share modal. Defaults to `"Share chat"`. */ modalTitle?: string; /** Custom trigger element. When omitted, a default share button is rendered. */ @@ -33,11 +32,11 @@ export const ShareThread = ({ generateShareLink, modalTitle, customTrigger }: Sh const isMobile = layout === "mobile"; const { portalThemeClassName } = useTheme(); - const { selectedMessages, getShareThreadLink, shouldDisableShareButton } = useShareThread({ + const { hasMessages, getShareThreadLink, shouldDisableShareButton } = useShareThread({ generateShareLink, }); - if (selectedMessages.length === 0) return null; + if (!hasMessages) return null; return ( { +const mockGenerateShareLink = async (threadId: string) => { await new Promise((r) => setTimeout(r, 1000)); - return `https://example.com/shared/${crypto.randomUUID().slice(0, 8)}`; + return `https://example.com/shared/${threadId}`; }; const mockProcessMessage = async ({ messages }: { messages: Message[] }) => { diff --git a/packages/react-ui/src/components/OpenUIChat/types.ts b/packages/react-ui/src/components/OpenUIChat/types.ts index d340fe700..c76857581 100644 --- a/packages/react-ui/src/components/OpenUIChat/types.ts +++ b/packages/react-ui/src/components/OpenUIChat/types.ts @@ -1,4 +1,3 @@ -import type { Message } from "@openuidev/react-headless"; import type { Library } from "@openuidev/react-lang"; import { ReactNode } from "react"; import { ScrollVariant } from "../../hooks/useScrollToBottom"; @@ -108,8 +107,8 @@ export interface SharedChatUIProps { */ componentLibrary?: Library; /** - * Async function that receives the current messages and returns a shareable URL. + * Async function that receives the selected threadId and returns a shareable URL. * When provided, a share button appears in the chat header. */ - generateShareLink?: (messages: Message[]) => Promise; + generateShareLink?: (threadId: string) => Promise; } diff --git a/packages/react-ui/src/components/OpenUIChat/useShareMessages.ts b/packages/react-ui/src/components/OpenUIChat/useShareMessages.ts deleted file mode 100644 index 8c0185b99..000000000 --- a/packages/react-ui/src/components/OpenUIChat/useShareMessages.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { Message } from "@openuidev/react-headless"; -import { useCallback, useState } from "react"; - -interface UseShareMessagesOptions { - initialMessages?: Message[]; - shareMode?: boolean; -} - -/** - * Hook for managing a list of messages that can be selected for sharing. - * - * @category Hooks - */ -export const useShareMessages = ({ - initialMessages = [], - shareMode = false, -}: UseShareMessagesOptions = {}) => { - const [messages, setMessages] = useState(initialMessages); - const [selectedMessageIds, setSelectedMessageIds] = useState>( - new Set(initialMessages.map((message) => message.id)), - ); - - const addMessage = useCallback( - (message: Message) => { - if (!shareMode) return; - - setSelectedMessageIds((prevIds) => { - if (prevIds.has(message.id)) return prevIds; - const newIds = new Set(prevIds); - newIds.add(message.id); - return newIds; - }); - - setMessages((prevMessages) => { - if (prevMessages.some((m) => m.id === message.id)) return prevMessages; - return [...prevMessages, message]; - }); - }, - [shareMode], - ); - - const removeMessage = useCallback( - (messageToRemove: Message) => { - if (!shareMode) return; - - setSelectedMessageIds((prevIds) => { - if (!prevIds.has(messageToRemove.id)) return prevIds; - const newIds = new Set(prevIds); - newIds.delete(messageToRemove.id); - return newIds; - }); - - setMessages((prevMessages) => - prevMessages.filter((message) => message.id !== messageToRemove.id), - ); - }, - [shareMode], - ); - - const toggleMessageSelection = useCallback( - (message: Message) => { - if (!shareMode) return; - - setSelectedMessageIds((prevIds) => { - if (prevIds.has(message.id)) { - setMessages((prevMessages) => prevMessages.filter((m) => m.id !== message.id)); - const newIds = new Set(prevIds); - newIds.delete(message.id); - return newIds; - } else { - setMessages((prevMessages) => { - if (prevMessages.some((m) => m.id === message.id)) return prevMessages; - return [...prevMessages, message]; - }); - const newIds = new Set(prevIds); - newIds.add(message.id); - return newIds; - } - }); - }, - [shareMode], - ); - - const updateSelectedMessages = useCallback( - (messagesToUpdate: Message[], mode: "append" | "replace" = "replace") => { - if (!shareMode) return; - - if (mode === "append") { - setMessages((prevMessages) => { - const existingIds = new Set(prevMessages.map((m) => m.id)); - const newMessages = messagesToUpdate.filter((message) => !existingIds.has(message.id)); - return [...prevMessages, ...newMessages]; - }); - - setSelectedMessageIds((prevIds) => { - const newIds = new Set(prevIds); - messagesToUpdate.forEach((message) => newIds.add(message.id)); - return newIds; - }); - } else { - const filteredMessages = messagesToUpdate.filter( - (message, index, self) => index === self.findIndex((m) => m.id === message.id), - ); - - setMessages(filteredMessages); - setSelectedMessageIds(new Set(filteredMessages.map((message) => message.id))); - } - }, - [shareMode], - ); - - const isMessageSelected = useCallback( - (message: Message) => { - return selectedMessageIds.has(message.id); - }, - [selectedMessageIds], - ); - - return { - addMessage, - isMessageSelected, - removeMessage, - selectedMessages: messages, - shareMode, - toggleMessageSelection, - updateSelectedMessages, - }; -}; diff --git a/packages/react-ui/src/components/OpenUIChat/useShareThread.ts b/packages/react-ui/src/components/OpenUIChat/useShareThread.ts index 859fdb73b..237eedd07 100644 --- a/packages/react-ui/src/components/OpenUIChat/useShareThread.ts +++ b/packages/react-ui/src/components/OpenUIChat/useShareThread.ts @@ -1,38 +1,28 @@ -import type { Message } from "@openuidev/react-headless"; -import { useThread } from "@openuidev/react-headless"; -import { useCallback, useEffect } from "react"; -import { useShareMessages } from "./useShareMessages"; +import { useThread, useThreadList } from "@openuidev/react-headless"; +import { useCallback } from "react"; /** - * Hook for sharing entire conversation threads. - * Auto-selects all messages and provides a function to generate a shareable link. + * Hook for sharing conversation threads by threadId. + * The consumer's backend looks up messages by threadId. * * @category Hooks */ export const useShareThread = ({ generateShareLink, }: { - generateShareLink: (messages: Message[]) => Promise; + generateShareLink: (threadId: string) => Promise; }) => { - const { isLoadingMessages, messages, isRunning } = useThread(); - - const { selectedMessages, updateSelectedMessages } = useShareMessages({ - shareMode: true, - }); - - useEffect(() => { - if (!isLoadingMessages && messages.length > 0) { - updateSelectedMessages(messages, "replace"); - } - }, [isLoadingMessages, messages, updateSelectedMessages]); + const { isRunning, isLoadingMessages, messages } = useThread(); + const { selectedThreadId } = useThreadList(); const getShareThreadLink = useCallback(async () => { - return generateShareLink(selectedMessages); - }, [generateShareLink, selectedMessages]); + if (!selectedThreadId) throw new Error("No thread selected"); + return generateShareLink(selectedThreadId); + }, [generateShareLink, selectedThreadId]); return { - shouldDisableShareButton: isRunning || isLoadingMessages, - selectedMessages, + shouldDisableShareButton: isRunning || isLoadingMessages || !selectedThreadId, + hasMessages: messages.length > 0, getShareThreadLink, }; }; From 6308563181587702e5f23aa9a2a8e98ddd0a8653 Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Mon, 30 Mar 2026 13:46:29 +0530 Subject: [PATCH 4/4] export --- packages/react-ui/src/components/OpenUIChat/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-ui/src/components/OpenUIChat/index.ts b/packages/react-ui/src/components/OpenUIChat/index.ts index 7a9c8d7ca..ddbe429ee 100644 --- a/packages/react-ui/src/components/OpenUIChat/index.ts +++ b/packages/react-ui/src/components/OpenUIChat/index.ts @@ -4,6 +4,8 @@ export { FullScreen } from "./ComposedStandalone"; export { GenUIUserMessage } from "./GenUIUserMessage"; export { ShareThread } from "./ShareThread"; export type { ShareThreadProps } from "./ShareThread"; +export { ShareThreadModal } from "./ShareThreadModal"; +export type { ShareThreadModalProps } from "./ShareThreadModal"; export type { AssistantMessageComponent, ComposerComponent,