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
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { SidebarItem } from "./SidebarItem";
import { SidebarSlot } from "./SidebarSlot";
import { MessageLoading, Messages, ScrollArea, ThreadContainer, ThreadHeader } from "./Thread";
import { ThreadList } from "./ThreadList";
import { WelcomeGlow } from "./WelcomeGlow";
import { WelcomeScreen } from "./WelcomeScreen";
import { Workspace } from "./Workspace";

Expand Down Expand Up @@ -167,6 +168,7 @@ interface AgentInterfaceComponent extends FC<AgentInterfaceProps> {
MobileHeader: typeof MobileHeader;
ThreadHeader: typeof ThreadHeader;
Welcome: typeof WelcomeScreen;
WelcomeGlow: typeof WelcomeGlow;
Composer: typeof Composer;
NewChatButton: typeof NewChatButton;
ThreadList: typeof ThreadList;
Expand Down Expand Up @@ -462,6 +464,7 @@ AgentInterface.Route = Route;
AgentInterface.MobileHeader = MobileHeader;
AgentInterface.ThreadHeader = ThreadHeader;
AgentInterface.Welcome = WelcomeScreen;
AgentInterface.WelcomeGlow = WelcomeGlow;
AgentInterface.Composer = Composer;
AgentInterface.NewChatButton = NewChatButton;
AgentInterface.ThreadList = ThreadList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { Boxes, Search, X } from "lucide-react";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { Button } from "../Button";
import { DotMatrixLoader } from "../DotMatrixLoader";
import { IconButton } from "../IconButton";
import { artifactListPath, artifactViewPath } from "./_shared/artifactPaths";
import { useAgentInterfaceLabels } from "./_shared/labelsContext";
Expand Down Expand Up @@ -102,6 +103,7 @@ export const ArtifactBrowserPage = ({ categoryName }: { categoryName?: string })
// is decided synchronously with no loading race.
const notFound = categoryName !== undefined && category === undefined;
const typeFilter = category?.filter.type;
const categoryIllustration = useArtifactIcon(typeFilter?.[0] ?? "");

const [search, setSearch] = useState("");
const [debouncedSearch, setDebouncedSearch] = useState("");
Expand All @@ -120,7 +122,7 @@ export const ArtifactBrowserPage = ({ categoryName }: { categoryName?: string })
// Noun derived from the category name (not a hardcoded "Apps" match).
const emptyItemLabel = categoryName ? categoryName.toLowerCase() : "artifacts";
const emptyMessage = debouncedSearch
? `No ${emptyItemLabel} match your search`
? `No results found for "${debouncedSearch}"`
: `No ${emptyItemLabel} yet`;

// Initial page + reload on search/category change.
Expand Down Expand Up @@ -235,7 +237,7 @@ export const ArtifactBrowserPage = ({ categoryName }: { categoryName?: string })
{!error && artifacts.length === 0 && !isLoading && (
<div className="openui-agent-artifact-browser__empty">
<span className="openui-agent-artifact-browser__empty-illustration">
<Boxes size="1em" />
{debouncedSearch ? categoryIllustration : <Boxes size="1em" />}
</span>
<span className="openui-agent-artifact-browser__empty-text">{emptyMessage}</span>
</div>
Expand All @@ -251,7 +253,11 @@ export const ArtifactBrowserPage = ({ categoryName }: { categoryName?: string })
/>
);
})}
{isLoading && <div className="openui-agent-artifact-browser__loading">Loading…</div>}
{isLoading && (
<div className="openui-agent-artifact-browser__loading">
<DotMatrixLoader />
</div>
)}
{!isLoading && nextCursor !== undefined && (
<div className="openui-agent-artifact-browser__load-more">
<Button variant="secondary" size="small" onClick={loadMore}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { ArrowLeft, MessageSquare } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Button } from "../Button";
import { DotMatrixLoader } from "../DotMatrixLoader";
import { IconButton } from "../IconButton";
import { artifactListPath } from "./_shared/artifactPaths";
import { useNav } from "./_shared/navContext";
Expand Down Expand Up @@ -99,7 +100,11 @@ export const ArtifactViewPage = ({
</div>
);
} else if (!artifact) {
body = <div className="openui-agent-artifact-view__loading">Loading…</div>;
body = (
<div className="openui-agent-artifact-view__loading">
<DotMatrixLoader />
</div>
);
} else if (!renderer) {
body = (
<div className="openui-agent-artifact-view__error">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isChatEmpty } from "./_shared/utils";
export type ConversationStarterVariant = "short" | "long";

interface ConversationStarterItemProps extends ConversationStarterProps {
onClick: (prompt: string) => void;
onClick: () => void;
variant: ConversationStarterVariant;
}

Expand Down Expand Up @@ -39,7 +39,6 @@ const hasRenderableIcon = (icon: ReactNode): boolean => {

const ConversationStarterItem = ({
displayText,
prompt,
onClick,
variant,
icon,
Expand All @@ -52,7 +51,7 @@ const ConversationStarterItem = ({
<button
type="button"
className="openui-agent-conversation-starter-item-short"
onClick={() => onClick(prompt)}
onClick={onClick}
>
{shouldRenderIcon && (
<span className="openui-agent-conversation-starter-item-short__icon">{renderedIcon}</span>
Expand All @@ -67,7 +66,7 @@ const ConversationStarterItem = ({
<button
type="button"
className="openui-agent-conversation-starter-item-long"
onClick={() => onClick(prompt)}
onClick={onClick}
>
<div className="openui-agent-conversation-starter-item-long__content">
{shouldRenderIcon && (
Expand All @@ -85,6 +84,10 @@ const ConversationStarterItem = ({
export interface ConversationStarterContainerProps {
starters: ConversationStarterProps[];
className?: string;
/**
* Overrides the default send behavior when a starter is selected.
*/
onSelect?: (starter: ConversationStarterProps) => void;
/**
* Variant of the conversation starter
* - "short": Pill-style horizontal buttons (default)
Expand All @@ -96,18 +99,23 @@ export interface ConversationStarterContainerProps {
export const ConversationStarter = ({
starters,
className,
onSelect,
variant = "short",
}: ConversationStarterContainerProps) => {
const processMessage = useThread((s) => s.processMessage);
const isRunning = useThread((s) => s.isRunning);
const messages = useThread((s) => s.messages);
const isLoadingMessages = useThread((s) => s.isLoadingMessages);

const handleClick = (prompt: string) => {
const handleClick = (starter: ConversationStarterProps) => {
if (isRunning) return;
if (onSelect) {
onSelect(starter);
return;
}
processMessage({
role: "user",
content: prompt,
content: starter.prompt,
});
};

Expand Down Expand Up @@ -137,7 +145,7 @@ export const ConversationStarter = ({
displayText={item.displayText}
prompt={item.prompt}
icon={item.icon}
onClick={handleClick}
onClick={() => handleClick(item)}
variant={variant}
/>
))}
Expand All @@ -156,11 +164,14 @@ export const ConversationStarter = ({
>
{starters.map((item, index) => (
<Fragment key={`${item.displayText}-${index}`}>
{index > 0 && (
<div className="openui-agent-conversation-starter__separator" aria-hidden="true" />
)}
<ConversationStarterItem
displayText={item.displayText}
prompt={item.prompt}
icon={item.icon}
onClick={handleClick}
onClick={() => handleClick(item)}
variant={variant}
/>
</Fragment>
Expand Down
7 changes: 4 additions & 3 deletions packages/react-ui/src/components/AgentInterface/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import { TimelineEntry } from "./_shared/tool-renderer";
import type { AssistantMessageComponent, UserMessageComponent } from "./_shared/types";

import { Callout } from "../Callout";
import { DotMatrixLoader } from "../DotMatrixLoader";
import { IconButton } from "../IconButton";
import { MarkDownRenderer } from "../MarkDownRenderer";
import { MessageLoading as MessageLoadingComponent } from "../MessageLoading";
import { ResizableSeparator } from "./ResizableSeparator";
import { UserMessageContent } from "./UserMessageContent";
import { AgentInterfaceTooltip } from "./_shared/AgentInterfaceTooltip";
Expand Down Expand Up @@ -120,7 +120,8 @@ export const ScrollArea = ({
children,
className,
scrollVariant = "user-message-anchor",
userMessageSelector = ".openui-agent-thread-message-user",
userMessageSelector =
".openui-agent-thread-message-user, .openui-shell-thread-message-user",
scrollOnLoad = true,
}: {
children?: React.ReactNode;
Expand Down Expand Up @@ -289,7 +290,7 @@ export const RenderMessage = memo(
export const MessageLoading = () => {
return (
<div className="openui-agent-thread-message-loading">
<MessageLoadingComponent />
<DotMatrixLoader variant="compact" />
</div>
);
};
Expand Down
72 changes: 5 additions & 67 deletions packages/react-ui/src/components/AgentInterface/ThreadList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { Thread } from "@openuidev/react-headless";
import { useThreadList } from "@openuidev/react-headless";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import clsx from "clsx";
import { EllipsisIcon, Trash2Icon } from "lucide-react";
import { Fragment, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useLayoutContext } from "../../context/LayoutContext";
import { Button } from "../Button";
import { IconButton } from "../IconButton";
Expand Down Expand Up @@ -104,73 +103,12 @@ export const ThreadList = ({ className }: { className?: string }) => {
loadThreads();
}, []);

const groupThreads = () => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const last7Days = new Date(today);
last7Days.setDate(last7Days.getDate() - 7);
const last30Days = new Date(today);
last30Days.setDate(last30Days.getDate() - 30);
const thisYear = new Date(today);
thisYear.setMonth(0, 1);

return threads.reduce(
(groups, thread) => {
const threadDate = new Date(thread.createdAt);

if (threadDate >= today) {
groups.today = [...(groups.today || []), thread];
} else if (threadDate >= yesterday) {
groups.yesterday = [...(groups.yesterday || []), thread];
} else if (threadDate >= last7Days) {
groups.last7Days = [...(groups.last7Days || []), thread];
} else if (threadDate >= last30Days) {
groups.last30Days = [...(groups.last30Days || []), thread];
} else if (threadDate >= thisYear) {
groups.thisYear = [...(groups.thisYear || []), thread];
} else {
groups.older = [...(groups.older || []), thread];
}

return groups;
},
{
today: [] as Thread[],
yesterday: [] as Thread[],
last7Days: [] as Thread[],
last30Days: [] as Thread[],
thisYear: [] as Thread[],
older: [] as Thread[],
},
);
};

const groupedThreads = groupThreads();
const groupLabels: { [key in keyof typeof groupedThreads]: string } = {
today: "Today",
yesterday: "Yesterday",
last7Days: "Previous 7 Days",
last30Days: "Previous 30 Days",
thisYear: "This Year",
older: "Older",
};

return (
<div className={clsx("openui-agent-thread-list", className)}>
{Object.entries(groupedThreads)
.filter(([_, groupThreads]) => groupThreads.length > 0)
.map(([group, groupThreads]) => (
<Fragment key={group}>
<div className="openui-agent-thread-list-group">
{groupLabels[group as keyof typeof groupLabels]}
</div>
{groupThreads.map((thread) => (
<ThreadButton key={thread.id} id={thread.id} title={thread.title} />
))}
</Fragment>
))}
{threads.length > 0 && <div className="openui-agent-thread-list-group">Threads</div>}
{threads.map((thread) => (
<ThreadButton key={thread.id} id={thread.id} title={thread.title} />
))}
</div>
);
};
43 changes: 43 additions & 0 deletions packages/react-ui/src/components/AgentInterface/WelcomeGlow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import clsx from "clsx";
import { createContext, useContext, type ReactNode } from "react";

const WelcomeGlowContext = createContext(false);

export const WelcomeGlowProvider = ({
children,
enabled,
}: {
children: ReactNode;
enabled: boolean;
}) => <WelcomeGlowContext.Provider value={enabled}>{children}</WelcomeGlowContext.Provider>;

export interface WelcomeGlowProps {
children: ReactNode;
className?: string;
}

/**
* Positions the optional welcome glow around a custom composer. The animation
* is enabled by the nearest <AgentInterface.Welcome glowAnimation> ancestor.
*/
export const WelcomeGlow = ({ children, className }: WelcomeGlowProps) => {
const enabled = useContext(WelcomeGlowContext);

if (!enabled) {
return <>{children}</>;
}

return (
<div className={clsx("openui-agent-welcome-glow", className)}>
<span
aria-hidden="true"
className="openui-agent-welcome-glow__blob openui-agent-welcome-glow__blob--accent"
/>
<span
aria-hidden="true"
className="openui-agent-welcome-glow__blob openui-agent-welcome-glow__blob--foreground"
/>
{children}
</div>
);
};
Loading