Improve chat navigation, model selection, and streaming

This commit is contained in:
Rami Bitar
2026-09-16 13:14:55 -04:00
parent 8790cfb1e2
commit dd04493e25
73 changed files with 1272 additions and 1367 deletions
+3 -3
View File
@@ -8,7 +8,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { type LucideIcon, XIcon } from "lucide-react";
import { type RemixiconComponentType, RiCloseLine } from "@remixicon/react";
import type { ComponentProps, HTMLAttributes } from "react";
export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
@@ -57,7 +57,7 @@ export const ArtifactClose = ({
variant={variant}
{...props}
>
{children ?? <XIcon className="size-4" />}
{children ?? <RiCloseLine className="size-4" />}
<span className="sr-only">Close</span>
</Button>
);
@@ -92,7 +92,7 @@ export const ArtifactActions = ({
export type ArtifactActionProps = ComponentProps<typeof Button> & {
tooltip?: string;
label?: string;
icon?: LucideIcon;
icon?: RemixiconComponentType;
};
export const ArtifactAction = ({
+9 -9
View File
@@ -9,11 +9,11 @@ import {
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import {
BrainIcon,
ChevronDownIcon,
DotIcon,
type LucideIcon,
} from "lucide-react";
RiBrainLine,
RiArrowDownSLine,
RiCircleFill,
type RemixiconComponentType,
} from "@remixicon/react";
import type { ComponentProps, ReactNode } from "react";
import { createContext, memo, useContext, useMemo } from "react";
@@ -92,11 +92,11 @@ export const ChainOfThoughtHeader = memo(
)}
{...props}
>
<BrainIcon className="size-4" />
<RiBrainLine className="size-4" />
<span className="flex-1 text-left">
{children ?? "Chain of Thought"}
</span>
<ChevronDownIcon
<RiArrowDownSLine
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0"
@@ -109,7 +109,7 @@ export const ChainOfThoughtHeader = memo(
);
export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
icon?: LucideIcon;
icon?: RemixiconComponentType;
label: ReactNode;
description?: ReactNode;
status?: "complete" | "active" | "pending";
@@ -118,7 +118,7 @@ export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
export const ChainOfThoughtStep = memo(
({
className,
icon: Icon = DotIcon,
icon: Icon = RiCircleFill,
label,
description,
status = "complete",
+6 -4
View File
@@ -8,8 +8,8 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { BookmarkIcon, type LucideProps } from "lucide-react";
import type { ComponentProps, HTMLAttributes } from "react";
import { RiBookmarkLine } from "@remixicon/react";
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
@@ -27,7 +27,9 @@ export const Checkpoint = ({
</div>
);
export type CheckpointIconProps = LucideProps;
export type CheckpointIconProps = Omit<ComponentProps<typeof RiBookmarkLine>, "children"> & {
children?: ReactNode;
};
export const CheckpointIcon = ({
className,
@@ -35,7 +37,7 @@ export const CheckpointIcon = ({
...props
}: CheckpointIconProps) =>
children ?? (
<BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />
<RiBookmarkLine className={cn("size-4 shrink-0", className)} {...props} />
);
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
+2 -2
View File
@@ -2,7 +2,7 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react";
import { RiCheckLine, RiFileCopyLine } from "@remixicon/react";
import {
type ComponentProps,
createContext,
@@ -162,7 +162,7 @@ export const CodeBlockCopyButton = ({
}
};
const Icon = isCopied ? CheckIcon : CopyIcon;
const Icon = isCopied ? RiCheckLine : RiFileCopyLine;
return (
<Button
+2 -2
View File
@@ -2,7 +2,7 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ArrowDownIcon } from "lucide-react";
import { RiArrowDownLine } from "@remixicon/react";
import type { ComponentProps } from "react";
import { useCallback } from "react";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
@@ -93,7 +93,7 @@ export const ConversationScrollButton = ({
variant="outline"
{...props}
>
<ArrowDownIcon className="size-4" />
<RiArrowDownLine className="size-4" />
</Button>
)
);
+3 -3
View File
@@ -13,7 +13,7 @@ import {
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { cn } from "@/lib/utils";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react";
import {
type ComponentProps,
createContext,
@@ -206,7 +206,7 @@ export const InlineCitationCarouselPrev = ({
type="button"
{...props}
>
<ArrowLeftIcon className="size-4 text-muted-foreground" />
<RiArrowLeftLine className="size-4 text-muted-foreground" />
</button>
);
};
@@ -233,7 +233,7 @@ export const InlineCitationCarouselNext = ({
type="button"
{...props}
>
<ArrowRightIcon className="size-4 text-muted-foreground" />
<RiArrowRightLine className="size-4 text-muted-foreground" />
</button>
);
};
+11 -12
View File
@@ -14,11 +14,11 @@ import {
import { cn } from "@/lib/utils";
import type { FileUIPart, UIMessage } from "ai";
import {
ChevronLeftIcon,
ChevronRightIcon,
PaperclipIcon,
XIcon,
} from "lucide-react";
RiArrowLeftSLine,
RiArrowRightSLine,
RiAttachment2,
RiCloseLine,
} from "@remixicon/react";
import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode } from "react";
import {
Children,
@@ -267,7 +267,7 @@ export const MessageBranchPrevious = ({
variant="ghost"
{...props}
>
{children ?? <ChevronLeftIcon size={14} />}
{children ?? <RiArrowLeftSLine size={14} />}
</Button>
);
};
@@ -291,7 +291,7 @@ export const MessageBranchNext = ({
variant="ghost"
{...props}
>
{children ?? <ChevronRightIcon size={14} />}
{children ?? <RiArrowRightSLine size={14} />}
</Button>
);
};
@@ -338,8 +338,7 @@ export const MessageResponse = memo(
}}
{...props}
/>
),
(prevProps, nextProps) => prevProps.children === nextProps.children
)
);
MessageResponse.displayName = "MessageResponse";
@@ -390,7 +389,7 @@ export function MessageAttachment({
type="button"
variant="ghost"
>
<XIcon />
<RiCloseLine />
<span className="sr-only">Remove</span>
</Button>
)}
@@ -400,7 +399,7 @@ export function MessageAttachment({
<Tooltip>
<TooltipTrigger asChild>
<div className="flex size-full shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<PaperclipIcon className="size-4" />
<RiAttachment2 className="size-4" />
</div>
</TooltipTrigger>
<TooltipContent>
@@ -418,7 +417,7 @@ export function MessageAttachment({
type="button"
variant="ghost"
>
<XIcon />
<RiCloseLine />
<span className="sr-only">Remove</span>
</Button>
)}
+12 -12
View File
@@ -11,10 +11,10 @@ import {
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import {
ChevronDownIcon,
ExternalLinkIcon,
MessageCircleIcon,
} from "lucide-react";
RiArrowDownSLine,
RiExternalLinkLine,
RiChat3Line,
} from "@remixicon/react";
import { type ComponentProps, createContext, useContext } from "react";
const providers = {
@@ -140,7 +140,7 @@ const providers = {
`https://t3.chat/new?${new URLSearchParams({
q,
})}`,
icon: <MessageCircleIcon />,
icon: <RiChat3Line />,
},
v0: {
title: "Open in v0",
@@ -238,7 +238,7 @@ export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
{children ?? (
<Button type="button" variant="outline">
Open in chat
<ChevronDownIcon className="size-4" />
<RiArrowDownSLine className="size-4" />
</Button>
)}
</DropdownMenuTrigger>
@@ -258,7 +258,7 @@ export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
>
<span className="shrink-0">{providers.chatgpt.icon}</span>
<span className="flex-1">{providers.chatgpt.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
<RiExternalLinkLine className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
@@ -278,7 +278,7 @@ export const OpenInClaude = (props: OpenInClaudeProps) => {
>
<span className="shrink-0">{providers.claude.icon}</span>
<span className="flex-1">{providers.claude.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
<RiExternalLinkLine className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
@@ -298,7 +298,7 @@ export const OpenInT3 = (props: OpenInT3Props) => {
>
<span className="shrink-0">{providers.t3.icon}</span>
<span className="flex-1">{providers.t3.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
<RiExternalLinkLine className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
@@ -318,7 +318,7 @@ export const OpenInScira = (props: OpenInSciraProps) => {
>
<span className="shrink-0">{providers.scira.icon}</span>
<span className="flex-1">{providers.scira.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
<RiExternalLinkLine className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
@@ -338,7 +338,7 @@ export const OpenInv0 = (props: OpenInv0Props) => {
>
<span className="shrink-0">{providers.v0.icon}</span>
<span className="flex-1">{providers.v0.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
<RiExternalLinkLine className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
@@ -358,7 +358,7 @@ export const OpenInCursor = (props: OpenInCursorProps) => {
>
<span className="shrink-0">{providers.cursor.icon}</span>
<span className="flex-1">{providers.cursor.title}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
<RiExternalLinkLine className="size-4 shrink-0" />
</a>
</DropdownMenuItem>
);
+2 -2
View File
@@ -16,7 +16,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { ChevronsUpDownIcon } from "lucide-react";
import { RiExpandUpDownLine } from "@remixicon/react";
import type { ComponentProps } from "react";
import { createContext, useContext } from "react";
import { Shimmer } from "./shimmer";
@@ -135,7 +135,7 @@ export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => (
variant="ghost"
{...props}
>
<ChevronsUpDownIcon className="size-4" />
<RiExpandUpDownLine className="size-4" />
<span className="sr-only">Toggle plan</span>
</Button>
</CollapsibleTrigger>
+16 -16
View File
@@ -37,14 +37,14 @@ import {
import { cn } from "@/lib/utils";
import type { ChatStatus, FileUIPart } from "ai";
import {
CornerDownLeftIcon,
ImageIcon,
MicIcon,
PaperclipIcon,
PlusIcon,
SquareIcon,
XIcon,
} from "lucide-react";
RiCornerDownLeftLine,
RiImageLine,
RiMicLine,
RiAttachment2,
RiAddLine,
RiStopFill,
RiCloseLine,
} from "@remixicon/react";
import { Loader } from "@/components/ai-elements/loader";
import { nanoid } from "nanoid";
import {
@@ -319,7 +319,7 @@ export function PromptInputAttachment({
/>
) : (
<div className="flex size-5 items-center justify-center text-muted-foreground">
<PaperclipIcon className="size-3" />
<RiAttachment2 className="size-3" />
</div>
)}
</div>
@@ -333,7 +333,7 @@ export function PromptInputAttachment({
type="button"
variant="ghost"
>
<XIcon />
<RiCloseLine />
<span className="sr-only">Remove</span>
</Button>
</div>
@@ -421,7 +421,7 @@ export const PromptInputActionAddAttachments = ({
attachments.openFileDialog();
}}
>
<ImageIcon className="mr-2 size-4" /> {label}
<RiImageLine className="mr-2 size-4" /> {label}
</DropdownMenuItem>
);
};
@@ -989,7 +989,7 @@ export const PromptInputActionMenuTrigger = ({
}: PromptInputActionMenuTriggerProps) => (
<DropdownMenuTrigger asChild>
<PromptInputButton className={className} {...props}>
{children ?? <PlusIcon className="size-4" />}
{children ?? <RiAddLine className="size-4" />}
</PromptInputButton>
</DropdownMenuTrigger>
);
@@ -1029,14 +1029,14 @@ export const PromptInputSubmit = ({
children,
...props
}: PromptInputSubmitProps) => {
let Icon = <CornerDownLeftIcon className="size-4" />;
let Icon = <RiCornerDownLeftLine className="size-4" />;
if (status === "submitted") {
Icon = <Loader size={16} />;
} else if (status === "streaming") {
Icon = <SquareIcon className="size-4" />;
Icon = <RiStopFill className="size-4" />;
} else if (status === "error") {
Icon = <XIcon className="size-4" />;
Icon = <RiCloseLine className="size-4" />;
}
return (
@@ -1208,7 +1208,7 @@ export const PromptInputSpeechButton = ({
onClick={toggleListening}
{...props}
>
<MicIcon className="size-4" />
<RiMicLine className="size-4" />
</PromptInputButton>
);
};
+3 -3
View File
@@ -8,7 +8,7 @@ import {
} from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { ChevronDownIcon, PaperclipIcon } from "lucide-react";
import { RiArrowDownSLine, RiAttachment2 } from "@remixicon/react";
import type { ComponentProps } from "react";
export type QueueMessagePart = {
@@ -174,7 +174,7 @@ export const QueueItemFile = ({
)}
{...props}
>
<PaperclipIcon size={12} />
<RiAttachment2 size={12} />
<span className="max-w-[100px] truncate">{children}</span>
</span>
);
@@ -241,7 +241,7 @@ export const QueueSectionLabel = ({
...props
}: QueueSectionLabelProps) => (
<span className={cn("flex items-center gap-2", className)} {...props}>
<ChevronDownIcon className="group-data-[state=closed]:-rotate-90 size-4 transition-transform" />
<RiArrowDownSLine className="group-data-[state=closed]:-rotate-90 size-4 transition-transform" />
{icon}
<span>
{count} {label}
+3 -3
View File
@@ -7,7 +7,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { BrainIcon, ChevronDownIcon } from "lucide-react";
import { RiBrainLine, RiArrowDownSLine } from "@remixicon/react";
import type { ComponentProps, ReactNode } from "react";
import { createContext, memo, useContext, useEffect, useState } from "react";
import { Streamdown } from "streamdown";
@@ -139,9 +139,9 @@ export const ReasoningTrigger = memo(
>
{children ?? (
<>
<BrainIcon className="size-4" />
<RiBrainLine className="size-4" />
{getThinkingMessage(isStreaming, duration)}
<ChevronDownIcon
<RiArrowDownSLine
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0"
+3 -3
View File
@@ -15,7 +15,7 @@ import {
TabsTrigger,
} from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { ChevronDownIcon, Code } from "lucide-react";
import { RiArrowDownSLine, RiCodeLine } from "@remixicon/react";
import { getStatusBadge } from "./tool";
@@ -52,11 +52,11 @@ export const SandboxHeader = ({
{...props}
>
<div className="flex items-center gap-2">
<Code className="size-4 text-muted-foreground" />
<RiCodeLine className="size-4 text-muted-foreground" />
<span className="font-medium text-sm">{title}</span>
{getStatusBadge(state)}
</div>
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
<RiArrowDownSLine className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
</CollapsibleTrigger>
);
+3 -3
View File
@@ -6,7 +6,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { BookIcon, ChevronDownIcon } from "lucide-react";
import { RiBookOpenLine, RiArrowDownSLine } from "@remixicon/react";
import type { ComponentProps } from "react";
export type SourcesProps = ComponentProps<"div">;
@@ -35,7 +35,7 @@ export const SourcesTrigger = ({
{children ?? (
<>
<p className="font-medium">Used {count} sources</p>
<ChevronDownIcon className="h-4 w-4" />
<RiArrowDownSLine className="h-4 w-4" />
</>
)}
</CollapsibleTrigger>
@@ -69,7 +69,7 @@ export const Source = ({ href, title, children, ...props }: SourceProps) => (
>
{children ?? (
<>
<BookIcon className="h-4 w-4" />
<RiBookOpenLine className="h-4 w-4" />
<span className="block font-medium">{title}</span>
</>
)}
+3 -3
View File
@@ -6,7 +6,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { ChevronDownIcon, SearchIcon } from "lucide-react";
import { RiArrowDownSLine, RiSearchLine } from "@remixicon/react";
import type { ComponentProps, ReactNode } from "react";
export type TaskItemFileProps = ComponentProps<"div">;
@@ -60,9 +60,9 @@ export const TaskTrigger = ({
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
{children ?? (
<div className="flex w-full cursor-pointer items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground">
{icon ?? <SearchIcon className="size-4" />}
{icon ?? <RiSearchLine className="size-4" />}
<p className="text-sm">{title}</p>
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
<RiArrowDownSLine className="size-4 transition-transform group-data-[state=open]:rotate-180" />
</div>
)}
</CollapsibleTrigger>
+16 -16
View File
@@ -9,13 +9,13 @@ import {
import { cn } from "@/lib/utils";
import type { ToolUIPart } from "ai";
import {
CheckCircleIcon,
ChevronDownIcon,
CircleIcon,
ClockIcon,
WrenchIcon,
XCircleIcon,
} from "lucide-react";
RiCheckboxCircleLine,
RiArrowDownSLine,
RiCircleLine,
RiTimeLine,
RiToolsLine,
RiCloseCircleLine,
} from "@remixicon/react";
import type { ComponentProps, ReactNode } from "react";
import { isValidElement } from "react";
import { CodeBlock } from "./code-block";
@@ -48,13 +48,13 @@ export const getStatusBadge = (status: ToolUIPart["state"]) => {
};
const icons: Record<ToolUIPart["state"], ReactNode> = {
"input-streaming": <CircleIcon className="size-4" />,
"input-available": <ClockIcon className="size-4 animate-pulse" />,
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
"approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
"output-error": <XCircleIcon className="size-4 text-red-600" />,
"output-denied": <XCircleIcon className="size-4 text-orange-600" />,
"input-streaming": <RiCircleLine className="size-4" />,
"input-available": <RiTimeLine className="size-4 animate-pulse" />,
"approval-requested": <RiTimeLine className="size-4 text-yellow-600" />,
"approval-responded": <RiCheckboxCircleLine className="size-4 text-blue-600" />,
"output-available": <RiCheckboxCircleLine className="size-4 text-green-600" />,
"output-error": <RiCloseCircleLine className="size-4 text-red-600" />,
"output-denied": <RiCloseCircleLine className="size-4 text-orange-600" />,
};
return (
@@ -80,13 +80,13 @@ export const ToolHeader = ({
{...props}
>
<div className="flex items-center gap-2">
<WrenchIcon className="size-4 text-muted-foreground" />
<RiToolsLine className="size-4 text-muted-foreground" />
<span className="font-medium text-sm">
{title ?? type.split("-").slice(1).join("-")}
</span>
{getStatusBadge(state)}
</div>
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
<RiArrowDownSLine className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
</CollapsibleTrigger>
);
+2 -2
View File
@@ -14,7 +14,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { ChevronDownIcon } from "lucide-react";
import { RiArrowDownSLine } from "@remixicon/react";
import type { ComponentProps, ReactNode } from "react";
import { createContext, useContext, useEffect, useState } from "react";
@@ -220,7 +220,7 @@ export const WebPreviewConsole = ({
variant="ghost"
>
Console
<ChevronDownIcon
<RiArrowDownSLine
className={cn(
"h-4 w-4 transition-transform duration-200",
consoleOpen && "rotate-180"