Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/react-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand All @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions packages/react-ui/src/components/BottomTray/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,20 +191,22 @@ 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;
}

if (message.role === "assistant") {
if (CustomAssistantMessage) {
return <CustomAssistantMessage message={message} />;
return <CustomAssistantMessage message={message} isStreaming={isStreaming} />;
}
return (
<AssistantMessageContainer className={className}>
Expand Down Expand Up @@ -252,14 +254,15 @@ export const Messages = ({

return (
<div className={clsx("openui-bottom-tray-thread-messages", className)}>
{messages.map((message) => {
{messages.map((message, i) => {
return (
<MessageProvider key={message.id} message={message}>
<RenderMessage
message={message}
allMessages={messages}
assistantMessage={assistantMessage}
userMessage={userMessage}
isStreaming={isRunning && i === messages.length - 1}
/>
</MessageProvider>
);
Expand Down
9 changes: 3 additions & 6 deletions packages/react-ui/src/components/Charts/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
export { AreaChart as ScrollableAreaChart } from "./AreaChart";
export * from "./AreaChart";
export * from "./AreaChartCondensed";
export { AreaChartCondensed as AreaChart } from "./AreaChartCondensed";
export { BarChart as ScrollableBarChart } from "./BarChart";
export * from "./BarChart";
export * from "./BarChartCondensed";
export { BarChartCondensed as BarChart } from "./BarChartCondensed";
export * from "./HorizontalBarChart";
export { LineChart as ScrollableLineChart } from "./LineChart";
export * from "./LineChart";
export * from "./LineChartCondensed";
export { LineChartCondensed as LineChart } from "./LineChartCondensed";
export * from "./MiniAreaChart";
export * from "./MiniBarChart";
export * from "./MiniLineChart";
Expand Down
7 changes: 5 additions & 2 deletions packages/react-ui/src/components/CopilotShell/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,20 +189,22 @@ 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;
}

if (message.role === "assistant") {
if (CustomAssistantMessage) {
return <CustomAssistantMessage message={message} />;
return <CustomAssistantMessage message={message} isStreaming={isStreaming} />;
}
return (
<AssistantMessageContainer className={className}>
Expand Down Expand Up @@ -250,14 +252,15 @@ export const Messages = ({

return (
<div className={clsx("openui-copilot-shell-thread-messages", className)}>
{messages.map((message) => {
{messages.map((message, i) => {
return (
<MessageProvider key={message.id} message={message}>
<RenderMessage
message={message}
allMessages={messages}
assistantMessage={assistantMessage}
userMessage={userMessage}
isStreaming={isRunning && i === messages.length - 1}
/>
</MessageProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -82,6 +83,7 @@ const BottomTrayInner = ({
userMessage,
composer: ComposerComponent,
headerActions,
generateShareLink,
}: BottomTraySpecificProps) => {
const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState(defaultOpen);

Expand All @@ -94,6 +96,10 @@ const BottomTrayInner = ({
onOpenChange?.(newIsOpen);
};

const shareButton = generateShareLink ? (
<ShareThread generateShareLink={generateShareLink} />
) : null;

return (
<>
<Trigger onClick={() => handleOpenChange(!isOpen)} isOpen={isOpen}>
Expand All @@ -104,7 +110,15 @@ const BottomTrayInner = ({

<Container logoUrl={logoUrl} agentName={agentName} isOpen={isOpen}>
<ThreadContainer>
<Header onMinimize={() => handleOpenChange(false)} rightChildren={headerActions} />
<Header
onMinimize={() => handleOpenChange(false)}
rightChildren={
<>
{shareButton}
{headerActions}
</>
}
/>
<WelcomeMessageRenderer welcomeMessage={welcomeMessage} />
<ScrollArea scrollVariant={scrollVariant}>
<Messages
Expand Down
15 changes: 14 additions & 1 deletion packages/react-ui/src/components/OpenUIChat/ComposedCopilot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
WelcomeScreen,
} from "../CopilotShell";
import { CustomComposerAdapter } from "./CustomComposerAdapter";
import { ShareThread } from "./ShareThread";
import type { SharedChatUIProps } from "./types";
import { isChatEmpty, isWelcomeComponent } from "./utils";
import { withChatProvider } from "./withChatProvider";
Expand Down Expand Up @@ -74,11 +75,23 @@ const CopilotInner = ({
userMessage,
composer: ComposerComponent,
headerActions,
generateShareLink,
}: CopilotSpecificProps) => {
const shareButton = generateShareLink ? (
<ShareThread generateShareLink={generateShareLink} />
) : null;

return (
<Container logoUrl={logoUrl} agentName={agentName}>
<ThreadContainer>
<Header rightChildren={headerActions} />
<Header
rightChildren={
<>
{shareButton}
{headerActions}
</>
}
/>
<WelcomeMessageRenderer welcomeMessage={welcomeMessage} />
<ScrollArea scrollVariant={scrollVariant}>
<Messages
Expand Down
22 changes: 20 additions & 2 deletions packages/react-ui/src/components/OpenUIChat/ComposedStandalone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
WelcomeScreen,
} from "../Shell";
import { CustomComposerAdapter } from "./CustomComposerAdapter";
import { ShareThread } from "./ShareThread";
import type { SharedChatUIProps } from "./types";
import { isChatEmpty, isWelcomeComponent } from "./utils";
import { withChatProvider } from "./withChatProvider";
Expand Down Expand Up @@ -88,7 +89,12 @@ const FullScreenInner = ({
composer: ComposerComponent,
threadHeader,
mobileHeaderActions,
generateShareLink,
}: FullScreenSpecificProps) => {
const shareButton = generateShareLink ? (
<ShareThread generateShareLink={generateShareLink} />
) : null;

return (
<Container logoUrl={logoUrl} agentName={agentName}>
<SidebarContainer>
Expand All @@ -100,8 +106,20 @@ const FullScreenInner = ({
</SidebarContent>
</SidebarContainer>
<ThreadContainer>
<MobileHeader rightChildren={mobileHeaderActions} />
{threadHeader && <ThreadHeader>{threadHeader}</ThreadHeader>}
<MobileHeader
rightChildren={
<>
{shareButton}
{mobileHeaderActions}
</>
}
/>
{(threadHeader || shareButton) && (
<ThreadHeader>
{threadHeader}
{shareButton}
</ThreadHeader>
)}
<WelcomeMessageRenderer
welcomeMessage={welcomeMessage}
conversationStarters={conversationStarters}
Expand Down
90 changes: 90 additions & 0 deletions packages/react-ui/src/components/OpenUIChat/ShareThread.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Share2 } from "lucide-react";
import React, { type ReactNode } from "react";
import { useLayoutContext } from "../../context/LayoutContext";
import { Button } from "../Button";
import { IconButton } from "../IconButton";
import { useTheme } from "../ThemeProvider/ThemeProvider";
import { ShareThreadModal } from "./ShareThreadModal";
import { useShareThread } from "./useShareThread";

/**
* Props for {@link ShareThread}.
*
* @category Components
*/
export interface ShareThreadProps {
/** Async function that receives the threadId and returns a shareable URL. */
generateShareLink: (threadId: string) => Promise<string>;
/** 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 { hasMessages, getShareThreadLink, shouldDisableShareButton } = useShareThread({
generateShareLink,
});

if (!hasMessages) return null;

return (
<ShareThreadModal
title={modalTitle}
trigger={
customTrigger ?? (
<DefaultShareButton
isMobile={isMobile}
shouldDisableShareButton={shouldDisableShareButton}
/>
)
}
generateLink={getShareThreadLink}
themeClassName={portalThemeClassName}
/>
);
};

ShareThread.displayName = "ShareThread";

type DefaultShareButtonProps = {
isMobile: boolean;
shouldDisableShareButton?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

const DefaultShareButton = React.forwardRef<HTMLButtonElement, DefaultShareButtonProps>(
({ isMobile, shouldDisableShareButton, ...props }, ref) => {
return isMobile ? (
<IconButton
ref={ref as React.RefObject<HTMLButtonElement>}
size="medium"
icon={<Share2 size="1em" />}
variant="secondary"
disabled={shouldDisableShareButton}
{...props}
/>
) : (
<Button
ref={ref as React.RefObject<HTMLButtonElement>}
variant="secondary"
disabled={shouldDisableShareButton}
{...props}
>
<Share2 />
Share
</Button>
);
},
);

DefaultShareButton.displayName = "DefaultShareButton";
Loading