refactor: enhance chat UI components for mobile responsiveness

- Updated the layout of the ComposerAction and ChatHeader components to improve mobile compatibility.
- Added a new prop to ImageModelSelector for mobile-specific rendering.
- Adjusted ModelSelector to conditionally render elements based on mobile view, enhancing user experience on smaller screens.
This commit is contained in:
Anish Sarkar 2026-06-19 01:41:21 +05:30
parent 03e57bdf7e
commit 37559fcc6d
4 changed files with 28 additions and 14 deletions

View file

@ -1577,7 +1577,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
<span>Select a model</span> <span>Select a model</span>
</div> </div>
)} )}
<div className="flex items-center gap-2"> <div className="ml-auto flex min-w-0 shrink-0 items-center gap-2">
<ChatHeader <ChatHeader
searchSpaceId={searchSpaceId} searchSpaceId={searchSpaceId}
className="h-9 max-w-[44vw] px-2 sm:max-w-[220px] sm:px-3" className="h-9 max-w-[44vw] px-2 sm:max-w-[220px] sm:px-3"
@ -1600,7 +1600,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
variant="default" variant="default"
size="icon" size="icon"
className={cn( className={cn(
"aui-composer-send size-9 rounded-full", "aui-composer-send size-9 shrink-0 rounded-full",
isSendDisabled && "cursor-not-allowed opacity-50" isSendDisabled && "cursor-not-allowed opacity-50"
)} )}
aria-label="Send message" aria-label="Send message"
@ -1617,7 +1617,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
type="button" type="button"
variant="default" variant="default"
size="icon" size="icon"
className="aui-composer-cancel size-9 rounded-full" className="aui-composer-cancel size-9 shrink-0 rounded-full"
aria-label="Stop generating" aria-label="Stop generating"
> >
<SquareIcon className="aui-composer-cancel-icon size-3.5 fill-current" /> <SquareIcon className="aui-composer-cancel-icon size-3.5 fill-current" />

View file

@ -11,13 +11,13 @@ interface ChatHeaderProps {
export function ChatHeader({ searchSpaceId, className, onChatModelSelected }: ChatHeaderProps) { export function ChatHeader({ searchSpaceId, className, onChatModelSelected }: ChatHeaderProps) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex min-w-0 shrink-0 items-center gap-2">
<ModelSelector <ModelSelector
searchSpaceId={searchSpaceId} searchSpaceId={searchSpaceId}
className={className} className={className}
onChatModelSelected={onChatModelSelected} onChatModelSelected={onChatModelSelected}
/> />
<ImageModelSelector searchSpaceId={searchSpaceId} className={className} /> <ImageModelSelector searchSpaceId={searchSpaceId} className={className} mobileIconOnly />
</div> </div>
); );
} }

View file

@ -33,6 +33,7 @@ import { providerDisplay } from "../settings/model-connections/provider-metadata
interface ImageModelSelectorProps { interface ImageModelSelectorProps {
searchSpaceId: number; searchSpaceId: number;
className?: string; className?: string;
mobileIconOnly?: boolean;
} }
type ImageModel = ModelRead & { type ImageModel = ModelRead & {
@ -95,7 +96,11 @@ function groupedModels(models: ImageModel[]) {
}, {}); }, {});
} }
export function ImageModelSelector({ searchSpaceId, className }: ImageModelSelectorProps) { export function ImageModelSelector({
searchSpaceId,
className,
mobileIconOnly = false,
}: ImageModelSelectorProps) {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -126,6 +131,7 @@ export function ImageModelSelector({ searchSpaceId, className }: ImageModelSelec
const groups = useMemo(() => groupedModels(visibleImageModels), [visibleImageModels]); const groups = useMemo(() => groupedModels(visibleImageModels), [visibleImageModels]);
const loading = globalLoading || connectionsLoading; const loading = globalLoading || connectionsLoading;
const hasSearchQuery = search.trim().length > 0; const hasSearchQuery = search.trim().length > 0;
const showIconOnlyTrigger = isMobile && mobileIconOnly;
function handleOpenChange(nextOpen: boolean) { function handleOpenChange(nextOpen: boolean) {
if (!nextOpen) setSearch(""); if (!nextOpen) setSearch("");
@ -252,12 +258,14 @@ export function ImageModelSelector({ searchSpaceId, className }: ImageModelSelec
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
aria-label="Select image model"
className={cn( className={cn(
"h-8 min-w-0 gap-2 rounded-md px-3 text-muted-foreground transition-colors", "h-8 min-w-0 gap-2 rounded-md px-3 text-muted-foreground transition-colors",
"select-none", "select-none",
"hover:bg-foreground/10 hover:text-foreground", "hover:bg-foreground/10 hover:text-foreground",
"data-[state=open]:bg-foreground/10 data-[state=open]:text-foreground", "data-[state=open]:bg-foreground/10 data-[state=open]:text-foreground",
className className,
showIconOnlyTrigger && "h-9 w-auto shrink-0 justify-center gap-1 px-2"
)} )}
> >
{selected ? ( {selected ? (
@ -265,9 +273,11 @@ export function ImageModelSelector({ searchSpaceId, className }: ImageModelSelec
) : ( ) : (
<ImagePlus className="size-4 shrink-0" /> <ImagePlus className="size-4 shrink-0" />
)} )}
<span className="min-w-0 flex-1 truncate text-sm"> {showIconOnlyTrigger ? null : (
{selected ? modelName(selected) : "Auto"} <span className="min-w-0 flex-1 truncate text-sm">
</span> {selected ? modelName(selected) : "Auto"}
</span>
)}
<ChevronDown className="h-3.5 w-3.5 shrink-0" /> <ChevronDown className="h-3.5 w-3.5 shrink-0" />
</Button> </Button>
); );

View file

@ -131,6 +131,7 @@ export function ModelSelector({
const groups = useMemo(() => groupedModels(visibleChatModels), [visibleChatModels]); const groups = useMemo(() => groupedModels(visibleChatModels), [visibleChatModels]);
const loading = globalLoading || connectionsLoading; const loading = globalLoading || connectionsLoading;
const hasSearchQuery = search.trim().length > 0; const hasSearchQuery = search.trim().length > 0;
const showIconOnlyTrigger = isMobile;
function handleOpenChange(nextOpen: boolean) { function handleOpenChange(nextOpen: boolean) {
if (!nextOpen) setSearch(""); if (!nextOpen) setSearch("");
@ -276,15 +277,18 @@ export function ModelSelector({
"select-none", "select-none",
"hover:bg-foreground/10 hover:text-foreground", "hover:bg-foreground/10 hover:text-foreground",
"data-[state=open]:bg-foreground/10 data-[state=open]:text-foreground", "data-[state=open]:bg-foreground/10 data-[state=open]:text-foreground",
className className,
showIconOnlyTrigger && "h-9 w-auto shrink-0 justify-center gap-1 px-2"
)} )}
> >
{selected {selected
? getProviderIcon(selected.provider, { className: "size-4 shrink-0" }) ? getProviderIcon(selected.provider, { className: "size-4 shrink-0" })
: getProviderIcon(AUTO_PROVIDER_ICON_KEY, { className: "size-4 shrink-0" })} : getProviderIcon(AUTO_PROVIDER_ICON_KEY, { className: "size-4 shrink-0" })}
<span className="min-w-0 flex-1 truncate text-sm"> {showIconOnlyTrigger ? null : (
{selected ? modelName(selected) : "Auto"} <span className="min-w-0 flex-1 truncate text-sm">
</span> {selected ? modelName(selected) : "Auto"}
</span>
)}
<ChevronDown className="h-3.5 w-3.5 shrink-0" /> <ChevronDown className="h-3.5 w-3.5 shrink-0" />
</Button> </Button>
); );