Template
Improve chat navigation, model selection, and streaming
This commit is contained in:
@@ -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,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",
|
||||
|
||||
@@ -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,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,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>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+37
-230
@@ -1,20 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import type { ChatStatus } from 'ai';
|
||||
import { ChatInput, type PromptInputMessage } from './ChatInput';
|
||||
import { ChatInput } from './ChatInput';
|
||||
import { ChatMessages } from './ChatMessages';
|
||||
import { ChatPrompt, PromptSuggestion } from './ChatPrompt';
|
||||
import { ChatSidebar, type Chat } from './ChatSidebar';
|
||||
import { useChat, UIMessage } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
import {
|
||||
createChat,
|
||||
deleteChat,
|
||||
getMessages,
|
||||
listChats,
|
||||
saveMessages,
|
||||
} from '@/services/indexed-db/chat-store';
|
||||
import { ChatPrompt } from './ChatPrompt';
|
||||
import { ChatSidebar } from './ChatSidebar';
|
||||
import { useChatSession } from '@/hooks/use-chat-session';
|
||||
import type { PromptSuggestion } from '@/config/suggestions';
|
||||
import { DEFAULT_OPENROUTER_MODEL_ID, type OpenRouterModelId } from '@/lib/openrouter';
|
||||
import {
|
||||
SidebarProvider,
|
||||
SidebarInset,
|
||||
@@ -24,228 +16,32 @@ import {
|
||||
interface ChatAgentProps {
|
||||
suggestions?: PromptSuggestion[];
|
||||
webSearchEnabled?: boolean;
|
||||
}
|
||||
|
||||
async function convertToDataUrl(url: string): Promise<string> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Could not read the selected file');
|
||||
|
||||
const blob = await response.blob();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error('Could not encode the selected file'));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
defaultModelId?: OpenRouterModelId;
|
||||
}
|
||||
|
||||
export function ChatAgent({
|
||||
suggestions,
|
||||
webSearchEnabled = false,
|
||||
defaultModelId = DEFAULT_OPENROUTER_MODEL_ID,
|
||||
}: ChatAgentProps) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [chatId, setChatId] = useState<string | null>(null);
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
const chatIdRef = useRef(chatId);
|
||||
const pendingChatIdRef = useRef<string | null>(null);
|
||||
chatIdRef.current = chatId;
|
||||
|
||||
const fetchChats = useCallback(async () => {
|
||||
try {
|
||||
setChats(await listChats());
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch chats:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const transportRef = useRef(
|
||||
new DefaultChatTransport({
|
||||
api: '/api/chat',
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
inputValue,
|
||||
setInputValue,
|
||||
modelId,
|
||||
setModelId,
|
||||
chatId,
|
||||
chats,
|
||||
chatMessages,
|
||||
status,
|
||||
messages: chatMessages,
|
||||
setMessages: setChatMessages,
|
||||
sendMessage,
|
||||
stop,
|
||||
error: aiError,
|
||||
addToolApprovalResponse,
|
||||
} = useChat({
|
||||
transport: transportRef.current,
|
||||
onFinish: async ({ messages }) => {
|
||||
const activeChatId = pendingChatIdRef.current ?? chatIdRef.current;
|
||||
if (!activeChatId) return;
|
||||
|
||||
try {
|
||||
await saveMessages(activeChatId, messages);
|
||||
await fetchChats();
|
||||
} catch (error) {
|
||||
console.error('Failed to save messages:', error);
|
||||
} finally {
|
||||
if (pendingChatIdRef.current === activeChatId) {
|
||||
pendingChatIdRef.current = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Chat error:', error);
|
||||
},
|
||||
});
|
||||
|
||||
const [isPreparingFiles, setIsPreparingFiles] = useState(false);
|
||||
|
||||
const chatStatus: ChatStatus = isPreparingFiles
|
||||
? 'submitted'
|
||||
: status === 'submitted' || status === 'streaming'
|
||||
? status
|
||||
: 'ready';
|
||||
|
||||
useEffect(() => {
|
||||
fetchChats();
|
||||
}, [fetchChats]);
|
||||
|
||||
const handleSelectChat = useCallback(
|
||||
async (selectedChatId: string) => {
|
||||
try {
|
||||
const messages = await getMessages(selectedChatId);
|
||||
chatIdRef.current = selectedChatId;
|
||||
setChatId(selectedChatId);
|
||||
|
||||
setChatMessages(messages);
|
||||
} catch (error) {
|
||||
console.error('Failed to load chat:', error);
|
||||
}
|
||||
},
|
||||
[setChatMessages]
|
||||
);
|
||||
|
||||
const handleNewChat = useCallback(() => {
|
||||
chatIdRef.current = null;
|
||||
setChatId(null);
|
||||
setChatMessages([]);
|
||||
setInputValue('');
|
||||
}, [setChatMessages]);
|
||||
|
||||
const handleDeleteChat = useCallback(
|
||||
async (deletedChatId: string) => {
|
||||
try {
|
||||
await deleteChat(deletedChatId);
|
||||
setChats((prev) => prev.filter((c) => c.id !== deletedChatId));
|
||||
if (chatId === deletedChatId) {
|
||||
chatIdRef.current = null;
|
||||
setChatId(null);
|
||||
setChatMessages([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete chat:', error);
|
||||
}
|
||||
},
|
||||
[chatId, setChatMessages]
|
||||
);
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
async (message: PromptInputMessage) => {
|
||||
const trimmedValue = message.text?.trim();
|
||||
if (!trimmedValue && !message.files?.length) return;
|
||||
if (
|
||||
status === 'submitted' ||
|
||||
status === 'streaming' ||
|
||||
isPreparingFiles
|
||||
)
|
||||
return;
|
||||
|
||||
let activeChatId = chatIdRef.current;
|
||||
|
||||
if (!chatIdRef.current) {
|
||||
const newChatId = crypto.randomUUID();
|
||||
const title = trimmedValue ? trimmedValue.slice(0, 100) : 'New Chat';
|
||||
|
||||
activeChatId = newChatId;
|
||||
chatIdRef.current = newChatId;
|
||||
setChatId(newChatId);
|
||||
|
||||
try {
|
||||
await createChat(newChatId, title);
|
||||
await fetchChats();
|
||||
} catch (error) {
|
||||
console.error('Failed to create chat:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const parts: Array<
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'file'; url: string; mediaType: string; filename: string }
|
||||
> = [];
|
||||
|
||||
if (trimmedValue) {
|
||||
parts.push({ type: 'text', text: trimmedValue });
|
||||
}
|
||||
|
||||
// PromptInput supplies files as base64 data URLs. Keep them inline so the
|
||||
// demo never needs a remote storage bucket.
|
||||
if (message.files?.length) {
|
||||
setIsPreparingFiles(true);
|
||||
for (const file of message.files) {
|
||||
try {
|
||||
const dataUrl = file.url.startsWith('data:')
|
||||
? file.url
|
||||
: await convertToDataUrl(file.url);
|
||||
|
||||
parts.push({
|
||||
type: 'file',
|
||||
url: dataUrl,
|
||||
mediaType: file.mediaType || 'application/octet-stream',
|
||||
filename: file.filename || 'file',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('File encoding error:', err);
|
||||
}
|
||||
}
|
||||
setIsPreparingFiles(false);
|
||||
}
|
||||
|
||||
if (parts.length === 0) return;
|
||||
|
||||
const userMessage: UIMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'user',
|
||||
parts,
|
||||
};
|
||||
|
||||
setInputValue('');
|
||||
|
||||
if (activeChatId) {
|
||||
try {
|
||||
await saveMessages(activeChatId, [...chatMessages, userMessage]);
|
||||
} catch (error) {
|
||||
console.error('Failed to save user message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
pendingChatIdRef.current = activeChatId;
|
||||
await sendMessage(userMessage);
|
||||
},
|
||||
[
|
||||
status,
|
||||
sendMessage,
|
||||
chatMessages,
|
||||
isPreparingFiles,
|
||||
fetchChats,
|
||||
]
|
||||
);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
stop();
|
||||
}, [stop]);
|
||||
chatStatus,
|
||||
aiError,
|
||||
isPreparingFiles,
|
||||
handleSelectChat,
|
||||
handleNewChat,
|
||||
handleDeleteChat,
|
||||
handleSendMessage,
|
||||
handleStop,
|
||||
} = useChatSession(defaultModelId);
|
||||
|
||||
const hasMessages = chatMessages.length > 0;
|
||||
|
||||
@@ -268,6 +64,9 @@ export function ChatAgent({
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleSendMessage}
|
||||
modelId={modelId}
|
||||
onModelChange={setModelId}
|
||||
onStop={isPreparingFiles ? undefined : handleStop}
|
||||
status={chatStatus}
|
||||
suggestions={suggestions}
|
||||
webSearchEnabled={webSearchEnabled}
|
||||
@@ -281,15 +80,23 @@ export function ChatAgent({
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ChatMessages
|
||||
messages={chatMessages}
|
||||
isLoading={status === 'submitted' || status === 'streaming'}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
<div className="sticky bottom-0 bg-muted p-4">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
{aiError && (
|
||||
<p role="alert" className="mb-3 text-sm text-destructive">
|
||||
The response could not be completed. Please try again.
|
||||
</p>
|
||||
)}
|
||||
<ChatInput
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleSendMessage}
|
||||
modelId={modelId}
|
||||
onModelChange={setModelId}
|
||||
onStop={isPreparingFiles ? undefined : handleStop}
|
||||
status={chatStatus}
|
||||
/>
|
||||
</div>
|
||||
@@ -301,4 +108,4 @@ export function ChatAgent({
|
||||
);
|
||||
}
|
||||
|
||||
export type { UIMessage as Message };
|
||||
export type { UIMessage as Message } from 'ai';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import type { ChatStatus } from 'ai';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
import { RiAddLine } from '@remixicon/react';
|
||||
import { ChatModelSelector } from './ChatModelSelector';
|
||||
import type { OpenRouterModelId } from '@/lib/openrouter';
|
||||
import {
|
||||
PromptInput,
|
||||
PromptInputActionAddAttachments,
|
||||
@@ -23,6 +25,9 @@ interface ChatInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: (message: PromptInputMessage) => void;
|
||||
modelId: OpenRouterModelId;
|
||||
onModelChange: (modelId: OpenRouterModelId) => void;
|
||||
onStop?: () => void;
|
||||
status?: ChatStatus;
|
||||
placeholder?: string;
|
||||
}
|
||||
@@ -31,9 +36,14 @@ export function ChatInput({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
modelId,
|
||||
onModelChange,
|
||||
onStop,
|
||||
status = 'ready',
|
||||
placeholder,
|
||||
}: ChatInputProps) {
|
||||
const isGenerating = status === 'submitted' || status === 'streaming';
|
||||
|
||||
return (
|
||||
<PromptInput
|
||||
className="[&_[data-slot=input-group]]:bg-background"
|
||||
@@ -55,18 +65,22 @@ export function ChatInput({
|
||||
/>
|
||||
</PromptInputBody>
|
||||
<PromptInputFooter className="pt-0 pb-2">
|
||||
<PromptInputTools>
|
||||
<PromptInputTools className="min-w-0">
|
||||
<PromptInputActionMenu>
|
||||
<PromptInputActionMenuTrigger>
|
||||
<PlusIcon className="size-4" />
|
||||
<RiAddLine className="size-4" />
|
||||
</PromptInputActionMenuTrigger>
|
||||
<PromptInputActionMenuContent>
|
||||
<PromptInputActionAddAttachments />
|
||||
</PromptInputActionMenuContent>
|
||||
</PromptInputActionMenu>
|
||||
<ChatModelSelector value={modelId} onChange={onModelChange} disabled={isGenerating} />
|
||||
</PromptInputTools>
|
||||
<PromptInputSubmit
|
||||
disabled={status === 'streaming'}
|
||||
aria-label={isGenerating ? 'Stop response' : 'Send message'}
|
||||
type={isGenerating ? 'button' : 'submit'}
|
||||
onClick={isGenerating ? onStop : undefined}
|
||||
disabled={isGenerating && !onStop}
|
||||
status={status}
|
||||
/>
|
||||
</PromptInputFooter>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Tool, UIMessage, UIMessagePart } from "ai";
|
||||
import { MessagePartRenderer } from "./ChatMessageParts";
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: UIMessage;
|
||||
}
|
||||
|
||||
function getTextContent(parts: UIMessagePart<any, any>[]): string {
|
||||
return parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text || "")
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function ChatMessage({ message }: ChatMessageProps) {
|
||||
const isUser = message.role === "user";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full",
|
||||
isUser ? "justify-end" : "justify-start"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-[80%] rounded-2xl px-4 py-3",
|
||||
isUser
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-foreground"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"prose prose-sm max-w-none",
|
||||
isUser
|
||||
? "prose-invert prose-p:text-primary-foreground prose-headings:text-primary-foreground prose-strong:text-primary-foreground prose-code:text-primary-foreground"
|
||||
: "prose-p:text-foreground prose-headings:text-foreground prose-strong:text-foreground"
|
||||
)}
|
||||
>
|
||||
{message.parts.map((part, index) => (
|
||||
<MessagePartRenderer key={index} part={part} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { getTextContent };
|
||||
export type { UIMessage as Message };
|
||||
@@ -1,96 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { UIMessage } from "ai";
|
||||
import {
|
||||
Tool,
|
||||
ToolContent,
|
||||
ToolHeader,
|
||||
ToolInput,
|
||||
ToolOutput,
|
||||
} from "@/components/ai-elements/tool";
|
||||
import { Loader } from "@/components/ai-elements/loader";
|
||||
import { toolComponents } from "@/chat.config";
|
||||
|
||||
type MessagePart = UIMessage["parts"][number];
|
||||
type ReasoningPart = Extract<MessagePart, { type: "reasoning" }>;
|
||||
|
||||
// Helper functions
|
||||
export function getTextContent(parts: MessagePart[]): string {
|
||||
return parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text || "")
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function getReasoningPart(parts: MessagePart[]): ReasoningPart | undefined {
|
||||
return parts.find((p): p is ReasoningPart => p.type === "reasoning");
|
||||
}
|
||||
|
||||
function extractToolName(part: MessagePart): string {
|
||||
if (part.type === "dynamic-tool") {
|
||||
return (part as any).toolName || "unknown";
|
||||
}
|
||||
if (part.type.startsWith("tool-")) {
|
||||
return part.type.slice(5);
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function DefaultToolRenderer({ part, toolName }: { part: MessagePart; toolName: string }) {
|
||||
const { input, output, state, errorText } = part as any;
|
||||
|
||||
return (
|
||||
<Tool>
|
||||
<ToolHeader
|
||||
title={toolName}
|
||||
type={part.type as any}
|
||||
state={state || "input-streaming"}
|
||||
/>
|
||||
<ToolContent>
|
||||
{input && <ToolInput input={input} />}
|
||||
<ToolOutput output={output} errorText={errorText} />
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolLoadingIndicator({ toolName }: { toolName: string }) {
|
||||
return (
|
||||
<div className="mb-4 flex items-center gap-2 text-muted-foreground text-sm">
|
||||
<Loader size={14} />
|
||||
<span>Running {toolName}...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderTool({ part }: { part: MessagePart }) {
|
||||
const toolName = extractToolName(part);
|
||||
const state = (part as any).state;
|
||||
const isComplete = state === "output-available";
|
||||
|
||||
const CustomRenderer = (
|
||||
toolComponents as Record<
|
||||
string,
|
||||
React.ComponentType<{ part: MessagePart; toolName: string }>
|
||||
>
|
||||
)[toolName];
|
||||
if (CustomRenderer) {
|
||||
return <CustomRenderer part={part} toolName={toolName} />;
|
||||
}
|
||||
|
||||
if (!isComplete && !(part as any).input) {
|
||||
return <ToolLoadingIndicator toolName={toolName} />;
|
||||
}
|
||||
return <DefaultToolRenderer part={part} toolName={toolName} />;
|
||||
}
|
||||
|
||||
export function ToolPartRenderer({ part }: { part: MessagePart }) {
|
||||
return <RenderTool part={part} />;
|
||||
}
|
||||
|
||||
export function MessagePartRenderer({ part }: { part: MessagePart }) {
|
||||
if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
|
||||
return <RenderTool part={part} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { UIMessage } from "ai";
|
||||
import { isToolUIPart, type ChatStatus, type UIMessage } from "ai";
|
||||
import {
|
||||
Conversation,
|
||||
ConversationContent,
|
||||
@@ -19,14 +19,14 @@ import {
|
||||
ReasoningTrigger,
|
||||
} from "@/components/ai-elements/reasoning";
|
||||
import { Loader } from "@/components/ai-elements/loader";
|
||||
import { ToolPartRenderer, getTextContent, getReasoningPart } from "./ChatMessageParts";
|
||||
import { ToolPartRenderer } from "@/components/tools/tool-part-renderer";
|
||||
import { getTextContent, getReasoningPart } from "@/lib/chat/messages";
|
||||
|
||||
interface ChatMessagesProps {
|
||||
messages: UIMessage[];
|
||||
isLoading?: boolean;
|
||||
status?: ChatStatus;
|
||||
}
|
||||
|
||||
|
||||
function LoadingIndicator() {
|
||||
return (
|
||||
<Message from="assistant">
|
||||
@@ -37,8 +37,7 @@ function LoadingIndicator() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
|
||||
const isToolPart = (type: string) => type === "file" || type.startsWith("tool-");
|
||||
export function ChatMessages({ messages, status = "ready" }: ChatMessagesProps) {
|
||||
const hasReasoningPart = (parts: typeof messages[0]["parts"]) =>
|
||||
parts.some((p) => p.type === "reasoning" && p.text);
|
||||
|
||||
@@ -46,26 +45,31 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
|
||||
(message) =>
|
||||
message.role === "user" ||
|
||||
getTextContent(message.parts).trim() !== "" ||
|
||||
message.parts.some((p) => isToolPart(p.type)) ||
|
||||
message.parts.some((p) => p.type === "file" || isToolUIPart(p)) ||
|
||||
hasReasoningPart(message.parts)
|
||||
);
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const hasContent =
|
||||
getTextContent(lastMessage?.parts || []).trim() !== "" ||
|
||||
lastMessage?.parts.some((p) => isToolPart(p.type)) ||
|
||||
lastMessage?.parts.some((p) => p.type === "file" || isToolUIPart(p)) ||
|
||||
hasReasoningPart(lastMessage?.parts || []);
|
||||
const isStreaming = lastMessage?.role === "assistant" && hasContent;
|
||||
const showLoading = isLoading && !isStreaming;
|
||||
const isLoading = status === "submitted" || status === "streaming";
|
||||
const showLoading =
|
||||
isLoading && !(lastMessage?.role === "assistant" && hasContent);
|
||||
|
||||
return (
|
||||
<Conversation>
|
||||
<ConversationContent className="mx-auto max-w-3xl px-4 pb-4">
|
||||
{visibleMessages.map((message) => {
|
||||
const isStreaming =
|
||||
status === "streaming" &&
|
||||
message.role === "assistant" &&
|
||||
message.id === lastMessage?.id;
|
||||
const textContent = getTextContent(message.parts);
|
||||
const reasoningPart = getReasoningPart(message.parts);
|
||||
const fileParts = message.parts.filter((p) => p.type === "file");
|
||||
const toolParts = message.parts.filter((p) => p.type.startsWith("tool-"));
|
||||
const toolParts = message.parts.filter((p) => isToolUIPart(p));
|
||||
return (
|
||||
<Message key={message.id} from={message.role}>
|
||||
{fileParts.length > 0 && (
|
||||
@@ -80,7 +84,9 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
|
||||
)}
|
||||
|
||||
{message.role === "assistant" && reasoningPart && (
|
||||
<Reasoning isStreaming={reasoningPart.state !== "done"}>
|
||||
<Reasoning
|
||||
isStreaming={isStreaming && reasoningPart.state === "streaming"}
|
||||
>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{reasoningPart.text || ""}</ReasoningContent>
|
||||
</Reasoning>
|
||||
@@ -92,7 +98,16 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
|
||||
|
||||
{textContent && (
|
||||
<MessageContent from={message.role}>
|
||||
<MessageResponse>{textContent}</MessageResponse>
|
||||
<MessageResponse
|
||||
mode={message.role === "assistant" ? "streaming" : "static"}
|
||||
isAnimating={
|
||||
isStreaming && message.parts.some(
|
||||
(part) => part.type === "text" && part.state === "streaming"
|
||||
)
|
||||
}
|
||||
>
|
||||
{textContent}
|
||||
</MessageResponse>
|
||||
</MessageContent>
|
||||
)}
|
||||
</Message>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react';
|
||||
import {
|
||||
ModelSelector,
|
||||
ModelSelectorContent,
|
||||
ModelSelectorEmpty,
|
||||
ModelSelectorGroup,
|
||||
ModelSelectorInput,
|
||||
ModelSelectorItem,
|
||||
ModelSelectorList,
|
||||
ModelSelectorName,
|
||||
ModelSelectorTrigger,
|
||||
} from '@/components/ai-elements/model-selector';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { openRouterModels, type OpenRouterModelId } from '@/lib/openrouter';
|
||||
|
||||
const providers: Record<string, string> = {
|
||||
anthropic: 'Anthropic',
|
||||
openai: 'OpenAI',
|
||||
google: 'Google',
|
||||
'x-ai': 'SpaceXAI',
|
||||
'z-ai': 'Z.ai',
|
||||
moonshotai: 'MoonshotAI',
|
||||
minimax: 'MiniMax',
|
||||
deepseek: 'DeepSeek',
|
||||
meta: 'Meta',
|
||||
qwen: 'Qwen',
|
||||
};
|
||||
|
||||
const modelGroups = Object.entries(providers).map(([id, name]) => ({
|
||||
name,
|
||||
models: openRouterModels.filter((model) => model.id.startsWith(`${id}/`)),
|
||||
}));
|
||||
|
||||
interface ChatModelSelectorProps {
|
||||
value: OpenRouterModelId;
|
||||
onChange: (modelId: OpenRouterModelId) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ChatModelSelector({ value, onChange, disabled }: ChatModelSelectorProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selectedModel = openRouterModels.find((model) => model.id === value);
|
||||
|
||||
return (
|
||||
<ModelSelector open={open} onOpenChange={setOpen}>
|
||||
<ModelSelectorTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="min-w-0 max-w-[min(16rem,calc(100vw-10rem))] text-muted-foreground"
|
||||
aria-label={`Select model, current model: ${selectedModel?.name}`}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ModelSelectorName>{selectedModel?.name}</ModelSelectorName>
|
||||
<RiArrowDownSLine className="size-4 shrink-0" aria-hidden="true" />
|
||||
</Button>
|
||||
</ModelSelectorTrigger>
|
||||
<ModelSelectorContent title="Choose a model" className="max-w-[calc(100%-2rem)] sm:max-w-lg">
|
||||
<ModelSelectorInput placeholder="Search models..." aria-label="Search models" className="pr-10" />
|
||||
<ModelSelectorList className="max-h-[min(24rem,60dvh)]">
|
||||
<ModelSelectorEmpty>No models found.</ModelSelectorEmpty>
|
||||
{modelGroups.map((group) => (
|
||||
<ModelSelectorGroup heading={group.name} key={group.name}>
|
||||
{group.models.map((model) => (
|
||||
<ModelSelectorItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
keywords={[model.name, group.name]}
|
||||
onSelect={() => {
|
||||
onChange(model.id);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<ModelSelectorName>{model.name}</ModelSelectorName>
|
||||
{model.id === value && (
|
||||
<>
|
||||
<RiCheckLine className="ml-auto size-4" aria-hidden="true" />
|
||||
<span className="sr-only">Selected</span>
|
||||
</>
|
||||
)}
|
||||
</ModelSelectorItem>
|
||||
))}
|
||||
</ModelSelectorGroup>
|
||||
))}
|
||||
</ModelSelectorList>
|
||||
</ModelSelectorContent>
|
||||
</ModelSelector>
|
||||
);
|
||||
}
|
||||
@@ -5,72 +5,40 @@ import { ChatInput, type PromptInputMessage } from './ChatInput';
|
||||
import { Suggestion } from '@/components/ai-elements/suggestion';
|
||||
import { RiOpenaiLine } from '@remixicon/react';
|
||||
import { Shimmer } from '@/components/ai-elements/shimmer';
|
||||
import type { OpenRouterModelId } from '@/lib/openrouter';
|
||||
import {
|
||||
offlineSuggestions,
|
||||
webSearchSuggestions,
|
||||
type PromptSuggestion,
|
||||
} from '@/config/suggestions';
|
||||
|
||||
export interface PromptSuggestion {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
export type { PromptSuggestion } from '@/config/suggestions';
|
||||
|
||||
interface ChatPromptProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: (message: PromptInputMessage) => void;
|
||||
modelId: OpenRouterModelId;
|
||||
onModelChange: (modelId: OpenRouterModelId) => void;
|
||||
onStop?: () => void;
|
||||
status?: ChatStatus;
|
||||
suggestions?: PromptSuggestion[];
|
||||
webSearchEnabled?: boolean;
|
||||
}
|
||||
|
||||
const defaultSuggestions: PromptSuggestion[] = [
|
||||
{
|
||||
key: 'Latest AI news',
|
||||
value:
|
||||
'What are the latest developments in AI in 2026? Search for recent news.',
|
||||
},
|
||||
{
|
||||
key: 'Research a company',
|
||||
value: 'Research OpenAI: their latest products, funding, and news.',
|
||||
},
|
||||
{
|
||||
key: 'Fact check',
|
||||
value:
|
||||
'What is the current population of the world and how is it changing?',
|
||||
},
|
||||
{
|
||||
key: 'Compare topics',
|
||||
value:
|
||||
'Compare the latest iPhone vs Samsung Galaxy flagship models with sources.',
|
||||
},
|
||||
];
|
||||
|
||||
const offlineSuggestions: PromptSuggestion[] = [
|
||||
{
|
||||
key: 'Explain a concept',
|
||||
value: 'Explain retrieval-augmented generation in plain language.',
|
||||
},
|
||||
{
|
||||
key: 'Compare approaches',
|
||||
value: 'Compare REST and GraphQL, including when to choose each.',
|
||||
},
|
||||
{
|
||||
key: 'Draft an outline',
|
||||
value: 'Draft an outline for a practical guide to evaluating AI agents.',
|
||||
},
|
||||
{
|
||||
key: 'Brainstorm ideas',
|
||||
value: 'Brainstorm useful AI agent ideas for a small business.',
|
||||
},
|
||||
];
|
||||
|
||||
export function ChatPrompt({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
modelId,
|
||||
onModelChange,
|
||||
onStop,
|
||||
status = 'ready',
|
||||
suggestions,
|
||||
webSearchEnabled = false,
|
||||
}: ChatPromptProps) {
|
||||
const displayedSuggestions =
|
||||
suggestions ?? (webSearchEnabled ? defaultSuggestions : offlineSuggestions);
|
||||
suggestions ?? (webSearchEnabled ? webSearchSuggestions : offlineSuggestions);
|
||||
|
||||
const handleSuggestionClick = (suggestion: string) => {
|
||||
onChange(suggestion);
|
||||
@@ -99,6 +67,9 @@ export function ChatPrompt({
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
modelId={modelId}
|
||||
onModelChange={onModelChange}
|
||||
onStop={onStop}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { RiCheckLine, RiSearchLine } from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import type { StoredChat } from '@/services/indexed-db/chat-store';
|
||||
|
||||
interface ChatSearchProps {
|
||||
chats: StoredChat[];
|
||||
activeChatId: string | null;
|
||||
onSelectChat: (chatId: string) => void;
|
||||
}
|
||||
|
||||
export function ChatSearch({ chats, activeChatId, onSelectChat }: ChatSearchProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="ghost" size="icon" className="size-7" aria-label="Search chats">
|
||||
<RiSearchLine className="size-5" aria-hidden="true" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[calc(100%-2rem)] overflow-hidden p-0 sm:max-w-lg [&>button]:top-2.5">
|
||||
<DialogTitle className="sr-only">Search chats</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Search your saved chats by title and select one to continue the conversation.
|
||||
</DialogDescription>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search chats..." aria-label="Search chats" className="pr-10" />
|
||||
<CommandList className="max-h-[min(24rem,60dvh)]">
|
||||
<CommandEmpty>
|
||||
{chats.length === 0 ? 'No chats yet. Start a new chat to get started.' : 'No chats found.'}
|
||||
</CommandEmpty>
|
||||
<CommandGroup heading="Recent chats">
|
||||
{chats.map((chat) => (
|
||||
<CommandItem
|
||||
key={chat.id}
|
||||
value={chat.id}
|
||||
keywords={[chat.title]}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
onSelectChat(chat.id);
|
||||
}}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{chat.title}</span>
|
||||
{chat.id === activeChatId && (
|
||||
<>
|
||||
<RiCheckLine className="size-4" aria-hidden="true" />
|
||||
<span className="sr-only">Current chat</span>
|
||||
</>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { PlusIcon, MoreHorizontalIcon, TrashIcon } from 'lucide-react';
|
||||
import { RiAddLine, RiMoreLine, RiDeleteBinLine } from '@remixicon/react';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SidebarMenuItem,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuAction,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -20,12 +21,10 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChatSearch } from './ChatSearch';
|
||||
import type { StoredChat } from '@/services/indexed-db/chat-store';
|
||||
|
||||
export interface Chat {
|
||||
id: string;
|
||||
title: string;
|
||||
created_at: string;
|
||||
}
|
||||
export type Chat = StoredChat;
|
||||
|
||||
interface ChatSidebarProps {
|
||||
chats: Chat[];
|
||||
@@ -42,15 +41,29 @@ export function ChatSidebar({
|
||||
onNewChat,
|
||||
onDeleteChat,
|
||||
}: ChatSidebarProps) {
|
||||
const { setOpenMobile } = useSidebar();
|
||||
|
||||
const selectChat = (chatId: string) => {
|
||||
onSelectChat(chatId);
|
||||
setOpenMobile(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
<SidebarHeader>
|
||||
<div className="flex h-7 items-center justify-between gap-2 pl-2">
|
||||
<span className="text-xl font-semibold tracking-tight">ChatGPT</span>
|
||||
<ChatSearch chats={chats} activeChatId={activeChatId} onSelectChat={selectChat} />
|
||||
</div>
|
||||
<Button
|
||||
onClick={onNewChat}
|
||||
onClick={() => {
|
||||
onNewChat();
|
||||
setOpenMobile(false);
|
||||
}}
|
||||
variant="outline"
|
||||
className="w-full justify-start gap-2"
|
||||
className="mt-2 w-full justify-start gap-2"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
<RiAddLine className="size-4" />
|
||||
New Chat
|
||||
</Button>
|
||||
</SidebarHeader>
|
||||
@@ -63,23 +76,22 @@ export function ChatSidebar({
|
||||
<SidebarMenuItem key={chat.id}>
|
||||
<SidebarMenuButton
|
||||
isActive={chat.id === activeChatId}
|
||||
onClick={() => onSelectChat(chat.id)}
|
||||
onClick={() => selectChat(chat.id)}
|
||||
tooltip={chat.title}
|
||||
>
|
||||
<span>{chat.title}</span>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction showOnHover>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<SidebarMenuAction showOnHover aria-label={`Options for ${chat.title}`}>
|
||||
<RiMoreLine className="size-4" />
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDeleteChat(chat.id)}
|
||||
className=""
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
<RiDeleteBinLine className="size-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
export { ChatAgent } from "./ChatAgent";
|
||||
export { ChatInput } from "./ChatInput";
|
||||
export { ChatMessage } from "./ChatMessage";
|
||||
export { ChatMessages } from "./ChatMessages";
|
||||
export { ChatPrompt } from "./ChatPrompt";
|
||||
export { ChatSidebar } from "./ChatSidebar";
|
||||
export type { Chat } from "./ChatSidebar";
|
||||
export type { Message } from "./ChatMessage";
|
||||
export type { UIMessage as Message } from "ai";
|
||||
export type { PromptSuggestion } from "./ChatPrompt";
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentType } from 'react';
|
||||
import { FirecrawlWebFetchTool } from './firecrawl-web-fetch-tool';
|
||||
import { FirecrawlWebSearchTool } from './firecrawl-web-search-tool';
|
||||
import type { ToolComponentProps } from './types';
|
||||
|
||||
// Add a renderer here for each tool that needs a custom presentation.
|
||||
export const toolComponents: Record<string, ComponentType<ToolComponentProps> | undefined> = {
|
||||
firecrawlWebSearch: FirecrawlWebSearchTool,
|
||||
firecrawlWebFetch: FirecrawlWebFetchTool,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { getToolName } from 'ai';
|
||||
import {
|
||||
Tool,
|
||||
ToolContent,
|
||||
ToolHeader,
|
||||
ToolInput,
|
||||
ToolOutput,
|
||||
} from '@/components/ai-elements/tool';
|
||||
import { Loader } from '@/components/ai-elements/loader';
|
||||
import { toolComponents } from './registry';
|
||||
import type { ToolComponentProps } from './types';
|
||||
|
||||
export function ToolPartRenderer({ part }: Pick<ToolComponentProps, 'part'>) {
|
||||
const toolName = getToolName(part);
|
||||
const CustomRenderer = toolComponents[toolName];
|
||||
|
||||
if (CustomRenderer) {
|
||||
return <CustomRenderer part={part} toolName={toolName} />;
|
||||
}
|
||||
|
||||
if (part.state === 'input-streaming' && !part.input) {
|
||||
return (
|
||||
<div className="mb-4 flex items-center gap-2 text-muted-foreground text-sm">
|
||||
<Loader size={14} />
|
||||
<span>Running {toolName}...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tool>
|
||||
<ToolHeader
|
||||
title={toolName}
|
||||
type={part.type === 'dynamic-tool' ? `tool-${toolName}` : part.type}
|
||||
state={part.state}
|
||||
/>
|
||||
<ToolContent>
|
||||
{part.input != null && <ToolInput input={part.input} />}
|
||||
<ToolOutput output={part.output} errorText={part.errorText} />
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { UIMessage } from "ai";
|
||||
|
||||
type MessagePart = UIMessage["parts"][number];
|
||||
import type { DynamicToolUIPart, ToolUIPart } from "ai";
|
||||
|
||||
export interface ToolComponentProps {
|
||||
part: MessagePart;
|
||||
part: ToolUIPart | DynamicToolUIPart;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { RiArrowDownSLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -41,7 +41,7 @@ function AccordionTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
<RiArrowDownSLine className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import { RiArrowRightSLine, RiMoreLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -75,7 +75,7 @@ function BreadcrumbSeparator({
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
{children ?? <RiArrowRightSLine />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -92,7 +92,7 @@ function BreadcrumbEllipsis({
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<RiMoreLine className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
RiArrowDownSLine,
|
||||
RiArrowLeftSLine,
|
||||
RiArrowRightSLine,
|
||||
} from "@remixicon/react"
|
||||
import {
|
||||
DayPicker,
|
||||
getDefaultClassNames,
|
||||
@@ -145,13 +145,13 @@ function Calendar({
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
<RiArrowLeftSLine className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
<RiArrowRightSLine
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -159,7 +159,7 @@ function Calendar({
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
<RiArrowDownSLine className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
import { RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -77,10 +77,10 @@ function Carousel({
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
if (event.key === "RiArrowLeftLine") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
} else if (event.key === "RiArrowRightLine") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
@@ -195,7 +195,7 @@ function CarouselPrevious({
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<RiArrowLeftLine />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
@@ -225,7 +225,7 @@ function CarouselNext({
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight />
|
||||
<RiArrowRightLine />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import { RiCheckLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -23,7 +23,7 @@ function Checkbox({
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
<RiCheckLine className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
|
||||
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"
|
||||
import { RiCheckLine, RiArrowDownSLine, RiCloseLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -31,7 +31,7 @@ function ComboboxTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon
|
||||
<RiArrowDownSLine
|
||||
data-slot="combobox-trigger-icon"
|
||||
className="text-muted-foreground pointer-events-none size-4"
|
||||
/>
|
||||
@@ -47,7 +47,7 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
>
|
||||
<XIcon className="pointer-events-none" />
|
||||
<RiCloseLine className="pointer-events-none" />
|
||||
</ComboboxPrimitive.Clear>
|
||||
)
|
||||
}
|
||||
@@ -160,7 +160,7 @@ function ComboboxItem({
|
||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
||||
}
|
||||
>
|
||||
<CheckIcon className="pointer-events-none size-4 pointer-coarse:size-5" />
|
||||
<RiCheckLine className="pointer-events-none size-4 pointer-coarse:size-5" />
|
||||
</ComboboxPrimitive.ItemIndicator>
|
||||
</ComboboxPrimitive.Item>
|
||||
)
|
||||
@@ -265,7 +265,7 @@ function ComboboxChip({
|
||||
className="-ml-1 opacity-50 hover:opacity-100"
|
||||
data-slot="combobox-chip-remove"
|
||||
>
|
||||
<XIcon className="pointer-events-none" />
|
||||
<RiCloseLine className="pointer-events-none" />
|
||||
</ComboboxPrimitive.ChipRemove>
|
||||
)}
|
||||
</ComboboxPrimitive.Chip>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
import { RiSearchLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
@@ -69,7 +69,7 @@ function CommandInput({
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<RiSearchLine className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import { RiCheckLine, RiArrowRightSLine, RiCircleLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -72,7 +72,7 @@ function ContextMenuSubTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
<RiArrowRightSLine className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
@@ -152,7 +152,7 @@ function ContextMenuCheckboxItem({
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
<RiCheckLine className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
@@ -176,7 +176,7 @@ function ContextMenuRadioItem({
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
<RiCircleLine className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
|
||||
@@ -45,7 +45,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative z-50 grid w-full max-w-lg gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg',
|
||||
'relative z-50 grid w-full max-w-lg gap-4 border border-border bg-background p-6 shadow-lg duration-200 rounded-lg',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import { RiCheckLine, RiArrowRightSLine, RiCircleLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -100,7 +100,7 @@ function DropdownMenuCheckboxItem({
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
<RiCheckLine className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
@@ -135,7 +135,7 @@ function DropdownMenuRadioItem({
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
<RiCircleLine className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
@@ -217,7 +217,7 @@ function DropdownMenuSubTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
<RiArrowRightSLine className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { MinusIcon } from "lucide-react"
|
||||
import { RiSubtractLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -69,7 +69,7 @@ function InputOTPSlot({
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
<RiSubtractLine />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import { RiCheckLine, RiArrowRightSLine, RiCircleLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -129,7 +129,7 @@ function MenubarCheckboxItem({
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
<RiCheckLine className="size-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
@@ -153,7 +153,7 @@ function MenubarRadioItem({
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
<RiCircleLine className="size-2 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
@@ -235,7 +235,7 @@ function MenubarSubTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
<RiArrowRightSLine className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { RiArrowDownSLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -24,7 +24,7 @@ function NativeSelect({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<ChevronDownIcon
|
||||
<RiArrowDownSLine
|
||||
className="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||
aria-hidden="true"
|
||||
data-slot="native-select-icon"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { RiArrowDownSLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -74,7 +74,7 @@ function NavigationMenuTrigger({
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
<RiArrowDownSLine
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
RiArrowLeftSLine,
|
||||
RiArrowRightSLine,
|
||||
RiMoreLine,
|
||||
} from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants, type Button } from "@/components/ui/button"
|
||||
@@ -76,7 +76,7 @@ function PaginationPrevious({
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<RiArrowLeftSLine />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
@@ -94,7 +94,7 @@ function PaginationNext({
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
<RiArrowRightSLine />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
@@ -110,7 +110,7 @@ function PaginationEllipsis({
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<RiMoreLine className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
import { RiCircleFill } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -36,7 +36,7 @@ function RadioGroupItem({
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
<RiCircleFill className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { GripVerticalIcon } from "lucide-react"
|
||||
import { RiDraggable } from "@remixicon/react"
|
||||
import * as ResizablePrimitive from "react-resizable-panels"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -46,7 +46,7 @@ function ResizableHandle({
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||
<GripVerticalIcon className="size-2.5" />
|
||||
<RiDraggable className="size-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.Separator>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
import { RiCheckLine, RiArrowDownSLine, RiArrowUpSLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -44,7 +44,7 @@ function SelectTrigger({
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
<RiArrowDownSLine className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
@@ -119,7 +119,7 @@ function SelectItem({
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
<RiCheckLine className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
@@ -153,7 +153,7 @@ function SelectScrollUpButton({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
<RiArrowUpSLine className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
@@ -171,7 +171,7 @@ function SelectScrollDownButton({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
<RiArrowDownSLine className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { RiCloseLine } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -76,7 +76,7 @@ function SheetContent({
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<RiCloseLine className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
import { RiLayoutLeftLine } from "@remixicon/react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -273,7 +273,7 @@ function SidebarTrigger({
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<RiLayoutLeftLine className="size-5" aria-hidden="true" />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
|
||||
+11
-11
@@ -1,12 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
} from "lucide-react"
|
||||
RiCheckboxCircleLine,
|
||||
RiInformationLine,
|
||||
RiLoader4Line,
|
||||
RiCloseCircleLine,
|
||||
RiErrorWarningLine,
|
||||
} from "@remixicon/react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
|
||||
@@ -18,11 +18,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
success: <RiCheckboxCircleLine className="size-4" />,
|
||||
info: <RiInformationLine className="size-4" />,
|
||||
warning: <RiErrorWarningLine className="size-4" />,
|
||||
error: <RiCloseCircleLine className="size-4" />,
|
||||
loading: <RiLoader4Line className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Loader2Icon } from "lucide-react"
|
||||
import { RiLoader4Line } from "@remixicon/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
function Spinner({ className, ...props }: React.ComponentProps<typeof RiLoader4Line>) {
|
||||
return (
|
||||
<Loader2Icon
|
||||
<RiLoader4Line
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cn("size-4 animate-spin", className)}
|
||||
|
||||
Reference in New Issue
Block a user