feat: add assistant ui chat component boundary
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/styles/global.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
@@ -10,16 +10,23 @@
|
|||||||
"preview": "vite preview --host 127.0.0.1"
|
"preview": "vite preview --host 127.0.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@assistant-ui/react": "^0.14.26",
|
||||||
"@dagrejs/dagre": "3.0.0",
|
"@dagrejs/dagre": "3.0.0",
|
||||||
"@fontsource-variable/newsreader": "5.2.10",
|
"@fontsource-variable/newsreader": "5.2.10",
|
||||||
"@fontsource-variable/source-sans-3": "5.2.9",
|
"@fontsource-variable/source-sans-3": "5.2.9",
|
||||||
"@fontsource/barlow-condensed": "5.2.8",
|
"@fontsource/barlow-condensed": "5.2.8",
|
||||||
"@fontsource/ibm-plex-mono": "5.2.7",
|
"@fontsource/ibm-plex-mono": "5.2.7",
|
||||||
"@xyflow/react": "12.11.1",
|
"@xyflow/react": "12.11.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
"motion": "^12.42.2",
|
"motion": "^12.42.2",
|
||||||
|
"radix-ui": "^1.6.2",
|
||||||
"react": "19.2.7",
|
"react": "19.2.7",
|
||||||
"react-dom": "19.2.7",
|
"react-dom": "19.2.7",
|
||||||
"react-router-dom": "^7.18.1",
|
"react-router-dom": "^7.18.1",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tw-shimmer": "^0.4.11",
|
||||||
"valibot": "1.4.2"
|
"valibot": "1.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,605 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo, useCallback, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
AlertCircleIcon,
|
||||||
|
CheckIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
useScrollLock,
|
||||||
|
useToolCallElapsed,
|
||||||
|
type ToolApprovalOption,
|
||||||
|
type ToolCallMessagePart,
|
||||||
|
type ToolCallMessagePartProps,
|
||||||
|
type ToolCallMessagePartStatus,
|
||||||
|
type ToolCallMessagePartComponent,
|
||||||
|
} from "@assistant-ui/react";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
|
const pressable = "active:scale-[0.98]";
|
||||||
|
|
||||||
|
export type ToolFallbackRootProps = Omit<
|
||||||
|
React.ComponentProps<typeof Collapsible>,
|
||||||
|
"open" | "onOpenChange"
|
||||||
|
> & {
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ToolFallbackRoot({
|
||||||
|
className,
|
||||||
|
open: controlledOpen,
|
||||||
|
onOpenChange: controlledOnOpenChange,
|
||||||
|
defaultOpen = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ToolFallbackRootProps) {
|
||||||
|
const collapsibleRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
||||||
|
const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION);
|
||||||
|
|
||||||
|
const isControlled = controlledOpen !== undefined;
|
||||||
|
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
lockScroll();
|
||||||
|
if (!isControlled) {
|
||||||
|
setUncontrolledOpen(open);
|
||||||
|
}
|
||||||
|
controlledOnOpenChange?.(open);
|
||||||
|
},
|
||||||
|
[lockScroll, isControlled, controlledOnOpenChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
ref={collapsibleRef}
|
||||||
|
data-slot="tool-fallback-root"
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-root group/tool-fallback-root w-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--animation-duration": `${ANIMATION_DURATION}ms`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolStatus = ToolCallMessagePartStatus["type"];
|
||||||
|
|
||||||
|
const statusIconMap: Record<ToolStatus, React.ElementType> = {
|
||||||
|
running: LoaderIcon,
|
||||||
|
complete: CheckIcon,
|
||||||
|
incomplete: XCircleIcon,
|
||||||
|
"requires-action": AlertCircleIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatToolDuration = (ms: number) => {
|
||||||
|
if (ms < 1000) return "<1s";
|
||||||
|
const seconds = ms / 1000;
|
||||||
|
if (seconds < 10) return `${(Math.floor(seconds * 10) / 10).toFixed(1)}s`;
|
||||||
|
if (seconds < 60) return `${Math.floor(seconds)}s`;
|
||||||
|
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ToolFallbackDuration({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
const elapsedMs = useToolCallElapsed();
|
||||||
|
if (elapsedMs === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="tool-fallback-duration"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-duration text-muted-foreground text-xs tabular-nums",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{formatToolDuration(elapsedMs)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolFallbackTrigger({
|
||||||
|
toolName,
|
||||||
|
status,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsibleTrigger> & {
|
||||||
|
toolName: string;
|
||||||
|
status?: ToolCallMessagePartStatus;
|
||||||
|
}) {
|
||||||
|
const statusType = status?.type ?? "complete";
|
||||||
|
const isRunning = statusType === "running";
|
||||||
|
const isCancelled =
|
||||||
|
status?.type === "incomplete" && status.reason === "cancelled";
|
||||||
|
|
||||||
|
const Icon = statusIconMap[statusType];
|
||||||
|
const label = isCancelled ? "Cancelled tool" : "Used tool";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleTrigger
|
||||||
|
data-slot="tool-fallback-trigger"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-trigger group/trigger text-muted-foreground hover:text-foreground flex w-fit origin-left items-center gap-2 py-1.5 text-sm transition-[color,scale] active:scale-[0.98]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
data-slot="tool-fallback-trigger-icon"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-trigger-icon size-4 shrink-0",
|
||||||
|
isCancelled && "text-muted-foreground",
|
||||||
|
isRunning && "animate-spin [animation-duration:0.6s]",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
data-slot="tool-fallback-trigger-label"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-trigger-label-wrapper relative inline-block text-start leading-none",
|
||||||
|
isCancelled && "text-muted-foreground line-through",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{label}: <b>{toolName}</b>
|
||||||
|
</span>
|
||||||
|
{isRunning && (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="tool-fallback-trigger-shimmer"
|
||||||
|
className="aui-tool-fallback-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
|
||||||
|
>
|
||||||
|
{label}: <b>{toolName}</b>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ToolFallbackDuration />
|
||||||
|
<ChevronDownIcon
|
||||||
|
data-slot="tool-fallback-trigger-chevron"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-trigger-chevron size-4 shrink-0",
|
||||||
|
"transition-transform duration-(--animation-duration) ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none",
|
||||||
|
"group-data-[state=closed]/trigger:-rotate-90",
|
||||||
|
"group-data-[state=open]/trigger:rotate-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolFallbackContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsibleContent
|
||||||
|
data-slot="tool-fallback-content"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-content relative overflow-hidden text-sm outline-none",
|
||||||
|
"group/collapsible-content ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:animate-none",
|
||||||
|
"data-[state=closed]:animate-collapsible-up",
|
||||||
|
"data-[state=open]:animate-collapsible-down",
|
||||||
|
"data-[state=closed]:fill-mode-forwards",
|
||||||
|
"data-[state=closed]:pointer-events-none",
|
||||||
|
"data-[state=open]:duration-(--animation-duration)",
|
||||||
|
"data-[state=closed]:duration-(--animation-duration)",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-2 ps-6 pt-1 pb-2 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:animate-none",
|
||||||
|
"group-data-[state=open]/collapsible-content:animate-in group-data-[state=open]/collapsible-content:fade-in-0 group-data-[state=open]/collapsible-content:blur-in-[2px] group-data-[state=open]/collapsible-content:slide-in-from-top-1",
|
||||||
|
"group-data-[state=closed]/collapsible-content:animate-out group-data-[state=closed]/collapsible-content:fade-out-0 group-data-[state=closed]/collapsible-content:blur-out-[2px] group-data-[state=closed]/collapsible-content:slide-out-to-top-1",
|
||||||
|
"group-data-[state=closed]/collapsible-content:duration-(--animation-duration) group-data-[state=open]/collapsible-content:duration-(--animation-duration)",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolFallbackArgs({
|
||||||
|
argsText,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
argsText?: string;
|
||||||
|
}) {
|
||||||
|
if (!argsText) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="tool-fallback-args"
|
||||||
|
className={cn("aui-tool-fallback-args", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<pre className="aui-tool-fallback-args-value bg-muted/50 text-foreground/90 rounded-md p-2.5 text-xs whitespace-pre-wrap">
|
||||||
|
{argsText}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolFallbackResult({
|
||||||
|
result,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
result?: unknown;
|
||||||
|
}) {
|
||||||
|
if (result === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="tool-fallback-result"
|
||||||
|
className={cn("aui-tool-fallback-result", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<p className="aui-tool-fallback-result-header text-muted-foreground text-xs font-medium">
|
||||||
|
Result:
|
||||||
|
</p>
|
||||||
|
<pre className="aui-tool-fallback-result-content bg-muted/50 text-foreground/90 mt-1 rounded-md p-2.5 text-xs whitespace-pre-wrap">
|
||||||
|
{typeof result === "string" ? result : JSON.stringify(result, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolFallbackError({
|
||||||
|
status,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
status?: ToolCallMessagePartStatus;
|
||||||
|
}) {
|
||||||
|
if (status?.type !== "incomplete") return null;
|
||||||
|
|
||||||
|
const error = status.error;
|
||||||
|
const errorText = error
|
||||||
|
? typeof error === "string"
|
||||||
|
? error
|
||||||
|
: JSON.stringify(error)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!errorText) return null;
|
||||||
|
|
||||||
|
const isCancelled = status.reason === "cancelled";
|
||||||
|
const headerText = isCancelled ? "Cancelled reason:" : "Error:";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="tool-fallback-error"
|
||||||
|
className={cn("aui-tool-fallback-error", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<p className="aui-tool-fallback-error-header text-muted-foreground font-semibold">
|
||||||
|
{headerText}
|
||||||
|
</p>
|
||||||
|
<p className="aui-tool-fallback-error-reason text-muted-foreground">
|
||||||
|
{errorText}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const APPROVED_RESULT = "Approved by user";
|
||||||
|
const DENIED_RESULT = "User denied tool execution";
|
||||||
|
|
||||||
|
const APPROVAL_OPTION_DEFAULT_LABELS: Record<string, string> = {
|
||||||
|
"allow-once": "Allow",
|
||||||
|
"allow-always": "Always allow",
|
||||||
|
"reject-once": "Deny",
|
||||||
|
"reject-always": "Always deny",
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAllowKind = (kind: string) =>
|
||||||
|
kind === "allow-once" || kind === "allow-always";
|
||||||
|
|
||||||
|
const approvalOptionLabel = (option: ToolApprovalOption) =>
|
||||||
|
option.label ??
|
||||||
|
(Object.hasOwn(APPROVAL_OPTION_DEFAULT_LABELS, option.kind)
|
||||||
|
? APPROVAL_OPTION_DEFAULT_LABELS[option.kind]
|
||||||
|
: undefined) ??
|
||||||
|
option.id;
|
||||||
|
|
||||||
|
function ToolFallbackApproval({
|
||||||
|
className,
|
||||||
|
addResult,
|
||||||
|
resume,
|
||||||
|
interrupt,
|
||||||
|
approval,
|
||||||
|
respondToApproval,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> &
|
||||||
|
Partial<
|
||||||
|
Pick<ToolCallMessagePartProps, "addResult" | "resume" | "respondToApproval">
|
||||||
|
> & {
|
||||||
|
interrupt?: ToolCallMessagePart["interrupt"];
|
||||||
|
approval?: ToolCallMessagePart["approval"];
|
||||||
|
}) {
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
const [confirmingId, setConfirmingId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (
|
||||||
|
approval != null &&
|
||||||
|
(approval.approved !== undefined || approval.resolution !== undefined)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Custom (`_`-prefixed) kinds cannot be resolved to a boolean by the kit;
|
||||||
|
// hosts using custom kinds render their own bar. A declared option list is
|
||||||
|
// a host constraint: the kit never adds an approval path beyond it, but
|
||||||
|
// always preserves a refusal path.
|
||||||
|
const declaredOptions = respondToApproval ? approval?.options : undefined;
|
||||||
|
const options = declaredOptions?.filter((o) =>
|
||||||
|
Object.hasOwn(APPROVAL_OPTION_DEFAULT_LABELS, o.kind),
|
||||||
|
);
|
||||||
|
|
||||||
|
const respond = (approved: boolean) => {
|
||||||
|
if (submitted) return;
|
||||||
|
if (
|
||||||
|
approval != null &&
|
||||||
|
approval.approved === undefined &&
|
||||||
|
respondToApproval
|
||||||
|
) {
|
||||||
|
respondToApproval({ approved });
|
||||||
|
} else if (interrupt) {
|
||||||
|
resume?.({ approved });
|
||||||
|
} else {
|
||||||
|
addResult?.(approved ? APPROVED_RESULT : DENIED_RESULT);
|
||||||
|
}
|
||||||
|
setSubmitted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const respondWithOption = (option: ToolApprovalOption) => {
|
||||||
|
if (submitted) return;
|
||||||
|
respondToApproval?.({ optionId: option.id });
|
||||||
|
setSubmitted(true);
|
||||||
|
setConfirmingId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOption = (option: ToolApprovalOption) => {
|
||||||
|
if (option.confirm) {
|
||||||
|
setConfirmingId(option.id);
|
||||||
|
} else {
|
||||||
|
respondWithOption(option);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirming =
|
||||||
|
confirmingId != null
|
||||||
|
? options?.find((o) => o.id === confirmingId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (confirming) {
|
||||||
|
const confirmMeta =
|
||||||
|
typeof confirming.confirm === "object" ? confirming.confirm : undefined;
|
||||||
|
const confirmDescription =
|
||||||
|
confirmMeta?.description ?? confirming.description;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="tool-fallback-approval-confirm"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-approval-confirm flex flex-col gap-2 pt-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<p className="aui-tool-fallback-approval-confirm-title font-semibold">
|
||||||
|
{confirmMeta?.title ?? `${approvalOptionLabel(confirming)}?`}
|
||||||
|
</p>
|
||||||
|
{confirmDescription && (
|
||||||
|
<p className="aui-tool-fallback-approval-confirm-description text-muted-foreground">
|
||||||
|
{confirmDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{confirming.grants && confirming.grants.length > 0 && (
|
||||||
|
<ul className="aui-tool-fallback-approval-confirm-grants flex flex-col gap-1">
|
||||||
|
{confirming.grants.map((grant) => (
|
||||||
|
<li key={grant}>
|
||||||
|
<code className="aui-tool-fallback-approval-confirm-grant bg-muted rounded px-1.5 py-0.5 text-xs">
|
||||||
|
{grant}
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={pressable}
|
||||||
|
onClick={() => respondWithOption(confirming)}
|
||||||
|
disabled={submitted}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={pressable}
|
||||||
|
onClick={() => setConfirmingId(null)}
|
||||||
|
disabled={submitted}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (declaredOptions && declaredOptions.length > 0) {
|
||||||
|
const allowOptions = options?.filter((o) => isAllowKind(o.kind)) ?? [];
|
||||||
|
const rejectOptions = options?.filter((o) => !isAllowKind(o.kind)) ?? [];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="tool-fallback-approval"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-approval flex flex-wrap items-center gap-2 pt-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{[...allowOptions, ...rejectOptions].map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.id}
|
||||||
|
size="sm"
|
||||||
|
variant={option === allowOptions[0] ? "default" : "outline"}
|
||||||
|
className={pressable}
|
||||||
|
onClick={() => handleOption(option)}
|
||||||
|
disabled={submitted}
|
||||||
|
>
|
||||||
|
{approvalOptionLabel(option)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
{rejectOptions.length === 0 && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={pressable}
|
||||||
|
onClick={() => respond(false)}
|
||||||
|
disabled={submitted}
|
||||||
|
>
|
||||||
|
Deny
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="tool-fallback-approval"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-fallback-approval flex items-center gap-2 pt-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={pressable}
|
||||||
|
onClick={() => respond(true)}
|
||||||
|
disabled={submitted}
|
||||||
|
>
|
||||||
|
Allow
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={pressable}
|
||||||
|
onClick={() => respond(false)}
|
||||||
|
disabled={submitted}
|
||||||
|
>
|
||||||
|
Deny
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolFallbackImpl: ToolCallMessagePartComponent = ({
|
||||||
|
toolName,
|
||||||
|
argsText,
|
||||||
|
result,
|
||||||
|
status,
|
||||||
|
addResult,
|
||||||
|
resume,
|
||||||
|
interrupt,
|
||||||
|
approval,
|
||||||
|
respondToApproval,
|
||||||
|
}) => {
|
||||||
|
const isCancelled =
|
||||||
|
status?.type === "incomplete" && status.reason === "cancelled";
|
||||||
|
const isRequiresAction = status?.type === "requires-action";
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(isRequiresAction);
|
||||||
|
const [prevRequiresAction, setPrevRequiresAction] =
|
||||||
|
useState(isRequiresAction);
|
||||||
|
if (isRequiresAction !== prevRequiresAction) {
|
||||||
|
setPrevRequiresAction(isRequiresAction);
|
||||||
|
if (isRequiresAction) setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolFallbackRoot open={open} onOpenChange={setOpen}>
|
||||||
|
<ToolFallbackTrigger toolName={toolName} status={status} />
|
||||||
|
<ToolFallbackContent>
|
||||||
|
<ToolFallbackError status={status} />
|
||||||
|
<ToolFallbackArgs
|
||||||
|
argsText={argsText}
|
||||||
|
className={cn(isCancelled && "opacity-60")}
|
||||||
|
/>
|
||||||
|
{isRequiresAction && (
|
||||||
|
<ToolFallbackApproval
|
||||||
|
addResult={addResult}
|
||||||
|
resume={resume}
|
||||||
|
interrupt={interrupt}
|
||||||
|
approval={approval}
|
||||||
|
respondToApproval={respondToApproval}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isCancelled && <ToolFallbackResult result={result} />}
|
||||||
|
</ToolFallbackContent>
|
||||||
|
</ToolFallbackRoot>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolFallback = memo(
|
||||||
|
ToolFallbackImpl,
|
||||||
|
) as unknown as ToolCallMessagePartComponent & {
|
||||||
|
Root: typeof ToolFallbackRoot;
|
||||||
|
Trigger: typeof ToolFallbackTrigger;
|
||||||
|
Content: typeof ToolFallbackContent;
|
||||||
|
Args: typeof ToolFallbackArgs;
|
||||||
|
Result: typeof ToolFallbackResult;
|
||||||
|
Error: typeof ToolFallbackError;
|
||||||
|
Approval: typeof ToolFallbackApproval;
|
||||||
|
};
|
||||||
|
|
||||||
|
ToolFallback.displayName = "ToolFallback";
|
||||||
|
ToolFallback.Root = ToolFallbackRoot;
|
||||||
|
ToolFallback.Trigger = ToolFallbackTrigger;
|
||||||
|
ToolFallback.Content = ToolFallbackContent;
|
||||||
|
ToolFallback.Args = ToolFallbackArgs;
|
||||||
|
ToolFallback.Result = ToolFallbackResult;
|
||||||
|
ToolFallback.Error = ToolFallbackError;
|
||||||
|
ToolFallback.Approval = ToolFallbackApproval;
|
||||||
|
|
||||||
|
export {
|
||||||
|
ToolFallback,
|
||||||
|
ToolFallbackRoot,
|
||||||
|
ToolFallbackTrigger,
|
||||||
|
ToolFallbackContent,
|
||||||
|
ToolFallbackArgs,
|
||||||
|
ToolFallbackResult,
|
||||||
|
ToolFallbackError,
|
||||||
|
ToolFallbackApproval,
|
||||||
|
};
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type FC,
|
||||||
|
type PropsWithChildren,
|
||||||
|
} from "react";
|
||||||
|
import { ChevronDownIcon, LoaderIcon } from "lucide-react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { useScrollLock } from "@assistant-ui/react";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
|
const toolGroupVariants = cva("aui-tool-group-root group/tool-group w-full", {
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
outline: "rounded-lg border py-3",
|
||||||
|
ghost: "",
|
||||||
|
muted: "border-muted-foreground/30 bg-muted/30 rounded-lg border py-3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: { variant: "outline" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ToolGroupRootProps = Omit<
|
||||||
|
React.ComponentProps<typeof Collapsible>,
|
||||||
|
"open" | "onOpenChange"
|
||||||
|
> &
|
||||||
|
VariantProps<typeof toolGroupVariants> & {
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ToolGroupRoot({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
open: controlledOpen,
|
||||||
|
onOpenChange: controlledOnOpenChange,
|
||||||
|
defaultOpen = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ToolGroupRootProps) {
|
||||||
|
const collapsibleRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
||||||
|
const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION);
|
||||||
|
|
||||||
|
const isControlled = controlledOpen !== undefined;
|
||||||
|
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
lockScroll();
|
||||||
|
if (!isControlled) {
|
||||||
|
setUncontrolledOpen(open);
|
||||||
|
}
|
||||||
|
controlledOnOpenChange?.(open);
|
||||||
|
},
|
||||||
|
[lockScroll, isControlled, controlledOnOpenChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
ref={collapsibleRef}
|
||||||
|
data-slot="tool-group-root"
|
||||||
|
data-variant={variant ?? "outline"}
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
className={cn(
|
||||||
|
toolGroupVariants({ variant }),
|
||||||
|
"group/tool-group-root",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--animation-duration": `${ANIMATION_DURATION}ms`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolGroupTrigger({
|
||||||
|
count,
|
||||||
|
active = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsibleTrigger> & {
|
||||||
|
count: number;
|
||||||
|
active?: boolean;
|
||||||
|
}) {
|
||||||
|
const label = `${count} tool ${count === 1 ? "call" : "calls"}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleTrigger
|
||||||
|
data-slot="tool-group-trigger"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-group-trigger group/trigger flex origin-left items-center gap-2 text-sm transition-[color,scale] active:scale-[0.98]",
|
||||||
|
"group-data-[variant=ghost]/tool-group-root:text-muted-foreground group-data-[variant=ghost]/tool-group-root:hover:text-foreground group-data-[variant=ghost]/tool-group-root:py-1.5",
|
||||||
|
"group-data-[variant=outline]/tool-group-root:w-full group-data-[variant=outline]/tool-group-root:px-4",
|
||||||
|
"group-data-[variant=muted]/tool-group-root:w-full group-data-[variant=muted]/tool-group-root:px-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{active && (
|
||||||
|
<LoaderIcon
|
||||||
|
data-slot="tool-group-trigger-loader"
|
||||||
|
className="aui-tool-group-trigger-loader size-3 shrink-0 animate-spin [animation-duration:0.6s]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
data-slot="tool-group-trigger-label"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-group-trigger-label-wrapper relative inline-block text-start leading-none font-medium",
|
||||||
|
"group-data-[variant=ghost]/tool-group-root:font-normal",
|
||||||
|
"group-data-[variant=outline]/tool-group-root:grow",
|
||||||
|
"group-data-[variant=muted]/tool-group-root:grow",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-xs">{label}</span>
|
||||||
|
{active && (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="tool-group-trigger-shimmer"
|
||||||
|
className="aui-tool-group-trigger-shimmer shimmer pointer-events-none absolute inset-0 text-xs motion-reduce:animate-none"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
data-slot="tool-group-trigger-chevron"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-group-trigger-chevron size-3 shrink-0",
|
||||||
|
"transition-transform duration-(--animation-duration) ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none",
|
||||||
|
"group-data-[state=closed]/trigger:-rotate-90",
|
||||||
|
"group-data-[state=open]/trigger:rotate-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolGroupContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsibleContent
|
||||||
|
data-slot="tool-group-content"
|
||||||
|
className={cn(
|
||||||
|
"aui-tool-group-content relative overflow-hidden text-sm outline-none",
|
||||||
|
"group/collapsible-content ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:animate-none",
|
||||||
|
"data-[state=closed]:animate-collapsible-up",
|
||||||
|
"data-[state=open]:animate-collapsible-down",
|
||||||
|
"data-[state=closed]:fill-mode-forwards",
|
||||||
|
"data-[state=closed]:pointer-events-none",
|
||||||
|
"data-[state=open]:duration-(--animation-duration)",
|
||||||
|
"data-[state=closed]:duration-(--animation-duration)",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-2 flex flex-col gap-2",
|
||||||
|
"group-data-[variant=ghost]/tool-group-root:mt-1 group-data-[variant=ghost]/tool-group-root:gap-1",
|
||||||
|
"group-data-[variant=outline]/tool-group-root:mt-3 group-data-[variant=outline]/tool-group-root:border-t group-data-[variant=outline]/tool-group-root:px-4 group-data-[variant=outline]/tool-group-root:pt-3",
|
||||||
|
"group-data-[variant=muted]/tool-group-root:mt-3 group-data-[variant=muted]/tool-group-root:border-t group-data-[variant=muted]/tool-group-root:px-4 group-data-[variant=muted]/tool-group-root:pt-3",
|
||||||
|
"[&>*]:animate-in [&>*]:fade-in-0 [&>*]:blur-in-[2px] [&>*]:slide-in-from-top-1 [&>*]:duration-(--animation-duration) [&>*]:ease-[cubic-bezier(0.32,0.72,0,1)]",
|
||||||
|
"[&>*]:motion-reduce:animate-none",
|
||||||
|
"[&>*:nth-child(2)]:[animation-delay:40ms]",
|
||||||
|
"[&>*:nth-child(3)]:[animation-delay:80ms]",
|
||||||
|
"[&>*:nth-child(4)]:[animation-delay:120ms]",
|
||||||
|
"[&>*:nth-child(n+5)]:[animation-delay:160ms]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolGroupComponent = FC<
|
||||||
|
PropsWithChildren<{ startIndex: number; endIndex: number }>
|
||||||
|
> & {
|
||||||
|
Root: typeof ToolGroupRoot;
|
||||||
|
Trigger: typeof ToolGroupTrigger;
|
||||||
|
Content: typeof ToolGroupContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolGroupImpl: FC<
|
||||||
|
PropsWithChildren<{ startIndex: number; endIndex: number }>
|
||||||
|
> = ({ children, startIndex, endIndex }) => {
|
||||||
|
const toolCount = endIndex - startIndex + 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolGroupRoot>
|
||||||
|
<ToolGroupTrigger count={toolCount} />
|
||||||
|
<ToolGroupContent>{children}</ToolGroupContent>
|
||||||
|
</ToolGroupRoot>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This wrapper targets the legacy `components.ToolGroup` prop
|
||||||
|
* on `<MessagePrimitive.Parts>`. Use `<MessagePrimitive.GroupedParts>` with
|
||||||
|
* a `groupBy` returning `"group-tool"` and compose `ToolGroupRoot` /
|
||||||
|
* `ToolGroupTrigger` / `ToolGroupContent` directly. See `thread.tsx`.
|
||||||
|
*/
|
||||||
|
const ToolGroup = memo(ToolGroupImpl) as unknown as ToolGroupComponent;
|
||||||
|
|
||||||
|
ToolGroup.displayName = "ToolGroup";
|
||||||
|
ToolGroup.Root = ToolGroupRoot;
|
||||||
|
ToolGroup.Trigger = ToolGroupTrigger;
|
||||||
|
ToolGroup.Content = ToolGroupContent;
|
||||||
|
|
||||||
|
export {
|
||||||
|
ToolGroup,
|
||||||
|
ToolGroupRoot,
|
||||||
|
ToolGroupTrigger,
|
||||||
|
ToolGroupContent,
|
||||||
|
toolGroupVariants,
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export const cn = (...inputs: ClassValue[]): string => twMerge(clsx(inputs));
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
"module": "Preserve",
|
"module": "Preserve",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"types": ["node", "vite/client", "vitest/globals"]
|
"types": ["node", "vite/client", "vitest/globals"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
const backendPort = process.env.WEB_PORT ?? "8787";
|
const backendPort = process.env.WEB_PORT ?? "8787";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
// Server must listen on this port (set via WEB_PORT env or default 8787)
|
// Server must listen on this port (set via WEB_PORT env or default 8787)
|
||||||
|
|||||||
Generated
+1967
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user