Skip to content

Commit

Permalink
feat: ThreadList / ThreadListItem UI (#1214)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yonom authored Nov 24, 2024
1 parent b4e04cb commit 589d37b
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 72 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-snakes-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@assistant-ui/react": patch
---

feat: ThreadList / ThreadListItem UI
57 changes: 10 additions & 47 deletions apps/docs/components/shadcn/Shadcn.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ArchiveIcon, EditIcon, MenuIcon, ShareIcon } from "lucide-react";
import {
Thread,
ThreadList,
ThreadListItemPrimitive,
ThreadListPrimitive,
useThreadList,
Expand Down Expand Up @@ -65,58 +66,20 @@ const ButtonWithTooltip: FC<ButtonWithTooltipProps> = ({
};

const TopLeft: FC = () => {
const isNewSelected = useThreadList((t) => t.newThread === t.mainThreadId);
return (
<ThreadListPrimitive.New asChild>
<ButtonWithTooltip
variant="ghost"
className={cn(
"flex w-full justify-between px-3",
isNewSelected && "bg-aui-muted",
)}
tooltip="New Chat"
side="right"
>
<div className="flex items-center gap-2 text-sm font-semibold">
<Image
src={icon}
alt="logo"
className="inline size-4 dark:hue-rotate-180 dark:invert"
/>
<span>assistant-ui</span>
</div>

<EditIcon className="size-4" />
</ButtonWithTooltip>
</ThreadListPrimitive.New>
);
};

const ThreadListItem: FC = () => {
return (
<ThreadListItemPrimitive.Root className="hover:text-primary data-[active]:bg-muted data-[active]:text-primary flex items-center gap-2 rounded-lg px-3 py-2 transition-all">
<ThreadListItemPrimitive.Trigger className="flex-grow text-start">
<ThreadListItemPrimitive.Title fallback="New Chat" />
</ThreadListItemPrimitive.Trigger>
<ThreadListItemPrimitive.Archive asChild>
<ButtonWithTooltip
variant="ghost"
className="hover:text-foreground/60 ml-auto h-auto p-0"
tooltip="Archive"
>
<ArchiveIcon className="size-4" />
</ButtonWithTooltip>
</ThreadListItemPrimitive.Archive>
</ThreadListItemPrimitive.Root>
<div className="flex h-full w-full items-center gap-2 px-3 text-sm font-semibold">
<Image
src={icon}
alt="logo"
className="inline size-4 dark:hue-rotate-180 dark:invert"
/>
<span>assistant-ui</span>
</div>
);
};

const MainLeft: FC = () => {
return (
<nav className="flex flex-col items-stretch gap-1 text-sm font-medium">
<ThreadListPrimitive.Items components={{ ThreadListItem }} />
</nav>
);
return <ThreadList />;
};

const LeftBarSheet: FC = () => {
Expand Down
36 changes: 36 additions & 0 deletions apps/docs/content/docs/ui/styled/Decomposition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -591,3 +591,39 @@ const MyAssistantModal: FC<ThreadConfig> = (config) => {
```ts
<MyAssistantModal />
```

## ThreadList

Renders a thread list.

```tsx
import { ThreadList, ThreadListItem } from "@assistant-ui/react";

const MyThreadList = () => {
return (
<ThreadList.Root>
<ThreadList.New />
<ThreadList.Items />
</ThreadList.Root>
);
};
```

### ThreadListItem

Renders a thread list item.

```tsx
import { ThreadListItem, ThreadListItemPrimitive } from "@assistant-ui/react";

const MyThreadListItem = () => {
return (
<ThreadListItem.Root>
<ThreadListItemTrigger>
<ThreadListItemTitle />
</ThreadListItemTrigger>
<ThreadListItem.Archive />
</ThreadListItem.Root>
);
};
```
25 changes: 0 additions & 25 deletions packages/react/src/primitives/threadList/ThreadListNew.ts

This file was deleted.

45 changes: 45 additions & 0 deletions packages/react/src/primitives/threadList/ThreadListNew.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import {
ActionButtonElement,
ActionButtonProps,
} from "../../utils/createActionButton";
import { useAssistantRuntime, useThreadList } from "../../context";
import { forwardRef } from "react";
import { Primitive } from "@radix-ui/react-primitive";
import { composeEventHandlers } from "@radix-ui/primitive";

const useThreadListNew = () => {
const runtime = useAssistantRuntime();
return () => {
runtime.switchToNewThread();
};
};

export namespace ThreadListPrimitiveNew {
export type Element = ActionButtonElement;
export type Props = ActionButtonProps<typeof useThreadListNew>;
}

export const ThreadListPrimitiveNew = forwardRef<
ThreadListPrimitiveNew.Element,
ThreadListPrimitiveNew.Props
>(({ onClick, disabled, ...props }, forwardedRef) => {
const isMain = useThreadList((t) => t.newThread === t.mainThreadId);
const callback = useThreadListNew();

return (
<Primitive.button
type="button"
{...(isMain ? { "data-active": "true" } : null)}
{...props}
ref={forwardedRef}
disabled={disabled || !callback}
onClick={composeEventHandlers(onClick, () => {
callback?.();
})}
/>
);
});

ThreadListPrimitiveNew.displayName = "ThreadListPrimitive.New";
30 changes: 30 additions & 0 deletions packages/react/src/styles/tailwindcss/thread.css
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,33 @@
.aui-text-running::after {
@apply animate-pulse font-sans content-['\25CF'] ltr:ml-1 rtl:mr-1;
}

/* thread-list */

.aui-thread-list-root {
@apply flex flex-col items-stretch gap-1.5;
}

.aui-thread-list-item {
@apply data-[active]:bg-aui-muted hover:bg-aui-muted flex items-center gap-2 rounded-lg transition-all;
}

.aui-thread-list-new {
@apply data-[active]:bg-aui-muted hover:bg-aui-muted flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start;
}

.aui-thread-list-new > .lucide-plus {
@apply size-5;
}

.aui-thread-list-item-trigger {
@apply flex-grow px-3 py-2 text-start;
}

.aui-thread-list-item-title {
@apply text-sm;
}

.aui-thread-list-item-archive {
@apply hover:text-aui-primary text-aui-foreground ml-auto mr-3 size-4 p-0;
}
4 changes: 4 additions & 0 deletions packages/react/src/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ export { default as UserActionBar } from "./user-action-bar";
export { default as ThreadWelcome } from "./thread-welcome";

export { default as ContentPart } from "./content-part";

export { default as ThreadList } from "./thread-list";

export { default as ThreadListItem } from "./thread-list-item";
13 changes: 13 additions & 0 deletions packages/react/src/ui/thread-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ export type StringsConfig = {
};
};
};
threadList?: {
new?: {
label?: string | undefined;
};
item?: {
title?: {
fallback?: string | undefined;
};
archive?: {
label?: string | undefined;
};
};
};
thread?: {
scrollToBottom?: {
tooltip?: string | undefined;
Expand Down
99 changes: 99 additions & 0 deletions packages/react/src/ui/thread-list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"use client";

import { ComponentPropsWithoutRef, forwardRef, type FC } from "react";
import { ArchiveIcon } from "lucide-react";

import { withDefaults } from "./utils/withDefaults";
import { TooltipIconButton } from "./base/tooltip-icon-button";
import { ThreadListItemPrimitive } from "../primitives";
import { useThreadConfig } from "./thread-config";
import classNames from "classnames";

const ThreadListItem: FC = () => {
return (
<ThreadListItemRoot>
<ThreadListItemTrigger>
<ThreadListItemTitle />
</ThreadListItemTrigger>
<ThreadListItemArchive />
</ThreadListItemRoot>
);
};

const ThreadListItemRoot = withDefaults(ThreadListItemPrimitive.Root, {
className: "aui-thread-list-item",
});

ThreadListItemRoot.displayName = "ThreadListItemRoot";

const ThreadListItemTrigger = withDefaults(ThreadListItemPrimitive.Trigger, {
className: "aui-thread-list-item-trigger",
});

namespace ThreadListItemPrimitiveTitle {
export type Element = HTMLParagraphElement;
export type Props = ComponentPropsWithoutRef<"p">;
}

const ThreadListItemTitle = forwardRef<
ThreadListItemPrimitiveTitle.Element,
ThreadListItemPrimitiveTitle.Props
>(({ className, ...props }, ref) => {
const {
strings: {
threadList: { item: { title: { fallback = "New Chat" } = {} } = {} } = {},
} = {},
} = useThreadConfig();

return (
<p
ref={ref}
className={classNames("aui-thread-list-item-title", className)}
{...props}
>
<ThreadListItemPrimitive.Title fallback={fallback} />
</p>
);
});

ThreadListItemTitle.displayName = "ThreadListItemTitle";

const ThreadListItemArchive = withDefaults(
forwardRef<HTMLButtonElement, TooltipIconButton.Props>(
({ className, ...props }, ref) => {
const {
strings: {
threadList: {
item: { archive: { label = "Archive thread" } = {} } = {},
} = {},
} = {},
} = useThreadConfig();

return (
<ThreadListItemPrimitive.Archive asChild>
<TooltipIconButton
{...props}
ref={ref}
className={classNames("aui-thread-list-item-archive", className)}
variant="ghost"
tooltip={label}
>
<ArchiveIcon />
</TooltipIconButton>
</ThreadListItemPrimitive.Archive>
);
},
),
{},
);

ThreadListItemArchive.displayName = "ThreadListItemArchive";

const exports = {
Root: ThreadListItemRoot,
Title: ThreadListItemTitle,
Archive: ThreadListItemArchive,
};

export default Object.assign(ThreadListItem, exports) as typeof ThreadListItem &
typeof exports;
Loading

0 comments on commit 589d37b

Please sign in to comment.