Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -140,24 +140,6 @@
--color-sidebar-ring: var(--sidebar-ring);
}

/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.

If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}

@utility text-balance {
text-wrap: balance;
}
Expand Down
6 changes: 5 additions & 1 deletion components.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
Expand All @@ -10,11 +10,15 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@ai-elements": "https://registry.ai-sdk.dev/{name}.json"
}
}
2 changes: 1 addition & 1 deletion components/elements/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const Action = ({
const button = (
<Button
className={cn(
'relative size-9 p-1.5 text-muted-foreground hover:text-foreground',
'relative size-6 p-0.5 text-muted-foreground hover:text-foreground',
className,
)}
size={size}
Expand Down
12 changes: 10 additions & 2 deletions components/elements/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,19 @@ export const Context = ({
<ContextIcon percent={usedPercent} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="top" className="w-fit p-3">
<DropdownMenuContent
align="end"
alignOffset={-5}
sideOffset={5}
side="top"
className="w-fit p-3 rounded-xl"
>
<div className="min-w-[240px] space-y-2">
<div className="flex justify-between items-start text-sm">
<span>{displayPct}</span>
<span className="text-muted-foreground">{used} / {total} tokens</span>
<span className="text-muted-foreground">
{used} / {total} tokens
</span>
</div>
<div className="space-y-2">
<Progress className="h-2 bg-muted" value={usedPercent} />
Expand Down
68 changes: 66 additions & 2 deletions components/elements/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,25 @@ import {
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import type { ChatStatus } from 'ai';
import { Loader2Icon, SendIcon, SquareIcon, XIcon } from 'lucide-react';
import {
Loader2Icon,
PlusIcon,
SendIcon,
SquareIcon,
XIcon,
} from 'lucide-react';
import type {
ComponentProps,
HTMLAttributes,
KeyboardEventHandler,
} from 'react';
import { Children } from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

export type PromptInputProps = HTMLAttributes<HTMLFormElement>;

Expand All @@ -31,6 +43,15 @@ export const PromptInput = ({ className, ...props }: PromptInputProps) => (
/>
);

export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;

export const PromptInputBody = ({
className,
...props
}: PromptInputBodyProps) => (
<div className={cn(className, 'flex flex-col')} {...props} />
);

export type PromptInputTextareaProps = ComponentProps<typeof Textarea> & {
minHeight?: number;
maxHeight?: number;
Expand Down Expand Up @@ -148,6 +169,49 @@ export const PromptInputButton = ({
);
};

export type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>;
export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
<DropdownMenu {...props} />
);

export type PromptInputActionMenuTriggerProps = ComponentProps<
typeof Button
> & {};
export const PromptInputActionMenuTrigger = ({
className,
children,
...props
}: PromptInputActionMenuTriggerProps) => (
<DropdownMenuTrigger asChild>
<PromptInputButton className={className} {...props}>
{children ?? <PlusIcon className="size-4" />}
</PromptInputButton>
</DropdownMenuTrigger>
);

export type PromptInputActionMenuContentProps = ComponentProps<
typeof DropdownMenuContent
>;
export const PromptInputActionMenuContent = ({
className,
...props
}: PromptInputActionMenuContentProps) => (
<DropdownMenuContent align="start" className={cn(className)} {...props} />
);

export type PromptInputActionMenuItemProps = ComponentProps<
typeof DropdownMenuItem
>;
export const PromptInputActionMenuItem = ({
className,
...props
}: PromptInputActionMenuItemProps) => (
<DropdownMenuItem className={cn(className)} {...props} />
);

// Note: Actions that perform side-effects (like opening a file dialog)
// are provided in opt-in modules (e.g., prompt-input-attachments).

export type PromptInputSubmitProps = ComponentProps<typeof Button> & {
status?: ChatStatus;
};
Expand Down Expand Up @@ -200,7 +264,7 @@ export const PromptInputModelSelectTrigger = ({
<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',
'hover:bg-accent hover:text-foreground [&[aria-expanded="true"]]:bg-accent [&[aria-expanded="true"]]:text-foreground',
'h-auto px-2 py-1.5',
className,
)}
Expand Down
5 changes: 4 additions & 1 deletion components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,10 @@ export const CpuIcon = ({ size = 16 }: { size?: number }) => (
strokeWidth="2"
style={{ color: 'currentcolor' }}
>
<path d="M4 12C4 8.22876 4 6.34315 5.17157 5.17157C6.34315 4 8.22876 4 12 4C15.7712 4 17.6569 4 18.8284 5.17157C20 6.34315 20 8.22876 20 12C20 15.7712 20 17.6569 18.8284 18.8284C17.6569 20 15.7712 20 12 20C8.22876 20 6.34315 20 5.17157 18.8284C4 17.6569 4 15.7712 4 12Z" strokeLinejoin="round" />
<path
d="M4 12C4 8.22876 4 6.34315 5.17157 5.17157C6.34315 4 8.22876 4 12 4C15.7712 4 17.6569 4 18.8284 5.17157C20 6.34315 20 8.22876 20 12C20 15.7712 20 17.6569 18.8284 18.8284C17.6569 20 15.7712 20 12 20C8.22876 20 6.34315 20 5.17157 18.8284C4 17.6569 4 15.7712 4 12Z"
strokeLinejoin="round"
/>
<path d="M9.5 2V4" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.5 2V4" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.5 20V22" strokeLinecap="round" strokeLinejoin="round" />
Expand Down
51 changes: 41 additions & 10 deletions components/message-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,33 @@ import { useCopyToClipboard } from 'usehooks-ts';

import type { Vote } from '@/lib/db/schema';

import { CopyIcon, ThumbDownIcon, ThumbUpIcon, PencilEditIcon } from './icons';
import { ThumbDownIcon, ThumbUpIcon, PencilEditIcon } from './icons';
import { Copy, Check } from 'lucide-react';
import { Actions, Action } from './elements/actions';
import { memo } from 'react';
import { memo, useState } from 'react';
import equal from 'fast-deep-equal';
import { toast } from 'sonner';
import type { ChatMessage } from '@/lib/types';
import { cn } from '@/lib/utils';

export function PureMessageActions({
chatId,
message,
vote,
isLoading,
setMode,
mode,
}: {
chatId: string;
message: ChatMessage;
vote: Vote | undefined;
isLoading: boolean;
setMode?: (mode: 'view' | 'edit') => void;
mode: 'view' | 'edit';
}) {
const { mutate } = useSWRConfig();
const [_, copyToClipboard] = useCopyToClipboard();
const [copied, setCopied] = useState(false);

if (isLoading) return null;

Expand All @@ -41,25 +46,40 @@ export function PureMessageActions({
}

await copyToClipboard(textFromParts);
setCopied(true);
toast.success('Copied to clipboard!');
setTimeout(() => setCopied(false), 2000);
};

// User messages get edit (on hover) and copy actions
if (message.role === 'user') {
return (
<Actions className="-mr-0.5 justify-end">
<div className="relative">
<div
className={cn(
'opacity-100 md:opacity-0 transition-opacity group-hover/message:opacity-100 gap-1',
{
'md:opacity-100': mode === 'edit',
},
)}
>
{setMode && (
<Action
tooltip="Edit"
onClick={() => setMode('edit')}
className="-left-10 absolute top-0 opacity-0 transition-opacity group-hover/message:opacity-100"
>
<Action tooltip="Edit" onClick={() => setMode('edit')}>
<PencilEditIcon />
</Action>
)}
<Action tooltip="Copy" onClick={handleCopy}>
<CopyIcon />
<span className="sr-only">{copied ? 'Copied' : 'Copy'}</span>
<Copy
className={`size-4 transition-all duration-300 ${
copied ? 'scale-0' : 'scale-100'
}`}
/>
<Check
className={`absolute inset-0 m-auto size-4 transition-all duration-300 ${
copied ? 'scale-100' : 'scale-0'
}`}
/>
</Action>
</div>
</Actions>
Expand All @@ -69,7 +89,17 @@ export function PureMessageActions({
return (
<Actions className="-ml-0.5">
<Action tooltip="Copy" onClick={handleCopy}>
<CopyIcon />
<span className="sr-only">{copied ? 'Copied' : 'Copy'}</span>
<Copy
className={`size-4 transition-all duration-300 ${
copied ? 'scale-0' : 'scale-100'
}`}
/>
<Check
className={`absolute inset-0 m-auto size-4 transition-all duration-300 ${
copied ? 'scale-100' : 'scale-0'
}`}
/>
</Action>

<Action
Expand Down Expand Up @@ -174,6 +204,7 @@ export const MessageActions = memo(
(prevProps, nextProps) => {
if (!equal(prevProps.vote, nextProps.vote)) return false;
if (prevProps.isLoading !== nextProps.isLoading) return false;
if (prevProps.mode !== nextProps.mode) return false;

return true;
},
Expand Down
8 changes: 6 additions & 2 deletions components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ const PurePreviewMessage = ({
return (
<motion.div
data-testid={`message-${message.role}`}
className="group/message w-full"
className={cn('group/message w-full', {
'is-user': message.role === 'user',
'is-assistant': message.role === 'assistant',
})}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
data-role={message.role}
Expand Down Expand Up @@ -129,7 +132,7 @@ const PurePreviewMessage = ({
<MessageContent
data-testid="message-content"
className={cn({
'w-fit break-words rounded-2xl px-3 py-2 text-right text-white':
'w-fit break-words rounded-xl px-4 py-3 text-right text-white border border-secondary/30':
message.role === 'user',
'bg-transparent px-0 py-0 text-left':
message.role === 'assistant',
Expand Down Expand Up @@ -278,6 +281,7 @@ const PurePreviewMessage = ({
vote={vote}
isLoading={isLoading}
setMode={setMode}
mode={mode}
/>
)}
</div>
Expand Down
Loading