ui: changes for context menu plus alignment + padding (#1179)

This commit is contained in:
josh 2025-09-09 23:58:40 +01:00 committed by GitHub
parent eae41c635d
commit 2f9ba09606
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 65 additions and 36 deletions

View file

@ -1,10 +1,10 @@
'use client'; 'use client';
import { import {
HoverCard, DropdownMenu,
HoverCardContent, DropdownMenuContent,
HoverCardTrigger, DropdownMenuTrigger,
} from '@/components/ui/hover-card'; } from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import type { LanguageModelUsage } from 'ai'; import type { LanguageModelUsage } from 'ai';
@ -97,11 +97,11 @@ export const ContextIcon = ({ percent }: ContextIconProps) => {
return ( return (
<svg <svg
aria-label={`${formatPercent(percent)} of model context used`} aria-label={`${formatPercent(percent)} of model context used`}
height="20" height="28"
role="img" role="img"
style={{ color: 'currentcolor' }} style={{ color: 'currentcolor' }}
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`} viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
width="20" width="28"
> >
<circle <circle
cx={ICON_CENTER} cx={ICON_CENTER}
@ -271,8 +271,8 @@ export const Context = ({
n === undefined ? '—' : formatTokens(n); n === undefined ? '—' : formatTokens(n);
return ( return (
<HoverCard closeDelay={100} openDelay={100}> <DropdownMenu>
<HoverCardTrigger asChild> <DropdownMenuTrigger asChild>
<button <button
className={cn( className={cn(
'inline-flex select-none items-center gap-1 rounded-md px-1.5 py-1 text-sm', 'inline-flex select-none items-center gap-1 rounded-md px-1.5 py-1 text-sm',
@ -282,17 +282,18 @@ export const Context = ({
type="button" type="button"
{...props} {...props}
> >
<span className="font-medium text-muted-foreground"> <span className="font-medium text-muted-foreground hidden">
{displayPct} {displayPct}
</span> </span>
<ContextIcon percent={usedPercent} /> <ContextIcon percent={usedPercent} />
</button> </button>
</HoverCardTrigger> </DropdownMenuTrigger>
<HoverCardContent align="end" side="top" className="w-fit p-3"> <DropdownMenuContent align="end" side="top" className="w-fit p-3">
<div className="min-w-[240px] space-y-2"> <div className="min-w-[240px] space-y-2">
<p className="text-start text-sm"> <div className="flex justify-between items-start text-sm">
{displayPct} {used} / {total} tokens <span>{displayPct}</span>
</p> <span className="text-muted-foreground">{used} / {total} tokens</span>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Progress className="h-2 bg-muted" value={usedPercent} /> <Progress className="h-2 bg-muted" value={usedPercent} />
</div> </div>
@ -339,7 +340,7 @@ export const Context = ({
)} )}
</div> </div>
</div> </div>
</HoverCardContent> </DropdownMenuContent>
</HoverCard> </DropdownMenu>
); );
}; };

View file

@ -698,6 +698,30 @@ export const DeltaIcon = ({ size = 16 }: { size?: number }) => (
</svg> </svg>
); );
export const CpuIcon = ({ size = 16 }: { size?: number }) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
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="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" />
<path d="M14.5 20V22" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13 9L9 13" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15 13L13 15" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 14.5L20 14.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M4 9.5L2 9.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M4 14.5L2 14.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 9.5L20 9.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
export const PenIcon = ({ size = 16 }: { size?: number }) => ( export const PenIcon = ({ size = 16 }: { size?: number }) => (
<svg <svg
height={size} height={size}

View file

@ -15,7 +15,7 @@ import {
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useLocalStorage, useWindowSize } from 'usehooks-ts'; import { useLocalStorage, useWindowSize } from 'usehooks-ts';
import { ArrowUpIcon, PaperclipIcon, StopIcon } from './icons'; import { ArrowUpIcon, PaperclipIcon, CpuIcon, StopIcon, ChevronDownIcon } from './icons';
import { PreviewAttachment } from './preview-attachment'; import { PreviewAttachment } from './preview-attachment';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { SuggestedActions } from './suggested-actions'; import { SuggestedActions } from './suggested-actions';
@ -26,10 +26,10 @@ import {
PromptInputTools, PromptInputTools,
PromptInputSubmit, PromptInputSubmit,
PromptInputModelSelect, PromptInputModelSelect,
PromptInputModelSelectTrigger,
PromptInputModelSelectContent, PromptInputModelSelectContent,
} from './elements/prompt-input'; } from './elements/prompt-input';
import { SelectItem } from '@/components/ui/select'; import { SelectItem } from '@/components/ui/select';
import * as SelectPrimitive from '@radix-ui/react-select';
import equal from 'fast-deep-equal'; import equal from 'fast-deep-equal';
import type { UseChatHelpers } from '@ai-sdk/react'; import type { UseChatHelpers } from '@ai-sdk/react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
@ -352,14 +352,14 @@ function PureMultimodalInput({
minHeight={44} minHeight={44}
maxHeight={200} maxHeight={200}
disableAutoResize={true} disableAutoResize={true}
className='grow resize-none border-0! border-none! bg-transparent px-2 py-3 text-sm 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 sm:px-3 [&::-webkit-scrollbar]:hidden' className='grow resize-none border-0! border-none! bg-transparent px-2 pt-1 pb-3 ml-1 text-sm 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'
rows={1} rows={1}
autoFocus autoFocus
/>{' '} />{' '}
<Context {...contextProps} className="mr-1.5" /> <Context {...contextProps} className="mr-0.5 mt-1" />
</div> </div>
<PromptInputToolbar className='!border-top-0 border-t-0! px-2 py-2 shadow-none dark:border-0 dark:border-transparent!'> <PromptInputToolbar className='!border-top-0 border-t-0! px-2 py-2 shadow-none dark:border-0 dark:border-transparent!'>
<PromptInputTools className="gap-1 sm:gap-2"> <PromptInputTools className="gap-0 sm:gap-0.5">
<AttachmentsButton <AttachmentsButton
fileInputRef={fileInputRef} fileInputRef={fileInputRef}
status={status} status={status}
@ -374,7 +374,7 @@ function PureMultimodalInput({
<PromptInputSubmit <PromptInputSubmit
status={status} status={status}
disabled={!input.trim() || uploadQueue.length > 0} disabled={!input.trim() || uploadQueue.length > 0}
className="size-7 rounded-full bg-primary p-1 text-primary-foreground transition-colors duration-200 hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground" className="size-7 rounded-full bg-primary p-1 text-primary-foreground transition-colors duration-200 hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground -mt-2"
> >
<ArrowUpIcon size={14} /> <ArrowUpIcon size={14} />
</PromptInputSubmit> </PromptInputSubmit>
@ -413,7 +413,7 @@ function PureAttachmentsButton({
return ( return (
<Button <Button
data-testid="attachments-button" data-testid="attachments-button"
className='h-fit rounded-md p-1 transition-colors duration-200 hover:bg-muted' className='h-8 rounded-lg p-1 transition-colors hover:bg-accent'
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
fileInputRef.current?.click(); fileInputRef.current?.click();
@ -452,18 +452,22 @@ function PureModelSelectorCompact({
} }
}} }}
> >
<PromptInputModelSelectTrigger <SelectPrimitive.Trigger
type="button" type="button"
className='max-w-[120px] truncate text-xs focus:outline-hidden focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 data-[state=closed]:ring-0 data-[state=open]:ring-0 sm:max-w-none' className='flex items-center gap-2 px-2 h-8 rounded-lg border-0 bg-background text-foreground hover:bg-accent transition-colors focus:outline-none focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none'
> >
{selectedModel?.name || 'Select model'} <CpuIcon size={16} />
</PromptInputModelSelectTrigger> <span className="text-xs font-medium sm:block hidden">{selectedModel?.name}</span>
<PromptInputModelSelectContent className="max-w-[90vw] sm:max-w-[400px]"> <ChevronDownIcon size={16} />
</SelectPrimitive.Trigger>
<PromptInputModelSelectContent className="min-w-[260px] p-0">
{chatModels.map((model) => ( {chatModels.map((model) => (
<SelectItem key={model.id} value={model.name}> <SelectItem key={model.id} value={model.name} className="px-3 py-2 text-xs">
<div className='flex flex-col items-start gap-1 py-1'> <div className="flex flex-col min-w-0 flex-1">
<div className='font-medium text-sm'>{model.name}</div> <div className="font-medium truncate text-xs">
<div className='line-clamp-2 text-muted-foreground text-xs'> {model.name}
</div>
<div className="text-[10px] text-muted-foreground truncate leading-tight">
{model.description} {model.description}
</div> </div>
</div> </div>

View file

@ -86,7 +86,7 @@ const SelectContent = React.forwardRef<
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn( className={cn(
'p-1', 'p-2',
position === 'popper' && position === 'popper' &&
'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)', 'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)',
)} )}
@ -118,14 +118,14 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50', 'relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pr-8 pl-3 text-sm outline-hidden hover:bg-muted/50 focus:bg-muted data-[state=checked]:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50 transition-colors',
className, className,
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute right-3 flex h-4 w-4 items-center justify-center">
<SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className="h-4 w-4 text-foreground" />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
</span> </span>