fix: control elements padding in chatbox
This commit is contained in:
@@ -1132,7 +1132,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
event.target.value = '';
|
||||
}, [attachFiles]);
|
||||
|
||||
const footerGapClass = isMobile ? 'gap-x-0.5 gap-y-0' : 'gap-x-1.5 gap-y-0';
|
||||
const footerGapClass = 'gap-x-1.5 gap-y-0';
|
||||
const isVSCode = isVSCodeRuntime();
|
||||
const footerPaddingClass = isMobile ? 'px-1.5 py-1.5' : (isVSCode ? 'px-1.5 py-1' : 'px-2.5 py-1.5');
|
||||
const footerHeightClass = isMobile ? 'h-9 w-9' : (isVSCode ? 'h-[22px] w-[22px]' : 'h-7 w-7');
|
||||
@@ -1143,12 +1143,13 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
'flex items-center justify-center text-muted-foreground transition-none outline-none focus:outline-none flex-shrink-0'
|
||||
);
|
||||
|
||||
const cancelButton = canAbort ? (
|
||||
const stopButton = canAbort ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAbort}
|
||||
className={cn(
|
||||
iconButtonBaseClass,
|
||||
'absolute right-2 top-2 z-10',
|
||||
'text-[var(--status-error)] hover:text-[var(--status-error)]'
|
||||
)}
|
||||
aria-label="Stop generating"
|
||||
@@ -1157,7 +1158,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
const sendButton = (
|
||||
const actionButton = (
|
||||
<button
|
||||
type={isMobile ? 'button' : 'submit'}
|
||||
disabled={!canSend || (!currentSessionId && !newSessionDraftOpen)}
|
||||
@@ -1194,7 +1195,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
? 'text-primary hover:text-primary'
|
||||
: 'opacity-30'
|
||||
)}
|
||||
aria-label='Send message'
|
||||
aria-label="Send message"
|
||||
>
|
||||
<RiSendPlane2Line className={cn(iconSizeClass)} />
|
||||
</button>
|
||||
@@ -1382,6 +1383,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
)}
|
||||
style={chatInputWrapperStyle}
|
||||
>
|
||||
{stopButton}
|
||||
{}
|
||||
{showCommandAutocomplete && (
|
||||
<CommandAutocomplete
|
||||
@@ -1435,6 +1437,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
className={cn(
|
||||
'min-h-[52px] resize-none border-0 px-3 shadow-none rounded-b-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent',
|
||||
isMobile ? "py-2.5" : "pt-4 pb-2",
|
||||
canAbort && 'pr-10',
|
||||
"focus-visible:outline-none focus-visible:ring-0"
|
||||
)}
|
||||
style={{
|
||||
@@ -1450,7 +1453,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
className={cn(
|
||||
'bg-transparent',
|
||||
footerPaddingClass,
|
||||
cn('flex items-center justify-between', footerGapClass)
|
||||
isMobile ? 'flex items-center gap-x-1.5' : cn('flex items-center justify-between', footerGapClass)
|
||||
)}
|
||||
style={{
|
||||
borderBottomLeftRadius: cornerRadius,
|
||||
@@ -1458,18 +1461,29 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
}}
|
||||
data-chat-input-footer="true"
|
||||
>
|
||||
<div className={cn('flex items-center min-w-0 flex-1', footerGapClass)}>
|
||||
<div className={cn('flex items-center flex-shrink-0', footerGapClass)}>
|
||||
{attachmentsControls}
|
||||
{isMobile ? (
|
||||
<div className="flex w-full items-center gap-x-1.5">
|
||||
<div className="flex items-center flex-shrink-0 gap-x-1">
|
||||
{attachmentsControls}
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end gap-x-1 min-w-0">
|
||||
<div className="flex flex-1 min-w-0 justify-end overflow-hidden">
|
||||
<ModelControls className={cn('w-full flex items-center justify-end min-w-0')} />
|
||||
</div>
|
||||
{actionButton}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 overflow-hidden">
|
||||
<ModelControls className={cn('w-full min-w-0')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn('flex items-center flex-shrink-0', footerGapClass)}>
|
||||
{cancelButton}
|
||||
{sendButton}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={cn("flex items-center flex-shrink-0", footerGapClass)}>
|
||||
{attachmentsControls}
|
||||
</div>
|
||||
<div className={cn('flex items-center flex-1 justify-end', footerGapClass, 'md:gap-x-3')}>
|
||||
<ModelControls className={cn('flex-1 min-w-0 justify-end')} />
|
||||
{actionButton}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useProviderLogo } from '@/hooks/useProviderLogo';
|
||||
import { useIsDesktopRuntime, useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
|
||||
import { getAgentColor } from '@/lib/agentColors';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
@@ -139,27 +138,6 @@ type ModalityIcon = {
|
||||
|
||||
type ModelApplyResult = 'applied' | 'provider-missing' | 'model-missing';
|
||||
|
||||
const ProviderLogoOrFallback: React.FC<{
|
||||
providerId: string;
|
||||
className?: string;
|
||||
fallbackClassName?: string;
|
||||
}> = ({ providerId, className, fallbackClassName }) => {
|
||||
const { src, onError, hasLogo } = useProviderLogo(providerId);
|
||||
|
||||
if (hasLogo && src) {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={`${providerId} logo`}
|
||||
className={cn('dark:invert', className)}
|
||||
onError={onError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <RiPencilAiLine className={cn(className, fallbackClassName)} aria-hidden="true" />;
|
||||
};
|
||||
|
||||
const MODALITY_ICON_MAP: Record<string, ModalityIconDefinition> = {
|
||||
text: { icon: RiText, label: 'Text' },
|
||||
image: { icon: RiFileImageLine, label: 'Image' },
|
||||
@@ -437,7 +415,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
const editToggleIconClass = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-4';
|
||||
const controlIconSize = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-4';
|
||||
const controlTextSize = isCompact ? 'typography-micro' : 'typography-meta';
|
||||
const inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-0.5' : sizeVariant === 'vscode' ? 'gap-x-1' : 'gap-x-2';
|
||||
const inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-1' : sizeVariant === 'vscode' ? 'gap-x-1' : 'gap-x-3';
|
||||
const renderEditModeIcon = React.useCallback((mode: EditPermissionMode, iconClass = editToggleIconClass) => {
|
||||
const combinedClassName = cn(iconClass, 'flex-shrink-0');
|
||||
const modeColors = getEditModeColors(mode);
|
||||
@@ -1995,11 +1973,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
>
|
||||
{currentProviderId ? (
|
||||
<>
|
||||
<ProviderLogoOrFallback
|
||||
<ProviderLogo
|
||||
providerId={currentProviderId}
|
||||
className={cn(controlIconSize, 'flex-shrink-0')}
|
||||
fallbackClassName="text-muted-foreground"
|
||||
/>
|
||||
<RiPencilAiLine className={cn(controlIconSize, 'text-primary/60 hidden')} />
|
||||
</>
|
||||
) : (
|
||||
<RiPencilAiLine className={cn(controlIconSize, 'text-muted-foreground')} />
|
||||
@@ -2115,17 +2093,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
onTouchEnd={handleLongPressEnd}
|
||||
onTouchCancel={handleLongPressEnd}
|
||||
className={cn(
|
||||
'model-controls__model-trigger flex items-center gap-1 min-w-0 focus:outline-none',
|
||||
'model-controls__model-trigger flex items-center gap-1.5 min-w-0 focus:outline-none',
|
||||
'cursor-pointer hover:opacity-70',
|
||||
buttonHeight,
|
||||
'px-1'
|
||||
buttonHeight
|
||||
)}
|
||||
>
|
||||
{currentProviderId ? (
|
||||
<ProviderLogoOrFallback
|
||||
<ProviderLogo
|
||||
providerId={currentProviderId}
|
||||
className={cn(controlIconSize, 'flex-shrink-0')}
|
||||
fallbackClassName="text-muted-foreground"
|
||||
/>
|
||||
) : (
|
||||
<RiPencilAiLine className={cn(controlIconSize, 'text-muted-foreground')} />
|
||||
@@ -2133,11 +2109,13 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
<span
|
||||
ref={modelLabelRef}
|
||||
className={cn(
|
||||
'model-controls__model-label typography-micro font-medium min-w-0 truncate',
|
||||
'model-controls__model-label typography-micro font-medium overflow-hidden min-w-0',
|
||||
isMobile ? 'max-w-[120px]' : 'max-w-[220px]',
|
||||
)}
|
||||
>
|
||||
{currentModelDisplayName}
|
||||
<span className={cn('marquee-text', isModelLabelTruncated && 'marquee-text--active')}>
|
||||
{currentModelDisplayName}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -2283,15 +2261,21 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
type="button"
|
||||
onClick={() => setActiveMobilePanel('variant')}
|
||||
className={cn(
|
||||
'model-controls__variant-trigger flex items-center justify-center transition-opacity focus:outline-none',
|
||||
'model-controls__variant-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
|
||||
buttonHeight,
|
||||
'w-9',
|
||||
'cursor-pointer hover:opacity-70',
|
||||
)}
|
||||
aria-label={`Thinking: ${displayVariant}`}
|
||||
title={`Thinking: ${displayVariant}`}
|
||||
>
|
||||
<RiBrainAi3Line className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
|
||||
<span className={cn(
|
||||
'model-controls__variant-label',
|
||||
controlTextSize,
|
||||
'font-medium truncate min-w-0',
|
||||
isMobile && 'max-w-[60px]',
|
||||
colorClass
|
||||
)}>
|
||||
{displayVariant}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -2446,12 +2430,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
onTouchStart={() => handleLongPressStart('agent')}
|
||||
onTouchEnd={handleLongPressEnd}
|
||||
onTouchCancel={handleLongPressEnd}
|
||||
className={cn(
|
||||
'model-controls__agent-trigger flex items-center gap-1 transition-opacity min-w-0 focus:outline-none',
|
||||
buttonHeight,
|
||||
'cursor-pointer hover:opacity-70',
|
||||
'px-1',
|
||||
)}
|
||||
className={cn(
|
||||
'model-controls__agent-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
|
||||
buttonHeight,
|
||||
'cursor-pointer hover:opacity-70',
|
||||
)}
|
||||
>
|
||||
<RiAiAgentLine
|
||||
className={cn(
|
||||
@@ -2477,10 +2460,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
};
|
||||
|
||||
const inlineClassName = cn(
|
||||
(isMobile ? '@container' : '@container/model-controls'),
|
||||
'flex items-center min-w-0',
|
||||
'@container/model-controls flex items-center min-w-0',
|
||||
// Only force full-width + truncation behaviors on true mobile layouts.
|
||||
// VS Code uses desktop dropdowns.
|
||||
// VS Code also uses "compact" mode, but should keep its right-aligned inline sizing.
|
||||
isMobile && 'w-full',
|
||||
className,
|
||||
);
|
||||
@@ -2490,14 +2472,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
<div className={inlineClassName}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center min-w-0 flex-1 justify-start',
|
||||
'flex items-center min-w-0 flex-1 justify-end',
|
||||
inlineGapClass,
|
||||
isMobile && 'overflow-hidden'
|
||||
)}
|
||||
>
|
||||
{renderAgentSelector()}
|
||||
{renderModelSelector()}
|
||||
{renderVariantSelector()}
|
||||
{renderModelSelector()}
|
||||
{renderAgentSelector()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ interface StatusRowProps {
|
||||
abortActive?: boolean;
|
||||
completionId?: string | null;
|
||||
isComplete?: boolean;
|
||||
// Abort state (for mobile/vscode)
|
||||
showAbort?: boolean;
|
||||
onAbort?: () => void;
|
||||
// Abort status display
|
||||
showAbortStatus?: boolean;
|
||||
}
|
||||
@@ -68,6 +71,8 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
abortActive,
|
||||
completionId,
|
||||
isComplete,
|
||||
showAbort,
|
||||
onAbort,
|
||||
showAbortStatus,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
@@ -153,6 +158,18 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
|
||||
const toggleExpanded = () => setIsExpanded((prev) => !prev);
|
||||
|
||||
// Abort button for mobile/vscode
|
||||
const abortButton = showAbort && onAbort ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAbort}
|
||||
className="flex items-center justify-center h-[1.2rem] w-[1.2rem] text-[var(--status-error)] transition-opacity hover:opacity-80 focus-visible:outline-none flex-shrink-0"
|
||||
aria-label="Stop generating"
|
||||
>
|
||||
<RiCloseCircleLine size={18} aria-hidden="true" />
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
// Todo trigger button
|
||||
const todoTrigger = hasActiveTodos ? (
|
||||
<button
|
||||
@@ -206,8 +223,9 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Right: Todo */}
|
||||
{/* Right: Abort (mobile only) + Todo */}
|
||||
<div className="relative flex items-center gap-2 flex-shrink-0" ref={popoverRef}>
|
||||
{abortButton}
|
||||
{todoTrigger}
|
||||
|
||||
{/* Popover dropdown */}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export function StopIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M208,56V200a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H200A8,8,0,0,1,208,56Z"
|
||||
opacity="0.2"
|
||||
/>
|
||||
<path d="M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,160H56V56H200V200Z" />
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M208,56V200a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H200A8,8,0,0,1,208,56Z"
|
||||
opacity="0.2"
|
||||
/>
|
||||
<path d="M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,160H56V56H200V200Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user