Upgrades (#1419)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3bc77653ad
commit
453f5bb3e6
94 changed files with 4029 additions and 8199 deletions
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
|
@ -21,5 +21,5 @@ jobs:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
- name: Run lint
|
- name: Run check
|
||||||
run: pnpm lint
|
run: pnpm check
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -42,3 +42,5 @@ yarn-error.log*
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/*
|
/playwright/*
|
||||||
|
|
||||||
|
next-env.d.ts
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { checkBotId } from "botid/server";
|
|
||||||
import { geolocation, ipAddress } from "@vercel/functions";
|
import { geolocation, ipAddress } from "@vercel/functions";
|
||||||
import {
|
import {
|
||||||
convertToModelMessages,
|
convertToModelMessages,
|
||||||
|
|
@ -8,12 +7,13 @@ import {
|
||||||
stepCountIs,
|
stepCountIs,
|
||||||
streamText,
|
streamText,
|
||||||
} from "ai";
|
} from "ai";
|
||||||
|
import { checkBotId } from "botid/server";
|
||||||
import { after } from "next/server";
|
import { after } from "next/server";
|
||||||
import { createResumableStreamContext } from "resumable-stream";
|
import { createResumableStreamContext } from "resumable-stream";
|
||||||
import { auth, type UserType } from "@/app/(auth)/auth";
|
import { auth, type UserType } from "@/app/(auth)/auth";
|
||||||
import { entitlementsByUserType } from "@/lib/ai/entitlements";
|
import { entitlementsByUserType } from "@/lib/ai/entitlements";
|
||||||
import { type RequestHints, systemPrompt } from "@/lib/ai/prompts";
|
|
||||||
import { allowedModelIds } from "@/lib/ai/models";
|
import { allowedModelIds } from "@/lib/ai/models";
|
||||||
|
import { type RequestHints, systemPrompt } from "@/lib/ai/prompts";
|
||||||
import { getLanguageModel } from "@/lib/ai/providers";
|
import { getLanguageModel } from "@/lib/ai/providers";
|
||||||
import { createDocument } from "@/lib/ai/tools/create-document";
|
import { createDocument } from "@/lib/ai/tools/create-document";
|
||||||
import { getWeather } from "@/lib/ai/tools/get-weather";
|
import { getWeather } from "@/lib/ai/tools/get-weather";
|
||||||
|
|
@ -185,7 +185,7 @@ export async function POST(request: Request) {
|
||||||
});
|
});
|
||||||
|
|
||||||
dataStream.merge(
|
dataStream.merge(
|
||||||
result.toUIMessageStream({ sendReasoning: isReasoningModel }),
|
result.toUIMessageStream({ sendReasoning: isReasoningModel })
|
||||||
);
|
);
|
||||||
|
|
||||||
if (titlePromise) {
|
if (titlePromise) {
|
||||||
|
|
@ -236,7 +236,7 @@ export async function POST(request: Request) {
|
||||||
if (
|
if (
|
||||||
error instanceof Error &&
|
error instanceof Error &&
|
||||||
error.message?.includes(
|
error.message?.includes(
|
||||||
"AI Gateway requires a valid credit card on file to service requests",
|
"AI Gateway requires a valid credit card on file to service requests"
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return "AI Gateway requires a valid credit card on file to service requests. Please visit https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card to add a card and unlock your free credits.";
|
return "AI Gateway requires a valid credit card on file to service requests. Please visit https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card to add a card and unlock your free credits.";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
import { Analytics } from "@vercel/analytics/next";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
|
@ -81,6 +83,7 @@ export default function RootLayout({
|
||||||
<Toaster position="top-center" />
|
<Toaster position="top-center" />
|
||||||
<SessionProvider>{children}</SessionProvider>
|
<SessionProvider>{children}</SessionProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
10
biome.jsonc
10
biome.jsonc
|
|
@ -11,13 +11,9 @@
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"includes": [
|
"includes": [
|
||||||
"**/*.ts",
|
"**/*",
|
||||||
"**/*.tsx",
|
"!components/ai-elements",
|
||||||
"**/*.js",
|
"!components/elements",
|
||||||
"**/*.jsx",
|
|
||||||
"!node_modules",
|
|
||||||
"!.next",
|
|
||||||
"!ai-sdk",
|
|
||||||
"!components/ui",
|
"!components/ui",
|
||||||
"!lib/utils.ts",
|
"!lib/utils.ts",
|
||||||
"!hooks/use-mobile.ts"
|
"!hooks/use-mobile.ts"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "default",
|
"style": "new-york",
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
|
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { type LucideIcon, XIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps, HTMLAttributes } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const ArtifactHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ArtifactHeaderProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-between border-b bg-muted/50 px-4 py-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ArtifactCloseProps = ComponentProps<typeof Button>;
|
|
||||||
|
|
||||||
export const ArtifactClose = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
size = "sm",
|
|
||||||
variant = "ghost",
|
|
||||||
...props
|
|
||||||
}: ArtifactCloseProps) => (
|
|
||||||
<Button
|
|
||||||
className={cn(
|
|
||||||
"size-8 p-0 text-muted-foreground hover:text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
size={size}
|
|
||||||
type="button"
|
|
||||||
variant={variant}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? <XIcon className="size-4" />}
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
|
|
||||||
|
|
||||||
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
|
|
||||||
<p
|
|
||||||
className={cn("font-medium text-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
|
|
||||||
|
|
||||||
export const ArtifactDescription = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ArtifactDescriptionProps) => (
|
|
||||||
<p className={cn("text-muted-foreground text-sm", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const ArtifactActions = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ArtifactActionsProps) => (
|
|
||||||
<div className={cn("flex items-center gap-1", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ArtifactActionProps = ComponentProps<typeof Button> & {
|
|
||||||
tooltip?: string;
|
|
||||||
label?: string;
|
|
||||||
icon?: LucideIcon;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ArtifactAction = ({
|
|
||||||
tooltip,
|
|
||||||
label,
|
|
||||||
icon: Icon,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
size = "sm",
|
|
||||||
variant = "ghost",
|
|
||||||
...props
|
|
||||||
}: ArtifactActionProps) => {
|
|
||||||
const button = (
|
|
||||||
<Button
|
|
||||||
className={cn(
|
|
||||||
"size-8 p-0 text-muted-foreground hover:text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
size={size}
|
|
||||||
type="button"
|
|
||||||
variant={variant}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{Icon ? <Icon className="size-4" /> : children}
|
|
||||||
<span className="sr-only">{label || tooltip}</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tooltip) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{tooltip}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return button;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const ArtifactContent = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ArtifactContentProps) => (
|
|
||||||
<div className={cn("flex-1 overflow-auto p-4", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import "@xyflow/react/dist/style.css";
|
|
||||||
|
|
||||||
type CanvasProps = ReactFlowProps & {
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Canvas = ({ children, ...props }: CanvasProps) => (
|
|
||||||
<ReactFlow
|
|
||||||
deleteKeyCode={["Backspace", "Delete"]}
|
|
||||||
fitView
|
|
||||||
panOnDrag={false}
|
|
||||||
panOnScroll
|
|
||||||
selectionOnDrag={true}
|
|
||||||
zoomOnDoubleClick={false}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Background bgColor="var(--sidebar)" />
|
|
||||||
{children}
|
|
||||||
</ReactFlow>
|
|
||||||
);
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
||||||
import {
|
|
||||||
BrainIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
DotIcon,
|
|
||||||
type LucideIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
|
||||||
import { createContext, memo, useContext, useMemo } from "react";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type ChainOfThoughtContextValue = {
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: (open: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const useChainOfThought = () => {
|
|
||||||
const context = useContext(ChainOfThoughtContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"ChainOfThought components must be used within ChainOfThought"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ChainOfThoughtProps = ComponentProps<"div"> & {
|
|
||||||
open?: boolean;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
onOpenChange?: (open: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChainOfThought = memo(
|
|
||||||
({
|
|
||||||
className,
|
|
||||||
open,
|
|
||||||
defaultOpen = false,
|
|
||||||
onOpenChange,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ChainOfThoughtProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useControllableState({
|
|
||||||
prop: open,
|
|
||||||
defaultProp: defaultOpen,
|
|
||||||
onChange: onOpenChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
const chainOfThoughtContext = useMemo(
|
|
||||||
() => ({ isOpen, setIsOpen }),
|
|
||||||
[isOpen, setIsOpen]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChainOfThoughtContext.Provider value={chainOfThoughtContext}>
|
|
||||||
<div
|
|
||||||
className={cn("not-prose max-w-prose space-y-4", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</ChainOfThoughtContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ChainOfThoughtHeaderProps = ComponentProps<
|
|
||||||
typeof CollapsibleTrigger
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const ChainOfThoughtHeader = memo(
|
|
||||||
({ className, children, ...props }: ChainOfThoughtHeaderProps) => {
|
|
||||||
const { isOpen, setIsOpen } = useChainOfThought();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
|
|
||||||
<CollapsibleTrigger
|
|
||||||
className={cn(
|
|
||||||
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<BrainIcon className="size-4" />
|
|
||||||
<span className="flex-1 text-left">
|
|
||||||
{children ?? "Chain of Thought"}
|
|
||||||
</span>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={cn(
|
|
||||||
"size-4 transition-transform",
|
|
||||||
isOpen ? "rotate-180" : "rotate-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
|
|
||||||
icon?: LucideIcon;
|
|
||||||
label: ReactNode;
|
|
||||||
description?: ReactNode;
|
|
||||||
status?: "complete" | "active" | "pending";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChainOfThoughtStep = memo(
|
|
||||||
({
|
|
||||||
className,
|
|
||||||
icon: Icon = DotIcon,
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
status = "complete",
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ChainOfThoughtStepProps) => {
|
|
||||||
const statusStyles = {
|
|
||||||
complete: "text-muted-foreground",
|
|
||||||
active: "text-foreground",
|
|
||||||
pending: "text-muted-foreground/50",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex gap-2 text-sm",
|
|
||||||
statusStyles[status],
|
|
||||||
"fade-in-0 slide-in-from-top-2 animate-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="relative mt-0.5">
|
|
||||||
<Icon className="size-4" />
|
|
||||||
<div className="absolute top-7 bottom-0 left-1/2 -mx-px w-px bg-border" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 space-y-2 overflow-hidden">
|
|
||||||
<div>{label}</div>
|
|
||||||
{description && (
|
|
||||||
<div className="text-muted-foreground text-xs">{description}</div>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const ChainOfThoughtSearchResults = memo(
|
|
||||||
({ className, ...props }: ChainOfThoughtSearchResultsProps) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex flex-wrap items-center gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;
|
|
||||||
|
|
||||||
export const ChainOfThoughtSearchResult = memo(
|
|
||||||
({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (
|
|
||||||
<Badge
|
|
||||||
className={cn("gap-1 px-2 py-0.5 font-normal text-xs", className)}
|
|
||||||
variant="secondary"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Badge>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ChainOfThoughtContentProps = ComponentProps<
|
|
||||||
typeof CollapsibleContent
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const ChainOfThoughtContent = memo(
|
|
||||||
({ className, children, ...props }: ChainOfThoughtContentProps) => {
|
|
||||||
const { isOpen } = useChainOfThought();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapsible open={isOpen}>
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"mt-2 space-y-3",
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ChainOfThoughtImageProps = ComponentProps<"div"> & {
|
|
||||||
caption?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChainOfThoughtImage = memo(
|
|
||||||
({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (
|
|
||||||
<div className={cn("mt-2 space-y-2", className)} {...props}>
|
|
||||||
<div className="relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg bg-muted p-3">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
{caption && <p className="text-muted-foreground text-xs">{caption}</p>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
ChainOfThought.displayName = "ChainOfThought";
|
|
||||||
ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";
|
|
||||||
ChainOfThoughtStep.displayName = "ChainOfThoughtStep";
|
|
||||||
ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";
|
|
||||||
ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";
|
|
||||||
ChainOfThoughtContent.displayName = "ChainOfThoughtContent";
|
|
||||||
ChainOfThoughtImage.displayName = "ChainOfThoughtImage";
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { BookmarkIcon, type LucideProps } from "lucide-react";
|
|
||||||
import type { ComponentProps, HTMLAttributes } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const Checkpoint = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: CheckpointProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-0.5 overflow-hidden text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<Separator />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type CheckpointIconProps = LucideProps;
|
|
||||||
|
|
||||||
export const CheckpointIcon = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: CheckpointIconProps) =>
|
|
||||||
children ?? (
|
|
||||||
<BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
|
|
||||||
tooltip?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CheckpointTrigger = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
variant = "ghost",
|
|
||||||
size = "sm",
|
|
||||||
tooltip,
|
|
||||||
...props
|
|
||||||
}: CheckpointTriggerProps) =>
|
|
||||||
tooltip ? (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button size={size} type="button" variant={variant} {...props}>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent align="start" side="bottom">
|
|
||||||
{tooltip}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Button size={size} type="button" variant={variant} {...props}>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
555
components/ai-elements/code-block.tsx
Normal file
555
components/ai-elements/code-block.tsx
Normal file
|
|
@ -0,0 +1,555 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
|
||||||
|
import type {
|
||||||
|
BundledLanguage,
|
||||||
|
BundledTheme,
|
||||||
|
HighlighterGeneric,
|
||||||
|
ThemedToken,
|
||||||
|
} from "shiki";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { createHighlighter } from "shiki";
|
||||||
|
|
||||||
|
// Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline
|
||||||
|
// biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
|
||||||
|
// eslint-disable-next-line no-bitwise -- shiki bitflag check
|
||||||
|
const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;
|
||||||
|
// biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
|
||||||
|
// eslint-disable-next-line no-bitwise -- shiki bitflag check
|
||||||
|
// oxlint-disable-next-line eslint(no-bitwise)
|
||||||
|
const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;
|
||||||
|
const isUnderline = (fontStyle: number | undefined) =>
|
||||||
|
// biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
|
||||||
|
// oxlint-disable-next-line eslint(no-bitwise)
|
||||||
|
fontStyle && fontStyle & 4;
|
||||||
|
|
||||||
|
// Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint
|
||||||
|
interface KeyedToken {
|
||||||
|
token: ThemedToken;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
interface KeyedLine {
|
||||||
|
tokens: KeyedToken[];
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>
|
||||||
|
lines.map((line, lineIdx) => ({
|
||||||
|
key: `line-${lineIdx}`,
|
||||||
|
tokens: line.map((token, tokenIdx) => ({
|
||||||
|
key: `line-${lineIdx}-${tokenIdx}`,
|
||||||
|
token,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Token rendering component
|
||||||
|
const TokenSpan = ({ token }: { token: ThemedToken }) => (
|
||||||
|
<span
|
||||||
|
className="dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
backgroundColor: token.bgColor,
|
||||||
|
color: token.color,
|
||||||
|
fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,
|
||||||
|
fontWeight: isBold(token.fontStyle) ? "bold" : undefined,
|
||||||
|
textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,
|
||||||
|
...token.htmlStyle,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{token.content}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Line rendering component
|
||||||
|
const LineSpan = ({
|
||||||
|
keyedLine,
|
||||||
|
showLineNumbers,
|
||||||
|
}: {
|
||||||
|
keyedLine: KeyedLine;
|
||||||
|
showLineNumbers: boolean;
|
||||||
|
}) => (
|
||||||
|
<span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>
|
||||||
|
{keyedLine.tokens.length === 0
|
||||||
|
? "\n"
|
||||||
|
: keyedLine.tokens.map(({ token, key }) => (
|
||||||
|
<TokenSpan key={key} token={token} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Types
|
||||||
|
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
code: string;
|
||||||
|
language: BundledLanguage;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TokenizedCode {
|
||||||
|
tokens: ThemedToken[][];
|
||||||
|
fg: string;
|
||||||
|
bg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeBlockContextType {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context
|
||||||
|
const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||||
|
code: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlighter cache (singleton per language)
|
||||||
|
const highlighterCache = new Map<
|
||||||
|
string,
|
||||||
|
Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
// Token cache
|
||||||
|
const tokensCache = new Map<string, TokenizedCode>();
|
||||||
|
|
||||||
|
// Subscribers for async token updates
|
||||||
|
const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
|
||||||
|
|
||||||
|
const getTokensCacheKey = (code: string, language: BundledLanguage) => {
|
||||||
|
const start = code.slice(0, 100);
|
||||||
|
const end = code.length > 100 ? code.slice(-100) : "";
|
||||||
|
return `${language}:${code.length}:${start}:${end}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHighlighter = (
|
||||||
|
language: BundledLanguage
|
||||||
|
): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
|
||||||
|
const cached = highlighterCache.get(language);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlighterPromise = createHighlighter({
|
||||||
|
langs: [language],
|
||||||
|
themes: ["github-light", "github-dark"],
|
||||||
|
});
|
||||||
|
|
||||||
|
highlighterCache.set(language, highlighterPromise);
|
||||||
|
return highlighterPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create raw tokens for immediate display while highlighting loads
|
||||||
|
const createRawTokens = (code: string): TokenizedCode => ({
|
||||||
|
bg: "transparent",
|
||||||
|
fg: "inherit",
|
||||||
|
tokens: code.split("\n").map((line) =>
|
||||||
|
line === ""
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
color: "inherit",
|
||||||
|
content: line,
|
||||||
|
} as ThemedToken,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Synchronous highlight with callback for async results
|
||||||
|
export const highlightCode = (
|
||||||
|
code: string,
|
||||||
|
language: BundledLanguage,
|
||||||
|
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
|
||||||
|
callback?: (result: TokenizedCode) => void
|
||||||
|
): TokenizedCode | null => {
|
||||||
|
const tokensCacheKey = getTokensCacheKey(code, language);
|
||||||
|
|
||||||
|
// Return cached result if available
|
||||||
|
const cached = tokensCache.get(tokensCacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe callback if provided
|
||||||
|
if (callback) {
|
||||||
|
if (!subscribers.has(tokensCacheKey)) {
|
||||||
|
subscribers.set(tokensCacheKey, new Set());
|
||||||
|
}
|
||||||
|
subscribers.get(tokensCacheKey)?.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start highlighting in background - fire-and-forget async pattern
|
||||||
|
getHighlighter(language)
|
||||||
|
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
|
||||||
|
.then((highlighter) => {
|
||||||
|
const availableLangs = highlighter.getLoadedLanguages();
|
||||||
|
const langToUse = availableLangs.includes(language) ? language : "text";
|
||||||
|
|
||||||
|
const result = highlighter.codeToTokens(code, {
|
||||||
|
lang: langToUse,
|
||||||
|
themes: {
|
||||||
|
dark: "github-dark",
|
||||||
|
light: "github-light",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenized: TokenizedCode = {
|
||||||
|
bg: result.bg ?? "transparent",
|
||||||
|
fg: result.fg ?? "inherit",
|
||||||
|
tokens: result.tokens,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
tokensCache.set(tokensCacheKey, tokenized);
|
||||||
|
|
||||||
|
// Notify all subscribers
|
||||||
|
const subs = subscribers.get(tokensCacheKey);
|
||||||
|
if (subs) {
|
||||||
|
for (const sub of subs) {
|
||||||
|
sub(tokenized);
|
||||||
|
}
|
||||||
|
subscribers.delete(tokensCacheKey);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to highlight code:", error);
|
||||||
|
subscribers.delete(tokensCacheKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Line number styles using CSS counters
|
||||||
|
const LINE_NUMBER_CLASSES = cn(
|
||||||
|
"block",
|
||||||
|
"before:content-[counter(line)]",
|
||||||
|
"before:inline-block",
|
||||||
|
"before:[counter-increment:line]",
|
||||||
|
"before:w-8",
|
||||||
|
"before:mr-4",
|
||||||
|
"before:text-right",
|
||||||
|
"before:text-muted-foreground/50",
|
||||||
|
"before:font-mono",
|
||||||
|
"before:select-none"
|
||||||
|
);
|
||||||
|
|
||||||
|
const CodeBlockBody = memo(
|
||||||
|
({
|
||||||
|
tokenized,
|
||||||
|
showLineNumbers,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
tokenized: TokenizedCode;
|
||||||
|
showLineNumbers: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const preStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
backgroundColor: tokenized.bg,
|
||||||
|
color: tokenized.fg,
|
||||||
|
}),
|
||||||
|
[tokenized.bg, tokenized.fg]
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyedLines = useMemo(
|
||||||
|
() => addKeysToTokens(tokenized.tokens),
|
||||||
|
[tokenized.tokens]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
className={cn(
|
||||||
|
"dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={preStyle}
|
||||||
|
>
|
||||||
|
<code
|
||||||
|
className={cn(
|
||||||
|
"font-mono text-sm",
|
||||||
|
showLineNumbers && "[counter-increment:line_0] [counter-reset:line]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{keyedLines.map((keyedLine) => (
|
||||||
|
<LineSpan
|
||||||
|
key={keyedLine.key}
|
||||||
|
keyedLine={keyedLine}
|
||||||
|
showLineNumbers={showLineNumbers}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(prevProps, nextProps) =>
|
||||||
|
prevProps.tokenized === nextProps.tokenized &&
|
||||||
|
prevProps.showLineNumbers === nextProps.showLineNumbers &&
|
||||||
|
prevProps.className === nextProps.className
|
||||||
|
);
|
||||||
|
|
||||||
|
CodeBlockBody.displayName = "CodeBlockBody";
|
||||||
|
|
||||||
|
export const CodeBlockContainer = ({
|
||||||
|
className,
|
||||||
|
language,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement> & { language: string }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group relative w-full overflow-hidden rounded-md border bg-background text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
data-language={language}
|
||||||
|
style={{
|
||||||
|
containIntrinsicSize: "auto 200px",
|
||||||
|
contentVisibility: "auto",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CodeBlockHeader = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CodeBlockTitle = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex items-center gap-2", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CodeBlockFilename = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLSpanElement>) => (
|
||||||
|
<span className={cn("font-mono", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CodeBlockActions = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn("-my-1 -mr-1 flex items-center gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CodeBlockContent = ({
|
||||||
|
code,
|
||||||
|
language,
|
||||||
|
showLineNumbers = false,
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
language: BundledLanguage;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
|
}) => {
|
||||||
|
// Memoized raw tokens for immediate display
|
||||||
|
const rawTokens = useMemo(() => createRawTokens(code), [code]);
|
||||||
|
|
||||||
|
// Try to get cached result synchronously, otherwise use raw tokens
|
||||||
|
const [tokenized, setTokenized] = useState<TokenizedCode>(
|
||||||
|
() => highlightCode(code, language) ?? rawTokens
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
// Reset to raw tokens when code changes (shows current code, not stale tokens)
|
||||||
|
setTokenized(highlightCode(code, language) ?? rawTokens);
|
||||||
|
|
||||||
|
// Subscribe to async highlighting result
|
||||||
|
highlightCode(code, language, (result) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setTokenized(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [code, language, rawTokens]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative overflow-auto">
|
||||||
|
<CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlock = ({
|
||||||
|
code,
|
||||||
|
language,
|
||||||
|
showLineNumbers = false,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CodeBlockProps) => {
|
||||||
|
const contextValue = useMemo(() => ({ code }), [code]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CodeBlockContext.Provider value={contextValue}>
|
||||||
|
<CodeBlockContainer className={className} language={language} {...props}>
|
||||||
|
{children}
|
||||||
|
<CodeBlockContent
|
||||||
|
code={code}
|
||||||
|
language={language}
|
||||||
|
showLineNumbers={showLineNumbers}
|
||||||
|
/>
|
||||||
|
</CodeBlockContainer>
|
||||||
|
</CodeBlockContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
||||||
|
onCopy?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockCopyButton = ({
|
||||||
|
onCopy,
|
||||||
|
onError,
|
||||||
|
timeout = 2000,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockCopyButtonProps) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const timeoutRef = useRef<number>(0);
|
||||||
|
const { code } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(async () => {
|
||||||
|
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
||||||
|
onError?.(new Error("Clipboard API not available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isCopied) {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
setIsCopied(true);
|
||||||
|
onCopy?.();
|
||||||
|
timeoutRef.current = window.setTimeout(
|
||||||
|
() => setIsCopied(false),
|
||||||
|
timeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(error as Error);
|
||||||
|
}
|
||||||
|
}, [code, onCopy, onError, timeout, isCopied]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
window.clearTimeout(timeoutRef.current);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn("shrink-0", className)}
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <Icon size={14} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;
|
||||||
|
|
||||||
|
export const CodeBlockLanguageSelector = (
|
||||||
|
props: CodeBlockLanguageSelectorProps
|
||||||
|
) => <Select {...props} />;
|
||||||
|
|
||||||
|
export type CodeBlockLanguageSelectorTriggerProps = ComponentProps<
|
||||||
|
typeof SelectTrigger
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const CodeBlockLanguageSelectorTrigger = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockLanguageSelectorTriggerProps) => (
|
||||||
|
<SelectTrigger
|
||||||
|
className={cn(
|
||||||
|
"h-7 border-none bg-transparent px-2 text-xs shadow-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
size="sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockLanguageSelectorValueProps = ComponentProps<
|
||||||
|
typeof SelectValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const CodeBlockLanguageSelectorValue = (
|
||||||
|
props: CodeBlockLanguageSelectorValueProps
|
||||||
|
) => <SelectValue {...props} />;
|
||||||
|
|
||||||
|
export type CodeBlockLanguageSelectorContentProps = ComponentProps<
|
||||||
|
typeof SelectContent
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const CodeBlockLanguageSelectorContent = ({
|
||||||
|
align = "end",
|
||||||
|
...props
|
||||||
|
}: CodeBlockLanguageSelectorContentProps) => (
|
||||||
|
<SelectContent align={align} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockLanguageSelectorItemProps = ComponentProps<
|
||||||
|
typeof SelectItem
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const CodeBlockLanguageSelectorItem = (
|
||||||
|
props: CodeBlockLanguageSelectorItemProps
|
||||||
|
) => <SelectItem {...props} />;
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type { ToolUIPart } from "ai";
|
|
||||||
import {
|
|
||||||
type ComponentProps,
|
|
||||||
createContext,
|
|
||||||
type ReactNode,
|
|
||||||
useContext,
|
|
||||||
} from "react";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type ToolUIPartApproval =
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
approved?: never;
|
|
||||||
reason?: never;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
approved: boolean;
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
approved: true;
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
approved: true;
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
approved: false;
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
type ConfirmationContextValue = {
|
|
||||||
approval: ToolUIPartApproval;
|
|
||||||
state: ToolUIPart["state"];
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConfirmationContext = createContext<ConfirmationContextValue | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const useConfirmation = () => {
|
|
||||||
const context = useContext(ConfirmationContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("Confirmation components must be used within Confirmation");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConfirmationProps = ComponentProps<typeof Alert> & {
|
|
||||||
approval?: ToolUIPartApproval;
|
|
||||||
state: ToolUIPart["state"];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Confirmation = ({
|
|
||||||
className,
|
|
||||||
approval,
|
|
||||||
state,
|
|
||||||
...props
|
|
||||||
}: ConfirmationProps) => {
|
|
||||||
if (!approval || state === "input-streaming" || state === "input-available") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfirmationContext.Provider value={{ approval, state }}>
|
|
||||||
<Alert className={cn("flex flex-col gap-2", className)} {...props} />
|
|
||||||
</ConfirmationContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConfirmationTitleProps = ComponentProps<typeof AlertDescription>;
|
|
||||||
|
|
||||||
export const ConfirmationTitle = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ConfirmationTitleProps) => (
|
|
||||||
<AlertDescription className={cn("inline", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ConfirmationRequestProps = {
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConfirmationRequest = ({ children }: ConfirmationRequestProps) => {
|
|
||||||
const { state } = useConfirmation();
|
|
||||||
|
|
||||||
// Only show when approval is requested
|
|
||||||
if (state !== "approval-requested") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConfirmationAcceptedProps = {
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConfirmationAccepted = ({
|
|
||||||
children,
|
|
||||||
}: ConfirmationAcceptedProps) => {
|
|
||||||
const { approval, state } = useConfirmation();
|
|
||||||
|
|
||||||
// Only show when approved and in response states
|
|
||||||
if (
|
|
||||||
!approval?.approved ||
|
|
||||||
(state !== "approval-responded" &&
|
|
||||||
state !== "output-denied" &&
|
|
||||||
state !== "output-available")
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConfirmationRejectedProps = {
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConfirmationRejected = ({
|
|
||||||
children,
|
|
||||||
}: ConfirmationRejectedProps) => {
|
|
||||||
const { approval, state } = useConfirmation();
|
|
||||||
|
|
||||||
// Only show when rejected and in response states
|
|
||||||
if (
|
|
||||||
approval?.approved !== false ||
|
|
||||||
(state !== "approval-responded" &&
|
|
||||||
state !== "output-denied" &&
|
|
||||||
state !== "output-available")
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConfirmationActionsProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const ConfirmationActions = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ConfirmationActionsProps) => {
|
|
||||||
const { state } = useConfirmation();
|
|
||||||
|
|
||||||
// Only show when approval is requested
|
|
||||||
if (state !== "approval-requested") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn("flex items-center justify-end gap-2 self-end", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConfirmationActionProps = ComponentProps<typeof Button>;
|
|
||||||
|
|
||||||
export const ConfirmationAction = (props: ConfirmationActionProps) => (
|
|
||||||
<Button className="h-8 px-3 text-sm" type="button" {...props} />
|
|
||||||
);
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import type { ConnectionLineComponent } from "@xyflow/react";
|
|
||||||
|
|
||||||
const HALF = 0.5;
|
|
||||||
|
|
||||||
export const Connection: ConnectionLineComponent = ({
|
|
||||||
fromX,
|
|
||||||
fromY,
|
|
||||||
toX,
|
|
||||||
toY,
|
|
||||||
}) => (
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
className="animated"
|
|
||||||
d={`M${fromX},${fromY} C ${fromX + (toX - fromX) * HALF},${fromY} ${fromX + (toX - fromX) * HALF},${toY} ${toX},${toY}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="var(--color-ring)"
|
|
||||||
strokeWidth={1}
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx={toX}
|
|
||||||
cy={toY}
|
|
||||||
fill="#fff"
|
|
||||||
r={3}
|
|
||||||
stroke="var(--color-ring)"
|
|
||||||
strokeWidth={1}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Controls as ControlsPrimitive } from "@xyflow/react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
|
|
||||||
|
|
||||||
export const Controls = ({ className, ...props }: ControlsProps) => (
|
|
||||||
<ControlsPrimitive
|
|
||||||
className={cn(
|
|
||||||
"gap-px overflow-hidden rounded-md border bg-card p-1 shadow-none!",
|
|
||||||
"[&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ArrowDownIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useCallback } from "react";
|
|
||||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ArrowDownIcon, DownloadIcon } from "lucide-react";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
||||||
|
|
||||||
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||||
|
|
||||||
|
|
@ -84,7 +85,7 @@ export const ConversationScrollButton = ({
|
||||||
!isAtBottom && (
|
!isAtBottom && (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full",
|
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={handleScrollToBottom}
|
onClick={handleScrollToBottom}
|
||||||
|
|
@ -98,3 +99,69 @@ export const ConversationScrollButton = ({
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ConversationMessage {
|
||||||
|
role: "user" | "assistant" | "system" | "data" | "tool";
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConversationDownloadProps = Omit<
|
||||||
|
ComponentProps<typeof Button>,
|
||||||
|
"onClick"
|
||||||
|
> & {
|
||||||
|
messages: ConversationMessage[];
|
||||||
|
filename?: string;
|
||||||
|
formatMessage?: (message: ConversationMessage, index: number) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultFormatMessage = (message: ConversationMessage): string => {
|
||||||
|
const roleLabel =
|
||||||
|
message.role.charAt(0).toUpperCase() + message.role.slice(1);
|
||||||
|
return `**${roleLabel}:** ${message.content}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const messagesToMarkdown = (
|
||||||
|
messages: ConversationMessage[],
|
||||||
|
formatMessage: (
|
||||||
|
message: ConversationMessage,
|
||||||
|
index: number
|
||||||
|
) => string = defaultFormatMessage
|
||||||
|
): string => messages.map((msg, i) => formatMessage(msg, i)).join("\n\n");
|
||||||
|
|
||||||
|
export const ConversationDownload = ({
|
||||||
|
messages,
|
||||||
|
filename = "conversation.md",
|
||||||
|
formatMessage = defaultFormatMessage,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ConversationDownloadProps) => {
|
||||||
|
const handleDownload = useCallback(() => {
|
||||||
|
const markdown = messagesToMarkdown(messages, formatMessage);
|
||||||
|
const blob = new Blob([markdown], { type: "text/markdown" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.append(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, [messages, filename, formatMessage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"absolute top-4 right-4 rounded-full dark:bg-background dark:hover:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={handleDownload}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <DownloadIcon className="size-4" />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
import {
|
|
||||||
BaseEdge,
|
|
||||||
type EdgeProps,
|
|
||||||
getBezierPath,
|
|
||||||
getSimpleBezierPath,
|
|
||||||
type InternalNode,
|
|
||||||
type Node,
|
|
||||||
Position,
|
|
||||||
useInternalNode,
|
|
||||||
} from "@xyflow/react";
|
|
||||||
|
|
||||||
const Temporary = ({
|
|
||||||
id,
|
|
||||||
sourceX,
|
|
||||||
sourceY,
|
|
||||||
targetX,
|
|
||||||
targetY,
|
|
||||||
sourcePosition,
|
|
||||||
targetPosition,
|
|
||||||
}: EdgeProps) => {
|
|
||||||
const [edgePath] = getSimpleBezierPath({
|
|
||||||
sourceX,
|
|
||||||
sourceY,
|
|
||||||
sourcePosition,
|
|
||||||
targetX,
|
|
||||||
targetY,
|
|
||||||
targetPosition,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseEdge
|
|
||||||
className="stroke-1 stroke-ring"
|
|
||||||
id={id}
|
|
||||||
path={edgePath}
|
|
||||||
style={{
|
|
||||||
strokeDasharray: "5, 5",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHandleCoordsByPosition = (
|
|
||||||
node: InternalNode<Node>,
|
|
||||||
handlePosition: Position
|
|
||||||
) => {
|
|
||||||
// Choose the handle type based on position - Left is for target, Right is for source
|
|
||||||
const handleType = handlePosition === Position.Left ? "target" : "source";
|
|
||||||
|
|
||||||
const handle = node.internals.handleBounds?.[handleType]?.find(
|
|
||||||
(h) => h.position === handlePosition
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!handle) {
|
|
||||||
return [0, 0] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
let offsetX = handle.width / 2;
|
|
||||||
let offsetY = handle.height / 2;
|
|
||||||
|
|
||||||
// this is a tiny detail to make the markerEnd of an edge visible.
|
|
||||||
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
|
|
||||||
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
|
|
||||||
switch (handlePosition) {
|
|
||||||
case Position.Left:
|
|
||||||
offsetX = 0;
|
|
||||||
break;
|
|
||||||
case Position.Right:
|
|
||||||
offsetX = handle.width;
|
|
||||||
break;
|
|
||||||
case Position.Top:
|
|
||||||
offsetY = 0;
|
|
||||||
break;
|
|
||||||
case Position.Bottom:
|
|
||||||
offsetY = handle.height;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Invalid handle position: ${handlePosition}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const x = node.internals.positionAbsolute.x + handle.x + offsetX;
|
|
||||||
const y = node.internals.positionAbsolute.y + handle.y + offsetY;
|
|
||||||
|
|
||||||
return [x, y] as const;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEdgeParams = (
|
|
||||||
source: InternalNode<Node>,
|
|
||||||
target: InternalNode<Node>
|
|
||||||
) => {
|
|
||||||
const sourcePos = Position.Right;
|
|
||||||
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
|
|
||||||
const targetPos = Position.Left;
|
|
||||||
const [tx, ty] = getHandleCoordsByPosition(target, targetPos);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sx,
|
|
||||||
sy,
|
|
||||||
tx,
|
|
||||||
ty,
|
|
||||||
sourcePos,
|
|
||||||
targetPos,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
|
|
||||||
const sourceNode = useInternalNode(source);
|
|
||||||
const targetNode = useInternalNode(target);
|
|
||||||
|
|
||||||
if (!(sourceNode && targetNode)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
|
|
||||||
sourceNode,
|
|
||||||
targetNode
|
|
||||||
);
|
|
||||||
|
|
||||||
const [edgePath] = getBezierPath({
|
|
||||||
sourceX: sx,
|
|
||||||
sourceY: sy,
|
|
||||||
sourcePosition: sourcePos,
|
|
||||||
targetX: tx,
|
|
||||||
targetY: ty,
|
|
||||||
targetPosition: targetPos,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />
|
|
||||||
<circle fill="var(--primary)" r="4">
|
|
||||||
<animateMotion dur="2s" path={edgePath} repeatCount="indefinite" />
|
|
||||||
</circle>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Edge = {
|
|
||||||
Temporary,
|
|
||||||
Animated,
|
|
||||||
};
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import type { Experimental_GeneratedImage } from "ai";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ImageProps = Experimental_GeneratedImage & {
|
|
||||||
className?: string;
|
|
||||||
alt?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Image = ({
|
|
||||||
base64,
|
|
||||||
uint8Array,
|
|
||||||
mediaType,
|
|
||||||
...props
|
|
||||||
}: ImageProps) => (
|
|
||||||
// biome-ignore lint/performance/noImgElement: base64 data URLs require native img
|
|
||||||
<img
|
|
||||||
{...props}
|
|
||||||
alt={props.alt}
|
|
||||||
className={cn(
|
|
||||||
"h-auto max-w-full overflow-hidden rounded-md",
|
|
||||||
props.className
|
|
||||||
)}
|
|
||||||
src={`data:${mediaType};base64,${base64}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -1,287 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
|
||||||
import {
|
|
||||||
type ComponentProps,
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Carousel,
|
|
||||||
type CarouselApi,
|
|
||||||
CarouselContent,
|
|
||||||
CarouselItem,
|
|
||||||
} from "@/components/ui/carousel";
|
|
||||||
import {
|
|
||||||
HoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardTrigger,
|
|
||||||
} from "@/components/ui/hover-card";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type InlineCitationProps = ComponentProps<"span">;
|
|
||||||
|
|
||||||
export const InlineCitation = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationProps) => (
|
|
||||||
<span
|
|
||||||
className={cn("group inline items-center gap-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationTextProps = ComponentProps<"span">;
|
|
||||||
|
|
||||||
export const InlineCitationText = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationTextProps) => (
|
|
||||||
<span
|
|
||||||
className={cn("transition-colors group-hover:bg-accent", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
|
|
||||||
|
|
||||||
export const InlineCitationCard = (props: InlineCitationCardProps) => (
|
|
||||||
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
|
|
||||||
sources: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InlineCitationCardTrigger = ({
|
|
||||||
sources,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCardTriggerProps) => (
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<Badge
|
|
||||||
className={cn("ml-1 rounded-full", className)}
|
|
||||||
variant="secondary"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{sources[0] ? (
|
|
||||||
<>
|
|
||||||
{new URL(sources[0]).hostname}{" "}
|
|
||||||
{sources.length > 1 && `+${sources.length - 1}`}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"unknown"
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCardBodyProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCardBody = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCardBodyProps) => (
|
|
||||||
<HoverCardContent className={cn("relative w-80 p-0", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
|
|
||||||
|
|
||||||
const useCarouselApi = () => {
|
|
||||||
const context = useContext(CarouselApiContext);
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;
|
|
||||||
|
|
||||||
export const InlineCitationCarousel = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselProps) => {
|
|
||||||
const [api, setApi] = useState<CarouselApi>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CarouselApiContext.Provider value={api}>
|
|
||||||
<Carousel className={cn("w-full", className)} setApi={setApi} {...props}>
|
|
||||||
{children}
|
|
||||||
</Carousel>
|
|
||||||
</CarouselApiContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationCarouselContentProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselContent = (
|
|
||||||
props: InlineCitationCarouselContentProps
|
|
||||||
) => <CarouselContent {...props} />;
|
|
||||||
|
|
||||||
export type InlineCitationCarouselItemProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselItem = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselItemProps) => (
|
|
||||||
<CarouselItem
|
|
||||||
className={cn("w-full space-y-2 p-4 pl-8", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCarouselHeaderProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselHeaderProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCarouselIndexProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselIndex = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselIndexProps) => {
|
|
||||||
const api = useCarouselApi();
|
|
||||||
const [current, setCurrent] = useState(0);
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!api) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCount(api.scrollSnapList().length);
|
|
||||||
setCurrent(api.selectedScrollSnap() + 1);
|
|
||||||
|
|
||||||
api.on("select", () => {
|
|
||||||
setCurrent(api.selectedScrollSnap() + 1);
|
|
||||||
});
|
|
||||||
}, [api]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? `${current}/${count}`}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationCarouselPrevProps = ComponentProps<"button">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselPrev = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselPrevProps) => {
|
|
||||||
const api = useCarouselApi();
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
if (api) {
|
|
||||||
api.scrollPrev();
|
|
||||||
}
|
|
||||||
}, [api]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
aria-label="Previous"
|
|
||||||
className={cn("shrink-0", className)}
|
|
||||||
onClick={handleClick}
|
|
||||||
type="button"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon className="size-4 text-muted-foreground" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationCarouselNextProps = ComponentProps<"button">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselNext = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselNextProps) => {
|
|
||||||
const api = useCarouselApi();
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
if (api) {
|
|
||||||
api.scrollNext();
|
|
||||||
}
|
|
||||||
}, [api]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
aria-label="Next"
|
|
||||||
className={cn("shrink-0", className)}
|
|
||||||
onClick={handleClick}
|
|
||||||
type="button"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowRightIcon className="size-4 text-muted-foreground" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationSourceProps = ComponentProps<"div"> & {
|
|
||||||
title?: string;
|
|
||||||
url?: string;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InlineCitationSource = ({
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
description,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: InlineCitationSourceProps) => (
|
|
||||||
<div className={cn("space-y-1", className)} {...props}>
|
|
||||||
{title && (
|
|
||||||
<h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
|
|
||||||
)}
|
|
||||||
{url && (
|
|
||||||
<p className="truncate break-all text-muted-foreground text-xs">{url}</p>
|
|
||||||
)}
|
|
||||||
{description && (
|
|
||||||
<p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationQuoteProps = ComponentProps<"blockquote">;
|
|
||||||
|
|
||||||
export const InlineCitationQuote = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationQuoteProps) => (
|
|
||||||
<blockquote
|
|
||||||
className={cn(
|
|
||||||
"border-muted border-l-2 pl-3 text-muted-foreground text-sm italic",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</blockquote>
|
|
||||||
);
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import type { HTMLAttributes } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type LoaderIconProps = {
|
|
||||||
size?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
|
|
||||||
<svg
|
|
||||||
height={size}
|
|
||||||
strokeLinejoin="round"
|
|
||||||
style={{ color: "currentcolor" }}
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
width={size}
|
|
||||||
>
|
|
||||||
<title>Loader</title>
|
|
||||||
<g clipPath="url(#clip0_2393_1490)">
|
|
||||||
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
|
|
||||||
<path
|
|
||||||
d="M8 16V12"
|
|
||||||
opacity="0.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3.29773 1.52783L5.64887 4.7639"
|
|
||||||
opacity="0.9"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.7023 1.52783L10.3511 4.7639"
|
|
||||||
opacity="0.1"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.7023 14.472L10.3511 11.236"
|
|
||||||
opacity="0.4"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3.29773 14.472L5.64887 11.236"
|
|
||||||
opacity="0.6"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M15.6085 5.52783L11.8043 6.7639"
|
|
||||||
opacity="0.2"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M0.391602 10.472L4.19583 9.23598"
|
|
||||||
opacity="0.7"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M15.6085 10.4722L11.8043 9.2361"
|
|
||||||
opacity="0.3"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M0.391602 5.52783L4.19583 6.7639"
|
|
||||||
opacity="0.8"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_2393_1490">
|
|
||||||
<rect fill="white" height="16" width="16" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
size?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex animate-spin items-center justify-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<LoaderIcon size={size} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FileUIPart, UIMessage } from "ai";
|
import type { UIMessage } from "ai";
|
||||||
import {
|
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
PaperclipIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
||||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
|
||||||
import { Streamdown } from "streamdown";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
|
import {
|
||||||
|
ButtonGroup,
|
||||||
|
ButtonGroupText,
|
||||||
|
} from "@/components/ui/button-group";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
|
|
@ -19,6 +15,21 @@ import {
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { cjk } from "@streamdown/cjk";
|
||||||
|
import { code } from "@streamdown/code";
|
||||||
|
import { math } from "@streamdown/math";
|
||||||
|
import { mermaid } from "@streamdown/mermaid";
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Streamdown } from "streamdown";
|
||||||
|
|
||||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
from: UIMessage["role"];
|
from: UIMessage["role"];
|
||||||
|
|
@ -103,14 +114,14 @@ export const MessageAction = ({
|
||||||
return button;
|
return button;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MessageBranchContextType = {
|
interface MessageBranchContextType {
|
||||||
currentBranch: number;
|
currentBranch: number;
|
||||||
totalBranches: number;
|
totalBranches: number;
|
||||||
goToPrevious: () => void;
|
goToPrevious: () => void;
|
||||||
goToNext: () => void;
|
goToNext: () => void;
|
||||||
branches: ReactElement[];
|
branches: ReactElement[];
|
||||||
setBranches: (branches: ReactElement[]) => void;
|
setBranches: (branches: ReactElement[]) => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
const MessageBranchContext = createContext<MessageBranchContextType | null>(
|
const MessageBranchContext = createContext<MessageBranchContextType | null>(
|
||||||
null
|
null
|
||||||
|
|
@ -142,31 +153,37 @@ export const MessageBranch = ({
|
||||||
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
||||||
const [branches, setBranches] = useState<ReactElement[]>([]);
|
const [branches, setBranches] = useState<ReactElement[]>([]);
|
||||||
|
|
||||||
const handleBranchChange = (newBranch: number) => {
|
const handleBranchChange = useCallback(
|
||||||
setCurrentBranch(newBranch);
|
(newBranch: number) => {
|
||||||
onBranchChange?.(newBranch);
|
setCurrentBranch(newBranch);
|
||||||
};
|
onBranchChange?.(newBranch);
|
||||||
|
},
|
||||||
|
[onBranchChange]
|
||||||
|
);
|
||||||
|
|
||||||
const goToPrevious = () => {
|
const goToPrevious = useCallback(() => {
|
||||||
const newBranch =
|
const newBranch =
|
||||||
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
||||||
handleBranchChange(newBranch);
|
handleBranchChange(newBranch);
|
||||||
};
|
}, [currentBranch, branches.length, handleBranchChange]);
|
||||||
|
|
||||||
const goToNext = () => {
|
const goToNext = useCallback(() => {
|
||||||
const newBranch =
|
const newBranch =
|
||||||
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
||||||
handleBranchChange(newBranch);
|
handleBranchChange(newBranch);
|
||||||
};
|
}, [currentBranch, branches.length, handleBranchChange]);
|
||||||
|
|
||||||
const contextValue: MessageBranchContextType = {
|
const contextValue = useMemo<MessageBranchContextType>(
|
||||||
currentBranch,
|
() => ({
|
||||||
totalBranches: branches.length,
|
branches,
|
||||||
goToPrevious,
|
currentBranch,
|
||||||
goToNext,
|
goToNext,
|
||||||
branches,
|
goToPrevious,
|
||||||
setBranches,
|
setBranches,
|
||||||
};
|
totalBranches: branches.length,
|
||||||
|
}),
|
||||||
|
[branches, currentBranch, goToNext, goToPrevious]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageBranchContext.Provider value={contextValue}>
|
<MessageBranchContext.Provider value={contextValue}>
|
||||||
|
|
@ -185,7 +202,10 @@ export const MessageBranchContent = ({
|
||||||
...props
|
...props
|
||||||
}: MessageBranchContentProps) => {
|
}: MessageBranchContentProps) => {
|
||||||
const { currentBranch, setBranches, branches } = useMessageBranch();
|
const { currentBranch, setBranches, branches } = useMessageBranch();
|
||||||
const childrenArray = Array.isArray(children) ? children : [children];
|
const childrenArray = useMemo(
|
||||||
|
() => (Array.isArray(children) ? children : [children]),
|
||||||
|
[children]
|
||||||
|
);
|
||||||
|
|
||||||
// Use useEffect to update branches when they change
|
// Use useEffect to update branches when they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -208,13 +228,10 @@ export const MessageBranchContent = ({
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
|
export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
|
||||||
from: UIMessage["role"];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MessageBranchSelector = ({
|
export const MessageBranchSelector = ({
|
||||||
className,
|
className,
|
||||||
from,
|
|
||||||
...props
|
...props
|
||||||
}: MessageBranchSelectorProps) => {
|
}: MessageBranchSelectorProps) => {
|
||||||
const { totalBranches } = useMessageBranch();
|
const { totalBranches } = useMessageBranch();
|
||||||
|
|
@ -226,7 +243,10 @@ export const MessageBranchSelector = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
className="[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md"
|
className={cn(
|
||||||
|
"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -260,7 +280,6 @@ export type MessageBranchNextProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
export const MessageBranchNext = ({
|
export const MessageBranchNext = ({
|
||||||
children,
|
children,
|
||||||
className,
|
|
||||||
...props
|
...props
|
||||||
}: MessageBranchNextProps) => {
|
}: MessageBranchNextProps) => {
|
||||||
const { goToNext, totalBranches } = useMessageBranch();
|
const { goToNext, totalBranches } = useMessageBranch();
|
||||||
|
|
@ -303,6 +322,8 @@ export const MessageBranchPage = ({
|
||||||
|
|
||||||
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
||||||
|
|
||||||
|
const streamdownPlugins = { cjk, code, math, mermaid };
|
||||||
|
|
||||||
export const MessageResponse = memo(
|
export const MessageResponse = memo(
|
||||||
({ className, ...props }: MessageResponseProps) => (
|
({ className, ...props }: MessageResponseProps) => (
|
||||||
<Streamdown
|
<Streamdown
|
||||||
|
|
@ -310,6 +331,7 @@ export const MessageResponse = memo(
|
||||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
plugins={streamdownPlugins}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|
@ -318,115 +340,6 @@ export const MessageResponse = memo(
|
||||||
|
|
||||||
MessageResponse.displayName = "MessageResponse";
|
MessageResponse.displayName = "MessageResponse";
|
||||||
|
|
||||||
export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
data: FileUIPart;
|
|
||||||
className?: string;
|
|
||||||
onRemove?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function MessageAttachment({
|
|
||||||
data,
|
|
||||||
className,
|
|
||||||
onRemove,
|
|
||||||
...props
|
|
||||||
}: MessageAttachmentProps) {
|
|
||||||
const filename = data.filename || "";
|
|
||||||
const mediaType =
|
|
||||||
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
|
|
||||||
const isImage = mediaType === "image";
|
|
||||||
const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"group relative size-24 overflow-hidden rounded-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{isImage ? (
|
|
||||||
<>
|
|
||||||
{/* biome-ignore lint/performance/noImgElement: dynamic user-uploaded images */}
|
|
||||||
<img
|
|
||||||
alt={filename || "attachment"}
|
|
||||||
className="size-full object-cover"
|
|
||||||
height={100}
|
|
||||||
src={data.url}
|
|
||||||
width={100}
|
|
||||||
/>
|
|
||||||
{onRemove && (
|
|
||||||
<Button
|
|
||||||
aria-label="Remove attachment"
|
|
||||||
className="absolute top-2 right-2 size-6 rounded-full bg-background/80 p-0 opacity-0 backdrop-blur-sm transition-opacity hover:bg-background group-hover:opacity-100 [&>svg]:size-3"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onRemove();
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
<span className="sr-only">Remove</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{attachmentLabel}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
{onRemove && (
|
|
||||||
<Button
|
|
||||||
aria-label="Remove attachment"
|
|
||||||
className="size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity hover:bg-accent group-hover:opacity-100 [&>svg]:size-3"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onRemove();
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
<span className="sr-only">Remove</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MessageAttachmentsProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export function MessageAttachments({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: MessageAttachmentsProps) {
|
|
||||||
if (!children) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"ml-auto flex w-fit flex-wrap items-start gap-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MessageToolbarProps = ComponentProps<"div">;
|
export type MessageToolbarProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const MessageToolbar = ({
|
export const MessageToolbar = ({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Image from "next/image";
|
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
|
|
@ -41,7 +41,14 @@ export const ModelSelectorContent = ({
|
||||||
title = "Model Selector",
|
title = "Model Selector",
|
||||||
...props
|
...props
|
||||||
}: ModelSelectorContentProps) => (
|
}: ModelSelectorContentProps) => (
|
||||||
<DialogContent className={cn("p-0", className)} {...props}>
|
<DialogContent
|
||||||
|
aria-describedby={undefined}
|
||||||
|
className={cn(
|
||||||
|
"outline! border-none! p-0 outline-border! outline-solid!",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<DialogTitle className="sr-only">{title}</DialogTitle>
|
<DialogTitle className="sr-only">{title}</DialogTitle>
|
||||||
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
|
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -102,8 +109,10 @@ export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
|
||||||
<CommandSeparator {...props} />
|
<CommandSeparator {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorLogoProps = {
|
export type ModelSelectorLogoProps = Omit<
|
||||||
className?: string;
|
ComponentProps<"img">,
|
||||||
|
"src" | "alt"
|
||||||
|
> & {
|
||||||
provider:
|
provider:
|
||||||
| "moonshotai-cn"
|
| "moonshotai-cn"
|
||||||
| "lucidquery"
|
| "lucidquery"
|
||||||
|
|
@ -161,19 +170,21 @@ export type ModelSelectorLogoProps = {
|
||||||
| "scaleway"
|
| "scaleway"
|
||||||
| "amazon-bedrock"
|
| "amazon-bedrock"
|
||||||
| "cerebras"
|
| "cerebras"
|
||||||
|
// oxlint-disable-next-line typescript-eslint(ban-types) -- intentional pattern for autocomplete-friendly string union
|
||||||
| (string & {});
|
| (string & {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModelSelectorLogo = ({
|
export const ModelSelectorLogo = ({
|
||||||
provider,
|
provider,
|
||||||
className,
|
className,
|
||||||
|
...props
|
||||||
}: ModelSelectorLogoProps) => (
|
}: ModelSelectorLogoProps) => (
|
||||||
<Image
|
<img
|
||||||
|
{...props}
|
||||||
alt={`${provider} logo`}
|
alt={`${provider} logo`}
|
||||||
className={cn("size-3 dark:invert", className)}
|
className={cn("size-3 dark:invert", className)}
|
||||||
height={12}
|
height={12}
|
||||||
src={`https://models.dev/logos/${provider}.svg`}
|
src={`https://models.dev/logos/${provider}.svg`}
|
||||||
unoptimized
|
|
||||||
width={12}
|
width={12}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import { Handle, Position } from "@xyflow/react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardAction,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type NodeProps = ComponentProps<typeof Card> & {
|
|
||||||
handles: {
|
|
||||||
target: boolean;
|
|
||||||
source: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Node = ({ handles, className, ...props }: NodeProps) => (
|
|
||||||
<Card
|
|
||||||
className={cn(
|
|
||||||
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{handles.target && <Handle position={Position.Left} type="target" />}
|
|
||||||
{handles.source && <Handle position={Position.Right} type="source" />}
|
|
||||||
{props.children}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
|
|
||||||
|
|
||||||
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
|
|
||||||
<CardHeader
|
|
||||||
className={cn("gap-0.5 rounded-t-md border-b bg-secondary p-3!", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type NodeTitleProps = ComponentProps<typeof CardTitle>;
|
|
||||||
|
|
||||||
export const NodeTitle = (props: NodeTitleProps) => <CardTitle {...props} />;
|
|
||||||
|
|
||||||
export type NodeDescriptionProps = ComponentProps<typeof CardDescription>;
|
|
||||||
|
|
||||||
export const NodeDescription = (props: NodeDescriptionProps) => (
|
|
||||||
<CardDescription {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type NodeActionProps = ComponentProps<typeof CardAction>;
|
|
||||||
|
|
||||||
export const NodeAction = (props: NodeActionProps) => <CardAction {...props} />;
|
|
||||||
|
|
||||||
export type NodeContentProps = ComponentProps<typeof CardContent>;
|
|
||||||
|
|
||||||
export const NodeContent = ({ className, ...props }: NodeContentProps) => (
|
|
||||||
<CardContent className={cn("p-3", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type NodeFooterProps = ComponentProps<typeof CardFooter>;
|
|
||||||
|
|
||||||
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
|
|
||||||
<CardFooter
|
|
||||||
className={cn("rounded-b-md border-t bg-secondary p-3!", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -1,365 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
ExternalLinkIcon,
|
|
||||||
MessageCircleIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { type ComponentProps, createContext, useContext } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const providers = {
|
|
||||||
github: {
|
|
||||||
title: "Open in GitHub",
|
|
||||||
createUrl: (url: string) => url,
|
|
||||||
icon: (
|
|
||||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
|
|
||||||
<title>GitHub</title>
|
|
||||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
scira: {
|
|
||||||
title: "Open in Scira",
|
|
||||||
createUrl: (q: string) =>
|
|
||||||
`https://scira.ai/?${new URLSearchParams({
|
|
||||||
q,
|
|
||||||
})}`,
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
height="934"
|
|
||||||
viewBox="0 0 910 934"
|
|
||||||
width="910"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>Scira AI</title>
|
|
||||||
<path
|
|
||||||
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
|
|
||||||
fill="currentColor"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="8"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
|
|
||||||
fill="currentColor"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="8"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="20"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="20"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
|
|
||||||
fill="currentColor"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="8"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
|
|
||||||
fill="currentColor"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="8"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="30"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
chatgpt: {
|
|
||||||
title: "Open in ChatGPT",
|
|
||||||
createUrl: (prompt: string) =>
|
|
||||||
`https://chatgpt.com/?${new URLSearchParams({
|
|
||||||
hints: "search",
|
|
||||||
prompt,
|
|
||||||
})}`,
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>OpenAI</title>
|
|
||||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
claude: {
|
|
||||||
title: "Open in Claude",
|
|
||||||
createUrl: (q: string) =>
|
|
||||||
`https://claude.ai/new?${new URLSearchParams({
|
|
||||||
q,
|
|
||||||
})}`,
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 12 12"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>Claude</title>
|
|
||||||
<path
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"
|
|
||||||
fillRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
t3: {
|
|
||||||
title: "Open in T3 Chat",
|
|
||||||
createUrl: (q: string) =>
|
|
||||||
`https://t3.chat/new?${new URLSearchParams({
|
|
||||||
q,
|
|
||||||
})}`,
|
|
||||||
icon: <MessageCircleIcon />,
|
|
||||||
},
|
|
||||||
v0: {
|
|
||||||
title: "Open in v0",
|
|
||||||
createUrl: (q: string) =>
|
|
||||||
`https://v0.app?${new URLSearchParams({
|
|
||||||
q,
|
|
||||||
})}`,
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 147 70"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>v0</title>
|
|
||||||
<path d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z" />
|
|
||||||
<path d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
cursor: {
|
|
||||||
title: "Open in Cursor",
|
|
||||||
createUrl: (text: string) => {
|
|
||||||
const url = new URL("https://cursor.com/link/prompt");
|
|
||||||
url.searchParams.set("text", text);
|
|
||||||
return url.toString();
|
|
||||||
},
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
viewBox="0 0 466.73 532.09"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<title>Cursor</title>
|
|
||||||
<path
|
|
||||||
d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const OpenInContext = createContext<{ query: string } | undefined>(undefined);
|
|
||||||
|
|
||||||
const useOpenInContext = () => {
|
|
||||||
const context = useContext(OpenInContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("OpenIn components must be used within an OpenIn provider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenInProps = ComponentProps<typeof DropdownMenu> & {
|
|
||||||
query: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OpenIn = ({ query, ...props }: OpenInProps) => (
|
|
||||||
<OpenInContext.Provider value={{ query }}>
|
|
||||||
<DropdownMenu {...props} />
|
|
||||||
</OpenInContext.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type OpenInContentProps = ComponentProps<typeof DropdownMenuContent>;
|
|
||||||
|
|
||||||
export const OpenInContent = ({ className, ...props }: OpenInContentProps) => (
|
|
||||||
<DropdownMenuContent
|
|
||||||
align="start"
|
|
||||||
className={cn("w-[240px]", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type OpenInItemProps = ComponentProps<typeof DropdownMenuItem>;
|
|
||||||
|
|
||||||
export const OpenInItem = (props: OpenInItemProps) => (
|
|
||||||
<DropdownMenuItem {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type OpenInLabelProps = ComponentProps<typeof DropdownMenuLabel>;
|
|
||||||
|
|
||||||
export const OpenInLabel = (props: OpenInLabelProps) => (
|
|
||||||
<DropdownMenuLabel {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type OpenInSeparatorProps = ComponentProps<typeof DropdownMenuSeparator>;
|
|
||||||
|
|
||||||
export const OpenInSeparator = (props: OpenInSeparatorProps) => (
|
|
||||||
<DropdownMenuSeparator {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type OpenInTriggerProps = ComponentProps<typeof DropdownMenuTrigger>;
|
|
||||||
|
|
||||||
export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
|
|
||||||
<DropdownMenuTrigger {...props} asChild>
|
|
||||||
{children ?? (
|
|
||||||
<Button type="button" variant="outline">
|
|
||||||
Open in chat
|
|
||||||
<ChevronDownIcon className="size-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type OpenInChatGPTProps = ComponentProps<typeof DropdownMenuItem>;
|
|
||||||
|
|
||||||
export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
|
|
||||||
const { query } = useOpenInContext();
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem asChild {...props}>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
href={providers.chatgpt.createUrl(query)}
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span className="shrink-0">{providers.chatgpt.icon}</span>
|
|
||||||
<span className="flex-1">{providers.chatgpt.title}</span>
|
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenInClaudeProps = ComponentProps<typeof DropdownMenuItem>;
|
|
||||||
|
|
||||||
export const OpenInClaude = (props: OpenInClaudeProps) => {
|
|
||||||
const { query } = useOpenInContext();
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem asChild {...props}>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
href={providers.claude.createUrl(query)}
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span className="shrink-0">{providers.claude.icon}</span>
|
|
||||||
<span className="flex-1">{providers.claude.title}</span>
|
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenInT3Props = ComponentProps<typeof DropdownMenuItem>;
|
|
||||||
|
|
||||||
export const OpenInT3 = (props: OpenInT3Props) => {
|
|
||||||
const { query } = useOpenInContext();
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem asChild {...props}>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
href={providers.t3.createUrl(query)}
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span className="shrink-0">{providers.t3.icon}</span>
|
|
||||||
<span className="flex-1">{providers.t3.title}</span>
|
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenInSciraProps = ComponentProps<typeof DropdownMenuItem>;
|
|
||||||
|
|
||||||
export const OpenInScira = (props: OpenInSciraProps) => {
|
|
||||||
const { query } = useOpenInContext();
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem asChild {...props}>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
href={providers.scira.createUrl(query)}
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span className="shrink-0">{providers.scira.icon}</span>
|
|
||||||
<span className="flex-1">{providers.scira.title}</span>
|
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenInv0Props = ComponentProps<typeof DropdownMenuItem>;
|
|
||||||
|
|
||||||
export const OpenInv0 = (props: OpenInv0Props) => {
|
|
||||||
const { query } = useOpenInContext();
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem asChild {...props}>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
href={providers.v0.createUrl(query)}
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span className="shrink-0">{providers.v0.icon}</span>
|
|
||||||
<span className="flex-1">{providers.v0.title}</span>
|
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OpenInCursorProps = ComponentProps<typeof DropdownMenuItem>;
|
|
||||||
|
|
||||||
export const OpenInCursor = (props: OpenInCursorProps) => {
|
|
||||||
const { query } = useOpenInContext();
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem asChild {...props}>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
href={providers.cursor.createUrl(query)}
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span className="shrink-0">{providers.cursor.icon}</span>
|
|
||||||
<span className="flex-1">{providers.cursor.title}</span>
|
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { Panel as PanelPrimitive } from "@xyflow/react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type PanelProps = ComponentProps<typeof PanelPrimitive>;
|
|
||||||
|
|
||||||
export const Panel = ({ className, ...props }: PanelProps) => (
|
|
||||||
<PanelPrimitive
|
|
||||||
className={cn(
|
|
||||||
"m-4 overflow-hidden rounded-md border bg-card p-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronsUpDownIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { createContext, useContext } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardAction,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Shimmer } from "./shimmer";
|
|
||||||
|
|
||||||
type PlanContextValue = {
|
|
||||||
isStreaming: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PlanContext = createContext<PlanContextValue | null>(null);
|
|
||||||
|
|
||||||
const usePlan = () => {
|
|
||||||
const context = useContext(PlanContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("Plan components must be used within Plan");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PlanProps = ComponentProps<typeof Collapsible> & {
|
|
||||||
isStreaming?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Plan = ({
|
|
||||||
className,
|
|
||||||
isStreaming = false,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: PlanProps) => (
|
|
||||||
<PlanContext.Provider value={{ isStreaming }}>
|
|
||||||
<Collapsible asChild data-slot="plan" {...props}>
|
|
||||||
<Card className={cn("shadow-none", className)}>{children}</Card>
|
|
||||||
</Collapsible>
|
|
||||||
</PlanContext.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PlanHeaderProps = ComponentProps<typeof CardHeader>;
|
|
||||||
|
|
||||||
export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => (
|
|
||||||
<CardHeader
|
|
||||||
className={cn("flex items-start justify-between", className)}
|
|
||||||
data-slot="plan-header"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PlanTitleProps = Omit<
|
|
||||||
ComponentProps<typeof CardTitle>,
|
|
||||||
"children"
|
|
||||||
> & {
|
|
||||||
children: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PlanTitle = ({ children, ...props }: PlanTitleProps) => {
|
|
||||||
const { isStreaming } = usePlan();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardTitle data-slot="plan-title" {...props}>
|
|
||||||
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
|
|
||||||
</CardTitle>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PlanDescriptionProps = Omit<
|
|
||||||
ComponentProps<typeof CardDescription>,
|
|
||||||
"children"
|
|
||||||
> & {
|
|
||||||
children: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PlanDescription = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: PlanDescriptionProps) => {
|
|
||||||
const { isStreaming } = usePlan();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardDescription
|
|
||||||
className={cn("text-balance", className)}
|
|
||||||
data-slot="plan-description"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
|
|
||||||
</CardDescription>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PlanActionProps = ComponentProps<typeof CardAction>;
|
|
||||||
|
|
||||||
export const PlanAction = (props: PlanActionProps) => (
|
|
||||||
<CardAction data-slot="plan-action" {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PlanContentProps = ComponentProps<typeof CardContent>;
|
|
||||||
|
|
||||||
export const PlanContent = (props: PlanContentProps) => (
|
|
||||||
<CollapsibleContent asChild>
|
|
||||||
<CardContent data-slot="plan-content" {...props} />
|
|
||||||
</CollapsibleContent>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PlanFooterProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const PlanFooter = (props: PlanFooterProps) => (
|
|
||||||
<CardFooter data-slot="plan-footer" {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
|
|
||||||
|
|
||||||
export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => (
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className={cn("size-8", className)}
|
|
||||||
data-slot="plan-trigger"
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronsUpDownIcon className="size-4" />
|
|
||||||
<span className="sr-only">Toggle plan</span>
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
);
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,275 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronDownIcon, PaperclipIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type QueueMessagePart = {
|
|
||||||
type: string;
|
|
||||||
text?: string;
|
|
||||||
url?: string;
|
|
||||||
filename?: string;
|
|
||||||
mediaType?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QueueMessage = {
|
|
||||||
id: string;
|
|
||||||
parts: QueueMessagePart[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QueueTodo = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
status?: "pending" | "completed";
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QueueItemProps = ComponentProps<"li">;
|
|
||||||
|
|
||||||
export const QueueItem = ({ className, ...props }: QueueItemProps) => (
|
|
||||||
<li
|
|
||||||
className={cn(
|
|
||||||
"group flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors hover:bg-muted",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueItemIndicatorProps = ComponentProps<"span"> & {
|
|
||||||
completed?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QueueItemIndicator = ({
|
|
||||||
completed = false,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueItemIndicatorProps) => (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"mt-0.5 inline-block size-2.5 rounded-full border",
|
|
||||||
completed
|
|
||||||
? "border-muted-foreground/20 bg-muted-foreground/10"
|
|
||||||
: "border-muted-foreground/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueItemContentProps = ComponentProps<"span"> & {
|
|
||||||
completed?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QueueItemContent = ({
|
|
||||||
completed = false,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueItemContentProps) => (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"wrap-break-word line-clamp-1 grow",
|
|
||||||
completed
|
|
||||||
? "text-muted-foreground/50 line-through"
|
|
||||||
: "text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueItemDescriptionProps = ComponentProps<"div"> & {
|
|
||||||
completed?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QueueItemDescription = ({
|
|
||||||
completed = false,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueItemDescriptionProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"ml-6 text-xs",
|
|
||||||
completed
|
|
||||||
? "text-muted-foreground/40 line-through"
|
|
||||||
: "text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueItemActionsProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const QueueItemActions = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueItemActionsProps) => (
|
|
||||||
<div className={cn("flex gap-1", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueItemActionProps = Omit<
|
|
||||||
ComponentProps<typeof Button>,
|
|
||||||
"variant" | "size"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const QueueItemAction = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueItemActionProps) => (
|
|
||||||
<Button
|
|
||||||
className={cn(
|
|
||||||
"size-auto rounded p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-muted-foreground/10 hover:text-foreground group-hover:opacity-100",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
size="icon"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueItemAttachmentProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const QueueItemAttachment = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueItemAttachmentProps) => (
|
|
||||||
<div className={cn("mt-1 flex flex-wrap gap-2", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueItemImageProps = ComponentProps<"img">;
|
|
||||||
|
|
||||||
export const QueueItemImage = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueItemImageProps) => (
|
|
||||||
// biome-ignore lint/performance/noImgElement: dynamic blob/data URLs require native img
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
className={cn("h-8 w-8 rounded border object-cover", className)}
|
|
||||||
height={32}
|
|
||||||
width={32}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueItemFileProps = ComponentProps<"span">;
|
|
||||||
|
|
||||||
export const QueueItemFile = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueItemFileProps) => (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1 rounded border bg-muted px-2 py-1 text-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<PaperclipIcon size={12} />
|
|
||||||
<span className="max-w-[100px] truncate">{children}</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueListProps = ComponentProps<typeof ScrollArea>;
|
|
||||||
|
|
||||||
export const QueueList = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueListProps) => (
|
|
||||||
<ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
|
|
||||||
<div className="max-h-40 pr-4">
|
|
||||||
<ul>{children}</ul>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
|
|
||||||
// QueueSection - collapsible section container
|
|
||||||
export type QueueSectionProps = ComponentProps<typeof Collapsible>;
|
|
||||||
|
|
||||||
export const QueueSection = ({
|
|
||||||
className,
|
|
||||||
defaultOpen = true,
|
|
||||||
...props
|
|
||||||
}: QueueSectionProps) => (
|
|
||||||
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
// QueueSectionTrigger - section header/trigger
|
|
||||||
export type QueueSectionTriggerProps = ComponentProps<"button">;
|
|
||||||
|
|
||||||
export const QueueSectionTrigger = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueSectionTriggerProps) => (
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
"group flex w-full items-center justify-between rounded-md bg-muted/40 px-3 py-2 text-left font-medium text-muted-foreground text-sm transition-colors hover:bg-muted",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
type="button"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
// QueueSectionLabel - label content with icon and count
|
|
||||||
export type QueueSectionLabelProps = ComponentProps<"span"> & {
|
|
||||||
count?: number;
|
|
||||||
label: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QueueSectionLabel = ({
|
|
||||||
count,
|
|
||||||
label,
|
|
||||||
icon,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueSectionLabelProps) => (
|
|
||||||
<span className={cn("flex items-center gap-2", className)} {...props}>
|
|
||||||
<ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
||||||
{icon}
|
|
||||||
<span>
|
|
||||||
{count} {label}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
// QueueSectionContent - collapsible content area
|
|
||||||
export type QueueSectionContentProps = ComponentProps<
|
|
||||||
typeof CollapsibleContent
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const QueueSectionContent = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QueueSectionContentProps) => (
|
|
||||||
<CollapsibleContent className={cn(className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QueueProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const Queue = ({ className, ...props }: QueueProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-2 rounded-xl border border-border bg-background px-3 pt-2 pb-2 shadow-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -1,24 +1,39 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
||||||
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
|
||||||
import { Streamdown } from "streamdown";
|
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { cjk } from "@streamdown/cjk";
|
||||||
|
import { code } from "@streamdown/code";
|
||||||
|
import { math } from "@streamdown/math";
|
||||||
|
import { mermaid } from "@streamdown/mermaid";
|
||||||
|
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Streamdown } from "streamdown";
|
||||||
|
|
||||||
import { Shimmer } from "./shimmer";
|
import { Shimmer } from "./shimmer";
|
||||||
|
|
||||||
type ReasoningContextValue = {
|
interface ReasoningContextValue {
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: (open: boolean) => void;
|
setIsOpen: (open: boolean) => void;
|
||||||
duration: number | undefined;
|
duration: number | undefined;
|
||||||
};
|
}
|
||||||
|
|
||||||
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
||||||
|
|
||||||
|
|
@ -38,7 +53,7 @@ export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
||||||
duration?: number;
|
duration?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUTO_CLOSE_DELAY = 300;
|
const AUTO_CLOSE_DELAY = 1000;
|
||||||
const MS_IN_S = 1000;
|
const MS_IN_S = 1000;
|
||||||
|
|
||||||
export const Reasoning = memo(
|
export const Reasoning = memo(
|
||||||
|
|
@ -46,41 +61,58 @@ export const Reasoning = memo(
|
||||||
className,
|
className,
|
||||||
isStreaming = false,
|
isStreaming = false,
|
||||||
open,
|
open,
|
||||||
defaultOpen = true,
|
defaultOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
duration: durationProp,
|
duration: durationProp,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ReasoningProps) => {
|
}: ReasoningProps) => {
|
||||||
const [isOpen, setIsOpen] = useControllableState({
|
const resolvedDefaultOpen = defaultOpen ?? isStreaming;
|
||||||
prop: open,
|
// Track if defaultOpen was explicitly set to false (to prevent auto-open)
|
||||||
defaultProp: defaultOpen,
|
const isExplicitlyClosed = defaultOpen === false;
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useControllableState<boolean>({
|
||||||
|
defaultProp: resolvedDefaultOpen,
|
||||||
onChange: onOpenChange,
|
onChange: onOpenChange,
|
||||||
|
prop: open,
|
||||||
});
|
});
|
||||||
const [duration, setDuration] = useControllableState({
|
const [duration, setDuration] = useControllableState<number | undefined>({
|
||||||
prop: durationProp,
|
|
||||||
defaultProp: undefined,
|
defaultProp: undefined,
|
||||||
|
prop: durationProp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasEverStreamedRef = useRef(isStreaming);
|
||||||
const [hasAutoClosed, setHasAutoClosed] = useState(false);
|
const [hasAutoClosed, setHasAutoClosed] = useState(false);
|
||||||
const [startTime, setStartTime] = useState<number | null>(null);
|
const startTimeRef = useRef<number | null>(null);
|
||||||
|
|
||||||
// Track duration when streaming starts and ends
|
// Track when streaming starts and compute duration
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
if (startTime === null) {
|
hasEverStreamedRef.current = true;
|
||||||
setStartTime(Date.now());
|
if (startTimeRef.current === null) {
|
||||||
|
startTimeRef.current = Date.now();
|
||||||
}
|
}
|
||||||
} else if (startTime !== null) {
|
} else if (startTimeRef.current !== null) {
|
||||||
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S));
|
setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S));
|
||||||
setStartTime(null);
|
startTimeRef.current = null;
|
||||||
}
|
}
|
||||||
}, [isStreaming, startTime, setDuration]);
|
}, [isStreaming, setDuration]);
|
||||||
|
|
||||||
// Auto-open when streaming starts, auto-close when streaming ends (once only)
|
// Auto-open when streaming starts (unless explicitly closed)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
|
if (isStreaming && !isOpen && !isExplicitlyClosed) {
|
||||||
// Add a small delay before closing to allow user to see the content
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
}, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
|
||||||
|
|
||||||
|
// Auto-close when streaming ends (once only, and only if it ever streamed)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
hasEverStreamedRef.current &&
|
||||||
|
!isStreaming &&
|
||||||
|
isOpen &&
|
||||||
|
!hasAutoClosed
|
||||||
|
) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setHasAutoClosed(true);
|
setHasAutoClosed(true);
|
||||||
|
|
@ -88,18 +120,24 @@ export const Reasoning = memo(
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]);
|
}, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
|
||||||
|
|
||||||
const handleOpenChange = (newOpen: boolean) => {
|
const handleOpenChange = useCallback(
|
||||||
setIsOpen(newOpen);
|
(newOpen: boolean) => {
|
||||||
};
|
setIsOpen(newOpen);
|
||||||
|
},
|
||||||
|
[setIsOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({ duration, isOpen, isStreaming, setIsOpen }),
|
||||||
|
[duration, isOpen, isStreaming, setIsOpen]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReasoningContext.Provider
|
<ReasoningContext.Provider value={contextValue}>
|
||||||
value={{ isStreaming, isOpen, setIsOpen, duration }}
|
|
||||||
>
|
|
||||||
<Collapsible
|
<Collapsible
|
||||||
className={cn("not-prose mb-2", className)}
|
className={cn("not-prose mb-4", className)}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -119,12 +157,12 @@ export type ReasoningTriggerProps = ComponentProps<
|
||||||
|
|
||||||
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
||||||
if (isStreaming || duration === 0) {
|
if (isStreaming || duration === 0) {
|
||||||
return <Shimmer duration={1}>Thinking</Shimmer>;
|
return <Shimmer duration={1}>Thinking...</Shimmer>;
|
||||||
}
|
}
|
||||||
if (duration === undefined) {
|
if (duration === undefined) {
|
||||||
return <span>Thought</span>;
|
return <p>Thought for a few seconds</p>;
|
||||||
}
|
}
|
||||||
return <span>{duration}s</span>;
|
return <p>Thought for {duration} seconds</p>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReasoningTrigger = memo(
|
export const ReasoningTrigger = memo(
|
||||||
|
|
@ -139,18 +177,18 @@ export const ReasoningTrigger = memo(
|
||||||
return (
|
return (
|
||||||
<CollapsibleTrigger
|
<CollapsibleTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
<BrainIcon className="size-3" />
|
<BrainIcon className="size-4" />
|
||||||
{getThinkingMessage(isStreaming, duration)}
|
{getThinkingMessage(isStreaming, duration)}
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-2.5 transition-transform",
|
"size-4 transition-transform",
|
||||||
isOpen ? "rotate-180" : "rotate-0"
|
isOpen ? "rotate-180" : "rotate-0"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -167,19 +205,21 @@ export type ReasoningContentProps = ComponentProps<
|
||||||
children: string;
|
children: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const streamdownPlugins = { cjk, code, math, mermaid };
|
||||||
|
|
||||||
export const ReasoningContent = memo(
|
export const ReasoningContent = memo(
|
||||||
({ className, children, ...props }: ReasoningContentProps) => (
|
({ className, children, ...props }: ReasoningContentProps) => (
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-1.5 text-[11px] leading-relaxed",
|
"mt-4 text-sm",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="max-h-48 overflow-y-auto rounded-md border border-border/50 bg-muted/30 p-2.5 text-[11px] **:text-[11px] [&_li]:my-0 [&_ol]:my-1 [&_p]:my-0 [&_ul]:my-1">
|
<Streamdown plugins={streamdownPlugins} {...props}>
|
||||||
<Streamdown>{children}</Streamdown>
|
{children}
|
||||||
</div>
|
</Streamdown>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,36 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { motion } from "motion/react";
|
import type { MotionProps } from "motion/react";
|
||||||
import {
|
import type { CSSProperties, ElementType, JSX } from "react";
|
||||||
type CSSProperties,
|
|
||||||
type ElementType,
|
|
||||||
type JSX,
|
|
||||||
memo,
|
|
||||||
useMemo,
|
|
||||||
} from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type TextShimmerProps = {
|
import { cn } from "@/lib/utils";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { memo, useMemo } from "react";
|
||||||
|
|
||||||
|
type MotionHTMLProps = MotionProps & Record<string, unknown>;
|
||||||
|
|
||||||
|
// Cache motion components at module level to avoid creating during render
|
||||||
|
const motionComponentCache = new Map<
|
||||||
|
keyof JSX.IntrinsicElements,
|
||||||
|
React.ComponentType<MotionHTMLProps>
|
||||||
|
>();
|
||||||
|
|
||||||
|
const getMotionComponent = (element: keyof JSX.IntrinsicElements) => {
|
||||||
|
let component = motionComponentCache.get(element);
|
||||||
|
if (!component) {
|
||||||
|
component = motion.create(element);
|
||||||
|
motionComponentCache.set(element, component);
|
||||||
|
}
|
||||||
|
return component;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TextShimmerProps {
|
||||||
children: string;
|
children: string;
|
||||||
as?: ElementType;
|
as?: ElementType;
|
||||||
className?: string;
|
className?: string;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
spread?: number;
|
spread?: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
const ShimmerComponent = ({
|
const ShimmerComponent = ({
|
||||||
children,
|
children,
|
||||||
|
|
@ -25,7 +39,7 @@ const ShimmerComponent = ({
|
||||||
duration = 2,
|
duration = 2,
|
||||||
spread = 2,
|
spread = 2,
|
||||||
}: TextShimmerProps) => {
|
}: TextShimmerProps) => {
|
||||||
const MotionComponent = motion.create(
|
const MotionComponent = getMotionComponent(
|
||||||
Component as keyof JSX.IntrinsicElements
|
Component as keyof JSX.IntrinsicElements
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -51,9 +65,9 @@ const ShimmerComponent = ({
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
transition={{
|
transition={{
|
||||||
repeat: Number.POSITIVE_INFINITY,
|
|
||||||
duration,
|
duration,
|
||||||
ease: "linear",
|
ease: "linear",
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { BookIcon, ChevronDownIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type SourcesProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const Sources = ({ className, ...props }: SourcesProps) => (
|
|
||||||
<Collapsible
|
|
||||||
className={cn("not-prose mb-4 text-primary text-xs", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SourcesTrigger = ({
|
|
||||||
className,
|
|
||||||
count,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: SourcesTriggerProps) => (
|
|
||||||
<CollapsibleTrigger
|
|
||||||
className={cn("flex items-center gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? (
|
|
||||||
<>
|
|
||||||
<p className="font-medium">Used {count} sources</p>
|
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
|
|
||||||
|
|
||||||
export const SourcesContent = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: SourcesContentProps) => (
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"mt-3 flex w-fit flex-col gap-2",
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type SourceProps = ComponentProps<"a">;
|
|
||||||
|
|
||||||
export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
href={href}
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? (
|
|
||||||
<>
|
|
||||||
<BookIcon className="h-4 w-4" />
|
|
||||||
<span className="block font-medium">{title}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import {
|
||||||
|
ScrollArea,
|
||||||
|
ScrollBar,
|
||||||
|
} from "@/components/ui/scroll-area";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
||||||
|
|
||||||
|
|
@ -34,9 +39,9 @@ export const Suggestion = ({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: SuggestionProps) => {
|
}: SuggestionProps) => {
|
||||||
const handleClick = () => {
|
const handleClick = useCallback(() => {
|
||||||
onClick?.(suggestion);
|
onClick?.(suggestion);
|
||||||
};
|
}, [onClick, suggestion]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronDownIcon, SearchIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type TaskItemFileProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const TaskItemFile = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: TaskItemFileProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskItemProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
|
|
||||||
<div className={cn("text-muted-foreground text-sm", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskProps = ComponentProps<typeof Collapsible>;
|
|
||||||
|
|
||||||
export const Task = ({
|
|
||||||
defaultOpen = true,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: TaskProps) => (
|
|
||||||
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TaskTrigger = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
title,
|
|
||||||
...props
|
|
||||||
}: TaskTriggerProps) => (
|
|
||||||
<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">
|
|
||||||
<SearchIcon className="size-4" />
|
|
||||||
<p className="text-sm">{title}</p>
|
|
||||||
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
|
|
||||||
|
|
||||||
export const TaskContent = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: TaskContentProps) => (
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="mt-4 space-y-2 border-muted border-l-2 pl-4">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
);
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ToolUIPart } from "ai";
|
import type { DynamicToolUIPart, ToolUIPart } from "ai";
|
||||||
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
|
|
@ -9,92 +18,95 @@ import {
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
|
||||||
import { isValidElement } from "react";
|
import { isValidElement } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
import { CodeBlock } from "./code-block";
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ToolProps = ComponentProps<typeof Collapsible>;
|
export type ToolProps = ComponentProps<typeof Collapsible>;
|
||||||
|
|
||||||
export const Tool = ({ className, ...props }: ToolProps) => (
|
export const Tool = ({ className, ...props }: ToolProps) => (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
className={cn("not-prose mb-4 w-full rounded-md border", className)}
|
className={cn("group not-prose mb-4 w-full rounded-md border", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type ToolPart = ToolUIPart | DynamicToolUIPart;
|
||||||
|
|
||||||
export type ToolHeaderProps = {
|
export type ToolHeaderProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
type: ToolUIPart["type"];
|
|
||||||
state: ToolUIPart["state"];
|
|
||||||
className?: string;
|
className?: string;
|
||||||
|
} & (
|
||||||
|
| { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never }
|
||||||
|
| {
|
||||||
|
type: DynamicToolUIPart["type"];
|
||||||
|
state: DynamicToolUIPart["state"];
|
||||||
|
toolName: string;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusLabels: Record<ToolPart["state"], string> = {
|
||||||
|
"approval-requested": "Awaiting Approval",
|
||||||
|
"approval-responded": "Responded",
|
||||||
|
"input-available": "Running",
|
||||||
|
"input-streaming": "Pending",
|
||||||
|
"output-available": "Completed",
|
||||||
|
"output-denied": "Denied",
|
||||||
|
"output-error": "Error",
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status: ToolUIPart["state"]) => {
|
const statusIcons: Record<ToolPart["state"], ReactNode> = {
|
||||||
const labels: Record<ToolUIPart["state"], string> = {
|
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
|
||||||
"input-streaming": "Pending",
|
"approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
|
||||||
"input-available": "Running",
|
"input-available": <ClockIcon className="size-4 animate-pulse" />,
|
||||||
"approval-requested": "Awaiting Approval",
|
"input-streaming": <CircleIcon className="size-4" />,
|
||||||
"approval-responded": "Responded",
|
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
|
||||||
"output-available": "Completed",
|
"output-denied": <XCircleIcon className="size-4 text-orange-600" />,
|
||||||
"output-error": "Error",
|
"output-error": <XCircleIcon className="size-4 text-red-600" />,
|
||||||
"output-denied": "Denied",
|
|
||||||
};
|
|
||||||
|
|
||||||
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" />,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
|
|
||||||
{icons[status]}
|
|
||||||
{labels[status]}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getStatusBadge = (status: ToolPart["state"]) => (
|
||||||
|
<Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
|
||||||
|
{statusIcons[status]}
|
||||||
|
{statusLabels[status]}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
|
||||||
export const ToolHeader = ({
|
export const ToolHeader = ({
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
state,
|
state,
|
||||||
|
toolName,
|
||||||
...props
|
...props
|
||||||
}: ToolHeaderProps) => (
|
}: ToolHeaderProps) => {
|
||||||
<CollapsibleTrigger
|
const derivedName =
|
||||||
className={cn(
|
type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-");
|
||||||
"flex w-full items-center justify-between gap-4 p-3",
|
|
||||||
className
|
return (
|
||||||
)}
|
<CollapsibleTrigger
|
||||||
{...props}
|
className={cn(
|
||||||
>
|
"flex w-full items-center justify-between gap-4 p-3",
|
||||||
<div className="flex items-center gap-2">
|
className
|
||||||
<WrenchIcon className="size-4 text-muted-foreground" />
|
)}
|
||||||
<span className="font-medium text-sm">
|
{...props}
|
||||||
{title ?? type.split("-").slice(1).join("-")}
|
>
|
||||||
</span>
|
<div className="flex items-center gap-2">
|
||||||
{getStatusBadge(state)}
|
<WrenchIcon className="size-4 text-muted-foreground" />
|
||||||
</div>
|
<span className="font-medium text-sm">{title ?? derivedName}</span>
|
||||||
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
{getStatusBadge(state)}
|
||||||
</CollapsibleTrigger>
|
</div>
|
||||||
);
|
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
|
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||||
|
|
||||||
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
|
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -102,23 +114,23 @@ export const ToolContent = ({ className, ...props }: ToolContentProps) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ToolInputProps = ComponentProps<"div"> & {
|
export type ToolInputProps = ComponentProps<"div"> & {
|
||||||
input: ToolUIPart["input"];
|
input: ToolPart["input"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
||||||
<div className={cn("space-y-2 overflow-hidden p-4", className)} {...props}>
|
<div className={cn("space-y-2 overflow-hidden", className)} {...props}>
|
||||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
Parameters
|
Parameters
|
||||||
</h4>
|
</h4>
|
||||||
<pre className="overflow-x-auto rounded-md bg-muted/50 p-3 font-mono text-xs">
|
<div className="rounded-md bg-muted/50">
|
||||||
{JSON.stringify(input, null, 2)}
|
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
||||||
</pre>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ToolOutputProps = ComponentProps<"div"> & {
|
export type ToolOutputProps = ComponentProps<"div"> & {
|
||||||
output: ToolUIPart["output"];
|
output: ToolPart["output"];
|
||||||
errorText: ToolUIPart["errorText"];
|
errorText: ToolPart["errorText"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ToolOutput = ({
|
export const ToolOutput = ({
|
||||||
|
|
@ -131,24 +143,18 @@ export const ToolOutput = ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderOutput = () => {
|
let Output = <div>{output as ReactNode}</div>;
|
||||||
if (typeof output === "object" && !isValidElement(output)) {
|
|
||||||
return (
|
if (typeof output === "object" && !isValidElement(output)) {
|
||||||
<pre className="overflow-x-auto p-3 font-mono text-xs">
|
Output = (
|
||||||
{JSON.stringify(output, null, 2)}
|
<CodeBlock code={JSON.stringify(output, null, 2)} language="json" />
|
||||||
</pre>
|
);
|
||||||
);
|
} else if (typeof output === "string") {
|
||||||
}
|
Output = <CodeBlock code={output} language="json" />;
|
||||||
if (typeof output === "string") {
|
}
|
||||||
return (
|
|
||||||
<pre className="overflow-x-auto p-3 font-mono text-xs">{output}</pre>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <div className="p-3">{output as ReactNode}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-2 p-4", className)} {...props}>
|
<div className={cn("space-y-2", className)} {...props}>
|
||||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
{errorText ? "Error" : "Result"}
|
{errorText ? "Error" : "Result"}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
@ -160,8 +166,8 @@ export const ToolOutput = ({
|
||||||
: "bg-muted/50 text-foreground"
|
: "bg-muted/50 text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{errorText && <div className="p-3">{errorText}</div>}
|
{errorText && <div>{errorText}</div>}
|
||||||
{!errorText && renderOutput()}
|
{Output}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { NodeToolbar, Position } from "@xyflow/react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type ToolbarProps = ComponentProps<typeof NodeToolbar>;
|
|
||||||
|
|
||||||
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
|
|
||||||
<NodeToolbar
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1 rounded-sm border bg-background p-1.5",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
position={Position.Bottom}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -1,263 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type WebPreviewContextValue = {
|
|
||||||
url: string;
|
|
||||||
setUrl: (url: string) => void;
|
|
||||||
consoleOpen: boolean;
|
|
||||||
setConsoleOpen: (open: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
|
|
||||||
|
|
||||||
const useWebPreview = () => {
|
|
||||||
const context = useContext(WebPreviewContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("WebPreview components must be used within a WebPreview");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WebPreviewProps = ComponentProps<"div"> & {
|
|
||||||
defaultUrl?: string;
|
|
||||||
onUrlChange?: (url: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WebPreview = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
defaultUrl = "",
|
|
||||||
onUrlChange,
|
|
||||||
...props
|
|
||||||
}: WebPreviewProps) => {
|
|
||||||
const [url, setUrl] = useState(defaultUrl);
|
|
||||||
const [consoleOpen, setConsoleOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleUrlChange = (newUrl: string) => {
|
|
||||||
setUrl(newUrl);
|
|
||||||
onUrlChange?.(newUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
const contextValue: WebPreviewContextValue = {
|
|
||||||
url,
|
|
||||||
setUrl: handleUrlChange,
|
|
||||||
consoleOpen,
|
|
||||||
setConsoleOpen,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WebPreviewContext.Provider value={contextValue}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex size-full flex-col rounded-lg border bg-card",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</WebPreviewContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WebPreviewNavigationProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const WebPreviewNavigation = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: WebPreviewNavigationProps) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex items-center gap-1 border-b p-2", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
|
|
||||||
tooltip?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WebPreviewNavigationButton = ({
|
|
||||||
onClick,
|
|
||||||
disabled,
|
|
||||||
tooltip,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: WebPreviewNavigationButtonProps) => (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className="h-8 w-8 p-0 hover:text-foreground"
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={onClick}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{tooltip}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type WebPreviewUrlProps = ComponentProps<typeof Input>;
|
|
||||||
|
|
||||||
export const WebPreviewUrl = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onKeyDown,
|
|
||||||
...props
|
|
||||||
}: WebPreviewUrlProps) => {
|
|
||||||
const { url, setUrl } = useWebPreview();
|
|
||||||
const [inputValue, setInputValue] = useState(url);
|
|
||||||
|
|
||||||
// Sync input value with context URL when it changes externally
|
|
||||||
useEffect(() => {
|
|
||||||
setInputValue(url);
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setInputValue(event.target.value);
|
|
||||||
onChange?.(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
setUrl(target.value);
|
|
||||||
}
|
|
||||||
onKeyDown?.(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
className="h-8 flex-1 text-sm"
|
|
||||||
onChange={onChange ?? handleChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="Enter URL..."
|
|
||||||
value={value ?? inputValue}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
|
|
||||||
loading?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WebPreviewBody = ({
|
|
||||||
className,
|
|
||||||
loading,
|
|
||||||
src,
|
|
||||||
...props
|
|
||||||
}: WebPreviewBodyProps) => {
|
|
||||||
const { url } = useWebPreview();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex-1">
|
|
||||||
<iframe
|
|
||||||
className={cn("size-full", className)}
|
|
||||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
|
|
||||||
src={(src ?? url) || undefined}
|
|
||||||
title="Preview"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
{loading}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WebPreviewConsoleProps = ComponentProps<"div"> & {
|
|
||||||
logs?: Array<{
|
|
||||||
level: "log" | "warn" | "error";
|
|
||||||
message: string;
|
|
||||||
timestamp: Date;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WebPreviewConsole = ({
|
|
||||||
className,
|
|
||||||
logs = [],
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: WebPreviewConsoleProps) => {
|
|
||||||
const { consoleOpen, setConsoleOpen } = useWebPreview();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapsible
|
|
||||||
className={cn("border-t bg-muted/50 font-mono text-sm", className)}
|
|
||||||
onOpenChange={setConsoleOpen}
|
|
||||||
open={consoleOpen}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className="flex w-full items-center justify-between p-4 text-left font-medium hover:bg-muted/50"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
Console
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4 transition-transform duration-200",
|
|
||||||
consoleOpen && "rotate-180"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"px-4 pb-4",
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="max-h-48 space-y-1 overflow-y-auto">
|
|
||||||
{logs.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground">No console output</p>
|
|
||||||
) : (
|
|
||||||
logs.map((log, index) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"text-xs",
|
|
||||||
log.level === "error" && "text-destructive",
|
|
||||||
log.level === "warn" && "text-yellow-600",
|
|
||||||
log.level === "log" && "text-foreground"
|
|
||||||
)}
|
|
||||||
key={`${log.timestamp.getTime()}-${index}`}
|
|
||||||
>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{log.timestamp.toLocaleTimeString()}
|
|
||||||
</span>{" "}
|
|
||||||
{log.message}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -83,8 +83,8 @@ export function AppSidebar({ user }: { user: User | undefined }) {
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="h-8 p-1 md:h-fit md:p-2"
|
|
||||||
onClick={() => setShowDeleteAllDialog(true)}
|
onClick={() => setShowDeleteAllDialog(true)}
|
||||||
|
size="icon-sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
|
|
@ -99,12 +99,12 @@ export function AppSidebar({ user }: { user: User | undefined }) {
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="h-8 p-1 md:h-fit md:p-2"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenMobile(false);
|
setOpenMobile(false);
|
||||||
router.push("/");
|
router.push("/");
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}}
|
}}
|
||||||
|
size="icon-sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,12 @@ function PureChatHeader({
|
||||||
|
|
||||||
{(!open || windowWidth < 768) && (
|
{(!open || windowWidth < 768) && (
|
||||||
<Button
|
<Button
|
||||||
className="order-2 ml-auto h-8 px-2 md:order-1 md:ml-0 md:h-fit md:px-2"
|
className="order-2 ml-auto md:order-1 md:ml-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}}
|
}}
|
||||||
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ import {
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useArtifactSelector } from "@/hooks/use-artifact";
|
import { useArtifactSelector } from "@/hooks/use-artifact";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Loader } from "./elements/loader";
|
|
||||||
import { CrossSmallIcon, TerminalWindowIcon } from "./icons";
|
import { CrossSmallIcon, TerminalWindowIcon } from "./icons";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import { Spinner } from "./ui/spinner";
|
||||||
|
|
||||||
export type ConsoleOutputContent = {
|
export type ConsoleOutputContent = {
|
||||||
type: "text" | "image";
|
type: "text" | "image";
|
||||||
|
|
@ -148,7 +148,7 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
||||||
) ? (
|
) ? (
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<div className="mt-0.5 mb-auto size-fit self-center">
|
<div className="mt-0.5 mb-auto size-fit self-center">
|
||||||
<Loader size={16} />
|
<Spinner className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
{consoleOutput.status === "in_progress"
|
{consoleOutput.status === "in_progress"
|
||||||
|
|
@ -162,9 +162,9 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full flex-col gap-2 overflow-x-scroll text-zinc-900 dark:text-zinc-50">
|
<div className="flex w-full flex-col gap-2 overflow-x-scroll text-zinc-900 dark:text-zinc-50">
|
||||||
{consoleOutput.contents.map((content, contentIndex) =>
|
{consoleOutput.contents.map((content) =>
|
||||||
content.type === "image" ? (
|
content.type === "image" ? (
|
||||||
<picture key={`${consoleOutput.id}-${contentIndex}`}>
|
<picture key={`${consoleOutput.id}-${content.value}`}>
|
||||||
<img
|
<img
|
||||||
alt="output"
|
alt="output"
|
||||||
className="w-full max-w-(--breakpoint-toast-mobile) rounded-md"
|
className="w-full max-w-(--breakpoint-toast-mobile) rounded-md"
|
||||||
|
|
@ -174,7 +174,7 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="w-full whitespace-pre-line break-words"
|
className="w-full whitespace-pre-line break-words"
|
||||||
key={`${consoleOutput.id}-${contentIndex}`}
|
key={`${consoleOutput.id}-${content.value}`}
|
||||||
>
|
>
|
||||||
{content.value}
|
{content.value}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ActionsProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const Actions = ({ className, children, ...props }: ActionsProps) => (
|
|
||||||
<div className={cn("flex items-center gap-1", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ActionProps = ComponentProps<typeof Button> & {
|
|
||||||
tooltip?: string;
|
|
||||||
label?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Action = ({
|
|
||||||
tooltip,
|
|
||||||
children,
|
|
||||||
label,
|
|
||||||
className,
|
|
||||||
variant = "ghost",
|
|
||||||
size = "sm",
|
|
||||||
...props
|
|
||||||
}: ActionProps) => {
|
|
||||||
const button = (
|
|
||||||
<Button
|
|
||||||
className={cn(
|
|
||||||
"relative size-9 p-1.5 text-muted-foreground hover:text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
size={size}
|
|
||||||
type="button"
|
|
||||||
variant={variant}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<span className="sr-only">{label || tooltip}</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tooltip) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{tooltip}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return button;
|
|
||||||
};
|
|
||||||
|
|
@ -1,215 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type { UIMessage } from "ai";
|
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
|
||||||
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type BranchContextType = {
|
|
||||||
currentBranch: number;
|
|
||||||
totalBranches: number;
|
|
||||||
goToPrevious: () => void;
|
|
||||||
goToNext: () => void;
|
|
||||||
branches: ReactElement[];
|
|
||||||
setBranches: (branches: ReactElement[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BranchContext = createContext<BranchContextType | null>(null);
|
|
||||||
|
|
||||||
const useBranch = () => {
|
|
||||||
const context = useContext(BranchContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("Branch components must be used within Branch");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BranchProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
defaultBranch?: number;
|
|
||||||
onBranchChange?: (branchIndex: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Branch = ({
|
|
||||||
defaultBranch = 0,
|
|
||||||
onBranchChange,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: BranchProps) => {
|
|
||||||
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
|
||||||
const [branches, setBranches] = useState<ReactElement[]>([]);
|
|
||||||
|
|
||||||
const handleBranchChange = (newBranch: number) => {
|
|
||||||
setCurrentBranch(newBranch);
|
|
||||||
onBranchChange?.(newBranch);
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToPrevious = () => {
|
|
||||||
const newBranch =
|
|
||||||
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
|
||||||
handleBranchChange(newBranch);
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToNext = () => {
|
|
||||||
const newBranch =
|
|
||||||
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
|
||||||
handleBranchChange(newBranch);
|
|
||||||
};
|
|
||||||
|
|
||||||
const contextValue: BranchContextType = {
|
|
||||||
currentBranch,
|
|
||||||
totalBranches: branches.length,
|
|
||||||
goToPrevious,
|
|
||||||
goToNext,
|
|
||||||
branches,
|
|
||||||
setBranches,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BranchContext.Provider value={contextValue}>
|
|
||||||
<div
|
|
||||||
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</BranchContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BranchMessagesProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => {
|
|
||||||
const { currentBranch, setBranches, branches } = useBranch();
|
|
||||||
const childrenArray = useMemo(
|
|
||||||
() => (Array.isArray(children) ? children : [children]),
|
|
||||||
[children]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use useEffect to update branches when they change
|
|
||||||
useEffect(() => {
|
|
||||||
if (branches.length !== childrenArray.length) {
|
|
||||||
setBranches(childrenArray);
|
|
||||||
}
|
|
||||||
}, [childrenArray, branches, setBranches]);
|
|
||||||
|
|
||||||
return childrenArray.map((branch, index) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"grid gap-2 overflow-hidden [&>div]:pb-0",
|
|
||||||
index === currentBranch ? "block" : "hidden"
|
|
||||||
)}
|
|
||||||
key={branch.key}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{branch}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
from: UIMessage["role"];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BranchSelector = ({
|
|
||||||
className,
|
|
||||||
from,
|
|
||||||
...props
|
|
||||||
}: BranchSelectorProps) => {
|
|
||||||
const { totalBranches } = useBranch();
|
|
||||||
|
|
||||||
// Don't render if there's only one branch
|
|
||||||
if (totalBranches <= 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 self-end px-10",
|
|
||||||
from === "assistant" ? "justify-start" : "justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BranchPreviousProps = ComponentProps<typeof Button>;
|
|
||||||
|
|
||||||
export const BranchPrevious = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: BranchPreviousProps) => {
|
|
||||||
const { goToPrevious, totalBranches } = useBranch();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
aria-label="Previous branch"
|
|
||||||
className={cn(
|
|
||||||
"size-7 shrink-0 rounded-full text-muted-foreground transition-colors",
|
|
||||||
"hover:bg-accent hover:text-foreground",
|
|
||||||
"disabled:pointer-events-none disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
disabled={totalBranches <= 1}
|
|
||||||
onClick={goToPrevious}
|
|
||||||
size="icon"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? <ChevronLeftIcon size={14} />}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BranchNextProps = ComponentProps<typeof Button>;
|
|
||||||
|
|
||||||
export const BranchNext = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: BranchNextProps) => {
|
|
||||||
const { goToNext, totalBranches } = useBranch();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
aria-label="Next branch"
|
|
||||||
className={cn(
|
|
||||||
"size-7 shrink-0 rounded-full text-muted-foreground transition-colors",
|
|
||||||
"hover:bg-accent hover:text-foreground",
|
|
||||||
"disabled:pointer-events-none disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
disabled={totalBranches <= 1}
|
|
||||||
onClick={goToNext}
|
|
||||||
size="icon"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? <ChevronRightIcon size={14} />}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BranchPageProps = HTMLAttributes<HTMLSpanElement>;
|
|
||||||
|
|
||||||
export const BranchPage = ({ className, ...props }: BranchPageProps) => {
|
|
||||||
const { currentBranch, totalBranches } = useBranch();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"font-medium text-muted-foreground text-xs tabular-nums",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{currentBranch + 1} of {totalBranches}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ArrowDownIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
|
||||||
|
|
||||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
|
||||||
<StickToBottom
|
|
||||||
className={cn(
|
|
||||||
"relative flex-1 touch-pan-y overflow-y-auto will-change-scroll",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
initial="smooth"
|
|
||||||
resize="smooth"
|
|
||||||
role="log"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ConversationContentProps = ComponentProps<
|
|
||||||
typeof StickToBottom.Content
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const ConversationContent = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ConversationContentProps) => (
|
|
||||||
<StickToBottom.Content className={cn("p-4", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
|
|
||||||
|
|
||||||
export const ConversationScrollButton = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ConversationScrollButtonProps) => {
|
|
||||||
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
|
||||||
|
|
||||||
const handleScrollToBottom = useCallback(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
}, [scrollToBottom]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
!isAtBottom && (
|
|
||||||
<Button
|
|
||||||
className={cn(
|
|
||||||
"absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full shadow-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
onClick={handleScrollToBottom}
|
|
||||||
size="icon"
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowDownIcon className="size-4" />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import type { Experimental_GeneratedImage } from "ai";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ImageProps = Experimental_GeneratedImage & {
|
|
||||||
className?: string;
|
|
||||||
alt?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Image = ({
|
|
||||||
base64,
|
|
||||||
uint8Array,
|
|
||||||
mediaType,
|
|
||||||
...props
|
|
||||||
}: ImageProps) => (
|
|
||||||
// biome-ignore lint/performance/noImgElement: base64 data URLs require native img
|
|
||||||
<img
|
|
||||||
{...props}
|
|
||||||
alt={props.alt}
|
|
||||||
className={cn(
|
|
||||||
"h-auto max-w-full overflow-hidden rounded-md",
|
|
||||||
props.className
|
|
||||||
)}
|
|
||||||
src={`data:${mediaType};base64,${base64}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -1,287 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
|
||||||
import {
|
|
||||||
type ComponentProps,
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Carousel,
|
|
||||||
type CarouselApi,
|
|
||||||
CarouselContent,
|
|
||||||
CarouselItem,
|
|
||||||
} from "@/components/ui/carousel";
|
|
||||||
import {
|
|
||||||
HoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardTrigger,
|
|
||||||
} from "@/components/ui/hover-card";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type InlineCitationProps = ComponentProps<"span">;
|
|
||||||
|
|
||||||
export const InlineCitation = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationProps) => (
|
|
||||||
<span
|
|
||||||
className={cn("group inline items-center gap-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationTextProps = ComponentProps<"span">;
|
|
||||||
|
|
||||||
export const InlineCitationText = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationTextProps) => (
|
|
||||||
<span
|
|
||||||
className={cn("transition-colors group-hover:bg-accent", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
|
|
||||||
|
|
||||||
export const InlineCitationCard = (props: InlineCitationCardProps) => (
|
|
||||||
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
|
|
||||||
sources: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InlineCitationCardTrigger = ({
|
|
||||||
sources,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCardTriggerProps) => (
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<Badge
|
|
||||||
className={cn("ml-1 rounded-full", className)}
|
|
||||||
variant="secondary"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{sources.length ? (
|
|
||||||
<>
|
|
||||||
{new URL(sources[0]).hostname}{" "}
|
|
||||||
{sources.length > 1 && `+${sources.length - 1}`}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"unknown"
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCardBodyProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCardBody = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCardBodyProps) => (
|
|
||||||
<HoverCardContent className={cn("relative w-80 p-0", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
|
|
||||||
|
|
||||||
const useCarouselApi = () => {
|
|
||||||
const context = useContext(CarouselApiContext);
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;
|
|
||||||
|
|
||||||
export const InlineCitationCarousel = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselProps) => {
|
|
||||||
const [api, setApi] = useState<CarouselApi>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CarouselApiContext.Provider value={api}>
|
|
||||||
<Carousel className={cn("w-full", className)} setApi={setApi} {...props}>
|
|
||||||
{children}
|
|
||||||
</Carousel>
|
|
||||||
</CarouselApiContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationCarouselContentProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselContent = (
|
|
||||||
props: InlineCitationCarouselContentProps
|
|
||||||
) => <CarouselContent {...props} />;
|
|
||||||
|
|
||||||
export type InlineCitationCarouselItemProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselItem = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselItemProps) => (
|
|
||||||
<CarouselItem
|
|
||||||
className={cn("w-full space-y-2 p-4 pl-8", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCarouselHeaderProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselHeaderProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationCarouselIndexProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselIndex = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselIndexProps) => {
|
|
||||||
const api = useCarouselApi();
|
|
||||||
const [current, setCurrent] = useState(0);
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!api) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCount(api.scrollSnapList().length);
|
|
||||||
setCurrent(api.selectedScrollSnap() + 1);
|
|
||||||
|
|
||||||
api.on("select", () => {
|
|
||||||
setCurrent(api.selectedScrollSnap() + 1);
|
|
||||||
});
|
|
||||||
}, [api]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? `${current}/${count}`}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationCarouselPrevProps = ComponentProps<"button">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselPrev = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselPrevProps) => {
|
|
||||||
const api = useCarouselApi();
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
if (api) {
|
|
||||||
api.scrollPrev();
|
|
||||||
}
|
|
||||||
}, [api]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
aria-label="Previous"
|
|
||||||
className={cn("shrink-0", className)}
|
|
||||||
onClick={handleClick}
|
|
||||||
type="button"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon className="size-4 text-muted-foreground" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationCarouselNextProps = ComponentProps<"button">;
|
|
||||||
|
|
||||||
export const InlineCitationCarouselNext = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationCarouselNextProps) => {
|
|
||||||
const api = useCarouselApi();
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
if (api) {
|
|
||||||
api.scrollNext();
|
|
||||||
}
|
|
||||||
}, [api]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
aria-label="Next"
|
|
||||||
className={cn("shrink-0", className)}
|
|
||||||
onClick={handleClick}
|
|
||||||
type="button"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowRightIcon className="size-4 text-muted-foreground" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InlineCitationSourceProps = ComponentProps<"div"> & {
|
|
||||||
title?: string;
|
|
||||||
url?: string;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InlineCitationSource = ({
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
description,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: InlineCitationSourceProps) => (
|
|
||||||
<div className={cn("space-y-1", className)} {...props}>
|
|
||||||
{title && (
|
|
||||||
<h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
|
|
||||||
)}
|
|
||||||
{url && (
|
|
||||||
<p className="truncate break-all text-muted-foreground text-xs">{url}</p>
|
|
||||||
)}
|
|
||||||
{description && (
|
|
||||||
<p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type InlineCitationQuoteProps = ComponentProps<"blockquote">;
|
|
||||||
|
|
||||||
export const InlineCitationQuote = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: InlineCitationQuoteProps) => (
|
|
||||||
<blockquote
|
|
||||||
className={cn(
|
|
||||||
"border-muted border-l-2 pl-3 text-muted-foreground text-sm italic",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</blockquote>
|
|
||||||
);
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import type { HTMLAttributes } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type LoaderIconProps = {
|
|
||||||
size?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
|
|
||||||
<svg
|
|
||||||
height={size}
|
|
||||||
strokeLinejoin="round"
|
|
||||||
style={{ color: "currentcolor" }}
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
width={size}
|
|
||||||
>
|
|
||||||
<title>Loader</title>
|
|
||||||
<g clipPath="url(#clip0_2393_1490)">
|
|
||||||
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
|
|
||||||
<path
|
|
||||||
d="M8 16V12"
|
|
||||||
opacity="0.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3.29773 1.52783L5.64887 4.7639"
|
|
||||||
opacity="0.9"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.7023 1.52783L10.3511 4.7639"
|
|
||||||
opacity="0.1"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.7023 14.472L10.3511 11.236"
|
|
||||||
opacity="0.4"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3.29773 14.472L5.64887 11.236"
|
|
||||||
opacity="0.6"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M15.6085 5.52783L11.8043 6.7639"
|
|
||||||
opacity="0.2"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M0.391602 10.472L4.19583 9.23598"
|
|
||||||
opacity="0.7"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M15.6085 10.4722L11.8043 9.2361"
|
|
||||||
opacity="0.3"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M0.391602 5.52783L4.19583 6.7639"
|
|
||||||
opacity="0.8"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_2393_1490">
|
|
||||||
<rect fill="white" height="16" width="16" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
size?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex animate-spin items-center justify-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<LoaderIcon size={size} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import type { UIMessage } from "ai";
|
|
||||||
import type { ComponentProps, HTMLAttributes } from "react";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
from: UIMessage["role"];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Message = ({ className, from, ...props }: MessageProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"group flex w-full items-end justify-end gap-2 py-4",
|
|
||||||
from === "user" ? "is-user" : "is-assistant flex-row-reverse justify-end",
|
|
||||||
"[&>div]:max-w-[80%]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const MessageContent = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: MessageContentProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-2 overflow-hidden rounded-lg px-4 py-3 text-foreground text-sm",
|
|
||||||
"group-[.is-user]:bg-primary group-[.is-user]:text-primary-foreground",
|
|
||||||
"group-[.is-assistant]:bg-secondary group-[.is-assistant]:text-foreground",
|
|
||||||
"is-user:dark",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type MessageAvatarProps = ComponentProps<typeof Avatar> & {
|
|
||||||
src: string;
|
|
||||||
name?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MessageAvatar = ({
|
|
||||||
src,
|
|
||||||
name,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: MessageAvatarProps) => (
|
|
||||||
<Avatar className={cn("size-8 ring-1 ring-border", className)} {...props}>
|
|
||||||
<AvatarImage alt="" className="my-0" src={src} />
|
|
||||||
<AvatarFallback>{name?.slice(0, 2) || "ME"}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
);
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type { ChatStatus } from "ai";
|
|
||||||
import { Loader2Icon, SendIcon, SquareIcon, XIcon } from "lucide-react";
|
|
||||||
import type {
|
|
||||||
ComponentProps,
|
|
||||||
HTMLAttributes,
|
|
||||||
KeyboardEventHandler,
|
|
||||||
} from "react";
|
|
||||||
import { Children } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type PromptInputProps = HTMLAttributes<HTMLFormElement>;
|
|
||||||
|
|
||||||
export const PromptInput = ({ className, ...props }: PromptInputProps) => (
|
|
||||||
<form
|
|
||||||
className={cn(
|
|
||||||
"w-full overflow-hidden rounded-xl border bg-background shadow-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PromptInputTextareaProps = ComponentProps<typeof Textarea> & {
|
|
||||||
minHeight?: number;
|
|
||||||
maxHeight?: number;
|
|
||||||
disableAutoResize?: boolean;
|
|
||||||
resizeOnNewLinesOnly?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PromptInputTextarea = ({
|
|
||||||
onChange,
|
|
||||||
className,
|
|
||||||
placeholder = "What would you like to know?",
|
|
||||||
minHeight = 48,
|
|
||||||
maxHeight = 164,
|
|
||||||
disableAutoResize = false,
|
|
||||||
resizeOnNewLinesOnly = false,
|
|
||||||
...props
|
|
||||||
}: PromptInputTextareaProps) => {
|
|
||||||
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
if (e.nativeEvent.isComposing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.shiftKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const form = e.currentTarget.form;
|
|
||||||
const submitButton = form?.querySelector(
|
|
||||||
'button[type="submit"]'
|
|
||||||
) as HTMLButtonElement | null;
|
|
||||||
if (submitButton?.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
form?.requestSubmit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Textarea
|
|
||||||
className={cn(
|
|
||||||
"w-full resize-none rounded-none border-none p-3 shadow-none outline-hidden ring-0",
|
|
||||||
disableAutoResize
|
|
||||||
? "field-sizing-fixed"
|
|
||||||
: resizeOnNewLinesOnly
|
|
||||||
? "field-sizing-fixed"
|
|
||||||
: "field-sizing-content max-h-[6lh]",
|
|
||||||
"bg-transparent dark:bg-transparent",
|
|
||||||
"focus-visible:ring-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
name="message"
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange?.(e);
|
|
||||||
}}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder={placeholder}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PromptInputToolbarProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const PromptInputToolbar = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PromptInputToolbarProps) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex items-center justify-between p-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const PromptInputTools = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PromptInputToolsProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1",
|
|
||||||
"[&_button:first-child]:rounded-bl-xl",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PromptInputButtonProps = ComponentProps<typeof Button>;
|
|
||||||
|
|
||||||
export const PromptInputButton = ({
|
|
||||||
variant = "ghost",
|
|
||||||
className,
|
|
||||||
size,
|
|
||||||
...props
|
|
||||||
}: PromptInputButtonProps) => {
|
|
||||||
const newSize =
|
|
||||||
(size ?? Children.count(props.children) > 1) ? "default" : "icon";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 gap-1.5 rounded-lg",
|
|
||||||
variant === "ghost" && "text-muted-foreground",
|
|
||||||
newSize === "default" && "px-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
size={newSize}
|
|
||||||
type="button"
|
|
||||||
variant={variant}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PromptInputSubmitProps = ComponentProps<typeof Button> & {
|
|
||||||
status?: ChatStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PromptInputSubmit = ({
|
|
||||||
className,
|
|
||||||
variant = "default",
|
|
||||||
size = "icon",
|
|
||||||
status,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: PromptInputSubmitProps) => {
|
|
||||||
let Icon = <SendIcon className="size-4" />;
|
|
||||||
|
|
||||||
if (status === "submitted") {
|
|
||||||
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
|
||||||
} else if (status === "streaming") {
|
|
||||||
Icon = <SquareIcon className="size-4" />;
|
|
||||||
} else if (status === "error") {
|
|
||||||
Icon = <XIcon className="size-4" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={cn("gap-1.5 rounded-lg", className)}
|
|
||||||
size={size}
|
|
||||||
type="submit"
|
|
||||||
variant={variant}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? Icon}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PromptInputModelSelectProps = ComponentProps<typeof Select>;
|
|
||||||
|
|
||||||
export const PromptInputModelSelect = (props: PromptInputModelSelectProps) => (
|
|
||||||
<Select {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PromptInputModelSelectTriggerProps = ComponentProps<
|
|
||||||
typeof SelectTrigger
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const PromptInputModelSelectTrigger = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PromptInputModelSelectTriggerProps) => (
|
|
||||||
<SelectTrigger
|
|
||||||
className={cn(
|
|
||||||
"border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",
|
|
||||||
"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
|
|
||||||
"h-auto px-2 py-1.5",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PromptInputModelSelectContentProps = ComponentProps<
|
|
||||||
typeof SelectContent
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const PromptInputModelSelectContent = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PromptInputModelSelectContentProps) => (
|
|
||||||
<SelectContent className={cn(className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PromptInputModelSelectItemProps = ComponentProps<typeof SelectItem>;
|
|
||||||
|
|
||||||
export const PromptInputModelSelectItem = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PromptInputModelSelectItemProps) => (
|
|
||||||
<SelectItem className={cn(className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PromptInputModelSelectValueProps = ComponentProps<
|
|
||||||
typeof SelectValue
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const PromptInputModelSelectValue = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PromptInputModelSelectValueProps) => (
|
|
||||||
<SelectValue className={cn(className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
||||||
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Response } from "./response";
|
|
||||||
|
|
||||||
type ReasoningContextValue = {
|
|
||||||
isStreaming: boolean;
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: (open: boolean) => void;
|
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
|
||||||
|
|
||||||
const useReasoning = () => {
|
|
||||||
const context = useContext(ReasoningContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("Reasoning components must be used within Reasoning");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
|
||||||
isStreaming?: boolean;
|
|
||||||
open?: boolean;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
onOpenChange?: (open: boolean) => void;
|
|
||||||
duration?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AUTO_CLOSE_DELAY = 500;
|
|
||||||
const MS_IN_S = 1000;
|
|
||||||
|
|
||||||
export const Reasoning = memo(
|
|
||||||
({
|
|
||||||
className,
|
|
||||||
isStreaming = false,
|
|
||||||
open,
|
|
||||||
defaultOpen = true,
|
|
||||||
onOpenChange,
|
|
||||||
duration: durationProp,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ReasoningProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useControllableState({
|
|
||||||
prop: open,
|
|
||||||
defaultProp: defaultOpen,
|
|
||||||
onChange: onOpenChange,
|
|
||||||
});
|
|
||||||
const [duration, setDuration] = useControllableState({
|
|
||||||
prop: durationProp,
|
|
||||||
defaultProp: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [hasAutoClosedRef, setHasAutoClosedRef] = useState(false);
|
|
||||||
const [startTime, setStartTime] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// Track duration when streaming starts and ends
|
|
||||||
useEffect(() => {
|
|
||||||
if (isStreaming) {
|
|
||||||
if (startTime === null) {
|
|
||||||
setStartTime(Date.now());
|
|
||||||
}
|
|
||||||
} else if (startTime !== null) {
|
|
||||||
setDuration(Math.round((Date.now() - startTime) / MS_IN_S));
|
|
||||||
setStartTime(null);
|
|
||||||
}
|
|
||||||
}, [isStreaming, startTime, setDuration]);
|
|
||||||
|
|
||||||
// Auto-open when streaming starts, auto-close when streaming ends (once only)
|
|
||||||
useEffect(() => {
|
|
||||||
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosedRef) {
|
|
||||||
// Add a small delay before closing to allow user to see the content
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
setHasAutoClosedRef(true);
|
|
||||||
}, AUTO_CLOSE_DELAY);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosedRef]);
|
|
||||||
|
|
||||||
const handleOpenChange = (newOpen: boolean) => {
|
|
||||||
setIsOpen(newOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReasoningContext.Provider
|
|
||||||
value={{ isStreaming, isOpen, setIsOpen, duration }}
|
|
||||||
>
|
|
||||||
<Collapsible
|
|
||||||
className={cn("not-prose", className)}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
open={isOpen}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Collapsible>
|
|
||||||
</ReasoningContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
|
|
||||||
|
|
||||||
export const ReasoningTrigger = memo(
|
|
||||||
({ className, children, ...props }: ReasoningTriggerProps) => {
|
|
||||||
const { isStreaming, isOpen, duration } = useReasoning();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollapsibleTrigger
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? (
|
|
||||||
<>
|
|
||||||
<BrainIcon className="size-3" />
|
|
||||||
{isStreaming || duration === 0 ? (
|
|
||||||
<span>Thinking</span>
|
|
||||||
) : (
|
|
||||||
<span>{duration}s</span>
|
|
||||||
)}
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={cn(
|
|
||||||
"size-2.5 transition-transform",
|
|
||||||
isOpen ? "rotate-180" : "rotate-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ReasoningContentProps = ComponentProps<
|
|
||||||
typeof CollapsibleContent
|
|
||||||
> & {
|
|
||||||
children: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ReasoningContent = memo(
|
|
||||||
({ className, children, ...props }: ReasoningContentProps) => (
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"mt-1.5 text-[11px] text-muted-foreground leading-relaxed",
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="max-h-48 overflow-y-auto rounded-md border border-border/50 bg-muted/30 p-2.5">
|
|
||||||
<Response className="grid gap-1 text-[11px] **:text-[11px] [&_li]:my-0 [&_ol]:my-1 [&_p]:my-0 [&_ul]:my-1">
|
|
||||||
{children}
|
|
||||||
</Response>
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Reasoning.displayName = "Reasoning";
|
|
||||||
ReasoningTrigger.displayName = "ReasoningTrigger";
|
|
||||||
ReasoningContent.displayName = "ReasoningContent";
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { Streamdown } from "streamdown";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type ResponseProps = ComponentProps<typeof Streamdown>;
|
|
||||||
|
|
||||||
export function Response({ className, children, ...props }: ResponseProps) {
|
|
||||||
return (
|
|
||||||
<Streamdown
|
|
||||||
className={cn(
|
|
||||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Streamdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { BookIcon, ChevronDownIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type SourcesProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const Sources = ({ className, ...props }: SourcesProps) => (
|
|
||||||
<Collapsible
|
|
||||||
className={cn("not-prose mb-4 text-primary text-xs", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SourcesTrigger = ({
|
|
||||||
className,
|
|
||||||
count,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: SourcesTriggerProps) => (
|
|
||||||
<CollapsibleTrigger className="flex items-center gap-2" {...props}>
|
|
||||||
{children ?? (
|
|
||||||
<>
|
|
||||||
<p className="font-medium">Used {count} sources</p>
|
|
||||||
<ChevronDownIcon className="size-4" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
|
|
||||||
|
|
||||||
export const SourcesContent = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: SourcesContentProps) => (
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"mt-3 flex w-fit flex-col gap-2",
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type SourceProps = ComponentProps<"a">;
|
|
||||||
|
|
||||||
export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
href={href}
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? (
|
|
||||||
<>
|
|
||||||
<BookIcon className="size-4" />
|
|
||||||
<span className="block font-medium">{title}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
|
||||||
|
|
||||||
export const Suggestions = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: SuggestionsProps) => (
|
|
||||||
<ScrollArea className="w-full overflow-x-auto whitespace-nowrap" {...props}>
|
|
||||||
<div className={cn("flex w-max flex-nowrap items-center gap-2", className)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<ScrollBar className="hidden" orientation="horizontal" />
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
|
|
||||||
suggestion: string;
|
|
||||||
onClick?: (suggestion: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Suggestion = ({
|
|
||||||
suggestion,
|
|
||||||
onClick,
|
|
||||||
className,
|
|
||||||
variant = "outline",
|
|
||||||
size = "sm",
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: SuggestionProps) => {
|
|
||||||
const handleClick = () => {
|
|
||||||
onClick?.(suggestion);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={cn("cursor-pointer rounded-full px-4", className)}
|
|
||||||
onClick={handleClick}
|
|
||||||
size={size}
|
|
||||||
type="button"
|
|
||||||
variant={variant}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children || suggestion}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronDownIcon, SearchIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type TaskItemFileProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const TaskItemFile = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: TaskItemFileProps) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskItemProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
|
|
||||||
<div className={cn("text-muted-foreground text-sm", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskProps = ComponentProps<typeof Collapsible>;
|
|
||||||
|
|
||||||
export const Task = ({
|
|
||||||
defaultOpen = true,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: TaskProps) => (
|
|
||||||
<Collapsible
|
|
||||||
className={cn(
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
defaultOpen={defaultOpen}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TaskTrigger = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
title,
|
|
||||||
...props
|
|
||||||
}: TaskTriggerProps) => (
|
|
||||||
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
|
|
||||||
{children ?? (
|
|
||||||
<div className="flex cursor-pointer items-center gap-2 text-muted-foreground hover:text-foreground">
|
|
||||||
<SearchIcon className="size-4" />
|
|
||||||
<p className="text-sm">{title}</p>
|
|
||||||
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
|
|
||||||
|
|
||||||
export const TaskContent = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: TaskContentProps) => (
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="mt-4 space-y-2 border-muted border-l-2 pl-4">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
);
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type { ToolUIPart } from "ai";
|
|
||||||
import {
|
|
||||||
CheckCircleIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
CircleIcon,
|
|
||||||
ClockIcon,
|
|
||||||
WrenchIcon,
|
|
||||||
XCircleIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type ToolProps = ComponentProps<typeof Collapsible>;
|
|
||||||
|
|
||||||
export const Tool = ({ className, ...props }: ToolProps) => (
|
|
||||||
<Collapsible
|
|
||||||
className={cn("not-prose mb-4 w-full rounded-md border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ToolHeaderProps = {
|
|
||||||
type: ToolUIPart["type"];
|
|
||||||
state: ToolUIPart["state"];
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusBadge = (status: ToolUIPart["state"]) => {
|
|
||||||
const labels: Record<ToolUIPart["state"], string> = {
|
|
||||||
"input-streaming": "Pending",
|
|
||||||
"input-available": "Running",
|
|
||||||
"approval-requested": "Pending",
|
|
||||||
"approval-responded": "Approved",
|
|
||||||
"output-available": "Completed",
|
|
||||||
"output-error": "Error",
|
|
||||||
"output-denied": "Denied",
|
|
||||||
};
|
|
||||||
|
|
||||||
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" />,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
className="flex items-center gap-1 rounded-full text-xs"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
{icons[status]}
|
|
||||||
<span>{labels[status]}</span>
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ToolHeader = ({
|
|
||||||
className,
|
|
||||||
type,
|
|
||||||
state,
|
|
||||||
...props
|
|
||||||
}: ToolHeaderProps) => (
|
|
||||||
<CollapsibleTrigger
|
|
||||||
className={cn(
|
|
||||||
"flex w-full min-w-0 items-center justify-between gap-2 p-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
||||||
<WrenchIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="truncate font-medium text-sm">{type}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
|
||||||
{getStatusBadge(state)}
|
|
||||||
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
|
||||||
</div>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
|
|
||||||
|
|
||||||
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ToolInputProps = ComponentProps<"div"> & {
|
|
||||||
input: ToolUIPart["input"];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
|
||||||
<div className={cn("space-y-2 overflow-hidden p-4", className)} {...props}>
|
|
||||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
|
||||||
Parameters
|
|
||||||
</h4>
|
|
||||||
<pre className="overflow-x-auto rounded-md bg-muted/50 p-3 font-mono text-xs">
|
|
||||||
{JSON.stringify(input, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ToolOutputProps = ComponentProps<"div"> & {
|
|
||||||
output: ReactNode;
|
|
||||||
errorText: ToolUIPart["errorText"];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ToolOutput = ({
|
|
||||||
className,
|
|
||||||
output,
|
|
||||||
errorText,
|
|
||||||
...props
|
|
||||||
}: ToolOutputProps) => {
|
|
||||||
if (!(output || errorText)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("space-y-2 p-4", className)} {...props}>
|
|
||||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
|
||||||
{errorText ? "Error" : "Result"}
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"overflow-x-auto rounded-md text-xs [&_table]:w-full",
|
|
||||||
errorText
|
|
||||||
? "bg-destructive/10 text-destructive"
|
|
||||||
: "bg-muted/50 text-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{errorText && <div>{errorText}</div>}
|
|
||||||
{output && <div>{output}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,252 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
|
||||||
import { createContext, useContext, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export type WebPreviewContextValue = {
|
|
||||||
url: string;
|
|
||||||
setUrl: (url: string) => void;
|
|
||||||
consoleOpen: boolean;
|
|
||||||
setConsoleOpen: (open: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
|
|
||||||
|
|
||||||
const useWebPreview = () => {
|
|
||||||
const context = useContext(WebPreviewContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("WebPreview components must be used within a WebPreview");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WebPreviewProps = ComponentProps<"div"> & {
|
|
||||||
defaultUrl?: string;
|
|
||||||
onUrlChange?: (url: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WebPreview = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
defaultUrl = "",
|
|
||||||
onUrlChange,
|
|
||||||
...props
|
|
||||||
}: WebPreviewProps) => {
|
|
||||||
const [url, setUrl] = useState(defaultUrl);
|
|
||||||
const [consoleOpen, setConsoleOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleUrlChange = (newUrl: string) => {
|
|
||||||
setUrl(newUrl);
|
|
||||||
onUrlChange?.(newUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
const contextValue: WebPreviewContextValue = {
|
|
||||||
url,
|
|
||||||
setUrl: handleUrlChange,
|
|
||||||
consoleOpen,
|
|
||||||
setConsoleOpen,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WebPreviewContext.Provider value={contextValue}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex size-full flex-col rounded-lg border bg-card",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</WebPreviewContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WebPreviewNavigationProps = ComponentProps<"div">;
|
|
||||||
|
|
||||||
export const WebPreviewNavigation = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: WebPreviewNavigationProps) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex items-center gap-1 border-b p-2", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
|
|
||||||
tooltip?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WebPreviewNavigationButton = ({
|
|
||||||
onClick,
|
|
||||||
disabled,
|
|
||||||
tooltip,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: WebPreviewNavigationButtonProps) => (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className="size-8 p-0 hover:text-foreground"
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={onClick}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{tooltip}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type WebPreviewUrlProps = ComponentProps<typeof Input>;
|
|
||||||
|
|
||||||
export const WebPreviewUrl = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onKeyDown,
|
|
||||||
...props
|
|
||||||
}: WebPreviewUrlProps) => {
|
|
||||||
const { url, setUrl } = useWebPreview();
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
setUrl(target.value);
|
|
||||||
}
|
|
||||||
onKeyDown?.(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
className="h-8 flex-1 text-sm"
|
|
||||||
onChange={onChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="Enter URL..."
|
|
||||||
value={value ?? url}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
|
|
||||||
loading?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WebPreviewBody = ({
|
|
||||||
className,
|
|
||||||
loading,
|
|
||||||
src,
|
|
||||||
...props
|
|
||||||
}: WebPreviewBodyProps) => {
|
|
||||||
const { url } = useWebPreview();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex-1">
|
|
||||||
<iframe
|
|
||||||
className={cn("size-full", className)}
|
|
||||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
|
|
||||||
src={(src ?? url) || undefined}
|
|
||||||
title="Preview"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
{loading}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WebPreviewConsoleProps = ComponentProps<"div"> & {
|
|
||||||
logs?: Array<{
|
|
||||||
level: "log" | "warn" | "error";
|
|
||||||
message: string;
|
|
||||||
timestamp: Date;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WebPreviewConsole = ({
|
|
||||||
className,
|
|
||||||
logs = [],
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: WebPreviewConsoleProps) => {
|
|
||||||
const { consoleOpen, setConsoleOpen } = useWebPreview();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapsible
|
|
||||||
className={cn("border-t bg-muted/50 font-mono text-sm", className)}
|
|
||||||
onOpenChange={setConsoleOpen}
|
|
||||||
open={consoleOpen}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className="flex w-full items-center justify-between p-4 text-left font-medium hover:bg-muted/50"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
Console
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4 transition-transform duration-200",
|
|
||||||
consoleOpen && "rotate-180"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent
|
|
||||||
className={cn(
|
|
||||||
"px-4 pb-4",
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="max-h-48 space-y-1 overflow-y-auto">
|
|
||||||
{logs.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground">No console output</p>
|
|
||||||
) : (
|
|
||||||
logs.map((log, index) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"text-xs",
|
|
||||||
log.level === "error" && "text-destructive",
|
|
||||||
log.level === "warn" && "text-yellow-600",
|
|
||||||
log.level === "log" && "text-foreground"
|
|
||||||
)}
|
|
||||||
key={`${log.timestamp.getTime()}-${index}`}
|
|
||||||
>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{log.timestamp.toLocaleTimeString()}
|
|
||||||
</span>{" "}
|
|
||||||
{log.message}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -5,7 +5,10 @@ import { useSWRConfig } from "swr";
|
||||||
import { useCopyToClipboard } from "usehooks-ts";
|
import { useCopyToClipboard } from "usehooks-ts";
|
||||||
import type { Vote } from "@/lib/db/schema";
|
import type { Vote } from "@/lib/db/schema";
|
||||||
import type { ChatMessage } from "@/lib/types";
|
import type { ChatMessage } from "@/lib/types";
|
||||||
import { Action, Actions } from "./elements/actions";
|
import {
|
||||||
|
MessageAction as Action,
|
||||||
|
MessageActions as Actions,
|
||||||
|
} from "./ai-elements/message";
|
||||||
import { CopyIcon, PencilEditIcon, ThumbDownIcon, ThumbUpIcon } from "./icons";
|
import { CopyIcon, PencilEditIcon, ThumbDownIcon, ThumbUpIcon } from "./icons";
|
||||||
|
|
||||||
export function PureMessageActions({
|
export function PureMessageActions({
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
Reasoning,
|
Reasoning,
|
||||||
ReasoningContent,
|
ReasoningContent,
|
||||||
ReasoningTrigger,
|
ReasoningTrigger,
|
||||||
} from "./elements/reasoning";
|
} from "./ai-elements/reasoning";
|
||||||
|
|
||||||
type MessageReasoningProps = {
|
type MessageReasoningProps = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,17 @@ import { useState } from "react";
|
||||||
import type { Vote } from "@/lib/db/schema";
|
import type { Vote } from "@/lib/db/schema";
|
||||||
import type { ChatMessage } from "@/lib/types";
|
import type { ChatMessage } from "@/lib/types";
|
||||||
import { cn, sanitizeText } from "@/lib/utils";
|
import { cn, sanitizeText } from "@/lib/utils";
|
||||||
import { useDataStream } from "./data-stream-provider";
|
import { MessageContent, MessageResponse } from "./ai-elements/message";
|
||||||
import { DocumentToolResult } from "./document";
|
|
||||||
import { DocumentPreview } from "./document-preview";
|
|
||||||
import { MessageContent } from "./elements/message";
|
|
||||||
import { Response } from "./elements/response";
|
|
||||||
import {
|
import {
|
||||||
Tool,
|
Tool,
|
||||||
ToolContent,
|
ToolContent,
|
||||||
ToolHeader,
|
ToolHeader,
|
||||||
ToolInput,
|
ToolInput,
|
||||||
ToolOutput,
|
ToolOutput,
|
||||||
} from "./elements/tool";
|
} from "./ai-elements/tool";
|
||||||
|
import { useDataStream } from "./data-stream-provider";
|
||||||
|
import { DocumentToolResult } from "./document";
|
||||||
|
import { DocumentPreview } from "./document-preview";
|
||||||
import { SparklesIcon } from "./icons";
|
import { SparklesIcon } from "./icons";
|
||||||
import { MessageActions } from "./message-actions";
|
import { MessageActions } from "./message-actions";
|
||||||
import { MessageEditor } from "./message-editor";
|
import { MessageEditor } from "./message-editor";
|
||||||
|
|
@ -141,7 +140,9 @@ const PurePreviewMessage = ({
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Response>{sanitizeText(part.text)}</Response>
|
<MessageResponse>
|
||||||
|
{sanitizeText(part.text)}
|
||||||
|
</MessageResponse>
|
||||||
</MessageContent>
|
</MessageContent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import type { UseChatHelpers } from "@ai-sdk/react";
|
import type { UseChatHelpers } from "@ai-sdk/react";
|
||||||
import type { UIMessage } from "ai";
|
import type { UIMessage } from "ai";
|
||||||
import equal from "fast-deep-equal";
|
import equal from "fast-deep-equal";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { ArrowUpIcon, CheckIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
type Dispatch,
|
type Dispatch,
|
||||||
|
|
@ -36,12 +36,12 @@ import type { Attachment, ChatMessage } from "@/lib/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
PromptInput,
|
PromptInput,
|
||||||
|
PromptInputFooter,
|
||||||
PromptInputSubmit,
|
PromptInputSubmit,
|
||||||
PromptInputTextarea,
|
PromptInputTextarea,
|
||||||
PromptInputToolbar,
|
|
||||||
PromptInputTools,
|
PromptInputTools,
|
||||||
} from "./elements/prompt-input";
|
} from "./ai-elements/prompt-input";
|
||||||
import { ArrowUpIcon, PaperclipIcon, StopIcon } from "./icons";
|
import { PaperclipIcon, StopIcon } from "./icons";
|
||||||
import { PreviewAttachment } from "./preview-attachment";
|
import { PreviewAttachment } from "./preview-attachment";
|
||||||
import { SuggestedActions } from "./suggested-actions";
|
import { SuggestedActions } from "./suggested-actions";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
@ -87,18 +87,6 @@ function PureMultimodalInput({
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
|
|
||||||
const adjustHeight = useCallback(() => {
|
|
||||||
if (textareaRef.current) {
|
|
||||||
textareaRef.current.style.height = "44px";
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (textareaRef.current) {
|
|
||||||
adjustHeight();
|
|
||||||
}
|
|
||||||
}, [adjustHeight]);
|
|
||||||
|
|
||||||
const hasAutoFocused = useRef(false);
|
const hasAutoFocused = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasAutoFocused.current && width) {
|
if (!hasAutoFocused.current && width) {
|
||||||
|
|
@ -110,12 +98,6 @@ function PureMultimodalInput({
|
||||||
}
|
}
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
|
||||||
const resetHeight = useCallback(() => {
|
|
||||||
if (textareaRef.current) {
|
|
||||||
textareaRef.current.style.height = "44px";
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [localStorageInput, setLocalStorageInput] = useLocalStorage(
|
const [localStorageInput, setLocalStorageInput] = useLocalStorage(
|
||||||
"input",
|
"input",
|
||||||
""
|
""
|
||||||
|
|
@ -127,11 +109,10 @@ function PureMultimodalInput({
|
||||||
// Prefer DOM value over localStorage to handle hydration
|
// Prefer DOM value over localStorage to handle hydration
|
||||||
const finalValue = domValue || localStorageInput || "";
|
const finalValue = domValue || localStorageInput || "";
|
||||||
setInput(finalValue);
|
setInput(finalValue);
|
||||||
adjustHeight();
|
|
||||||
}
|
}
|
||||||
// Only run once after hydration
|
// Only run once after hydration
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [adjustHeight, localStorageInput, setInput]);
|
}, [localStorageInput, setInput]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalStorageInput(input);
|
setLocalStorageInput(input);
|
||||||
|
|
@ -169,7 +150,6 @@ function PureMultimodalInput({
|
||||||
|
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
setLocalStorageInput("");
|
setLocalStorageInput("");
|
||||||
resetHeight();
|
|
||||||
setInput("");
|
setInput("");
|
||||||
|
|
||||||
if (width && width > 768) {
|
if (width && width > 768) {
|
||||||
|
|
@ -184,7 +164,6 @@ function PureMultimodalInput({
|
||||||
setLocalStorageInput,
|
setLocalStorageInput,
|
||||||
width,
|
width,
|
||||||
chatId,
|
chatId,
|
||||||
resetHeight,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const uploadFile = useCallback(async (file: File) => {
|
const uploadFile = useCallback(async (file: File) => {
|
||||||
|
|
@ -324,16 +303,15 @@ function PureMultimodalInput({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PromptInput
|
<PromptInput
|
||||||
className="rounded-xl border border-border bg-background p-3 shadow-xs transition-all duration-200 focus-within:border-border hover:border-muted-foreground/50"
|
className="[&>div]:rounded-xl"
|
||||||
onSubmit={(event) => {
|
onSubmit={() => {
|
||||||
event.preventDefault();
|
|
||||||
if (!input.trim() && attachments.length === 0) {
|
if (!input.trim() && attachments.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status !== "ready") {
|
if (status === "ready") {
|
||||||
toast.error("Please wait for the model to finish its response!");
|
|
||||||
} else {
|
|
||||||
submitForm();
|
submitForm();
|
||||||
|
} else {
|
||||||
|
toast.error("Please wait for the model to finish its response!");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -370,22 +348,16 @@ function PureMultimodalInput({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row items-start gap-1 sm:gap-2">
|
<PromptInputTextarea
|
||||||
<PromptInputTextarea
|
className="p-6 min-h-24"
|
||||||
className="grow resize-none border-0! border-none! bg-transparent p-2 text-base outline-none ring-0 [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden"
|
data-testid="multimodal-input"
|
||||||
data-testid="multimodal-input"
|
onChange={handleInput}
|
||||||
disableAutoResize={true}
|
placeholder="Send a message..."
|
||||||
maxHeight={200}
|
ref={textareaRef}
|
||||||
minHeight={44}
|
value={input}
|
||||||
onChange={handleInput}
|
/>
|
||||||
placeholder="Send a message..."
|
<PromptInputFooter>
|
||||||
ref={textareaRef}
|
<PromptInputTools>
|
||||||
rows={1}
|
|
||||||
value={input}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<PromptInputToolbar className="border-top-0! border-t-0! p-0 shadow-none dark:border-0 dark:border-transparent!">
|
|
||||||
<PromptInputTools className="gap-0 sm:gap-0.5">
|
|
||||||
<AttachmentsButton
|
<AttachmentsButton
|
||||||
fileInputRef={fileInputRef}
|
fileInputRef={fileInputRef}
|
||||||
selectedModelId={selectedModelId}
|
selectedModelId={selectedModelId}
|
||||||
|
|
@ -401,15 +373,16 @@ function PureMultimodalInput({
|
||||||
<StopButton setMessages={setMessages} stop={stop} />
|
<StopButton setMessages={setMessages} stop={stop} />
|
||||||
) : (
|
) : (
|
||||||
<PromptInputSubmit
|
<PromptInputSubmit
|
||||||
className="size-8 rounded-full bg-primary text-primary-foreground transition-colors duration-200 hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground"
|
className="rounded-full"
|
||||||
data-testid="send-button"
|
data-testid="send-button"
|
||||||
disabled={!input.trim() || uploadQueue.length > 0}
|
disabled={!input.trim() || uploadQueue.length > 0}
|
||||||
status={status}
|
status={status}
|
||||||
|
variant="secondary"
|
||||||
>
|
>
|
||||||
<ArrowUpIcon size={14} />
|
<ArrowUpIcon className="size-4" />
|
||||||
</PromptInputSubmit>
|
</PromptInputSubmit>
|
||||||
)}
|
)}
|
||||||
</PromptInputToolbar>
|
</PromptInputFooter>
|
||||||
</PromptInput>
|
</PromptInput>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import type { Attachment } from "@/lib/types";
|
import type { Attachment } from "@/lib/types";
|
||||||
import { Loader } from "./elements/loader";
|
|
||||||
import { CrossSmallIcon } from "./icons";
|
import { CrossSmallIcon } from "./icons";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import { Spinner } from "./ui/spinner";
|
||||||
|
|
||||||
export const PreviewAttachment = ({
|
export const PreviewAttachment = ({
|
||||||
attachment,
|
attachment,
|
||||||
|
|
@ -39,7 +39,7 @@ export const PreviewAttachment = ({
|
||||||
className="absolute inset-0 flex items-center justify-center bg-black/50"
|
className="absolute inset-0 flex items-center justify-center bg-black/50"
|
||||||
data-testid="input-attachment-loader"
|
data-testid="input-attachment-loader"
|
||||||
>
|
>
|
||||||
<Loader size={16} />
|
<Spinner className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { SidebarLeftIcon } from "./icons";
|
import { SidebarLeftIcon } from "./icons";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
|
|
@ -19,9 +18,10 @@ export function SidebarToggle({
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className={cn("h-8 px-2 md:h-fit md:px-2", className)}
|
className={className}
|
||||||
data-testid="sidebar-toggle-button"
|
data-testid="sidebar-toggle-button"
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<SidebarLeftIcon size={16} />
|
<SidebarLeftIcon size={16} />
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type { UseChatHelpers } from "@ai-sdk/react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import type { ChatMessage } from "@/lib/types";
|
import type { ChatMessage } from "@/lib/types";
|
||||||
import { Suggestion } from "./elements/suggestion";
|
import { Suggestion } from "./ai-elements/suggestion";
|
||||||
import type { VisibilityType } from "./visibility-selector";
|
import type { VisibilityType } from "./visibility-selector";
|
||||||
|
|
||||||
type SuggestedActionsProps = {
|
type SuggestedActionsProps = {
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,11 @@ const Tool = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedTool !== description) {
|
if (selectedTool === description) {
|
||||||
setSelectedTool(description);
|
|
||||||
} else {
|
|
||||||
setSelectedTool(null);
|
setSelectedTool(null);
|
||||||
onClick({ sendMessage });
|
onClick({ sendMessage });
|
||||||
|
} else {
|
||||||
|
setSelectedTool(description);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,140 +1,196 @@
|
||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
|
import * as React from "react"
|
||||||
import * as React from "react";
|
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const AlertDialog = AlertDialogPrimitive.Root;
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
function AlertDialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
function AlertDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogOverlay = React.forwardRef<
|
function AlertDialogPortal({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
...props
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
>(({ className, ...props }, ref) => (
|
return (
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
className={cn(
|
)
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
}
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
function AlertDialogOverlay({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
<AlertDialogPortal>
|
return (
|
||||||
<AlertDialogOverlay />
|
<AlertDialogPrimitive.Overlay
|
||||||
<AlertDialogPrimitive.Content
|
data-slot="alert-dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:rounded-lg",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AlertDialogPortal>
|
)
|
||||||
));
|
}
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const AlertDialogHeader = ({
|
function AlertDialogContent({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||||
|
size?: "default" | "sm"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"bg-background 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 group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
data-slot="alert-dialog-header"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||||
{...props}
|
className
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
AlertDialogHeader.displayName = "AlertDialogHeader";
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogFooter = ({
|
function AlertDialogFooter({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
data-slot="alert-dialog-footer"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||||
{...props}
|
className
|
||||||
/>
|
)}
|
||||||
);
|
{...props}
|
||||||
AlertDialogFooter.displayName = "AlertDialogFooter";
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
function AlertDialogTitle({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
<AlertDialogPrimitive.Title
|
return (
|
||||||
className={cn("font-semibold text-lg", className)}
|
<AlertDialogPrimitive.Title
|
||||||
ref={ref}
|
data-slot="alert-dialog-title"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"text-lg font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
||||||
));
|
className
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
function AlertDialogDescription({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
<AlertDialogPrimitive.Description
|
return (
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
<AlertDialogPrimitive.Description
|
||||||
ref={ref}
|
data-slot="alert-dialog-description"
|
||||||
{...props}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
/>
|
{...props}
|
||||||
));
|
/>
|
||||||
AlertDialogDescription.displayName =
|
)
|
||||||
AlertDialogPrimitive.Description.displayName;
|
}
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
function AlertDialogMedia({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<AlertDialogPrimitive.Action
|
return (
|
||||||
className={cn(buttonVariants(), className)}
|
<div
|
||||||
ref={ref}
|
data-slot="alert-dialog-media"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"bg-muted mb-2 inline-flex size-16 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
|
||||||
));
|
className
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
function AlertDialogAction({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
variant = "default",
|
||||||
>(({ className, ...props }, ref) => (
|
size = "default",
|
||||||
<AlertDialogPrimitive.Cancel
|
...props
|
||||||
className={cn(
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
||||||
buttonVariants({ variant: "outline" }),
|
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||||
"mt-2 sm:mt-0",
|
return (
|
||||||
className
|
<Button variant={variant} size={size} asChild>
|
||||||
)}
|
<AlertDialogPrimitive.Action
|
||||||
ref={ref}
|
data-slot="alert-dialog-action"
|
||||||
{...props}
|
className={cn(className)}
|
||||||
/>
|
{...props}
|
||||||
));
|
/>
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogCancel({
|
||||||
|
className,
|
||||||
|
variant = "outline",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
||||||
|
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||||
|
return (
|
||||||
|
<Button variant={variant} size={size} asChild>
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
data-slot="alert-dialog-cancel"
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
};
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogMedia,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const alertVariants = cva(
|
|
||||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-background text-foreground",
|
|
||||||
destructive:
|
|
||||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
|
||||||
>(({ className, variant, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
role="alert"
|
|
||||||
className={cn(alertVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Alert.displayName = "Alert"
|
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<h5
|
|
||||||
ref={ref}
|
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertTitle.displayName = "AlertTitle"
|
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDescription.displayName = "AlertDescription"
|
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Avatar as AvatarPrimitive } from "radix-ui"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Image
|
|
||||||
ref={ref}
|
|
||||||
className={cn("aspect-square h-full w-full", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
||||||
secondary:
|
secondary:
|
||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline: "text-foreground",
|
outline:
|
||||||
|
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
@ -23,13 +26,22 @@ const badgeVariants = cva(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface BadgeProps
|
function Badge({
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
className,
|
||||||
VariantProps<typeof badgeVariants> {}
|
variant = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot.Root : "span"
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
|
||||||
const buttonGroupVariants = cva(
|
const buttonGroupVariants = cva(
|
||||||
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
orientation: {
|
orientation: {
|
||||||
|
|
@ -44,12 +44,12 @@ function ButtonGroupText({
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "div"
|
const Comp = asChild ? Slot.Root : "div"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,34 @@
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import * as React from "react"
|
||||||
import { Slot as SlotPrimitive } from "radix-ui";
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import * as React from "react";
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
sm: "h-9 rounded-md px-3",
|
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
lg: "h-11 rounded-md px-8",
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
icon: "h-10 w-10",
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
"icon-sm": "h-8 w-8",
|
icon: "size-9",
|
||||||
|
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
@ -32,26 +36,29 @@ const buttonVariants = cva(
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
function Button({
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
className,
|
||||||
VariantProps<typeof buttonVariants> {
|
variant = "default",
|
||||||
asChild?: boolean;
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
export { Button, buttonVariants }
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? SlotPrimitive.Slot : "button";
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
Button.displayName = "Button";
|
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
|
||||||
|
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
Card.displayName = "Card";
|
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
CardHeader.displayName = "CardHeader";
|
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"font-semibold text-2xl leading-none tracking-tight",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
CardTitle.displayName = "CardTitle";
|
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
CardDescription.displayName = "CardDescription";
|
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className={cn("p-6 pt-0", className)} ref={ref} {...props} />
|
|
||||||
));
|
|
||||||
CardContent.displayName = "CardContent";
|
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
CardFooter.displayName = "CardFooter";
|
|
||||||
|
|
||||||
const CardAction = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
className={cn("flex items-center gap-2", className)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
CardAction.displayName = "CardAction";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
CardAction,
|
|
||||||
};
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import useEmblaCarousel, {
|
|
||||||
type UseEmblaCarouselType,
|
|
||||||
} from "embla-carousel-react"
|
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
|
|
||||||
type CarouselApi = UseEmblaCarouselType[1]
|
|
||||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
|
||||||
type CarouselOptions = UseCarouselParameters[0]
|
|
||||||
type CarouselPlugin = UseCarouselParameters[1]
|
|
||||||
|
|
||||||
type CarouselProps = {
|
|
||||||
opts?: CarouselOptions
|
|
||||||
plugins?: CarouselPlugin
|
|
||||||
orientation?: "horizontal" | "vertical"
|
|
||||||
setApi?: (api: CarouselApi) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type CarouselContextProps = {
|
|
||||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
|
||||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
|
||||||
scrollPrev: () => void
|
|
||||||
scrollNext: () => void
|
|
||||||
canScrollPrev: boolean
|
|
||||||
canScrollNext: boolean
|
|
||||||
} & CarouselProps
|
|
||||||
|
|
||||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
|
||||||
|
|
||||||
function useCarousel() {
|
|
||||||
const context = React.useContext(CarouselContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useCarousel must be used within a <Carousel />")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
const Carousel = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
orientation = "horizontal",
|
|
||||||
opts,
|
|
||||||
setApi,
|
|
||||||
plugins,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const [carouselRef, api] = useEmblaCarousel(
|
|
||||||
{
|
|
||||||
...opts,
|
|
||||||
axis: orientation === "horizontal" ? "x" : "y",
|
|
||||||
},
|
|
||||||
plugins
|
|
||||||
)
|
|
||||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
|
||||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
|
||||||
|
|
||||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
||||||
if (!api) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setCanScrollPrev(api.canScrollPrev())
|
|
||||||
setCanScrollNext(api.canScrollNext())
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const scrollPrev = React.useCallback(() => {
|
|
||||||
api?.scrollPrev()
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
const scrollNext = React.useCallback(() => {
|
|
||||||
api?.scrollNext()
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (event.key === "ArrowLeft") {
|
|
||||||
event.preventDefault()
|
|
||||||
scrollPrev()
|
|
||||||
} else if (event.key === "ArrowRight") {
|
|
||||||
event.preventDefault()
|
|
||||||
scrollNext()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[scrollPrev, scrollNext]
|
|
||||||
)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!api || !setApi) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setApi(api)
|
|
||||||
}, [api, setApi])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!api) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(api)
|
|
||||||
api.on("reInit", onSelect)
|
|
||||||
api.on("select", onSelect)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
api?.off("select", onSelect)
|
|
||||||
}
|
|
||||||
}, [api, onSelect])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CarouselContext.Provider
|
|
||||||
value={{
|
|
||||||
carouselRef,
|
|
||||||
api: api,
|
|
||||||
opts,
|
|
||||||
orientation:
|
|
||||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
||||||
scrollPrev,
|
|
||||||
scrollNext,
|
|
||||||
canScrollPrev,
|
|
||||||
canScrollNext,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
onKeyDownCapture={handleKeyDown}
|
|
||||||
className={cn("relative", className)}
|
|
||||||
role="region"
|
|
||||||
aria-roledescription="carousel"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CarouselContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Carousel.displayName = "Carousel"
|
|
||||||
|
|
||||||
const CarouselContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { carouselRef, orientation } = useCarousel()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={carouselRef} className="overflow-hidden">
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex",
|
|
||||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
CarouselContent.displayName = "CarouselContent"
|
|
||||||
|
|
||||||
const CarouselItem = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { orientation } = useCarousel()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
role="group"
|
|
||||||
aria-roledescription="slide"
|
|
||||||
className={cn(
|
|
||||||
"min-w-0 shrink-0 grow-0 basis-full",
|
|
||||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
CarouselItem.displayName = "CarouselItem"
|
|
||||||
|
|
||||||
const CarouselPrevious = React.forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
React.ComponentProps<typeof Button>
|
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={ref}
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
className={cn(
|
|
||||||
"absolute h-8 w-8 rounded-full",
|
|
||||||
orientation === "horizontal"
|
|
||||||
? "-left-12 top-1/2 -translate-y-1/2"
|
|
||||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
disabled={!canScrollPrev}
|
|
||||||
onClick={scrollPrev}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Previous slide</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
CarouselPrevious.displayName = "CarouselPrevious"
|
|
||||||
|
|
||||||
const CarouselNext = React.forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
React.ComponentProps<typeof Button>
|
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={ref}
|
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
className={cn(
|
|
||||||
"absolute h-8 w-8 rounded-full",
|
|
||||||
orientation === "horizontal"
|
|
||||||
? "-right-12 top-1/2 -translate-y-1/2"
|
|
||||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
disabled={!canScrollNext}
|
|
||||||
onClick={scrollNext}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ArrowRight className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Next slide</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
CarouselNext.displayName = "CarouselNext"
|
|
||||||
|
|
||||||
export {
|
|
||||||
type CarouselApi,
|
|
||||||
Carousel,
|
|
||||||
CarouselContent,
|
|
||||||
CarouselItem,
|
|
||||||
CarouselPrevious,
|
|
||||||
CarouselNext,
|
|
||||||
}
|
|
||||||
|
|
@ -2,10 +2,32 @@
|
||||||
|
|
||||||
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||||
|
|
||||||
const Collapsible = CollapsiblePrimitive.Root
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,58 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
import { Search } from "lucide-react"
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
function Command({
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
<CommandPrimitive
|
return (
|
||||||
ref={ref}
|
<CommandPrimitive
|
||||||
className={cn(
|
data-slot="command"
|
||||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
className={cn(
|
||||||
className
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
Command.displayName = CommandPrimitive.displayName
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
<DialogHeader className="sr-only">
|
||||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn("overflow-hidden p-0", className)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
{children}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
@ -35,110 +60,116 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
function CommandInput({
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
<CommandPrimitive.Input
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName
|
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
||||||
>((props, ref) => (
|
|
||||||
<CommandPrimitive.Empty
|
|
||||||
ref={ref}
|
|
||||||
className="py-6 text-center text-sm"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Group
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const CommandShortcut = ({
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<div
|
||||||
|
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" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CommandShortcut.displayName = "CommandShortcut"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
|
|
|
||||||
|
|
@ -1,122 +1,158 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import { XIcon } from "lucide-react"
|
||||||
import { X } from "lucide-react"
|
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
function DialogOverlay({
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
<DialogPrimitive.Overlay
|
return (
|
||||||
ref={ref}
|
<DialogPrimitive.Overlay
|
||||||
className={cn(
|
data-slot="dialog-overlay"
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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 sm:rounded-lg",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground 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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
{showCloseButton && (
|
||||||
<X className="h-4 w-4" />
|
<DialogPrimitive.Close asChild>
|
||||||
<span className="sr-only">Close</span>
|
<Button variant="outline">Close</Button>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
)}
|
||||||
</DialogPortal>
|
</div>
|
||||||
))
|
)
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
const DialogHeader = ({
|
function DialogTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<DialogPrimitive.Title
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
data-slot="dialog-title"
|
||||||
className
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
DialogHeader.displayName = "DialogHeader"
|
|
||||||
|
|
||||||
const DialogFooter = ({
|
function DialogDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<DialogPrimitive.Description
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
data-slot="dialog-description"
|
||||||
className
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
DialogFooter.displayName = "DialogFooter"
|
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,200 +1,257 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
function DropdownMenu({
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
DropdownMenuSubTrigger.displayName =
|
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSubContent.displayName =
|
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
))
|
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuSubContent,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,40 @@ import { HoverCard as HoverCardPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const HoverCard = HoverCardPrimitive.Root
|
function HoverCard({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||||
|
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
function HoverCardTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const HoverCardContent = React.forwardRef<
|
function HoverCardContent({
|
||||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
align = "center",
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
sideOffset = 4,
|
||||||
<HoverCardPrimitive.Content
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||||
align={align}
|
return (
|
||||||
sideOffset={sideOffset}
|
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||||
className={cn(
|
<HoverCardPrimitive.Content
|
||||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
|
data-slot="hover-card-content"
|
||||||
className
|
align={align}
|
||||||
)}
|
sideOffset={sideOffset}
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
))
|
className
|
||||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</HoverCardPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]",
|
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
|
||||||
"h-9 has-[>textarea]:h-auto",
|
"h-9 min-w-0 has-[>textarea]:h-auto",
|
||||||
|
|
||||||
// Variants based on alignment.
|
// Variants based on alignment.
|
||||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
||||||
|
|
@ -24,7 +24,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
||||||
|
|
||||||
// Focus state.
|
// Focus state.
|
||||||
"has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1",
|
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
||||||
|
|
||||||
// Error state.
|
// Error state.
|
||||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
||||||
|
|
@ -37,18 +37,18 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
const inputGroupAddonVariants = cva(
|
||||||
"text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
align: {
|
align: {
|
||||||
"inline-start":
|
"inline-start":
|
||||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
||||||
"inline-end":
|
"inline-end":
|
||||||
"order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]",
|
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
||||||
"block-start":
|
"block-start":
|
||||||
"[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5",
|
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
||||||
"block-end":
|
"block-end":
|
||||||
"[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5",
|
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
@ -80,12 +80,12 @@ function InputGroupAddon({
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupButtonVariants = cva(
|
const inputGroupButtonVariants = cva(
|
||||||
"flex items-center gap-2 text-sm shadow-none",
|
"text-sm shadow-none flex gap-2 items-center",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
||||||
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
|
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
||||||
"icon-xs":
|
"icon-xs":
|
||||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
||||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||||
|
|
@ -120,7 +120,7 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,20 @@ import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
({ className, type, ...props }, ref) => {
|
return (
|
||||||
return (
|
<input
|
||||||
<input
|
type={type}
|
||||||
type={type}
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
)}
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
ref={ref}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
)
|
/>
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
Input.displayName = "Input"
|
|
||||||
|
|
||||||
export { Input }
|
export { Input }
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,23 @@
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Label as LabelPrimitive } from "radix-ui"
|
import { Label as LabelPrimitive } from "radix-ui"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const labelVariants = cva(
|
function Label({
|
||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
className,
|
||||||
)
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
const Label = React.forwardRef<
|
return (
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
<LabelPrimitive.Root
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
data-slot="label"
|
||||||
VariantProps<typeof labelVariants>
|
className={cn(
|
||||||
>(({ className, ...props }, ref) => (
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
<LabelPrimitive.Root
|
className
|
||||||
ref={ref}
|
)}
|
||||||
className={cn(labelVariants(), className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
))
|
}
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Label }
|
export { Label }
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Progress as ProgressPrimitive } from "radix-ui"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Progress = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
|
||||||
>(({ className, value, ...props }, ref) => (
|
|
||||||
<ProgressPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ProgressPrimitive.Indicator
|
|
||||||
className="h-full w-full flex-1 bg-primary transition-all"
|
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
||||||
/>
|
|
||||||
</ProgressPrimitive.Root>
|
|
||||||
))
|
|
||||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Progress }
|
|
||||||
|
|
@ -5,44 +5,54 @@ import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
function ScrollArea({
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
<ScrollAreaPrimitive.Root
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn("relative overflow-hidden", className)}
|
<ScrollAreaPrimitive.Root
|
||||||
{...props}
|
data-slot="scroll-area"
|
||||||
>
|
className={cn("relative", className)}
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
{...props}
|
||||||
{children}
|
>
|
||||||
</ScrollAreaPrimitive.Viewport>
|
<ScrollAreaPrimitive.Viewport
|
||||||
<ScrollBar />
|
data-slot="scroll-area-viewport"
|
||||||
<ScrollAreaPrimitive.Corner />
|
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
</ScrollAreaPrimitive.Root>
|
>
|
||||||
))
|
{children}
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
function ScrollBar({
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
orientation = "vertical",
|
||||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
...props
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
ref={ref}
|
return (
|
||||||
orientation={orientation}
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
className={cn(
|
data-slot="scroll-area-scrollbar"
|
||||||
"flex touch-none select-none transition-colors",
|
orientation={orientation}
|
||||||
orientation === "vertical" &&
|
className={cn(
|
||||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
"flex touch-none p-px transition-colors select-none",
|
||||||
orientation === "horizontal" &&
|
orientation === "vertical" &&
|
||||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
className
|
orientation === "horizontal" &&
|
||||||
)}
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
{...props}
|
className
|
||||||
>
|
)}
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
{...props}
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
>
|
||||||
))
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
data-slot="scroll-area-thumb"
|
||||||
|
className="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
export { ScrollArea, ScrollBar }
|
||||||
|
|
|
||||||
|
|
@ -1,160 +1,190 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
import { Select as SelectPrimitive } from "radix-ui"
|
import { Select as SelectPrimitive } from "radix-ui"
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
function SelectTrigger({
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
size = "default",
|
||||||
>(({ className, children, ...props }, ref) => (
|
children,
|
||||||
<SelectPrimitive.Trigger
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
className={cn(
|
size?: "sm" | "default"
|
||||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
}) {
|
||||||
className
|
return (
|
||||||
)}
|
<SelectPrimitive.Trigger
|
||||||
{...props}
|
data-slot="select-trigger"
|
||||||
>
|
data-size={size}
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
))
|
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
))
|
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
))
|
|
||||||
SelectScrollDownButton.displayName =
|
|
||||||
SelectPrimitive.ScrollDownButton.displayName
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
||||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
position={position}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
{children}
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "item-aligned",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
"bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
<SelectScrollUpButton />
|
||||||
</SelectPrimitive.Viewport>
|
<SelectPrimitive.Viewport
|
||||||
<SelectScrollDownButton />
|
className={cn(
|
||||||
</SelectPrimitive.Content>
|
"p-1",
|
||||||
</SelectPrimitive.Portal>
|
position === "popper" &&
|
||||||
))
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
function SelectLabel({
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
<SelectPrimitive.Label
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.Label
|
||||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
data-slot="select-label"
|
||||||
{...props}
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
function SelectItem({
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
<SelectPrimitive.Item
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn(
|
<SelectPrimitive.Item
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
data-slot="select-item"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
{...props}
|
className
|
||||||
>
|
)}
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
{...props}
|
||||||
<SelectPrimitive.ItemIndicator>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<span
|
||||||
</SelectPrimitive.ItemIndicator>
|
data-slot="select-item-indicator"
|
||||||
</span>
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
|
>
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
function SelectSeparator({
|
||||||
</SelectPrimitive.Item>
|
className,
|
||||||
))
|
...props
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
function SelectScrollUpButton({
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
<SelectPrimitive.Separator
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.ScrollUpButton
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
data-slot="select-scroll-up-button"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"flex cursor-default items-center justify-center py-1",
|
||||||
))
|
className
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectLabel,
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,24 @@ import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
function Separator({
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
orientation = "horizontal",
|
||||||
>(
|
decorative = true,
|
||||||
(
|
...props
|
||||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
ref
|
return (
|
||||||
) => (
|
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
data-slot="separator"
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 bg-border",
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Separator }
|
export { Separator }
|
||||||
|
|
|
||||||
|
|
@ -1,135 +1,138 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
import { Dialog as SheetPrimitive } from "radix-ui"
|
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
import { X } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Sheet = SheetPrimitive.Root
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetTrigger = SheetPrimitive.Trigger
|
function SheetTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetClose = SheetPrimitive.Close
|
function SheetClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetPortal = SheetPrimitive.Portal
|
function SheetPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const SheetOverlay = React.forwardRef<
|
function SheetOverlay({
|
||||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
<SheetPrimitive.Overlay
|
return (
|
||||||
className={cn(
|
<SheetPrimitive.Overlay
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
data-slot="sheet-overlay"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
{...props}
|
className
|
||||||
ref={ref}
|
)}
|
||||||
/>
|
|
||||||
))
|
|
||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const sheetVariants = cva(
|
|
||||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
side: {
|
|
||||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
||||||
bottom:
|
|
||||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
||||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
|
||||||
right:
|
|
||||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
side: "right",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
interface SheetContentProps
|
|
||||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
|
||||||
VariantProps<typeof sheetVariants> {}
|
|
||||||
|
|
||||||
const SheetContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
|
||||||
SheetContentProps
|
|
||||||
>(({ side = "right", className, children, ...props }, ref) => (
|
|
||||||
<SheetPortal>
|
|
||||||
<SheetOverlay />
|
|
||||||
<SheetPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(sheetVariants({ side }), className)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
/>
|
||||||
{children}
|
)
|
||||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
}
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</SheetPrimitive.Close>
|
|
||||||
</SheetPrimitive.Content>
|
|
||||||
</SheetPortal>
|
|
||||||
))
|
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const SheetHeader = ({
|
function SheetContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
data-slot="sheet-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
|
side === "right" &&
|
||||||
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||||
|
side === "left" &&
|
||||||
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||||
|
side === "top" &&
|
||||||
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
|
side === "bottom" &&
|
||||||
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{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" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<SheetPrimitive.Title
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
data-slot="sheet-title"
|
||||||
className
|
className={cn("text-foreground font-semibold", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
SheetHeader.displayName = "SheetHeader"
|
|
||||||
|
|
||||||
const SheetFooter = ({
|
function SheetDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<SheetPrimitive.Description
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
data-slot="sheet-description"
|
||||||
className
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
SheetFooter.displayName = "SheetFooter"
|
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold text-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const SheetDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetPortal,
|
|
||||||
SheetOverlay,
|
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
SheetClose,
|
SheetClose,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,10 @@
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Skeleton({
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
data-slot="skeleton"
|
||||||
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
16
components/ui/spinner.tsx
Normal file
16
components/ui/spinner.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Loader2Icon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||||
|
return (
|
||||||
|
<Loader2Icon
|
||||||
|
role="status"
|
||||||
|
aria-label="Loading"
|
||||||
|
className={cn("size-4 animate-spin", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Spinner }
|
||||||
|
|
@ -2,21 +2,17 @@ import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Textarea = React.forwardRef<
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
HTMLTextAreaElement,
|
|
||||||
React.ComponentProps<"textarea">
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
Textarea.displayName = "Textarea"
|
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea }
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,53 @@ import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Tooltip = TooltipPrimitive.Root
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
function TooltipContent({
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
sideOffset = 0,
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
children,
|
||||||
<TooltipPrimitive.Content
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
sideOffset={sideOffset}
|
return (
|
||||||
className={cn(
|
<TooltipPrimitive.Portal>
|
||||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
<TooltipPrimitive.Content
|
||||||
className
|
data-slot="tooltip-content"
|
||||||
)}
|
sideOffset={sideOffset}
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
))
|
className
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
|
|
|
||||||
|
|
@ -68,11 +68,7 @@ export function VisibilitySelector({
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button data-testid="visibility-selector" size="sm" variant="outline">
|
||||||
className="hidden h-8 md:flex md:h-fit md:px-2"
|
|
||||||
data-testid="visibility-selector"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{selectedVisibility?.icon}
|
{selectedVisibility?.icon}
|
||||||
<span className="md:sr-only">{selectedVisibility?.label}</span>
|
<span className="md:sr-only">{selectedVisibility?.label}</span>
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon />
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ export const patchTextNodes = (schema, oldNode, newNode) => {
|
||||||
const node = createTextNode(
|
const node = createTextNode(
|
||||||
schema,
|
schema,
|
||||||
sentence,
|
sentence,
|
||||||
type !== DiffType.Unchanged ? [createDiffMark(schema, type)] : []
|
type === DiffType.Unchanged ? [] : [createDiffMark(schema, type)]
|
||||||
);
|
);
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,16 @@ import { DOMParser, type Node } from "prosemirror-model";
|
||||||
import { Decoration, DecorationSet, type EditorView } from "prosemirror-view";
|
import { Decoration, DecorationSet, type EditorView } from "prosemirror-view";
|
||||||
import { renderToString } from "react-dom/server";
|
import { renderToString } from "react-dom/server";
|
||||||
|
|
||||||
import { Response } from "@/components/elements/response";
|
import { MessageResponse } from "@/components/ai-elements/message";
|
||||||
|
|
||||||
import { documentSchema } from "./config";
|
import { documentSchema } from "./config";
|
||||||
import { createSuggestionWidget, type UISuggestion } from "./suggestions";
|
import { createSuggestionWidget, type UISuggestion } from "./suggestions";
|
||||||
|
|
||||||
export const buildDocumentFromContent = (content: string) => {
|
export const buildDocumentFromContent = (content: string) => {
|
||||||
const parser = DOMParser.fromSchema(documentSchema);
|
const parser = DOMParser.fromSchema(documentSchema);
|
||||||
const stringFromMarkdown = renderToString(<Response>{content}</Response>);
|
const stringFromMarkdown = renderToString(
|
||||||
|
<MessageResponse>{content}</MessageResponse>
|
||||||
|
);
|
||||||
const tempContainer = document.createElement("div");
|
const tempContainer = document.createElement("div");
|
||||||
tempContainer.innerHTML = stringFromMarkdown;
|
tempContainer.innerHTML = stringFromMarkdown;
|
||||||
return parser.parse(tempContainer);
|
return parser.parse(tempContainer);
|
||||||
|
|
|
||||||
6
next-env.d.ts
vendored
6
next-env.d.ts
vendored
|
|
@ -1,6 +0,0 @@
|
||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
import "./.next/dev/types/routes.d.ts";
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
||||||
32
package.json
32
package.json
|
|
@ -6,8 +6,8 @@
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
"build": "tsx lib/db/migrate && next build",
|
"build": "tsx lib/db/migrate && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "ultracite check",
|
"check": "ultracite check",
|
||||||
"format": "ultracite fix",
|
"fix": "ultracite fix",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "npx tsx lib/db/migrate.ts",
|
"db:migrate": "npx tsx lib/db/migrate.ts",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
|
|
@ -21,32 +21,21 @@
|
||||||
"@ai-sdk/gateway": "^3.0.15",
|
"@ai-sdk/gateway": "^3.0.15",
|
||||||
"@ai-sdk/provider": "^3.0.3",
|
"@ai-sdk/provider": "^3.0.3",
|
||||||
"@ai-sdk/react": "3.0.39",
|
"@ai-sdk/react": "3.0.39",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@codemirror/view": "^6.35.3",
|
"@codemirror/view": "^6.35.3",
|
||||||
"@icons-pack/react-simple-icons": "^13.7.0",
|
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/api-logs": "^0.200.0",
|
"@opentelemetry/api-logs": "^0.200.0",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
|
||||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
"@radix-ui/react-visually-hidden": "^1.1.0",
|
"@streamdown/cjk": "^1.0.2",
|
||||||
|
"@streamdown/code": "^1.0.3",
|
||||||
|
"@streamdown/math": "^1.0.2",
|
||||||
|
"@streamdown/mermaid": "^1.0.2",
|
||||||
"@vercel/analytics": "^1.3.1",
|
"@vercel/analytics": "^1.3.1",
|
||||||
"@vercel/blob": "^0.24.1",
|
"@vercel/blob": "^0.24.1",
|
||||||
"@vercel/functions": "^2.0.0",
|
"@vercel/functions": "^2.0.0",
|
||||||
"@vercel/otel": "^1.12.0",
|
"@vercel/otel": "^1.12.0",
|
||||||
"@xyflow/react": "^12.10.0",
|
|
||||||
"ai": "6.0.37",
|
"ai": "6.0.37",
|
||||||
"bcrypt-ts": "^5.0.2",
|
"bcrypt-ts": "^5.0.2",
|
||||||
"botid": "^1.5.11",
|
"botid": "^1.5.11",
|
||||||
|
|
@ -59,11 +48,9 @@
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-orm": "^0.34.0",
|
"drizzle-orm": "^0.34.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"framer-motion": "^11.3.19",
|
"framer-motion": "^11.3.19",
|
||||||
"geist": "^1.3.1",
|
"katex": "^0.16.28",
|
||||||
"katex": "^0.16.25",
|
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
"motion": "^12.23.26",
|
"motion": "^12.23.26",
|
||||||
"nanoid": "^5.1.3",
|
"nanoid": "^5.1.3",
|
||||||
|
|
@ -85,14 +72,12 @@
|
||||||
"react": "19.0.1",
|
"react": "19.0.1",
|
||||||
"react-data-grid": "7.0.0-beta.47",
|
"react-data-grid": "7.0.0-beta.47",
|
||||||
"react-dom": "19.0.1",
|
"react-dom": "19.0.1",
|
||||||
"react-resizable-panels": "^2.1.7",
|
|
||||||
"react-syntax-highlighter": "^15.6.6",
|
|
||||||
"redis": "^5.0.0",
|
"redis": "^5.0.0",
|
||||||
"resumable-stream": "^2.2.10",
|
"resumable-stream": "^2.2.10",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shiki": "^3.21.0",
|
"shiki": "^3.21.0",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
"streamdown": "^2.0.1",
|
"streamdown": "^2.3.0",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|
@ -111,7 +96,6 @@
|
||||||
"@types/pdf-parse": "^1.1.4",
|
"@types/pdf-parse": "^1.1.4",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
|
||||||
"drizzle-kit": "^0.25.0",
|
"drizzle-kit": "^0.25.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
|
|
|
||||||
1708
pnpm-lock.yaml
generated
1708
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue