style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,15 +1,9 @@
"use client";
'use client';
import { Button } from "../ui/button";
import { cn } from "../../src/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react";
import {
type ComponentProps,
createContext,
type HTMLAttributes,
useContext,
useState,
} from "react";
import { Button } from '../ui/button';
import { cn } from '../../src/lib/utils';
import { CheckIcon, CopyIcon } from 'lucide-react';
import { type ComponentProps, createContext, type HTMLAttributes, useContext, useState } from 'react';
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
code: string;
@@ -22,7 +16,7 @@ type CodeBlockContextType = {
};
const CodeBlockContext = createContext<CodeBlockContextType>({
code: "",
code: '',
});
export const CodeBlock = ({
@@ -33,14 +27,14 @@ export const CodeBlock = ({
children,
...props
}: CodeBlockProps) => {
const lines = code.split("\n");
const lines = code.split('\n');
return (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
"group relative w-full overflow-hidden rounded-md border bg-background text-foreground",
className
'group relative w-full overflow-hidden rounded-md border bg-background text-foreground',
className,
)}
{...props}
>
@@ -57,7 +51,7 @@ export const CodeBlock = ({
)}
<td className="p-0">
<pre className="m-0 p-0 text-sm whitespace-pre font-mono">
<code className="text-sm">{line || "\u00A0"}</code>
<code className="text-sm">{line || '\u00A0'}</code>
</pre>
</td>
</tr>
@@ -65,11 +59,7 @@ export const CodeBlock = ({
</tbody>
</table>
</div>
{children && (
<div className="absolute top-2 right-2 flex items-center gap-2">
{children}
</div>
)}
{children && <div className="absolute top-2 right-2 flex items-center gap-2">{children}</div>}
</div>
</div>
</CodeBlockContext.Provider>
@@ -94,8 +84,8 @@ export const CodeBlockCopyButton = ({
const { code } = useContext(CodeBlockContext);
const copyToClipboard = async () => {
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
onError?.(new Error("Clipboard API not available"));
if (typeof window === 'undefined' || !navigator?.clipboard?.writeText) {
onError?.(new Error('Clipboard API not available'));
return;
}
@@ -112,13 +102,7 @@ export const CodeBlockCopyButton = ({
const Icon = isCopied ? CheckIcon : CopyIcon;
return (
<Button
className={cn("shrink-0", className)}
onClick={copyToClipboard}
size="icon"
variant="ghost"
{...props}
>
<Button className={cn('shrink-0', className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props}>
{children ?? <Icon size={14} />}
</Button>
);

View File

@@ -1,17 +1,17 @@
"use client";
'use client';
import { Button } from "../ui/button";
import { cn } from "../../src/lib/utils";
import { ArrowDownIcon, UserIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { useCallback } from "react";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
import { Button } from '../ui/button';
import { cn } from '../../src/lib/utils';
import { ArrowDownIcon, UserIcon } from 'lucide-react';
import type { ComponentProps } from 'react';
import { useCallback } from 'react';
import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
export type ConversationProps = ComponentProps<typeof StickToBottom>;
export const Conversation = ({ className, ...props }: ConversationProps) => (
<StickToBottom
className={cn("relative flex-1 overflow-y-hidden overflow-x-hidden", className)}
className={cn('relative flex-1 overflow-y-hidden overflow-x-hidden', className)}
initial="smooth"
resize="smooth"
role="log"
@@ -19,21 +19,16 @@ export const Conversation = ({ className, ...props }: ConversationProps) => (
/>
);
export type ConversationContentProps = ComponentProps<
typeof StickToBottom.Content
>;
export type ConversationContentProps = ComponentProps<typeof StickToBottom.Content>;
export const ConversationContent = ({
className,
...props
}: ConversationContentProps) => (
export const ConversationContent = ({ className, ...props }: ConversationContentProps) => (
<StickToBottom.Content
className={cn("mx-auto flex max-w-3xl flex-col gap-2 px-4 py-8 sm:px-8 sm:py-12 min-w-0", className)}
className={cn('mx-auto flex max-w-3xl flex-col gap-2 px-4 py-8 sm:px-8 sm:py-12 min-w-0', className)}
{...props}
/>
);
export type ConversationEmptyStateProps = ComponentProps<"div"> & {
export type ConversationEmptyStateProps = ComponentProps<'div'> & {
title?: string;
description?: string;
icon?: React.ReactNode;
@@ -41,17 +36,14 @@ export type ConversationEmptyStateProps = ComponentProps<"div"> & {
export const ConversationEmptyState = ({
className,
title = "No messages yet",
description = "Start a conversation to see messages here",
title = 'No messages yet',
description = 'Start a conversation to see messages here',
icon,
children,
...props
}: ConversationEmptyStateProps) => (
<div
className={cn(
"flex size-full flex-col items-center justify-center gap-4 p-8 text-center",
className
)}
className={cn('flex size-full flex-col items-center justify-center gap-4 p-8 text-center', className)}
{...props}
>
{children ?? (
@@ -59,9 +51,7 @@ export const ConversationEmptyState = ({
{icon && <div className="text-text-muted">{icon}</div>}
<div className="space-y-2">
<h3 className="font-semibold text-base font-display text-text-primary">{title}</h3>
{description && (
<p className="text-text-muted text-sm leading-relaxed max-w-xs">{description}</p>
)}
{description && <p className="text-text-muted text-sm leading-relaxed max-w-xs">{description}</p>}
</div>
</>
)}
@@ -76,10 +66,7 @@ export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
* When used standalone, it handles its own visibility based on isAtBottom.
* When used in ConversationScrollButtons, the container manages visibility.
*/
export const ConversationScrollButton = ({
className,
...props
}: ConversationScrollButtonProps) => {
export const ConversationScrollButton = ({ className, ...props }: ConversationScrollButtonProps) => {
const { scrollToBottom } = useStickToBottomContext();
const handleScrollToBottom = useCallback(() => {
@@ -88,10 +75,7 @@ export const ConversationScrollButton = ({
return (
<Button
className={cn(
"rounded-full",
className
)}
className={cn('rounded-full', className)}
onClick={handleScrollToBottom}
size="icon"
type="button"
@@ -108,7 +92,7 @@ export const ConversationScrollButton = ({
* Data attribute used to mark the last user message element.
* ChatInterface adds this attribute to the last user message for scroll targeting.
*/
export const LAST_USER_MESSAGE_ATTR = "data-last-user-message";
export const LAST_USER_MESSAGE_ATTR = 'data-last-user-message';
export type ConversationScrollToLastUserMessageButtonProps = ComponentProps<typeof Button>;
@@ -124,16 +108,13 @@ export const ConversationScrollToLastUserMessageButton = ({
// Find the last user message element by data attribute
const lastUserMessage = document.querySelector(`[${LAST_USER_MESSAGE_ATTR}="true"]`);
if (lastUserMessage) {
lastUserMessage.scrollIntoView({ behavior: "smooth", block: "start" });
lastUserMessage.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, []);
return (
<Button
className={cn(
"rounded-full",
className
)}
className={cn('rounded-full', className)}
onClick={handleScrollToLastUserMessage}
size="icon"
type="button"
@@ -146,7 +127,7 @@ export const ConversationScrollToLastUserMessageButton = ({
);
};
export type ConversationScrollButtonsProps = ComponentProps<"div"> & {
export type ConversationScrollButtonsProps = ComponentProps<'div'> & {
/** Whether there are user messages to scroll to */
hasUserMessages?: boolean;
};
@@ -166,16 +147,9 @@ export const ConversationScrollButtons = ({
if (isAtBottom) return null;
return (
<div
className={cn(
"absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2",
className
)}
{...props}
>
<div className={cn('absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2', className)} {...props}>
{hasUserMessages && <ConversationScrollToLastUserMessageButton />}
<ConversationScrollButton />
</div>
);
};

View File

@@ -1,9 +1,8 @@
export * from "./code-block";
export * from "./conversation";
export * from "./message";
export * from "./permission-request";
export * from "./prompt-input";
export * from "./reasoning";
export * from "./shimmer";
export * from "./tool";
export * from './code-block'
export * from './conversation'
export * from './message'
export * from './permission-request'
export * from './prompt-input'
export * from './reasoning'
export * from './shimmer'
export * from './tool'

View File

@@ -1,39 +1,26 @@
"use client";
'use client';
import { Button } from "../ui/button";
import {
ButtonGroup,
ButtonGroupText,
} from "../ui/button-group";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { cn } from "../../src/lib/utils";
import type { FileUIPart, UIMessage } from "ai";
import {
ChevronLeftIcon,
ChevronRightIcon,
PaperclipIcon,
XIcon,
} from "lucide-react";
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
import { createContext, lazy, memo, Suspense, useContext, useEffect, useState } from "react";
import { Button } from '../ui/button';
import { ButtonGroup, ButtonGroupText } from '../ui/button-group';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
import { cn } from '../../src/lib/utils';
import type { FileUIPart, UIMessage } from 'ai';
import { ChevronLeftIcon, ChevronRightIcon, PaperclipIcon, XIcon } from 'lucide-react';
import type { ComponentProps, HTMLAttributes, ReactElement } from 'react';
import { createContext, lazy, memo, Suspense, useContext, useEffect, useState } from 'react';
const LazyStreamdown = lazy(() => import("streamdown").then((m) => ({ default: m.Streamdown })));
const LazyStreamdown = lazy(() => import('streamdown').then(m => ({ default: m.Streamdown })));
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"];
from: UIMessage['role'];
};
export const Message = ({ className, from, ...props }: MessageProps) => (
<div
className={cn(
"group flex w-full max-w-[85%] min-w-0 flex-col gap-2",
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
className
'group flex w-full max-w-[85%] min-w-0 flex-col gap-2',
from === 'user' ? 'is-user ml-auto justify-end' : 'is-assistant',
className,
)}
{...props}
/>
@@ -41,33 +28,25 @@ export const Message = ({ className, from, ...props }: MessageProps) => (
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
export const MessageContent = ({
children,
className,
...props
}: MessageContentProps) => (
export const MessageContent = ({ children, className, ...props }: MessageContentProps) => (
<div
className={cn(
"is-user:dark flex w-fit max-w-full flex-col gap-2 overflow-hidden text-sm break-words",
"group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground",
"group-[.is-assistant]:text-foreground",
className
'is-user:dark flex w-fit max-w-full flex-col gap-2 overflow-hidden text-sm break-words',
'group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground',
'group-[.is-assistant]:text-foreground',
className,
)}
style={{ overflowWrap: "anywhere" }}
style={{ overflowWrap: 'anywhere' }}
{...props}
>
{children}
</div>
);
export type MessageActionsProps = ComponentProps<"div">;
export type MessageActionsProps = ComponentProps<'div'>;
export const MessageActions = ({
className,
children,
...props
}: MessageActionsProps) => (
<div className={cn("flex items-center gap-1", className)} {...props}>
export const MessageActions = ({ className, children, ...props }: MessageActionsProps) => (
<div className={cn('flex items-center gap-1', className)} {...props}>
{children}
</div>
);
@@ -81,8 +60,8 @@ export const MessageAction = ({
tooltip,
children,
label,
variant = "ghost",
size = "icon-sm",
variant = 'ghost',
size = 'icon-sm',
...props
}: MessageActionProps) => {
const button = (
@@ -117,17 +96,13 @@ type MessageBranchContextType = {
setBranches: (branches: ReactElement[]) => void;
};
const MessageBranchContext = createContext<MessageBranchContextType | null>(
null
);
const MessageBranchContext = createContext<MessageBranchContextType | null>(null);
const useMessageBranch = () => {
const context = useContext(MessageBranchContext);
if (!context) {
throw new Error(
"MessageBranch components must be used within MessageBranch"
);
throw new Error('MessageBranch components must be used within MessageBranch');
}
return context;
@@ -138,12 +113,7 @@ export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
onBranchChange?: (branchIndex: number) => void;
};
export const MessageBranch = ({
defaultBranch = 0,
onBranchChange,
className,
...props
}: MessageBranchProps) => {
export const MessageBranch = ({ defaultBranch = 0, onBranchChange, className, ...props }: MessageBranchProps) => {
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
const [branches, setBranches] = useState<ReactElement[]>([]);
@@ -153,14 +123,12 @@ export const MessageBranch = ({
};
const goToPrevious = () => {
const newBranch =
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
handleBranchChange(newBranch);
};
const goToNext = () => {
const newBranch =
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
handleBranchChange(newBranch);
};
@@ -175,20 +143,14 @@ export const MessageBranch = ({
return (
<MessageBranchContext.Provider value={contextValue}>
<div
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
{...props}
/>
<div className={cn('grid w-full gap-2 [&>div]:pb-0', className)} {...props} />
</MessageBranchContext.Provider>
);
};
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
export const MessageBranchContent = ({
children,
...props
}: MessageBranchContentProps) => {
export const MessageBranchContent = ({ children, ...props }: MessageBranchContentProps) => {
const { currentBranch, setBranches, branches } = useMessageBranch();
const childrenArray = Array.isArray(children) ? children : [children];
@@ -201,10 +163,7 @@ export const MessageBranchContent = ({
return childrenArray.map((branch, index) => (
<div
className={cn(
"grid gap-2 overflow-hidden [&>div]:pb-0",
index === currentBranch ? "block" : "hidden"
)}
className={cn('grid gap-2 overflow-hidden [&>div]:pb-0', index === currentBranch ? 'block' : 'hidden')}
key={branch.key}
{...props}
>
@@ -214,14 +173,10 @@ export const MessageBranchContent = ({
};
export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"];
from: UIMessage['role'];
};
export const MessageBranchSelector = ({
className,
from,
...props
}: MessageBranchSelectorProps) => {
export const MessageBranchSelector = ({ className, from, ...props }: MessageBranchSelectorProps) => {
const { totalBranches } = useMessageBranch();
// Don't render if there's only one branch
@@ -240,10 +195,7 @@ export const MessageBranchSelector = ({
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
export const MessageBranchPrevious = ({
children,
...props
}: MessageBranchPreviousProps) => {
export const MessageBranchPrevious = ({ children, ...props }: MessageBranchPreviousProps) => {
const { goToPrevious, totalBranches } = useMessageBranch();
return (
@@ -263,11 +215,7 @@ export const MessageBranchPrevious = ({
export type MessageBranchNextProps = ComponentProps<typeof Button>;
export const MessageBranchNext = ({
children,
className,
...props
}: MessageBranchNextProps) => {
export const MessageBranchNext = ({ children, className, ...props }: MessageBranchNextProps) => {
const { goToNext, totalBranches } = useMessageBranch();
return (
@@ -287,18 +235,12 @@ export const MessageBranchNext = ({
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
export const MessageBranchPage = ({
className,
...props
}: MessageBranchPageProps) => {
export const MessageBranchPage = ({ className, ...props }: MessageBranchPageProps) => {
const { currentBranch, totalBranches } = useMessageBranch();
return (
<ButtonGroupText
className={cn(
"border-none bg-transparent text-muted-foreground shadow-none",
className
)}
className={cn('border-none bg-transparent text-muted-foreground shadow-none', className)}
{...props}
>
{currentBranch + 1} of {totalBranches}
@@ -309,22 +251,16 @@ export const MessageBranchPage = ({
export type MessageResponseProps = {
children?: string;
className?: string;
mode?: "static" | "streaming";
mode?: 'static' | 'streaming';
};
export const MessageResponse = memo(
({ className, children, ...props }: MessageResponseProps) => (
<Suspense
fallback={
<div className={cn("whitespace-pre-wrap break-words", className)}>
{children}
</div>
}
>
<Suspense fallback={<div className={cn('whitespace-pre-wrap break-words', className)}>{children}</div>}>
<LazyStreamdown
className={cn(
"size-full break-words [overflow-wrap:anywhere] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className
'size-full break-words [overflow-wrap:anywhere] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
className,
)}
{...props}
>
@@ -332,10 +268,10 @@ export const MessageResponse = memo(
</LazyStreamdown>
</Suspense>
),
(prevProps, nextProps) => prevProps.children === nextProps.children
(prevProps, nextProps) => prevProps.children === nextProps.children,
);
MessageResponse.displayName = "MessageResponse";
MessageResponse.displayName = 'MessageResponse';
export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
data: FileUIPart;
@@ -343,30 +279,18 @@ export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
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");
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}
>
<div className={cn('group relative size-24 overflow-hidden rounded-lg', className)} {...props}>
{isImage ? (
<>
<img
alt={filename || "attachment"}
alt={filename || 'attachment'}
className="size-full object-cover"
height={100}
src={data.url}
@@ -376,7 +300,7 @@ export function MessageAttachment({
<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) => {
onClick={e => {
e.stopPropagation();
onRemove();
}}
@@ -404,7 +328,7 @@ export function MessageAttachment({
<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) => {
onClick={e => {
e.stopPropagation();
onRemove();
}}
@@ -421,45 +345,24 @@ export function MessageAttachment({
);
}
export type MessageAttachmentsProps = ComponentProps<"div">;
export type MessageAttachmentsProps = ComponentProps<'div'>;
export function MessageAttachments({
children,
className,
...props
}: MessageAttachmentsProps) {
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}
>
<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 = ({
className,
children,
...props
}: MessageToolbarProps) => (
<div
className={cn(
"mt-4 flex w-full items-center justify-between gap-4",
className
)}
{...props}
>
export const MessageToolbar = ({ className, children, ...props }: MessageToolbarProps) => (
<div className={cn('mt-4 flex w-full items-center justify-between gap-4', className)} {...props}>
{children}
</div>
);

View File

@@ -1,32 +1,32 @@
"use client";
'use client';
import { cn } from "../../src/lib/utils";
import { Button } from "../ui/button";
import { ShieldAlertIcon, CheckIcon, XIcon } from "lucide-react";
import type { PermissionOption } from "../../src/acp/types";
import { cn } from '../../src/lib/utils';
import { Button } from '../ui/button';
import { ShieldAlertIcon, CheckIcon, XIcon } from 'lucide-react';
import type { PermissionOption } from '../../src/acp/types';
// Get button variant based on option kind
function getButtonVariant(kind: PermissionOption["kind"]): "default" | "destructive" | "outline" | "secondary" {
function getButtonVariant(kind: PermissionOption['kind']): 'default' | 'destructive' | 'outline' | 'secondary' {
switch (kind) {
case "allow_once":
case "allow_always":
return "default";
case "reject_once":
case "reject_always":
return "destructive";
case 'allow_once':
case 'allow_always':
return 'default';
case 'reject_once':
case 'reject_always':
return 'destructive';
default:
return "outline";
return 'outline';
}
}
// Get button icon based on option kind
function getButtonIcon(kind: PermissionOption["kind"]) {
function getButtonIcon(kind: PermissionOption['kind']) {
switch (kind) {
case "allow_once":
case "allow_always":
case 'allow_once':
case 'allow_always':
return <CheckIcon className="size-4" />;
case "reject_once":
case "reject_always":
case 'reject_once':
case 'reject_always':
return <XIcon className="size-4" />;
default:
return null;
@@ -37,7 +37,7 @@ function getButtonIcon(kind: PermissionOption["kind"]) {
export interface ToolPermissionButtonsProps {
requestId: string;
options: PermissionOption[];
onRespond: (requestId: string, optionId: string | null, optionKind: PermissionOption["kind"] | null) => void;
onRespond: (requestId: string, optionId: string | null, optionKind: PermissionOption['kind'] | null) => void;
className?: string;
}
@@ -47,15 +47,13 @@ export function ToolPermissionButtons({ requestId, options, onRespond, className
};
return (
<div className={cn("p-3 border-t border-warning-border/30 bg-warning-bg/50", className)}>
<div className={cn('p-3 border-t border-warning-border/30 bg-warning-bg/50', className)}>
<div className="flex items-center gap-2 mb-2">
<ShieldAlertIcon className="size-4 text-warning-text" />
<span className="text-xs font-medium text-warning-text">
Permission Required
</span>
<span className="text-xs font-medium text-warning-text">Permission Required</span>
</div>
<div className="flex flex-wrap gap-2">
{options.map((option) => (
{options.map(option => (
<Button
key={option.optionId}
variant={getButtonVariant(option.kind)}
@@ -71,4 +69,3 @@ export function ToolPermissionButtons({ requestId, options, onRespond, className
</div>
);
}

View File

@@ -1,16 +1,12 @@
"use client";
'use client';
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
import { cn } from "../../src/lib/utils";
import { BrainIcon, ChevronDownIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { createContext, memo, useCallback, useContext, useEffect, useState } from "react";
import { Shimmer } from "./shimmer";
import { useControllableState } from '@radix-ui/react-use-controllable-state';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible';
import { cn } from '../../src/lib/utils';
import { BrainIcon, ChevronDownIcon } from 'lucide-react';
import type { ComponentProps, ReactNode } from 'react';
import { createContext, memo, useCallback, useContext, useEffect, useState } from 'react';
import { Shimmer } from './shimmer';
interface ReasoningContextValue {
isStreaming: boolean;
@@ -24,7 +20,7 @@ const ReasoningContext = createContext<ReasoningContextValue | null>(null);
export const useReasoning = () => {
const context = useContext(ReasoningContext);
if (!context) {
throw new Error("Reasoning components must be used within Reasoning");
throw new Error('Reasoning components must be used within Reasoning');
}
return context;
};
@@ -78,8 +74,8 @@ export const Reasoning = memo(
// Auto-open when streaming starts, auto-close when streaming ends (once only)
// Respect prefers-reduced-motion: skip animation auto-close
const prefersReducedMotion = typeof window !== "undefined"
&& window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const prefersReducedMotion =
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
useEffect(() => {
if (!prefersReducedMotion && defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
@@ -97,11 +93,9 @@ export const Reasoning = memo(
};
return (
<ReasoningContext.Provider
value={{ isStreaming, isOpen: isOpen ?? false, setIsOpen, duration }}
>
<ReasoningContext.Provider value={{ isStreaming, isOpen: isOpen ?? false, setIsOpen, duration }}>
<Collapsible
className={cn("not-prose mb-4", className)}
className={cn('not-prose mb-4', className)}
onOpenChange={handleOpenChange}
open={isOpen}
{...props}
@@ -110,12 +104,10 @@ export const Reasoning = memo(
</Collapsible>
</ReasoningContext.Provider>
);
}
},
);
export type ReasoningTriggerProps = ComponentProps<
typeof CollapsibleTrigger
> & {
export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
};
@@ -130,19 +122,14 @@ const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
};
export const ReasoningTrigger = memo(
({
className,
children,
getThinkingMessage = defaultGetThinkingMessage,
...props
}: ReasoningTriggerProps) => {
({ className, children, getThinkingMessage = defaultGetThinkingMessage, ...props }: ReasoningTriggerProps) => {
const { isStreaming, isOpen, duration } = useReasoning();
return (
<CollapsibleTrigger
className={cn(
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
className
'flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground',
className,
)}
{...props}
>
@@ -150,41 +137,31 @@ export const ReasoningTrigger = memo(
<>
<BrainIcon className="size-4" />
{getThinkingMessage(isStreaming, duration)}
<ChevronDownIcon
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0"
)}
/>
<ChevronDownIcon className={cn('size-4 transition-transform', isOpen ? 'rotate-180' : 'rotate-0')} />
</>
)}
</CollapsibleTrigger>
);
}
},
);
export type ReasoningContentProps = ComponentProps<
typeof CollapsibleContent
> & {
export type ReasoningContentProps = ComponentProps<typeof CollapsibleContent> & {
children: ReactNode;
};
export const ReasoningContent = memo(
({ className, children, ...props }: ReasoningContentProps) => (
<CollapsibleContent
className={cn(
"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",
className
)}
{...props}
>
{children}
</CollapsibleContent>
)
);
Reasoning.displayName = "Reasoning";
ReasoningTrigger.displayName = "ReasoningTrigger";
ReasoningContent.displayName = "ReasoningContent";
export const ReasoningContent = memo(({ className, children, ...props }: ReasoningContentProps) => (
<CollapsibleContent
className={cn(
'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',
className,
)}
{...props}
>
{children}
</CollapsibleContent>
));
Reasoning.displayName = 'Reasoning';
ReasoningTrigger.displayName = 'ReasoningTrigger';
ReasoningContent.displayName = 'ReasoningContent';

View File

@@ -1,12 +1,8 @@
"use client";
'use client';
import { cn } from "../../src/lib/utils";
import { motion } from "motion/react";
import {
type ElementType,
type JSX,
memo,
} from "react";
import { cn } from '../../src/lib/utils';
import { motion } from 'motion/react';
import { type ElementType, type JSX, memo } from 'react';
export interface TextShimmerProps {
children: string;
@@ -16,27 +12,17 @@ export interface TextShimmerProps {
spread?: number;
}
const ShimmerComponent = ({
children,
as: Component = "p",
className,
duration = 2,
}: TextShimmerProps) => {
const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements
);
const ShimmerComponent = ({ children, as: Component = 'p', className, duration = 2 }: TextShimmerProps) => {
const MotionComponent = motion.create(Component as keyof JSX.IntrinsicElements);
return (
<MotionComponent
animate={{ opacity: [0.5, 1, 0.5] }}
className={cn(
"relative inline-block text-muted-foreground",
className
)}
className={cn('relative inline-block text-muted-foreground', className)}
transition={{
repeat: Number.POSITIVE_INFINITY,
duration,
ease: "easeInOut",
ease: 'easeInOut',
}}
>
{children}

View File

@@ -1,67 +1,56 @@
"use client";
'use client';
import { Badge } from "../ui/badge";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
import { cn } from "../../src/lib/utils";
import type { ToolUIPart } from "ai";
import {
CheckCircleIcon,
ChevronDownIcon,
CircleIcon,
ClockIcon,
WrenchIcon,
XCircleIcon,
} from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { isValidElement } from "react";
import { CodeBlock } from "./code-block";
import { Badge } from '../ui/badge';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible';
import { cn } from '../../src/lib/utils';
import type { ToolUIPart } from 'ai';
import { CheckCircleIcon, ChevronDownIcon, CircleIcon, ClockIcon, WrenchIcon, XCircleIcon } from 'lucide-react';
import type { ComponentProps, ReactNode } from 'react';
import { isValidElement } from 'react';
import { CodeBlock } from './code-block';
export type ToolProps = ComponentProps<typeof Collapsible>;
export const Tool = ({ className, ...props }: ToolProps) => (
<Collapsible
className={cn("not-prose mb-4 w-full max-w-full overflow-hidden rounded-md border", className)}
className={cn('not-prose mb-4 w-full max-w-full overflow-hidden rounded-md border', className)}
{...props}
/>
);
// Extended state type to include our custom states
export type ExtendedToolState = ToolUIPart["state"] | "waiting-for-confirmation" | "rejected";
export type ExtendedToolState = ToolUIPart['state'] | 'waiting-for-confirmation' | 'rejected';
export type ToolHeaderProps = {
title?: string;
type: ToolUIPart["type"];
type: ToolUIPart['type'];
state: ExtendedToolState;
className?: string;
};
const getStatusBadge = (status: ExtendedToolState) => {
const labels: Record<ExtendedToolState, string> = {
"input-streaming": "Pending",
"input-available": "Running",
"approval-requested": "Awaiting Approval",
"approval-responded": "Responded",
"output-available": "Completed",
"output-error": "Error",
"output-denied": "Denied",
"waiting-for-confirmation": "Awaiting Approval",
"rejected": "Rejected",
'input-streaming': 'Pending',
'input-available': 'Running',
'approval-requested': 'Awaiting Approval',
'approval-responded': 'Responded',
'output-available': 'Completed',
'output-error': 'Error',
'output-denied': 'Denied',
'waiting-for-confirmation': 'Awaiting Approval',
rejected: 'Rejected',
};
const icons: Record<ExtendedToolState, 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" />,
"waiting-for-confirmation": <ClockIcon className="size-4 text-yellow-600" />,
"rejected": <XCircleIcon className="size-4 text-orange-600" />,
'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" />,
'waiting-for-confirmation': <ClockIcon className="size-4 text-yellow-600" />,
rejected: <XCircleIcon className="size-4 text-orange-600" />,
};
return (
@@ -72,25 +61,11 @@ const getStatusBadge = (status: ExtendedToolState) => {
);
};
export const ToolHeader = ({
className,
title,
type,
state,
...props
}: ToolHeaderProps) => (
<CollapsibleTrigger
className={cn(
"flex w-full items-center justify-between gap-4 p-3",
className
)}
{...props}
>
export const ToolHeader = ({ className, title, type, state, ...props }: ToolHeaderProps) => (
<CollapsibleTrigger className={cn('flex w-full items-center justify-between gap-4 p-3', className)} {...props}>
<div className="flex min-w-0 items-center gap-2">
<WrenchIcon className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate font-medium text-sm">
{title ?? type.split("-").slice(1).join("-")}
</span>
<span className="truncate font-medium text-sm">{title ?? type.split('-').slice(1).join('-')}</span>
{getStatusBadge(state)}
</div>
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
@@ -102,64 +77,53 @@ 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-none data-[state=closed]:animate-out data-[state=open]:animate-in",
className
'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}
/>
);
export type ToolInputProps = ComponentProps<"div"> & {
input: ToolUIPart["input"];
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 max-w-full", className)} {...props}>
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Parameters
</h4>
<div className={cn('space-y-2 overflow-hidden p-4 max-w-full', className)} {...props}>
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">Parameters</h4>
<div className="rounded-md bg-muted/50 overflow-hidden">
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
</div>
</div>
);
export type ToolOutputProps = ComponentProps<"div"> & {
output: ToolUIPart["output"];
errorText: ToolUIPart["errorText"];
export type ToolOutputProps = ComponentProps<'div'> & {
output: ToolUIPart['output'];
errorText: ToolUIPart['errorText'];
};
export const ToolOutput = ({
className,
output,
errorText,
...props
}: ToolOutputProps) => {
export const ToolOutput = ({ className, output, errorText, ...props }: ToolOutputProps) => {
if (!(output || errorText)) {
return null;
}
let Output = <div>{output as ReactNode}</div>;
if (typeof output === "object" && !isValidElement(output)) {
Output = (
<CodeBlock code={JSON.stringify(output, null, 2)} language="json" />
);
} else if (typeof output === "string") {
if (typeof output === 'object' && !isValidElement(output)) {
Output = <CodeBlock code={JSON.stringify(output, null, 2)} language="json" />;
} else if (typeof output === 'string') {
Output = <CodeBlock code={output} language="json" />;
}
return (
<div className={cn("space-y-2 p-4 max-w-full overflow-hidden", className)} {...props}>
<div className={cn('space-y-2 p-4 max-w-full overflow-hidden', className)} {...props}>
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
{errorText ? "Error" : "Result"}
{errorText ? 'Error' : 'Result'}
</h4>
<div
className={cn(
"overflow-hidden rounded-md text-xs [&_table]:w-full",
errorText
? "bg-destructive/10 text-destructive"
: "bg-muted/50 text-foreground"
'overflow-hidden rounded-md text-xs [&_table]:w-full',
errorText ? 'bg-destructive/10 text-destructive' : 'bg-muted/50 text-foreground',
)}
>
{errorText && <div className="p-2">{errorText}</div>}
@@ -168,4 +132,3 @@ export const ToolOutput = ({
</div>
);
};