diff --git a/examples/openui-chat/src/generated/system-prompt.txt b/examples/openui-chat/src/generated/system-prompt.txt index e98309971..9a097a075 100644 --- a/examples/openui-chat/src/generated/system-prompt.txt +++ b/examples/openui-chat/src/generated/system-prompt.txt @@ -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) 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) -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) +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, 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) 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) — Numeric slider input; supports continuous and discrete (stepped) variants diff --git a/packages/react-ui/src/components/BottomTray/Container.tsx b/packages/react-ui/src/components/BottomTray/Container.tsx index d92521399..2226ca54a 100644 --- a/packages/react-ui/src/components/BottomTray/Container.tsx +++ b/packages/react-ui/src/components/BottomTray/Container.tsx @@ -7,6 +7,7 @@ interface ContainerProps { logoUrl: string; agentName: string; className?: string; + showAssistantLogo?: boolean; /** Control the open state of the tray */ isOpen?: boolean; } @@ -16,10 +17,15 @@ export const Container = ({ logoUrl, agentName, className, + showAssistantLogo = false, isOpen = false, }: ContainerProps) => { return ( - +
{ 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, @@ -33,6 +45,7 @@ const ConversationStarterItem = ({ icon, }: ConversationStarterItemProps) => { const renderedIcon = renderIcon(icon); + const shouldRenderIcon = hasRenderableIcon(renderedIcon); if (variant === "short") { return ( @@ -41,7 +54,7 @@ const ConversationStarterItem = ({ className="openui-bottom-tray-conversation-starter-item-short" onClick={() => onClick(prompt)} > - {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} @@ -61,7 +74,7 @@ const ConversationStarterItem = ({ onClick={() => onClick(prompt)} >
- {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} @@ -115,6 +128,32 @@ export const ConversationStarter = ({ return null; } + if (variant === "short") { + return ( + + + {starters.map((item, index) => ( + + ))} + + + ); + } + return (
- {/* Add separator between items in long variant */} - {variant === "long" && index < starters.length - 1 && ( -
- -
- )} ))}
diff --git a/packages/react-ui/src/components/BottomTray/Thread.tsx b/packages/react-ui/src/components/BottomTray/Thread.tsx index 712e9f190..0a9578d13 100644 --- a/packages/react-ui/src/components/BottomTray/Thread.tsx +++ b/packages/react-ui/src/components/BottomTray/Thread.tsx @@ -79,8 +79,6 @@ export const ScrollArea = ({ > {children}
- {/* Gradient to hide the bottom of the scroll area */} -
); }; diff --git a/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx b/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx index e497f8755..1e43da432 100644 --- a/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx +++ b/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx @@ -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, @@ -16,6 +18,7 @@ const ThreadItem = ({ onSelect: () => void; onDelete: () => void; }) => { + const { portalThemeClassName } = useTheme(); return (
- + } + aria-label={`More actions for ${title}`} + variant="tertiary" + size="extra-small" + className="openui-bottom-tray-thread-item-menu-trigger" + /> { e.stopPropagation(); onDelete(); }} > - - Delete + @@ -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 ( @@ -78,7 +94,7 @@ export const ThreadListContainer = () => { 0 || undefined} onClick={(e) => { if (!(e.target as HTMLElement).closest("button, a, [role='button']")) { inputRef.current?.focus(); @@ -68,7 +69,7 @@ export const Composer = ({ className, placeholder = "Type your message..." }: Co : } - size="medium" + size="extra-small" variant="primary" className="openui-bottom-tray-thread-composer__submit-button" /> diff --git a/packages/react-ui/src/components/BottomTray/components/composer.scss b/packages/react-ui/src/components/BottomTray/components/composer.scss index 73f28ba53..d21e42aa9 100644 --- a/packages/react-ui/src/components/BottomTray/components/composer.scss +++ b/packages/react-ui/src/components/BottomTray/components/composer.scss @@ -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 { diff --git a/packages/react-ui/src/components/BottomTray/container.scss b/packages/react-ui/src/components/BottomTray/container.scss index 413791e1c..1187eedee 100644 --- a/packages/react-ui/src/components/BottomTray/container.scss +++ b/packages/react-ui/src/components/BottomTray/container.scss @@ -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; diff --git a/packages/react-ui/src/components/BottomTray/conversationStarter.scss b/packages/react-ui/src/components/BottomTray/conversationStarter.scss index 29b09cb7d..9cf48fa62 100644 --- a/packages/react-ui/src/components/BottomTray/conversationStarter.scss +++ b/packages/react-ui/src/components/BottomTray/conversationStarter.scss @@ -3,37 +3,48 @@ // Container styles .openui-bottom-tray-conversation-starter { display: flex; - padding: 0 cssUtils.$space-s; + flex-shrink: 0; + width: 100%; + padding: 0 cssUtils.$space-m; margin-bottom: cssUtils.$space-s; + box-sizing: border-box; + max-height: 480px; + overflow: hidden; + opacity: 1; + transform: translateY(0); + transition: + opacity 0.2s ease, + transform 0.2s ease, + max-height 0.2s ease, + margin-bottom 0.2s ease; // Short variant - horizontal pill buttons &--short { - flex-direction: column; - gap: cssUtils.$space-s; + display: block; } - // Long variant - vertical list items with separators + // Long variant - vertical list items &--long { flex-direction: column; gap: cssUtils.$space-2xs; } - // Separator wrapper for long variant - &__separator { - padding: cssUtils.$space-3xs cssUtils.$space-xs; + &__carousel-content { + gap: cssUtils.$space-xs; } } // Short variant item (pill-style buttons) .openui-bottom-tray-conversation-starter-item-short { display: flex; - align-items: flex-start; + align-items: center; gap: cssUtils.$space-xs; width: fit-content; - padding: cssUtils.$space-s cssUtils.$space-m; + flex: 0 0 auto; + padding: cssUtils.$space-s cssUtils.$space-s-m; background-color: cssUtils.$foreground; - border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-m; + border: 1px solid cssUtils.$border-interactive; + border-radius: cssUtils.$radius-l; cursor: pointer; transition: all 0.15s ease; @include cssUtils.typography(body, small); @@ -43,29 +54,34 @@ // Icon container &__icon { display: flex; - align-items: flex-start; + align-items: center; justify-content: center; flex-shrink: 0; - padding-top: cssUtils.$space-3xs; color: cssUtils.$text-neutral-primary; + + & > svg { + width: 14px; + height: 14px; + } } // Text &__text { flex: 1; + white-space: nowrap; } &:not(:disabled):hover { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight; border-color: cssUtils.$border-interactive-emphasis; } &:not(:disabled):active { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight-subtle; } @media (max-width: 480px) { - padding: cssUtils.$space-xs cssUtils.$space-s; + padding: cssUtils.$space-s cssUtils.$space-s-m; } } @@ -75,10 +91,10 @@ align-items: center; gap: cssUtils.$space-000; width: 100%; - padding: cssUtils.$space-xs cssUtils.$space-xs; + padding: cssUtils.$space-xs cssUtils.$space-s-m; background-color: transparent; border: none; - border-radius: cssUtils.$radius-m; + border-radius: cssUtils.$radius-l; cursor: pointer; transition: background-color 0.15s ease; @include cssUtils.typography(body, default); @@ -116,9 +132,11 @@ // Arrow icon (shown on hover) &__arrow { display: flex; + align-self: flex-start; align-items: center; justify-content: center; flex-shrink: 0; + padding-top: cssUtils.$space-2xs; opacity: 0; transition: opacity 0.15s ease; color: cssUtils.$text-neutral-primary; diff --git a/packages/react-ui/src/components/BottomTray/header.scss b/packages/react-ui/src/components/BottomTray/header.scss index 5d320fb9a..a7fc6b7bb 100644 --- a/packages/react-ui/src/components/BottomTray/header.scss +++ b/packages/react-ui/src/components/BottomTray/header.scss @@ -5,7 +5,7 @@ align-items: center; justify-content: space-between; padding: cssUtils.$space-m; - border-radius: cssUtils.$radius-l cssUtils.$radius-l 0 0; + border-radius: cssUtils.$radius-4xl cssUtils.$radius-4xl 0 0; // Mobile @media (max-width: 768px) { @@ -34,5 +34,5 @@ .openui-bottom-tray-header-actions { display: flex; align-items: center; - gap: cssUtils.$space-s; + gap: cssUtils.$space-2xs; } diff --git a/packages/react-ui/src/components/BottomTray/thread.scss b/packages/react-ui/src/components/BottomTray/thread.scss index 6cc739987..cc0ce23f4 100644 --- a/packages/react-ui/src/components/BottomTray/thread.scss +++ b/packages/react-ui/src/components/BottomTray/thread.scss @@ -8,22 +8,48 @@ position: relative; } +// Hide conversation starters while the user is drafting in the composer. +// The drafting flag is set on the composer root via `data-drafting`. +.openui-bottom-tray-thread-container:has([data-drafting]) { + .openui-bottom-tray-conversation-starter { + opacity: 0; + pointer-events: none; + } +} + +.openui-bottom-tray-conversation-starter { + transition: opacity 150ms ease; +} + .openui-bottom-tray-thread-scroll-container { width: 100%; flex: 1; position: relative; overflow: hidden; -} -.openui-bottom-tray-thread-scroll-gradient { - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 40px; - z-index: 1; - pointer-events: none; - background: linear-gradient(to top, cssUtils.$background 0%, transparent); + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to bottom, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } + + &::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to top, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } } .openui-bottom-tray-thread-scroll-area { @@ -31,6 +57,8 @@ height: 100%; overflow: auto; padding: cssUtils.$space-m; + scroll-padding-top: calc(cssUtils.$space-m + cssUtils.$space-xs); + &--user-message-anchor { & .openui-bottom-tray-thread-messages > *:last-child { min-height: calc(-200px + 100dvh); @@ -40,6 +68,7 @@ .openui-bottom-tray-thread-messages { margin: 0 auto; + padding-top: cssUtils.$space-s; display: flex; flex-direction: column; gap: cssUtils.$space-xl; @@ -70,10 +99,10 @@ &__content { @include cssUtils.typography(primary, default); - padding: cssUtils.$space-m cssUtils.$space-l; + padding: cssUtils.$space-s cssUtils.$space-m; background-color: cssUtils.$chat-user-response-bg; color: cssUtils.$chat-user-response-text; - border-radius: cssUtils.$radius-2xl; + border-radius: cssUtils.$radius-xl; overflow-wrap: break-word; max-width: 100%; height: fit-content; diff --git a/packages/react-ui/src/components/BottomTray/threadList.scss b/packages/react-ui/src/components/BottomTray/threadList.scss index 43f88cf58..fd576ddeb 100644 --- a/packages/react-ui/src/components/BottomTray/threadList.scss +++ b/packages/react-ui/src/components/BottomTray/threadList.scss @@ -7,9 +7,9 @@ min-width: 240px; max-width: 320px; max-height: 296px; - padding: cssUtils.$space-s; + padding: cssUtils.$space-xs; border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-l; + border-radius: cssUtils.$radius-2xl; background-color: cssUtils.$foreground; box-shadow: cssUtils.$shadow-l; z-index: 9999; @@ -18,7 +18,7 @@ .openui-bottom-tray-thread-list-header { @include cssUtils.typography(label, small); - color: cssUtils.$text-neutral-secondary; + color: cssUtils.$text-neutral-tertiary; padding: cssUtils.$space-xs cssUtils.$space-s; padding-bottom: cssUtils.$space-s; } @@ -26,7 +26,7 @@ .openui-bottom-tray-thread-list-items { display: flex; flex-direction: column; - gap: cssUtils.$space-2xs; + gap: cssUtils.$space-3xs; overflow-y: auto; } @@ -37,23 +37,38 @@ text-align: center; } +.openui-bottom-tray-thread-list-trigger { + &[data-state="open"], + &[aria-expanded="true"] { + background-color: cssUtils.$highlight; + border-color: cssUtils.$border-interactive; + } +} + // Thread Item .openui-bottom-tray-thread-item { display: flex; align-items: center; justify-content: space-between; width: 100%; - border-radius: cssUtils.$radius-s; + border-radius: calc(cssUtils.$radius-s + 1px); border: 1px solid transparent; - padding-right: cssUtils.$space-xs; + transition: all 0.12s ease; &--selected { - background-color: cssUtils.$sunk; - border-color: cssUtils.$border-default; + background-color: cssUtils.$highlight; } &:hover { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight; + + .openui-bottom-tray-thread-item-menu-trigger { + opacity: 1; + } + } + + &:has(.openui-bottom-tray-thread-item-menu-trigger[data-state="open"]) { + background-color: cssUtils.$highlight; .openui-bottom-tray-thread-item-menu-trigger { opacity: 1; @@ -65,8 +80,8 @@ @include cssUtils.button-reset; @include cssUtils.typography(body, small); color: cssUtils.$text-neutral-primary; - padding: cssUtils.$space-xs cssUtils.$space-s; - flex: 1; + padding: calc(cssUtils.$space-xs - 1.75px) cssUtils.$space-s; + width: 100%; text-align: left; cursor: pointer; white-space: nowrap; @@ -75,52 +90,56 @@ } .openui-bottom-tray-thread-item-menu-trigger { - @include cssUtils.button-reset; - outline: none; - color: cssUtils.$text-neutral-secondary; - padding: cssUtils.$space-xs; - flex-shrink: 0; - cursor: pointer; opacity: 0; - border-radius: cssUtils.$radius-xs; - &:hover { - background-color: cssUtils.$highlight; + &:focus-visible { + opacity: 1; } &[data-state="open"] { opacity: 1; + background-color: cssUtils.$highlight; + border-color: cssUtils.$border-interactive; } } .openui-bottom-tray-thread-item-menu { - display: flex; - flex-direction: column; - padding: cssUtils.$space-xs; + box-sizing: border-box; + position: relative; + z-index: 99999; + min-width: 160px; + overflow: hidden; border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-m; + border-radius: cssUtils.$radius-xl; background-color: cssUtils.$foreground; box-shadow: cssUtils.$shadow-m; - z-index: 10000; + color: cssUtils.$text-neutral-primary; + transform-origin: top center; + animation: openui-bottom-tray-thread-menu-show 0.18s ease-out forwards; + padding: 4px; } .openui-bottom-tray-thread-item-menu-action { - @include cssUtils.button-reset; - @include cssUtils.typography(body, small); - outline: none; - color: cssUtils.$text-neutral-primary; - padding: cssUtils.$space-xs cssUtils.$space-s; - display: flex; - align-items: center; - gap: cssUtils.$space-xs; - cursor: pointer; - border-radius: cssUtils.$radius-xs; - - &:hover { - background-color: cssUtils.$sunk; + width: 100%; + justify-content: flex-start; + padding-left: cssUtils.$space-s; + padding-right: cssUtils.$space-s; + + &:focus, + &:focus-visible { + outline: none; + box-shadow: none; } } -.openui-bottom-tray-thread-item-menu-icon { - color: cssUtils.$text-neutral-secondary; +@keyframes openui-bottom-tray-thread-menu-show { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } } diff --git a/packages/react-ui/src/components/BottomTray/welcomeScreen.scss b/packages/react-ui/src/components/BottomTray/welcomeScreen.scss index 556062e27..e679c3988 100644 --- a/packages/react-ui/src/components/BottomTray/welcomeScreen.scss +++ b/packages/react-ui/src/components/BottomTray/welcomeScreen.scss @@ -3,7 +3,7 @@ .openui-bottom-tray-welcome-screen { display: flex; flex-direction: column; - gap: cssUtils.$space-l; + gap: cssUtils.$space-xl; height: 100%; justify-content: center; align-items: center; @@ -29,13 +29,13 @@ display: flex; flex-direction: column; align-items: center; - gap: cssUtils.$space-xs; + gap: cssUtils.$space-s; text-align: center; } // Title/greeting text &__title { - @include cssUtils.typography(heading, small); + @include cssUtils.typography(heading, medium); color: cssUtils.$text-neutral-primary; margin: 0; } diff --git a/packages/react-ui/src/components/CopilotShell/Container.tsx b/packages/react-ui/src/components/CopilotShell/Container.tsx index 13e820d5c..89c8885a6 100644 --- a/packages/react-ui/src/components/CopilotShell/Container.tsx +++ b/packages/react-ui/src/components/CopilotShell/Container.tsx @@ -7,11 +7,22 @@ interface ContainerProps { logoUrl: string; agentName: string; className?: string; + showAssistantLogo?: boolean; } -export const Container = ({ children, logoUrl, agentName, className }: ContainerProps) => { +export const Container = ({ + children, + logoUrl, + agentName, + className, + showAssistantLogo = false, +}: ContainerProps) => { return ( - +
{children}
diff --git a/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx b/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx index b16c63f19..1d229a42b 100644 --- a/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx +++ b/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx @@ -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"; @@ -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, @@ -33,6 +45,7 @@ const ConversationStarterItem = ({ icon, }: ConversationStarterItemProps) => { const renderedIcon = renderIcon(icon); + const shouldRenderIcon = hasRenderableIcon(renderedIcon); if (variant === "short") { return ( @@ -41,7 +54,7 @@ const ConversationStarterItem = ({ className="openui-copilot-shell-conversation-starter-item-short" onClick={() => onClick(prompt)} > - {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} @@ -61,7 +74,7 @@ const ConversationStarterItem = ({ onClick={() => onClick(prompt)} >
- {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} @@ -115,6 +128,32 @@ export const ConversationStarter = ({ return null; } + if (variant === "short") { + return ( + + + {starters.map((item, index) => ( + + ))} + + + ); + } + return (
- {/* Add separator between items in long variant */} - {variant === "long" && index < starters.length - 1 && ( -
- -
- )} ))}
diff --git a/packages/react-ui/src/components/CopilotShell/Header.tsx b/packages/react-ui/src/components/CopilotShell/Header.tsx index ce4f0ccfc..469fe6e04 100644 --- a/packages/react-ui/src/components/CopilotShell/Header.tsx +++ b/packages/react-ui/src/components/CopilotShell/Header.tsx @@ -1,27 +1,60 @@ +import { useThreadList } from "@openuidev/react-headless"; import clsx from "clsx"; +import { SquarePen } from "lucide-react"; import { ReactNode } from "react"; +import { IconButton } from "../IconButton"; import { useShellStore } from "../_shared/store"; +import { ThreadListContainer } from "./ThreadListContainer"; + +export const CopilotNewChatButton = () => { + const switchToNewThread = useThreadList((s) => s.switchToNewThread); + + return ( + } + onClick={switchToNewThread} + variant="tertiary" + aria-label="New chat" + className="openui-copilot-shell-header-new-chat-button" + /> + ); +}; interface HeaderProps { className?: string; /** Custom content to render on the rightmost side of the logo container */ rightChildren?: ReactNode; + /** Hide the new chat button */ + hideNewChatButton?: boolean; + /** Hide the thread list container */ + hideThreadListContainer?: boolean; } -export const Header = ({ className, rightChildren }: HeaderProps) => { +export const Header = ({ + className, + rightChildren, + hideNewChatButton = false, + hideThreadListContainer = false, +}: HeaderProps) => { const { logoUrl, agentName } = useShellStore((state) => ({ logoUrl: state.logoUrl, agentName: state.agentName, })); + const shouldRenderActions = rightChildren || !hideThreadListContainer || !hideNewChatButton; + return (
Logo {agentName}
- {rightChildren && ( -
{rightChildren}
+ {shouldRenderActions && ( +
+ {rightChildren} + {!hideThreadListContainer && } + {!hideNewChatButton && } +
)}
); diff --git a/packages/react-ui/src/components/CopilotShell/Thread.tsx b/packages/react-ui/src/components/CopilotShell/Thread.tsx index 28f0bdcc6..ffc103890 100644 --- a/packages/react-ui/src/components/CopilotShell/Thread.tsx +++ b/packages/react-ui/src/components/CopilotShell/Thread.tsx @@ -4,6 +4,7 @@ import clsx from "clsx"; import React, { memo, useRef } from "react"; import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom"; import { ArtifactOverlay } from "../_shared/artifact"; +import { useShellStore } from "../_shared/store"; import type { AssistantMessageComponent, UserMessageComponent } from "../_shared/types"; import { MarkDownRenderer } from "../MarkDownRenderer"; import { MessageLoading as MessageLoadingComponent } from "../MessageLoading"; @@ -90,8 +91,20 @@ export const AssistantMessageContainer = ({ children?: React.ReactNode; className?: string; }) => { + const { logoUrl, showAssistantLogo } = useShellStore((store) => ({ + logoUrl: store.logoUrl, + showAssistantLogo: store.showAssistantLogo, + })); + return (
+ {showAssistantLogo && ( + Assistant + )}
{children}
); diff --git a/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx b/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx new file mode 100644 index 000000000..5cd903695 --- /dev/null +++ b/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx @@ -0,0 +1,121 @@ +import { useThreadList } from "@openuidev/react-headless"; +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, + isSelected, + onSelect, + onDelete, +}: { + title: string; + isSelected: boolean; + onSelect: () => void; + onDelete: () => void; +}) => { + const { portalThemeClassName } = useTheme(); + return ( +
+ + + + } + aria-label={`More actions for ${title}`} + variant="tertiary" + size="extra-small" + className="openui-copilot-shell-thread-item-menu-trigger" + /> + + + + { + e.stopPropagation(); + onDelete(); + }} + > + + + + + +
+ ); +}; + +export const ThreadListContainer = () => { + const threads = useThreadList((s) => s.threads); + const selectedThreadId = useThreadList((s) => s.selectedThreadId); + 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 ( + + + } + variant="tertiary" + aria-label="Thread list" + className="openui-copilot-shell-thread-list-trigger" + /> + + + +
All threads
+
+ {threads.map((thread) => ( + selectThread(thread.id)} + onDelete={() => deleteThread(thread.id)} + /> + ))} + {threads.length === 0 && ( +
No threads yet
+ )} +
+
+
+
+ ); +}; diff --git a/packages/react-ui/src/components/CopilotShell/components/Composer.tsx b/packages/react-ui/src/components/CopilotShell/components/Composer.tsx index c1ee67680..578d152bc 100644 --- a/packages/react-ui/src/components/CopilotShell/components/Composer.tsx +++ b/packages/react-ui/src/components/CopilotShell/components/Composer.tsx @@ -43,6 +43,7 @@ export const Composer = ({ className, placeholder = "Type your message..." }: Co return (
0 || undefined} onClick={(e) => { if (!(e.target as HTMLElement).closest("button, a, [role='button']")) { inputRef.current?.focus(); @@ -68,7 +69,7 @@ export const Composer = ({ className, placeholder = "Type your message..." }: Co : } - size="medium" + size="extra-small" variant="primary" aria-label={isRunning ? "Cancel message" : "Send message"} className="openui-copilot-shell-thread-composer__submit-button" diff --git a/packages/react-ui/src/components/CopilotShell/components/composer.scss b/packages/react-ui/src/components/CopilotShell/components/composer.scss index e19e7d912..e79da538d 100644 --- a/packages/react-ui/src/components/CopilotShell/components/composer.scss +++ b/packages/react-ui/src/components/CopilotShell/components/composer.scss @@ -1,19 +1,22 @@ @use "../../../cssUtils" as cssUtils; .openui-copilot-shell-thread-composer { + box-sizing: border-box; + flex-shrink: 0; width: 100%; - padding: cssUtils.$space-m cssUtils.$space-l cssUtils.$space-l; - background-color: cssUtils.$foreground; + 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-3xl; + 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 { diff --git a/packages/react-ui/src/components/CopilotShell/conversationStarter.scss b/packages/react-ui/src/components/CopilotShell/conversationStarter.scss index 5d7ec88cf..c6fb90500 100644 --- a/packages/react-ui/src/components/CopilotShell/conversationStarter.scss +++ b/packages/react-ui/src/components/CopilotShell/conversationStarter.scss @@ -3,37 +3,48 @@ // Container styles .openui-copilot-shell-conversation-starter { display: flex; - padding: 0 cssUtils.$space-l; + flex-shrink: 0; + width: 100%; + padding: 0 cssUtils.$space-m; margin-bottom: cssUtils.$space-s; + box-sizing: border-box; + max-height: 480px; + overflow: hidden; + opacity: 1; + transform: translateY(0); + transition: + opacity 0.2s ease, + transform 0.2s ease, + max-height 0.2s ease, + margin-bottom 0.2s ease; // Short variant - horizontal pill buttons &--short { - flex-direction: column; - gap: cssUtils.$space-s; + display: block; } - // Long variant - vertical list items with separators + // Long variant - vertical list items &--long { flex-direction: column; gap: cssUtils.$space-2xs; } - // Separator wrapper for long variant - &__separator { - padding: cssUtils.$space-3xs cssUtils.$space-xs; + &__carousel-content { + gap: cssUtils.$space-xs; } } // Short variant item (pill-style buttons) .openui-copilot-shell-conversation-starter-item-short { display: flex; - align-items: flex-start; + align-items: center; gap: cssUtils.$space-xs; width: fit-content; - padding: cssUtils.$space-s cssUtils.$space-m; + flex: 0 0 auto; + padding: cssUtils.$space-s cssUtils.$space-s-m; background-color: cssUtils.$foreground; - border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-m; + border: 1px solid cssUtils.$border-interactive; + border-radius: cssUtils.$radius-l; cursor: pointer; transition: all 0.15s ease; @include cssUtils.typography(body, small); @@ -43,29 +54,34 @@ // Icon container &__icon { display: flex; - align-items: flex-start; + align-items: center; justify-content: center; flex-shrink: 0; - padding-top: cssUtils.$space-3xs; color: cssUtils.$text-neutral-primary; + + & > svg { + width: 14px; + height: 14px; + } } // Text &__text { flex: 1; + white-space: nowrap; } &:not(:disabled):hover { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight; border-color: cssUtils.$border-interactive-emphasis; } &:not(:disabled):active { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight-subtle; } @media (max-width: 480px) { - padding: cssUtils.$space-xs cssUtils.$space-s; + padding: cssUtils.$space-s cssUtils.$space-s-m; } } @@ -75,10 +91,10 @@ align-items: center; gap: cssUtils.$space-000; width: 100%; - padding: cssUtils.$space-xs cssUtils.$space-s; + padding: cssUtils.$space-xs cssUtils.$space-s-m; background-color: transparent; border: none; - border-radius: cssUtils.$radius-m; + border-radius: cssUtils.$radius-l; cursor: pointer; transition: background-color 0.15s ease; @include cssUtils.typography(body, default); @@ -90,7 +106,7 @@ &__content { display: flex; align-items: flex-start; - gap: cssUtils.$space-s; + gap: cssUtils.$space-xs; flex: 1; min-width: 0; } @@ -116,9 +132,11 @@ // Arrow icon (shown on hover) &__arrow { display: flex; + align-self: flex-start; align-items: center; justify-content: center; flex-shrink: 0; + padding-top: cssUtils.$space-2xs; opacity: 0; transition: opacity 0.15s ease; color: cssUtils.$text-neutral-primary; diff --git a/packages/react-ui/src/components/CopilotShell/copilotShell.scss b/packages/react-ui/src/components/CopilotShell/copilotShell.scss index 1b1aa246c..c77b040f9 100644 --- a/packages/react-ui/src/components/CopilotShell/copilotShell.scss +++ b/packages/react-ui/src/components/CopilotShell/copilotShell.scss @@ -3,6 +3,7 @@ @use "./components/composer.scss"; @use "./header.scss"; @use "./conversationStarter.scss"; +@use "./threadList.scss"; @use "./welcomeScreen.scss"; .openui-copilot-shell-container { diff --git a/packages/react-ui/src/components/CopilotShell/header.scss b/packages/react-ui/src/components/CopilotShell/header.scss index 416938640..e1cc75a8a 100644 --- a/packages/react-ui/src/components/CopilotShell/header.scss +++ b/packages/react-ui/src/components/CopilotShell/header.scss @@ -4,16 +4,13 @@ display: flex; align-items: center; justify-content: space-between; - padding: cssUtils.$space-m cssUtils.$space-l; - border-bottom: 1px solid cssUtils.$border-default; - background-color: cssUtils.$foreground; + padding: cssUtils.$space-m; } .openui-copilot-shell-header-logo-container { display: flex; align-items: center; gap: cssUtils.$space-s; - @include cssUtils.typography(title, medium); } .openui-copilot-shell-header-logo { @@ -23,10 +20,11 @@ } .openui-copilot-shell-header-agent-name { - @include cssUtils.typography(title, medium); + @include cssUtils.typography(label, small); color: cssUtils.$text-neutral-primary; } +.openui-copilot-shell-header-actions, .openui-copilot-shell-header-right-content { display: flex; align-items: center; diff --git a/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx b/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx index 260bc1b9a..c20773b94 100644 --- a/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx +++ b/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx @@ -44,17 +44,17 @@ export default { const SAMPLE_STARTERS = [ { - displayText: "Tell me about my portfolio", + displayText: "My portfolio", prompt: "Tell me about the latest stock market trends and how they affect my portfolio", icon: , }, { - displayText: "Who is the president of Venezuela and where is he currently located?", + displayText: "Venezuela president", prompt: "Who is the president of Venezuela and where is he currently located?", // icon undefined = shows default lightbulb }, { - displayText: "Tell me about major stock (no icon)", + displayText: "Major stocks", prompt: "Tell me about major stock", icon: <>, // Empty fragment = no icon }, diff --git a/packages/react-ui/src/components/CopilotShell/thread.scss b/packages/react-ui/src/components/CopilotShell/thread.scss index 12b1c29ce..889a52520 100644 --- a/packages/react-ui/src/components/CopilotShell/thread.scss +++ b/packages/react-ui/src/components/CopilotShell/thread.scss @@ -8,18 +8,57 @@ position: relative; } +// Hide conversation starters while the user is drafting in the composer. +// The drafting flag is set on the composer root via `data-drafting`. +.openui-copilot-shell-thread-container:has([data-drafting]) { + .openui-copilot-shell-conversation-starter { + opacity: 0; + pointer-events: none; + } +} + +.openui-copilot-shell-conversation-starter { + transition: opacity 150ms ease; +} + .openui-copilot-shell-thread-scroll-container { width: 100%; flex: 1; position: relative; overflow: hidden; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to bottom, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } + + &::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to top, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } } .openui-copilot-shell-thread-scroll-area { width: 100%; height: 100%; overflow: auto; - padding: cssUtils.$space-l; + padding: cssUtils.$space-m-l; + scroll-padding-top: cssUtils.$space-m-l; + &--user-message-anchor { & .openui-copilot-shell-thread-messages > *:last-child { min-height: calc(-200px + 100dvh); @@ -28,6 +67,8 @@ } .openui-copilot-shell-thread-messages { + width: 100%; + max-width: 880px; margin: 0 auto; display: flex; flex-direction: column; @@ -36,6 +77,8 @@ .openui-copilot-shell-thread-message-assistant { width: 100%; + display: flex; + gap: cssUtils.$space-s; overflow: hidden; &__content { @@ -47,6 +90,13 @@ flex-grow: 1; } + &__logo { + width: 32px; + height: 32px; + border-radius: cssUtils.$radius-m; + flex-shrink: 0; + } + &__text { @include cssUtils.typography(primary, default); color: cssUtils.$text-neutral-primary; @@ -59,10 +109,10 @@ &__content { @include cssUtils.typography(primary, default); - padding: cssUtils.$space-m cssUtils.$space-l; + padding: cssUtils.$space-s cssUtils.$space-m; background-color: cssUtils.$chat-user-response-bg; color: cssUtils.$chat-user-response-text; - border-radius: cssUtils.$radius-2xl; + border-radius: cssUtils.$radius-xl; overflow-wrap: break-word; max-width: 100%; height: fit-content; diff --git a/packages/react-ui/src/components/CopilotShell/threadList.scss b/packages/react-ui/src/components/CopilotShell/threadList.scss new file mode 100644 index 000000000..2cdef6408 --- /dev/null +++ b/packages/react-ui/src/components/CopilotShell/threadList.scss @@ -0,0 +1,143 @@ +@use "../../cssUtils" as cssUtils; + +.openui-copilot-shell-thread-list-dropdown { + display: flex; + flex-direction: column; + min-width: 240px; + max-width: 320px; + max-height: 296px; + padding: cssUtils.$space-xs; + border: 1px solid cssUtils.$border-default; + border-radius: cssUtils.$radius-2xl; + background-color: cssUtils.$foreground; + box-shadow: cssUtils.$shadow-l; + z-index: 9999; + overflow: hidden; +} + +.openui-copilot-shell-thread-list-header { + @include cssUtils.typography(label, small); + color: cssUtils.$text-neutral-tertiary; + padding: cssUtils.$space-xs cssUtils.$space-s; + padding-bottom: cssUtils.$space-s; +} + +.openui-copilot-shell-thread-list-items { + display: flex; + flex-direction: column; + gap: cssUtils.$space-3xs; + overflow-y: auto; +} + +.openui-copilot-shell-thread-list-empty { + @include cssUtils.typography(body, small); + color: cssUtils.$text-neutral-secondary; + padding: cssUtils.$space-m; + text-align: center; +} + +.openui-copilot-shell-thread-list-trigger { + &[data-state="open"], + &[aria-expanded="true"] { + background-color: cssUtils.$highlight; + border-color: cssUtils.$border-interactive; + } +} + +.openui-copilot-shell-thread-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + border-radius: calc(cssUtils.$radius-s + 1px); + border: 1px solid transparent; + transition: all 0.12s ease; + + &--selected { + background-color: cssUtils.$highlight; + } + + &:hover { + background-color: cssUtils.$highlight; + + .openui-copilot-shell-thread-item-menu-trigger { + opacity: 1; + } + } + + &:has(.openui-copilot-shell-thread-item-menu-trigger[data-state="open"]) { + background-color: cssUtils.$highlight; + + .openui-copilot-shell-thread-item-menu-trigger { + opacity: 1; + } + } +} + +.openui-copilot-shell-thread-item-title { + @include cssUtils.button-reset; + @include cssUtils.typography(body, small); + color: cssUtils.$text-neutral-primary; + padding: calc(cssUtils.$space-xs - 1.75px) cssUtils.$space-s; + width: 100%; + text-align: left; + cursor: pointer; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.openui-copilot-shell-thread-item-menu-trigger { + opacity: 0; + + &:focus-visible { + opacity: 1; + } + + &[data-state="open"] { + opacity: 1; + background-color: cssUtils.$highlight; + border-color: cssUtils.$border-interactive; + } +} + +.openui-copilot-shell-thread-item-menu { + box-sizing: border-box; + position: relative; + z-index: 99999; + min-width: 160px; + overflow: hidden; + border: 1px solid cssUtils.$border-default; + border-radius: cssUtils.$radius-xl; + background-color: cssUtils.$foreground; + box-shadow: cssUtils.$shadow-m; + color: cssUtils.$text-neutral-primary; + transform-origin: top center; + animation: openui-copilot-shell-thread-menu-show 0.18s ease-out forwards; + padding: 4px; +} + +.openui-copilot-shell-thread-item-menu-action { + width: 100%; + justify-content: flex-start; + padding-left: cssUtils.$space-s; + padding-right: cssUtils.$space-s; + + &:focus, + &:focus-visible { + outline: none; + box-shadow: none; + } +} + +@keyframes openui-copilot-shell-thread-menu-show { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} diff --git a/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss b/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss index 443cf7ff5..61a135941 100644 --- a/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss +++ b/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss @@ -3,7 +3,7 @@ .openui-copilot-shell-welcome-screen { display: flex; flex-direction: column; - gap: cssUtils.$space-l; + gap: cssUtils.$space-xl; height: 100%; justify-content: center; align-items: center; @@ -29,13 +29,13 @@ display: flex; flex-direction: column; align-items: center; - gap: cssUtils.$space-xs; + gap: cssUtils.$space-s; text-align: center; } // Title/greeting text &__title { - @include cssUtils.typography(heading, small); + @include cssUtils.typography(heading, medium); color: cssUtils.$text-neutral-primary; margin: 0; } diff --git a/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx b/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx index 148fb6fc0..7ea231ab1 100644 --- a/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx +++ b/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx @@ -72,6 +72,7 @@ const ConversationStartersRenderer = ({ const BottomTrayInner = ({ logoUrl = "https://www.openui.com/favicon.svg", agentName = "My Agent", + showAssistantLogo = false, messageLoading: MessageLoadingComponent = MessageLoading, scrollVariant = "user-message-anchor", isOpen: controlledIsOpen, @@ -108,7 +109,12 @@ const BottomTrayInner = ({ ) : null} - +
handleOpenChange(false)} diff --git a/packages/react-ui/src/components/OpenUIChat/ComposedCopilot.tsx b/packages/react-ui/src/components/OpenUIChat/ComposedCopilot.tsx index f856b439a..83fcefbdd 100644 --- a/packages/react-ui/src/components/OpenUIChat/ComposedCopilot.tsx +++ b/packages/react-ui/src/components/OpenUIChat/ComposedCopilot.tsx @@ -67,6 +67,7 @@ interface CopilotSpecificProps extends SharedChatUIProps { const CopilotInner = ({ logoUrl = "https://www.openui.com/favicon.svg", agentName = "My Agent", + showAssistantLogo = false, messageLoading: MessageLoadingComponent = MessageLoading, scrollVariant = "user-message-anchor", welcomeMessage, @@ -82,7 +83,7 @@ const CopilotInner = ({ ) : null; return ( - +
+ diff --git a/packages/react-ui/src/components/OpenUIChat/types.ts b/packages/react-ui/src/components/OpenUIChat/types.ts index c76857581..6708d0078 100644 --- a/packages/react-ui/src/components/OpenUIChat/types.ts +++ b/packages/react-ui/src/components/OpenUIChat/types.ts @@ -92,6 +92,7 @@ export type ComposerComponent = React.ComponentType; export interface SharedChatUIProps { logoUrl?: string; agentName?: string; + showAssistantLogo?: boolean; messageLoading?: React.ComponentType; scrollVariant?: ScrollVariant; welcomeMessage?: WelcomeMessageConfig; diff --git a/packages/react-ui/src/components/Select/select.scss b/packages/react-ui/src/components/Select/select.scss index a596ecb65..d10f41ce5 100644 --- a/packages/react-ui/src/components/Select/select.scss +++ b/packages/react-ui/src/components/Select/select.scss @@ -120,7 +120,7 @@ .openui-select-label { box-sizing: border-box; padding: cssUtils.$space-3xs cssUtils.$space-xs; - @include cssUtils.typography(primary, default); + @include cssUtils.typography(body, default); color: cssUtils.$text-neutral-tertiary; } @@ -135,7 +135,7 @@ border-radius: cssUtils.$radius-s; padding-top: cssUtils.$space-2xs; padding-bottom: cssUtils.$space-2xs; - @include cssUtils.typography(primary, default); + @include cssUtils.typography(body, default); color: cssUtils.$text-neutral-primary; outline: 0; @@ -192,7 +192,7 @@ // Text content &-text { flex: 1; - @include cssUtils.typography(primary, default); + @include cssUtils.typography(body, default); color: cssUtils.$text-neutral-secondary; } diff --git a/packages/react-ui/src/components/Shell/Container.tsx b/packages/react-ui/src/components/Shell/Container.tsx index 728b80e38..bf2feb314 100644 --- a/packages/react-ui/src/components/Shell/Container.tsx +++ b/packages/react-ui/src/components/Shell/Container.tsx @@ -9,9 +9,16 @@ interface ContainerProps { logoUrl: string; agentName: string; className?: string; + showAssistantLogo?: boolean; } -export const Container = ({ children, logoUrl, agentName, className }: ContainerProps) => { +export const Container = ({ + children, + logoUrl, + agentName, + className, + showAssistantLogo = false, +}: ContainerProps) => { const ref = useRef(null); const { width } = useElementSize({ ref }) || {}; // TODO: revisit this logic @@ -20,7 +27,11 @@ export const Container = ({ children, logoUrl, agentName, className }: Container const layout = isMobile ? "mobile" : isFullScreen ? "fullscreen" : "tray"; return ( - +
{ 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, @@ -33,6 +45,7 @@ const ConversationStarterItem = ({ icon, }: ConversationStarterItemProps) => { const renderedIcon = renderIcon(icon); + const shouldRenderIcon = hasRenderableIcon(renderedIcon); if (variant === "short") { return ( @@ -41,7 +54,7 @@ const ConversationStarterItem = ({ className="openui-shell-conversation-starter-item-short" onClick={() => onClick(prompt)} > - {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} )} {displayText} @@ -57,7 +70,7 @@ const ConversationStarterItem = ({ onClick={() => onClick(prompt)} >
- {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} )} {displayText} @@ -107,6 +120,32 @@ export const ConversationStarter = ({ return null; } + if (variant === "short") { + return ( + + + {starters.map((item, index) => ( + + ))} + + + ); + } + return (
- {/* Add separator between items in long variant */} - {variant === "long" && index < starters.length - 1 && ( -
- -
- )} ))}
diff --git a/packages/react-ui/src/components/Shell/NewChatButton.tsx b/packages/react-ui/src/components/Shell/NewChatButton.tsx index b4dee9a27..f06fa7d5c 100644 --- a/packages/react-ui/src/components/Shell/NewChatButton.tsx +++ b/packages/react-ui/src/components/Shell/NewChatButton.tsx @@ -1,24 +1,34 @@ import { useThreadList } from "@openuidev/react-headless"; import clsx from "clsx"; -import { Plus, SquarePen } from "lucide-react"; +import { SquarePen } from "lucide-react"; +import { useLayoutContext } from "../../context/LayoutContext"; import { Button } from "../Button"; import { IconButton } from "../IconButton"; import { useShellStore } from "../_shared/store"; +import { useOptionalSidebarVisualState } from "./Sidebar"; export const NewChatButton = ({ className }: { className?: string }) => { const switchToNewThread = useThreadList((s) => s.switchToNewThread); const { isSidebarOpen } = useShellStore((state) => ({ isSidebarOpen: state.isSidebarOpen, })); + const { layout } = useLayoutContext(); + const sidebarVisualState = useOptionalSidebarVisualState(); + const showExpandedButton = sidebarVisualState + ? sidebarVisualState.visualState === "expanded" + : isSidebarOpen; + const isMobile = layout === "mobile"; - if (!isSidebarOpen) { + if (!showExpandedButton) { return ( } - onClick={switchToNewThread} - variant="primary" - size="small" - aria-label="New chat" + onClick={(e) => { + e.stopPropagation(); + switchToNewThread(); + }} + variant="secondary" + size={isMobile ? "medium" : "small"} className={clsx("openui-shell-new-chat-button_collapsed", className)} /> ); @@ -27,9 +37,9 @@ export const NewChatButton = ({ className }: { className?: string }) => { return ( - + } + aria-label={`More actions for ${title}`} + variant="tertiary" + size={layout === "mobile" ? "small" : "extra-small"} + className="openui-shell-thread-button-dropdown-trigger" + /> { deleteThread(id); }} > - - Delete + diff --git a/packages/react-ui/src/components/Shell/components/Composer.tsx b/packages/react-ui/src/components/Shell/components/Composer.tsx index 4ccd958f8..0ce1e6fca 100644 --- a/packages/react-ui/src/components/Shell/components/Composer.tsx +++ b/packages/react-ui/src/components/Shell/components/Composer.tsx @@ -43,6 +43,7 @@ export const Composer = ({ className, placeholder = "Type your query here" }: Co return (
0 || undefined} onClick={(e) => { if (!(e.target as HTMLElement).closest("button, a, [role='button']")) { inputRef.current?.focus(); @@ -68,7 +69,7 @@ export const Composer = ({ className, placeholder = "Type your query here" }: Co : } - size="medium" + size="extra-small" variant="primary" aria-label={isRunning ? "Cancel message" : "Send message"} className="openui-shell-thread-composer__submit-button" diff --git a/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx b/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx index a4debec01..d97168390 100644 --- a/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx +++ b/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx @@ -44,7 +44,10 @@ export const DesktopWelcomeComposer = ({ }, [textContent]); return ( -
+
0 || undefined} + >