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
2 changes: 1 addition & 1 deletion examples/openui-chat/src/generated/system-prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slide
Label(text: string) — Text label
Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<string>)
TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<string>)
Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<string>)
Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<string>, size?: "sm" | "md" | "lg")
SelectItem(value: string, label: string) — Option for Select
DatePicker(name: string, mode?: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<any>)
Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding<number[]>) — Numeric slider input; supports continuous and discrete (stepped) variants
Expand Down
8 changes: 7 additions & 1 deletion packages/react-ui/src/components/BottomTray/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface ContainerProps {
logoUrl: string;
agentName: string;
className?: string;
showAssistantLogo?: boolean;
/** Control the open state of the tray */
isOpen?: boolean;
}
Expand All @@ -16,10 +17,15 @@ export const Container = ({
logoUrl,
agentName,
className,
showAssistantLogo = false,
isOpen = false,
}: ContainerProps) => {
return (
<ShellStoreProvider logoUrl={logoUrl} agentName={agentName}>
<ShellStoreProvider
logoUrl={logoUrl}
agentName={agentName}
showAssistantLogo={showAssistantLogo}
>
<LayoutContextProvider layout="tray">
<div
className={clsx(
Expand Down
53 changes: 43 additions & 10 deletions packages/react-ui/src/components/BottomTray/ConversationStarter.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useThread } from "@openuidev/react-headless";
import clsx from "clsx";
import { ArrowUp, Lightbulb } from "lucide-react";
import { Fragment, ReactNode } from "react";
import { Fragment, ReactNode, isValidElement } from "react";
import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter";
import { Carousel, CarouselContent } from "../Carousel";
import { isChatEmpty } from "../_shared/utils";
import { Separator } from "../Separator";

export type ConversationStarterVariant = "short" | "long";

Expand All @@ -25,6 +25,18 @@ const renderIcon = (icon: ConversationStarterIcon | undefined): ReactNode => {
return icon;
};

const hasRenderableIcon = (icon: ReactNode): boolean => {
if (icon === null || icon === undefined || icon === false) {
return false;
}

if (isValidElement<{ children?: ReactNode }>(icon) && icon.type === Fragment) {
return Boolean(icon.props.children);
}

return true;
};

const ConversationStarterItem = ({
displayText,
prompt,
Expand All @@ -33,6 +45,7 @@ const ConversationStarterItem = ({
icon,
}: ConversationStarterItemProps) => {
const renderedIcon = renderIcon(icon);
const shouldRenderIcon = hasRenderableIcon(renderedIcon);

if (variant === "short") {
return (
Expand All @@ -41,7 +54,7 @@ const ConversationStarterItem = ({
className="openui-bottom-tray-conversation-starter-item-short"
onClick={() => onClick(prompt)}
>
{renderedIcon && (
{shouldRenderIcon && (
<span className="openui-bottom-tray-conversation-starter-item-short__icon">
{renderedIcon}
</span>
Expand All @@ -61,7 +74,7 @@ const ConversationStarterItem = ({
onClick={() => onClick(prompt)}
>
<div className="openui-bottom-tray-conversation-starter-item-long__content">
{renderedIcon && (
{shouldRenderIcon && (
<span className="openui-bottom-tray-conversation-starter-item-long__icon">
{renderedIcon}
</span>
Expand Down Expand Up @@ -115,6 +128,32 @@ export const ConversationStarter = ({
return null;
}

if (variant === "short") {
return (
<Carousel
showButtons={false}
className={clsx(
"openui-bottom-tray-conversation-starter",
"openui-bottom-tray-conversation-starter--short",
className,
)}
>
<CarouselContent className="openui-bottom-tray-conversation-starter__carousel-content">
{starters.map((item, index) => (
<ConversationStarterItem
key={`${item.displayText}-${index}`}
displayText={item.displayText}
prompt={item.prompt}
icon={item.icon}
onClick={handleClick}
variant={variant}
/>
))}
</CarouselContent>
</Carousel>
);
}

return (
<div
className={clsx(
Expand All @@ -132,12 +171,6 @@ export const ConversationStarter = ({
onClick={handleClick}
variant={variant}
/>
{/* Add separator between items in long variant */}
{variant === "long" && index < starters.length - 1 && (
<div className="openui-bottom-tray-conversation-starter__separator">
<Separator />
</div>
)}
</Fragment>
))}
</div>
Expand Down
2 changes: 0 additions & 2 deletions packages/react-ui/src/components/BottomTray/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ export const ScrollArea = ({
>
{children}
</div>
{/* Gradient to hide the bottom of the scroll area */}
<div className="openui-bottom-tray-thread-scroll-gradient" />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import clsx from "clsx";
import { EllipsisVerticalIcon, MenuIcon, Trash2Icon } from "lucide-react";
import { useEffect } from "react";
import { Button } from "../Button";
import { IconButton } from "../IconButton";
import { useTheme } from "../ThemeProvider";

const ThreadItem = ({
title,
Expand All @@ -16,6 +18,7 @@ const ThreadItem = ({
onSelect: () => void;
onDelete: () => void;
}) => {
const { portalThemeClassName } = useTheme();
return (
<div
className={clsx("openui-bottom-tray-thread-item", {
Expand All @@ -27,26 +30,38 @@ const ThreadItem = ({
</button>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="openui-bottom-tray-thread-item-menu-trigger">
<EllipsisVerticalIcon size={14} />
</button>
<IconButton
icon={<EllipsisVerticalIcon size="1em" />}
aria-label={`More actions for ${title}`}
variant="tertiary"
size="extra-small"
className="openui-bottom-tray-thread-item-menu-trigger"
/>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="openui-bottom-tray-thread-item-menu"
className={clsx("openui-bottom-tray-thread-item-menu", portalThemeClassName)}
side="right"
align="start"
sideOffset={4}
>
<DropdownMenu.Item
className="openui-bottom-tray-thread-item-menu-action"
asChild
onSelect={(e) => {
e.stopPropagation();
onDelete();
}}
>
<Trash2Icon size={14} className="openui-bottom-tray-thread-item-menu-icon" />
Delete
<Button
type="button"
variant="tertiary"
buttonType="destructive"
size="small"
iconLeft={<Trash2Icon size={14} />}
className="openui-bottom-tray-thread-item-menu-action"
>
Delete
</Button>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
Expand All @@ -61,10 +76,11 @@ export const ThreadListContainer = () => {
const loadThreads = useThreadList((s) => s.loadThreads);
const selectThread = useThreadList((s) => s.selectThread);
const deleteThread = useThreadList((s) => s.deleteThread);
const { portalThemeClassName } = useTheme();

useEffect(() => {
loadThreads();
}, []);
}, [loadThreads]);

return (
<DropdownMenu.Root>
Expand All @@ -78,7 +94,7 @@ export const ThreadListContainer = () => {
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="openui-bottom-tray-thread-list-dropdown"
className={clsx("openui-bottom-tray-thread-list-dropdown", portalThemeClassName)}
side="bottom"
align="end"
sideOffset={8}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const Composer = ({ className, placeholder = "Type your message..." }: Co
return (
<div
className={clsx("openui-bottom-tray-thread-composer", className)}
data-drafting={textContent.length > 0 || undefined}
onClick={(e) => {
if (!(e.target as HTMLElement).closest("button, a, [role='button']")) {
inputRef.current?.focus();
Expand All @@ -68,7 +69,7 @@ export const Composer = ({ className, placeholder = "Type your message..." }: Co
<IconButton
onClick={isRunning ? cancelMessage : handleSubmit}
icon={isRunning ? <Square size="1em" fill="currentColor" /> : <ArrowUp size="1em" />}
size="medium"
size="extra-small"
variant="primary"
className="openui-bottom-tray-thread-composer__submit-button"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
@use "../../../cssUtils" as cssUtils;

.openui-bottom-tray-thread-composer {
box-sizing: border-box;
flex-shrink: 0;
width: 100%;
padding: 0 cssUtils.$space-s cssUtils.$space-s;
margin: 0 0 cssUtils.$space-m;
padding: 0 cssUtils.$space-m;

&__input-wrapper {
background-color: cssUtils.$foreground;
border: 1px solid cssUtils.$border-interactive;
border-radius: cssUtils.$radius-l;
box-shadow: cssUtils.$shadow-s;
border-radius: cssUtils.$radius-2xl;
box-shadow: cssUtils.$shadow-m;
overflow: clip;
display: flex;
flex-direction: column;
gap: cssUtils.$space-s;
padding: cssUtils.$space-s;
padding: cssUtils.$space-s-m;
}

&__input {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-ui/src/components/BottomTray/container.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
clip-path 0.25s ease-in-out;

border: 1px solid cssUtils.$border-default;
border-radius: cssUtils.$radius-2xl cssUtils.$radius-2xl cssUtils.$radius-3xl cssUtils.$radius-3xl;
border-radius: cssUtils.$radius-4xl;
box-shadow: cssUtils.$shadow-2xl;

background: cssUtils.$chat-container-bg;
Expand Down
Loading
Loading