Reduce React Doctor diagnostics in ModelControls (#1265)
* fix: reduce React Doctor diagnostics in ModelControls (69/88) * Move EditModeIcon and IconBadge to module scope to prevent remount on every render EditModeIcon and IconBadge were defined as React.FC inside ModelControls function body, producing a new component type reference on every render and forcing React to unmount/remount subtrees. Moves both to module scope; EditModeIcon no longer closes over editToggleIconClass since all callers already pass an explicit className prop.
This commit is contained in:
committed by
GitHub
parent
5e3afa3aa7
commit
8056fae275
@@ -191,19 +191,19 @@ const getModalityIcons = (metadata: ModelMetadata | undefined, direction: 'input
|
|||||||
|
|
||||||
const uniqueValues = Array.from(new Set(modalityList.map((item) => normalizeModality(item))));
|
const uniqueValues = Array.from(new Set(modalityList.map((item) => normalizeModality(item))));
|
||||||
|
|
||||||
return uniqueValues
|
const result: ModalityIcon[] = [];
|
||||||
.map((modality) => {
|
for (const modality of uniqueValues) {
|
||||||
const definition = MODALITY_ICON_MAP[modality];
|
const definition = MODALITY_ICON_MAP[modality];
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return null;
|
continue;
|
||||||
}
|
}
|
||||||
return {
|
result.push({
|
||||||
key: modality,
|
key: modality,
|
||||||
icon: definition.icon,
|
icon: definition.icon,
|
||||||
label: definition.label,
|
label: definition.label,
|
||||||
} satisfies ModalityIcon;
|
});
|
||||||
})
|
}
|
||||||
.filter((entry): entry is ModalityIcon => Boolean(entry));
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', {
|
const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', {
|
||||||
@@ -220,8 +220,45 @@ const CURRENCY_FORMATTER = new Intl.NumberFormat('en-US', {
|
|||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const KNOWLEDGE_DATE_FORMATTER = new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' });
|
||||||
|
|
||||||
|
const DATE_FORMATTER = new Intl.DateTimeFormat('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
const ADD_PROVIDER_ID = '__add_provider__';
|
const ADD_PROVIDER_ID = '__add_provider__';
|
||||||
|
|
||||||
|
const IconBadge: React.FC<{ iconName: IconComponent; label: string }> = ({ iconName, label }) => (
|
||||||
|
<span
|
||||||
|
className="flex size-5 items-center justify-center rounded-xl bg-muted/60 text-muted-foreground"
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<Icon name={iconName} className="size-3.5" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EditModeIcon: React.FC<{ mode: EditPermissionMode; className?: string }> = ({ mode, className }) => {
|
||||||
|
const combinedClassName = cn(className, 'flex-shrink-0');
|
||||||
|
const modeColors = getEditModeColors(mode);
|
||||||
|
const iconColor = modeColors ? modeColors.text : 'var(--foreground)';
|
||||||
|
const iconStyle = { color: iconColor };
|
||||||
|
|
||||||
|
if (mode === 'full') {
|
||||||
|
return <Icon name="pencil-ai" className={combinedClassName} style={iconStyle} />;
|
||||||
|
}
|
||||||
|
if (mode === 'allow') {
|
||||||
|
return <Icon name="checkbox-circle" className={combinedClassName} style={iconStyle} />;
|
||||||
|
}
|
||||||
|
if (mode === 'deny') {
|
||||||
|
return <Icon name="close-circle" className={combinedClassName} style={iconStyle} />;
|
||||||
|
}
|
||||||
|
return <Icon name="question" className={combinedClassName} style={iconStyle} />;
|
||||||
|
};
|
||||||
|
|
||||||
const formatTokens = (value?: number | null) => {
|
const formatTokens = (value?: number | null) => {
|
||||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||||
return '—';
|
return '—';
|
||||||
@@ -266,11 +303,13 @@ const formatCompactPrice = (metadata?: ModelMetadata): string | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCapabilityIcons = (metadata?: ModelMetadata) => {
|
const getCapabilityIcons = (metadata?: ModelMetadata) => {
|
||||||
return CAPABILITY_DEFINITIONS.filter((definition) => definition.isActive(metadata)).map((definition) => ({
|
const result: { key: string; icon: IconComponent; label: string }[] = [];
|
||||||
key: definition.key,
|
for (const definition of CAPABILITY_DEFINITIONS) {
|
||||||
icon: definition.icon,
|
if (definition.isActive(metadata)) {
|
||||||
label: definition.label,
|
result.push({ key: definition.key, icon: definition.icon, label: definition.label });
|
||||||
}));
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatKnowledge = (knowledge?: string) => {
|
const formatKnowledge = (knowledge?: string) => {
|
||||||
@@ -284,7 +323,7 @@ const formatKnowledge = (knowledge?: string) => {
|
|||||||
const monthIndex = Number.parseInt(match[2], 10) - 1;
|
const monthIndex = Number.parseInt(match[2], 10) - 1;
|
||||||
const knowledgeDate = new Date(Date.UTC(year, monthIndex, 1));
|
const knowledgeDate = new Date(Date.UTC(year, monthIndex, 1));
|
||||||
if (!Number.isNaN(knowledgeDate.getTime())) {
|
if (!Number.isNaN(knowledgeDate.getTime())) {
|
||||||
return new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' }).format(knowledgeDate);
|
return KNOWLEDGE_DATE_FORMATTER.format(knowledgeDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,11 +340,7 @@ const formatDate = (value?: string) => {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Intl.DateTimeFormat('en-US', {
|
return DATE_FORMATTER.format(parsedDate);
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
}).format(parsedDate);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ModelControlsProps {
|
interface ModelControlsProps {
|
||||||
@@ -394,10 +429,16 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||||
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
||||||
const hiddenModels = useUIStore((state) => state.hiddenModels);
|
const hiddenModels = useUIStore((state) => state.hiddenModels);
|
||||||
const collapsedProviderSet = React.useMemo(
|
const collapsedProviderSet = React.useMemo(() => {
|
||||||
() => new Set(collapsedModelProviders.map((providerId) => providerId.trim()).filter(Boolean)),
|
const result = new Set<string>();
|
||||||
[collapsedModelProviders]
|
for (const providerId of collapsedModelProviders) {
|
||||||
);
|
const trimmed = providerId.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
result.add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [collapsedModelProviders]);
|
||||||
|
|
||||||
// Separate state for agent selector to avoid conflict with model selector
|
// Separate state for agent selector to avoid conflict with model selector
|
||||||
const [isAgentSelectorOpen, setIsAgentSelectorOpen] = React.useState(false);
|
const [isAgentSelectorOpen, setIsAgentSelectorOpen] = React.useState(false);
|
||||||
@@ -473,13 +514,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (activeMobilePanel !== 'model') {
|
if (activeMobilePanel !== 'model') {
|
||||||
setMobileModelQuery('');
|
setMobileModelQuery('');
|
||||||
|
setExpandedMobileModelKey(null);
|
||||||
}
|
}
|
||||||
}, [activeMobilePanel]);
|
}, [activeMobilePanel]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setExpandedMobileModelKey(null);
|
|
||||||
}, [mobileModelQuery]);
|
|
||||||
|
|
||||||
// Handle model selector close behavior (separate from agent selector)
|
// Handle model selector close behavior (separate from agent selector)
|
||||||
const prevModelSelectorOpenRef = React.useRef(isModelSelectorOpen);
|
const prevModelSelectorOpenRef = React.useRef(isModelSelectorOpen);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -552,44 +590,28 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
|
|
||||||
const sizeVariant: 'mobile' | 'vscode' | 'default' = isMobile ? 'mobile' : isVSCodeRuntime ? 'vscode' : 'default';
|
const sizeVariant: 'mobile' | 'vscode' | 'default' = isMobile ? 'mobile' : isVSCodeRuntime ? 'vscode' : 'default';
|
||||||
const buttonHeight = sizeVariant === 'mobile' ? 'h-9' : sizeVariant === 'vscode' ? 'h-6' : 'h-8';
|
const buttonHeight = sizeVariant === 'mobile' ? 'h-9' : sizeVariant === 'vscode' ? 'h-6' : 'h-8';
|
||||||
const editToggleIconClass = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-4';
|
const controlIconSize = sizeVariant === 'mobile' ? 'size-5' : sizeVariant === 'vscode' ? 'size-4' : 'size-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 controlTextSize = isCompact ? 'typography-micro' : 'typography-meta';
|
||||||
const inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-1' : sizeVariant === 'vscode' ? 'gap-x-2' : 'gap-x-3';
|
const inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-1' : sizeVariant === 'vscode' ? 'gap-x-2' : 'gap-x-3';
|
||||||
const renderEditModeIcon = React.useCallback((mode: EditPermissionMode, iconClass = editToggleIconClass) => {
|
|
||||||
const combinedClassName = cn(iconClass, 'flex-shrink-0');
|
|
||||||
const modeColors = getEditModeColors(mode);
|
|
||||||
const iconColor = modeColors ? modeColors.text : 'var(--foreground)';
|
|
||||||
const iconStyle = { color: iconColor };
|
|
||||||
|
|
||||||
if (mode === 'full') {
|
|
||||||
return <Icon name="pencil-ai" className={combinedClassName} style={iconStyle} />;
|
|
||||||
}
|
|
||||||
if (mode === 'allow') {
|
|
||||||
return <Icon name="checkbox-circle" className={combinedClassName} style={iconStyle} />;
|
|
||||||
}
|
|
||||||
if (mode === 'deny') {
|
|
||||||
return <Icon name="close-circle" className={combinedClassName} style={iconStyle} />;
|
|
||||||
}
|
|
||||||
return <Icon name="question" className={combinedClassName} style={iconStyle} />;
|
|
||||||
}, [editToggleIconClass]);
|
|
||||||
|
|
||||||
const currentProvider = getCurrentProvider();
|
const currentProvider = getCurrentProvider();
|
||||||
const models = Array.isArray(currentProvider?.models) ? currentProvider.models : [];
|
const models = Array.isArray(currentProvider?.models) ? currentProvider.models : [];
|
||||||
|
|
||||||
const visibleProviders = React.useMemo(() => {
|
const visibleProviders = React.useMemo(() => {
|
||||||
return providers
|
const result: typeof providers = [];
|
||||||
.map((provider) => {
|
for (const provider of providers) {
|
||||||
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
||||||
const visibleModels = providerModels.filter((model: ProviderModel) => {
|
const visibleModels = providerModels.filter((model: ProviderModel) => {
|
||||||
const modelId = typeof model?.id === 'string' ? model.id : '';
|
const modelId = typeof model?.id === 'string' ? model.id : '';
|
||||||
return !hiddenModels.some(
|
return !hiddenModels.some(
|
||||||
(item) => item.providerID === String(provider.id) && item.modelID === modelId
|
(item) => item.providerID === String(provider.id) && item.modelID === modelId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return { ...provider, models: visibleModels };
|
if (visibleModels.length > 0) {
|
||||||
})
|
result.push({ ...provider, models: visibleModels });
|
||||||
.filter((provider) => provider.models.length > 0);
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}, [providers, hiddenModels]);
|
}, [providers, hiddenModels]);
|
||||||
|
|
||||||
const normalizeModelSearchValue = React.useCallback((value: string) => {
|
const normalizeModelSearchValue = React.useCallback((value: string) => {
|
||||||
@@ -632,11 +654,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
if (!normalizedQuery) return true;
|
if (!normalizedQuery) return true;
|
||||||
return matchesModelSearch(modelName, normalizedQuery) || matchesModelSearch(providerName, normalizedQuery);
|
return matchesModelSearch(modelName, normalizedQuery) || matchesModelSearch(providerName, normalizedQuery);
|
||||||
};
|
};
|
||||||
|
const providersById = new Map(providers.map((p) => [p.id, p]));
|
||||||
|
|
||||||
let flatIndex = 0;
|
let flatIndex = 0;
|
||||||
|
|
||||||
for (const { model, providerID, modelID } of favoriteModelsList) {
|
for (const { model, providerID, modelID } of favoriteModelsList) {
|
||||||
const provider = providers.find((entry) => entry.id === providerID);
|
const provider = providersById.get(providerID);
|
||||||
const providerName = provider?.name || providerID;
|
const providerName = provider?.name || providerID;
|
||||||
const modelName = getModelDisplayName(model);
|
const modelName = getModelDisplayName(model);
|
||||||
if (!matchesQuery(modelName, providerName)) {
|
if (!matchesQuery(modelName, providerName)) {
|
||||||
@@ -649,7 +672,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const { model, providerID, modelID } of recentModelsList) {
|
for (const { model, providerID, modelID } of recentModelsList) {
|
||||||
const provider = providers.find((entry) => entry.id === providerID);
|
const provider = providersById.get(providerID);
|
||||||
const providerName = provider?.name || providerID;
|
const providerName = provider?.name || providerID;
|
||||||
const modelName = getModelDisplayName(model);
|
const modelName = getModelDisplayName(model);
|
||||||
if (!matchesQuery(modelName, providerName)) {
|
if (!matchesQuery(modelName, providerName)) {
|
||||||
@@ -1123,6 +1146,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
if (!contextHydrated) {
|
if (!contextHydrated) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
const handleAgentSwitch = async () => {
|
const handleAgentSwitch = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -1130,7 +1154,17 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
prevAgentNameRef.current = currentAgentName;
|
prevAgentNameRef.current = currentAgentName;
|
||||||
|
|
||||||
if (currentAgentName && currentSessionId) {
|
if (currentAgentName && currentSessionId) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await new Promise<void>((resolve) => {
|
||||||
|
const timer = setTimeout(resolve, 50);
|
||||||
|
abortController.signal.addEventListener('abort', () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (abortController.signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const persistedChoice = getAgentModelForSession(currentSessionId, currentAgentName);
|
const persistedChoice = getAgentModelForSession(currentSessionId, currentAgentName);
|
||||||
|
|
||||||
@@ -1152,6 +1186,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleAgentSwitch();
|
handleAgentSwitch();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
}, [currentAgentName, currentSessionId, getAgentModelForSession, tryApplyModelSelection, contextHydrated]);
|
}, [currentAgentName, currentSessionId, getAgentModelForSession, tryApplyModelSelection, contextHydrated]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -1335,18 +1373,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderIconBadge = (iconName: IconComponent, label: string, key: string) => (
|
|
||||||
<span
|
|
||||||
key={key}
|
|
||||||
className="flex h-5 w-5 items-center justify-center rounded-xl bg-muted/60 text-muted-foreground"
|
|
||||||
title={label}
|
|
||||||
aria-label={label}
|
|
||||||
role="img"
|
|
||||||
>
|
|
||||||
<Icon name={iconName} className="h-3.5 w-3.5" />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleMobileProviderExpansion = React.useCallback((providerId: string) => {
|
const toggleMobileProviderExpansion = React.useCallback((providerId: string) => {
|
||||||
setExpandedMobileProviders((prev) => {
|
setExpandedMobileProviders((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -1405,7 +1431,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{currentCapabilityIcons.map(({ key, icon, label }) => (
|
{currentCapabilityIcons.map(({ key, icon, label }) => (
|
||||||
<div key={key} className="flex items-center gap-1.5">
|
<div key={key} className="flex items-center gap-1.5">
|
||||||
{renderIconBadge(icon, label, `cap-${key}`)}
|
<IconBadge key={`cap-${key}`} iconName={icon} label={label} />
|
||||||
<span className="typography-meta text-foreground">{label}</span>
|
<span className="typography-meta text-foreground">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -1422,7 +1448,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.input')}</span>
|
<span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.input')}</span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{inputModalityIcons.map(({ key, icon, label }) => renderIconBadge(icon, `${label} input`, `input-${key}`))}
|
{inputModalityIcons.map(({ key, icon, label }) => <IconBadge key={`input-${key}`} iconName={icon} label={`${label} input`} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1430,7 +1456,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.output')}</span>
|
<span className="typography-meta text-muted-foreground/80 w-12">{t('chat.modelControls.output')}</span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{outputModalityIcons.map(({ key, icon, label }) => renderIconBadge(icon, `${label} output`, `output-${key}`))}
|
{outputModalityIcons.map(({ key, icon, label }) => <IconBadge key={`output-${key}`} iconName={icon} label={`${label} output`} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1560,7 +1586,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.edit')}</span>
|
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.edit')}</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{renderEditModeIcon(editPermissionSummary.mode, 'h-3.5 w-3.5')}
|
<EditModeIcon mode={editPermissionSummary.mode} className="size-3.5" />
|
||||||
<span className="typography-meta font-medium text-foreground">
|
<span className="typography-meta font-medium text-foreground">
|
||||||
{editPermissionSummary.label}
|
{editPermissionSummary.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -1569,7 +1595,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.bash')}</span>
|
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.bash')}</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{renderEditModeIcon(bashPermissionSummary.mode, 'h-3.5 w-3.5')}
|
<EditModeIcon mode={bashPermissionSummary.mode} className="size-3.5" />
|
||||||
<span className="typography-meta font-medium text-foreground">
|
<span className="typography-meta font-medium text-foreground">
|
||||||
{bashPermissionSummary.label}
|
{bashPermissionSummary.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -1578,7 +1604,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.webFetch')}</span>
|
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.webFetch')}</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{renderEditModeIcon(webfetchPermissionSummary.mode, 'h-3.5 w-3.5')}
|
<EditModeIcon mode={webfetchPermissionSummary.mode} className="size-3.5" />
|
||||||
<span className="typography-meta font-medium text-foreground">
|
<span className="typography-meta font-medium text-foreground">
|
||||||
{webfetchPermissionSummary.label}
|
{webfetchPermissionSummary.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -1592,7 +1618,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
<div className="rounded-xl border border-border/40 bg-sidebar/30 px-2 py-1.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
|
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
|
||||||
<Icon name="checkbox-circle" className="h-4 w-4 text-foreground" />
|
<Icon name="checkbox-circle" className="size-4 text-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1623,26 +1649,28 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
|| matchesModelSearch(providerName, normalizedQuery);
|
|| matchesModelSearch(providerName, normalizedQuery);
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredProviders = visibleProviders
|
const filteredProviders: {
|
||||||
.map((provider) => {
|
provider: (typeof visibleProviders)[number];
|
||||||
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
providerModels: ProviderModel[];
|
||||||
const matchesProvider = normalizedQuery.length === 0
|
matchesProvider: boolean;
|
||||||
? true
|
}[] = [];
|
||||||
: matchesModelSearch(provider.name, normalizedQuery) || matchesModelSearch(provider.id, normalizedQuery);
|
for (const provider of visibleProviders) {
|
||||||
const matchingModels = normalizedQuery.length === 0
|
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
||||||
? providerModels
|
const matchesProvider = normalizedQuery.length === 0
|
||||||
: providerModels.filter((model: ProviderModel) => {
|
? true
|
||||||
const name = getModelDisplayName(model);
|
: matchesModelSearch(provider.name, normalizedQuery) || matchesModelSearch(provider.id, normalizedQuery);
|
||||||
const id = typeof model.id === 'string' ? model.id : '';
|
const matchingModels = normalizedQuery.length === 0
|
||||||
return matchesModelSearch(name, normalizedQuery) || matchesModelSearch(id, normalizedQuery);
|
? providerModels
|
||||||
});
|
: providerModels.filter((model: ProviderModel) => {
|
||||||
return {
|
const name = getModelDisplayName(model);
|
||||||
provider,
|
const id = typeof model.id === 'string' ? model.id : '';
|
||||||
providerModels: matchesProvider && normalizedQuery.length > 0 ? providerModels : matchingModels,
|
return matchesModelSearch(name, normalizedQuery) || matchesModelSearch(id, normalizedQuery);
|
||||||
matchesProvider,
|
});
|
||||||
};
|
const resolvedModels = matchesProvider && normalizedQuery.length > 0 ? providerModels : matchingModels;
|
||||||
})
|
if (matchesProvider || resolvedModels.length > 0) {
|
||||||
.filter(({ matchesProvider, providerModels }) => matchesProvider || providerModels.length > 0);
|
filteredProviders.push({ provider, providerModels: resolvedModels, matchesProvider });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const focusMobileComposer = () => {
|
const focusMobileComposer = () => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -1724,14 +1752,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{showProviderLogo ? (
|
{showProviderLogo ? (
|
||||||
<ProviderLogo providerId={providerId} className="mt-0.5 h-3.5 w-3.5 flex-shrink-0" />
|
<ProviderLogo providerId={providerId} className="mt-0.5 size-3.5 flex-shrink-0" />
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
<div className="flex min-w-0 items-start gap-2">
|
<div className="flex min-w-0 items-start gap-2">
|
||||||
<span className="typography-meta font-medium text-foreground truncate">
|
<span className="typography-meta font-medium text-foreground truncate">
|
||||||
{getModelDisplayName(model)}
|
{getModelDisplayName(model)}
|
||||||
</span>
|
</span>
|
||||||
{isSelected ? <Icon name="check" className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" /> : null}
|
{isSelected ? <Icon name="check" className="mt-0.5 size-4 flex-shrink-0 text-primary" /> : null}
|
||||||
</div>
|
</div>
|
||||||
{contextText || indicatorIcons.length > 0 ? (
|
{contextText || indicatorIcons.length > 0 ? (
|
||||||
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden typography-micro text-muted-foreground">
|
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden typography-micro text-muted-foreground">
|
||||||
@@ -1748,11 +1776,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{indicatorIcons.map(({ key, icon: iconName, label }) => (
|
{indicatorIcons.map(({ key, icon: iconName, label }) => (
|
||||||
<span
|
<span
|
||||||
key={`meta-${providerId}-${modelId}-${key}`}
|
key={`meta-${providerId}-${modelId}-${key}`}
|
||||||
className="flex h-4 w-4 flex-shrink-0 items-center justify-center text-muted-foreground"
|
className="flex size-4 flex-shrink-0 items-center justify-center text-muted-foreground"
|
||||||
title={label}
|
title={label}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
<Icon name={iconName} className="h-3 w-3" />
|
<Icon name={iconName} className="size-3" />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1770,7 +1798,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
aria-label={isExpanded ? t('chat.modelControls.hideThinkingModes') : t('chat.modelControls.showThinkingModes')}
|
aria-label={isExpanded ? t('chat.modelControls.hideThinkingModes') : t('chat.modelControls.showThinkingModes')}
|
||||||
>
|
>
|
||||||
<span className="whitespace-nowrap">{variantLabel}</span>
|
<span className="whitespace-nowrap">{variantLabel}</span>
|
||||||
{isExpanded ? <Icon name="arrow-down-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-right-s" className="h-3.5 w-3.5" />}
|
{isExpanded ? <Icon name="arrow-down-s" className="size-3.5" /> : <Icon name="arrow-right-s" className="size-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex flex-shrink-0 items-start gap-1.5">
|
<div className="flex flex-shrink-0 items-start gap-1.5">
|
||||||
@@ -1782,7 +1810,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
toggleFavoriteModel(providerId, modelId);
|
toggleFavoriteModel(providerId, modelId);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'model-favorite-button flex h-5 w-5 items-center justify-center hover:text-primary/80 flex-shrink-0',
|
'model-favorite-button flex size-5 items-center justify-center hover:text-primary/80 flex-shrink-0',
|
||||||
isFavoriteModel(providerId, modelId) ? 'text-primary' : 'text-muted-foreground'
|
isFavoriteModel(providerId, modelId) ? 'text-primary' : 'text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
aria-label={isFavoriteModel(providerId, modelId)
|
aria-label={isFavoriteModel(providerId, modelId)
|
||||||
@@ -1793,15 +1821,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
: t('chat.modelControls.addToFavorites')}
|
: t('chat.modelControls.addToFavorites')}
|
||||||
>
|
>
|
||||||
{isFavoriteModel(providerId, modelId) ? (
|
{isFavoriteModel(providerId, modelId) ? (
|
||||||
<Icon name="star-fill" className="h-4 w-4" />
|
<Icon name="star-fill" className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<Icon name="star" className="h-4 w-4" />
|
<Icon name="star" className="size-4" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isExpanded && hasVariants ? (
|
{isExpanded && hasVariants ? (
|
||||||
<div className="border-t border-border/30 px-2 py-2">
|
<div className="border-t border-border/30 p-2">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{inlineVariantOptions.map((variantOption) => {
|
{inlineVariantOptions.map((variantOption) => {
|
||||||
const isVariantSelected = variantOption === resolvedVariant || (!variantOption && !resolvedVariant);
|
const isVariantSelected = variantOption === resolvedVariant || (!variantOption && !resolvedVariant);
|
||||||
@@ -1850,21 +1878,27 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon name="search" className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
<Icon name="search" className="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
value={mobileModelQuery}
|
value={mobileModelQuery}
|
||||||
onChange={(event) => setMobileModelQuery(event.target.value)}
|
onChange={(event) => {
|
||||||
|
setMobileModelQuery(event.target.value);
|
||||||
|
setExpandedMobileModelKey(null);
|
||||||
|
}}
|
||||||
placeholder={t('chat.modelControls.searchProvidersOrModels')}
|
placeholder={t('chat.modelControls.searchProvidersOrModels')}
|
||||||
className="pl-7 h-9 rounded-xl border-border/40 bg-[var(--surface-elevated)] typography-meta"
|
className="pl-7 h-9 rounded-xl border-border/40 bg-[var(--surface-elevated)] typography-meta"
|
||||||
/>
|
/>
|
||||||
{mobileModelQuery && (
|
{mobileModelQuery && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMobileModelQuery('')}
|
onClick={() => {
|
||||||
|
setMobileModelQuery('');
|
||||||
|
setExpandedMobileModelKey(null);
|
||||||
|
}}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
aria-label={t('chat.modelControls.clearSearch')}
|
aria-label={t('chat.modelControls.clearSearch')}
|
||||||
>
|
>
|
||||||
<Icon name="close-circle" className="h-4 w-4" />
|
<Icon name="close-circle" className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1880,7 +1914,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{filteredFavorites.length > 0 && (
|
{filteredFavorites.length > 0 && (
|
||||||
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
|
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
|
||||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
<Icon name="star-fill" className="h-3 w-3 inline-block mr-1.5 text-primary" />
|
<Icon name="star-fill" className="size-3 inline-block mr-1.5 text-primary" />
|
||||||
{t('chat.modelControls.favorites')}
|
{t('chat.modelControls.favorites')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col border-t border-border/30">
|
<div className="flex flex-col border-t border-border/30">
|
||||||
@@ -1898,7 +1932,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{filteredRecents.length > 0 && (
|
{filteredRecents.length > 0 && (
|
||||||
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
|
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
|
||||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
<Icon name="time" className="h-3 w-3 inline-block mr-1.5" />
|
<Icon name="time" className="size-3 inline-block mr-1.5" />
|
||||||
{t('chat.modelControls.recent')}
|
{t('chat.modelControls.recent')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col border-t border-border/30">
|
<div className="flex flex-col border-t border-border/30">
|
||||||
@@ -1936,7 +1970,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ProviderLogo
|
<ProviderLogo
|
||||||
providerId={provider.id}
|
providerId={provider.id}
|
||||||
className="h-3.5 w-3.5"
|
className="size-3.5"
|
||||||
/>
|
/>
|
||||||
<span className="typography-meta font-medium text-foreground">
|
<span className="typography-meta font-medium text-foreground">
|
||||||
{provider.name}
|
{provider.name}
|
||||||
@@ -1946,9 +1980,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<Icon name="arrow-down-s" className="h-3 w-3 text-muted-foreground" />
|
<Icon name="arrow-down-s" className="size-3 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<Icon name="arrow-right-s" className="h-3 w-3 text-muted-foreground" />
|
<Icon name="arrow-right-s" className="size-3 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -2012,7 +2046,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
className="flex items-center gap-1 rounded-lg px-1.5 py-1 typography-meta text-muted-foreground hover:bg-interactive-hover"
|
className="flex items-center gap-1 rounded-lg px-1.5 py-1 typography-meta text-muted-foreground hover:bg-interactive-hover"
|
||||||
>
|
>
|
||||||
<Icon name="arrow-go-back" className="h-4 w-4" />
|
<Icon name="arrow-go-back" className="size-4" />
|
||||||
<span>{t('onboarding.common.actions.back')}</span>
|
<span>{t('onboarding.common.actions.back')}</span>
|
||||||
</button>
|
</button>
|
||||||
<h2 className="typography-ui-label font-semibold text-foreground">{t('chat.modelControls.thinking')}</h2>
|
<h2 className="typography-ui-label font-semibold text-foreground">{t('chat.modelControls.thinking')}</h2>
|
||||||
@@ -2031,7 +2065,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
onClick={() => handleSelect(undefined)}
|
onClick={() => handleSelect(undefined)}
|
||||||
>
|
>
|
||||||
<span className="typography-meta font-medium text-foreground">{t('chat.modelControls.default')}</span>
|
<span className="typography-meta font-medium text-foreground">{t('chat.modelControls.default')}</span>
|
||||||
{isDefault && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />}
|
{isDefault && <Icon name="check" className="size-4 text-primary flex-shrink-0" />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{targetVariants.map((variant) => {
|
{targetVariants.map((variant) => {
|
||||||
@@ -2050,7 +2084,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
onClick={() => handleSelect(variant)}
|
onClick={() => handleSelect(variant)}
|
||||||
>
|
>
|
||||||
<span className="typography-meta font-medium text-foreground">{label}</span>
|
<span className="typography-meta font-medium text-foreground">{label}</span>
|
||||||
{selected && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />}
|
{selected && <Icon name="check" className="size-4 text-primary flex-shrink-0" />}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -2089,7 +2123,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
onClick={() => handleAgentChange(agent.name)}
|
onClick={() => handleAgentChange(agent.name)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className={cn('h-2.5 w-2.5 rounded-full flex-shrink-0', agentColor.class)} />
|
<div className={cn('size-2.5 rounded-full flex-shrink-0', agentColor.class)} />
|
||||||
<span
|
<span
|
||||||
className="typography-ui-label font-semibold"
|
className="typography-ui-label font-semibold"
|
||||||
style={isSelected ? { color: `var(${agentColor.var})` } : undefined}
|
style={isSelected ? { color: `var(${agentColor.var})` } : undefined}
|
||||||
@@ -2097,7 +2131,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{capitalizeAgentName(agent.name)}
|
{capitalizeAgentName(agent.name)}
|
||||||
</span>
|
</span>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<Icon name="check" className="h-4 w-4 text-primary ml-auto flex-shrink-0" />
|
<Icon name="check" className="size-4 text-primary ml-auto flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{agent.description && (
|
{agent.description && (
|
||||||
@@ -2128,7 +2162,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
{currentCapabilityIcons.length > 0 ? (
|
{currentCapabilityIcons.length > 0 ? (
|
||||||
currentCapabilityIcons.map(({ key, icon, label }) =>
|
currentCapabilityIcons.map(({ key, icon, label }) =>
|
||||||
renderIconBadge(icon, label, `cap-${key}`)
|
<IconBadge key={`cap-${key}`} iconName={icon} label={label} />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<span className="typography-meta text-muted-foreground">{t('chat.modelControls.modeValue.none')}</span>
|
<span className="typography-meta text-muted-foreground">{t('chat.modelControls.modeValue.none')}</span>
|
||||||
@@ -2143,9 +2177,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{inputModalityIcons.length > 0
|
{inputModalityIcons.length > 0
|
||||||
? inputModalityIcons.map(({ key, icon, label }) =>
|
? inputModalityIcons.map(({ key, icon, label }) =>
|
||||||
renderIconBadge(icon, `${label} input`, `input-${key}`)
|
<IconBadge key={`input-${key}`} iconName={icon} label={`${label} input`} />
|
||||||
)
|
)
|
||||||
: <span className="typography-meta text-muted-foreground">—</span>}
|
: <span className="typography-meta text-muted-foreground">-</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
@@ -2153,9 +2187,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{outputModalityIcons.length > 0
|
{outputModalityIcons.length > 0
|
||||||
? outputModalityIcons.map(({ key, icon, label }) =>
|
? outputModalityIcons.map(({ key, icon, label }) =>
|
||||||
renderIconBadge(icon, `${label} output`, `output-${key}`)
|
<IconBadge key={`output-${key}`} iconName={icon} label={`${label} output`} />
|
||||||
)
|
)
|
||||||
: <span className="typography-meta text-muted-foreground">—</span>}
|
: <span className="typography-meta text-muted-foreground">-</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2266,12 +2300,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{indicatorIcons.map(({ id, icon: iconName, label }) => (
|
{indicatorIcons.map(({ id, icon: iconName, label }) => (
|
||||||
<span
|
<span
|
||||||
key={id}
|
key={id}
|
||||||
className="flex h-3.5 w-3.5 items-center justify-center text-muted-foreground"
|
className="flex size-3.5 items-center justify-center text-muted-foreground"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
role="img"
|
role="img"
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
<Icon name={iconName} className="h-2.5 w-2.5" />
|
<Icon name={iconName} className="size-2.5" />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -2308,11 +2342,19 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div
|
<div
|
||||||
key={`${keyPrefix}-${providerID}-${modelID}`}
|
key={`${keyPrefix}-${providerID}-${modelID}`}
|
||||||
ref={(el) => { modelItemRefs.current[flatIndex] = el; }}
|
ref={(el) => { modelItemRefs.current[flatIndex] = el; }}
|
||||||
|
role="option"
|
||||||
|
aria-selected={isSelected}
|
||||||
className={cn(
|
className={cn(
|
||||||
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
|
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
|
||||||
isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50"
|
isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50"
|
||||||
)}
|
)}
|
||||||
onClick={() => handleProviderAndModelChange(providerID, modelID)}
|
onClick={() => handleProviderAndModelChange(providerID, modelID)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleProviderAndModelChange(providerID, modelID);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onMouseEnter={handlePointerActivity}
|
onMouseEnter={handlePointerActivity}
|
||||||
onMouseMove={handlePointerActivity}
|
onMouseMove={handlePointerActivity}
|
||||||
>
|
>
|
||||||
@@ -2326,16 +2368,16 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="model-favorite-drag-handle flex h-4 w-4 flex-shrink-0 items-center justify-center text-muted-foreground hover:text-foreground"
|
className="model-favorite-drag-handle flex size-4 flex-shrink-0 items-center justify-center text-muted-foreground hover:text-foreground"
|
||||||
aria-label={t('chat.modelControls.reorderFavoriteAria')}
|
aria-label={t('chat.modelControls.reorderFavoriteAria')}
|
||||||
title={t('chat.modelControls.reorderFavoriteTitle')}
|
title={t('chat.modelControls.reorderFavoriteTitle')}
|
||||||
>
|
>
|
||||||
<Icon name="draggable" className="h-3.5 w-3.5" />
|
<Icon name="draggable" className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||||
{showProviderLogo && (
|
{showProviderLogo && (
|
||||||
<ProviderLogo providerId={providerID} className="h-3.5 w-3.5 flex-shrink-0" />
|
<ProviderLogo providerId={providerID} className="size-3.5 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="font-medium truncate">
|
<span className="font-medium truncate">
|
||||||
{getModelDisplayName(model)}
|
{getModelDisplayName(model)}
|
||||||
@@ -2370,7 +2412,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<Icon name="check" className="h-4 w-4 text-primary" />
|
<Icon name="check" className="size-4 text-primary" />
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -2379,7 +2421,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
toggleFavoriteModel(providerID, modelID);
|
toggleFavoriteModel(providerID, modelID);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-primary/80",
|
"model-favorite-button flex size-4 items-center justify-center hover:text-primary/80",
|
||||||
isFavorite ? "text-primary" : "text-muted-foreground"
|
isFavorite ? "text-primary" : "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
aria-label={isFavorite
|
aria-label={isFavorite
|
||||||
@@ -2390,9 +2432,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
: t('chat.modelControls.addToFavorites')}
|
: t('chat.modelControls.addToFavorites')}
|
||||||
>
|
>
|
||||||
{isFavorite ? (
|
{isFavorite ? (
|
||||||
<Icon name="star-fill" className="h-3.5 w-3.5" />
|
<Icon name="star-fill" className="size-3.5" />
|
||||||
) : (
|
) : (
|
||||||
<Icon name="star" className="h-3.5 w-3.5" />
|
<Icon name="star" className="size-3.5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2429,16 +2471,17 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
return filterByQuery(modelName, providerName, desktopModelQuery);
|
return filterByQuery(modelName, providerName, desktopModelQuery);
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredProviders = visibleProviders
|
const filteredProviders: typeof visibleProviders = [];
|
||||||
.map((provider) => {
|
for (const provider of visibleProviders) {
|
||||||
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
||||||
const filteredModels = providerModels.filter((model: ProviderModel) => {
|
const filteredModels = providerModels.filter((model: ProviderModel) => {
|
||||||
const modelName = getModelDisplayName(model);
|
const modelName = getModelDisplayName(model);
|
||||||
return filterByQuery(modelName, provider.name || provider.id || '', desktopModelQuery);
|
return filterByQuery(modelName, provider.name || provider.id || '', desktopModelQuery);
|
||||||
});
|
});
|
||||||
return { ...provider, models: filteredModels };
|
if (filteredModels.length > 0) {
|
||||||
})
|
filteredProviders.push({ ...provider, models: filteredModels });
|
||||||
.filter((provider) => provider.models.length > 0);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const providerSections = filteredProviders.map((provider) => {
|
const providerSections = filteredProviders.map((provider) => {
|
||||||
const providerId = typeof provider.id === 'string' ? provider.id : '';
|
const providerId = typeof provider.id === 'string' ? provider.id : '';
|
||||||
@@ -2457,9 +2500,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
filteredRecents.length > 0 ||
|
filteredRecents.length > 0 ||
|
||||||
filteredProviders.length > 0;
|
filteredProviders.length > 0;
|
||||||
|
|
||||||
const filteredProviderIds = filteredProviders
|
const filteredProviderIds: string[] = [];
|
||||||
.map((provider) => (typeof provider.id === 'string' ? provider.id : ''))
|
for (const provider of filteredProviders) {
|
||||||
.filter(Boolean);
|
if (typeof provider.id === 'string' && provider.id) {
|
||||||
|
filteredProviderIds.push(provider.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const favoriteModelLookup = new Map(
|
const favoriteModelLookup = new Map(
|
||||||
filteredFavorites.map(({ providerID, modelID }) => [buildModelRefKey(providerID, modelID), { providerID, modelID }])
|
filteredFavorites.map(({ providerID, modelID }) => [buildModelRefKey(providerID, modelID), { providerID, modelID }])
|
||||||
@@ -2687,7 +2733,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{/* Search Input */}
|
{/* Search Input */}
|
||||||
<div className="p-2 border-b border-border/40">
|
<div className="p-2 border-b border-border/40">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('chat.modelControls.searchModels')}
|
placeholder={t('chat.modelControls.searchModels')}
|
||||||
@@ -2695,7 +2741,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
onChange={(e) => setDesktopModelQuery(e.target.value)}
|
onChange={(e) => setDesktopModelQuery(e.target.value)}
|
||||||
onKeyDown={handleModelKeyDown}
|
onKeyDown={handleModelKeyDown}
|
||||||
className="pl-8 h-8 typography-meta"
|
className="pl-8 h-8 typography-meta"
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2718,8 +2763,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
}}
|
}}
|
||||||
className="typography-meta group flex items-center gap-1 rounded-md px-2 py-1.5 cursor-pointer hover:bg-interactive-hover/50"
|
className="typography-meta group flex items-center gap-1 rounded-md px-2 py-1.5 cursor-pointer hover:bg-interactive-hover/50"
|
||||||
>
|
>
|
||||||
<span className="flex h-4 w-4 items-center justify-center text-muted-foreground">
|
<span className="flex size-4 items-center justify-center text-muted-foreground">
|
||||||
<Icon name="add" className="h-4 w-4 -mr-0.5" />
|
<Icon name="add" className="size-4 -mr-0.5" />
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium text-foreground">{t('chat.modelControls.addNewProvider')}</span>
|
<span className="font-medium text-foreground">{t('chat.modelControls.addNewProvider')}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -2738,7 +2783,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<DropdownMenuLabel
|
<DropdownMenuLabel
|
||||||
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
|
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
|
||||||
>
|
>
|
||||||
<Icon name="star-fill" className="h-4 w-4 text-primary" />
|
<Icon name="star-fill" className="size-4 text-primary" />
|
||||||
{t('chat.modelControls.favorites')}
|
{t('chat.modelControls.favorites')}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{favoriteSortingEnabled ? (
|
{favoriteSortingEnabled ? (
|
||||||
@@ -2788,7 +2833,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<DropdownMenuLabel
|
<DropdownMenuLabel
|
||||||
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
|
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 border-b border-border/30"
|
||||||
>
|
>
|
||||||
<Icon name="time" className="h-4 w-4" />
|
<Icon name="time" className="size-4" />
|
||||||
{t('chat.modelControls.recent')}
|
{t('chat.modelControls.recent')}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{filteredRecents.map(({ model, providerID, modelID }) => {
|
{filteredRecents.map(({ model, providerID, modelID }) => {
|
||||||
@@ -2849,14 +2894,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<ProviderLogo
|
<ProviderLogo
|
||||||
providerId={provider.id}
|
providerId={provider.id}
|
||||||
className="h-4 w-4 flex-shrink-0"
|
className="size-4 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 truncate">{provider.name}</span>
|
<span className="min-w-0 truncate">{provider.name}</span>
|
||||||
<span className="flex h-4 w-4 flex-shrink-0 items-center justify-center text-muted-foreground">
|
<span className="flex size-4 flex-shrink-0 items-center justify-center text-muted-foreground">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<Icon name="arrow-down-s" className="h-4 w-4" />
|
<Icon name="arrow-down-s" className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<Icon name="arrow-right-s" className="h-4 w-4" />
|
<Icon name="arrow-right-s" className="size-4" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -3023,7 +3068,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.edit')}</span>
|
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.edit')}</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{renderEditModeIcon(editPermissionSummary.mode, 'h-3.5 w-3.5')}
|
<EditModeIcon mode={editPermissionSummary.mode} className="size-3.5" />
|
||||||
<span className="typography-meta font-medium text-foreground w-12">
|
<span className="typography-meta font-medium text-foreground w-12">
|
||||||
{editPermissionSummary.label}
|
{editPermissionSummary.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -3032,7 +3077,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.bash')}</span>
|
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.bash')}</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{renderEditModeIcon(bashPermissionSummary.mode, 'h-3.5 w-3.5')}
|
<EditModeIcon mode={bashPermissionSummary.mode} className="size-3.5" />
|
||||||
<span className="typography-meta font-medium text-foreground w-12">
|
<span className="typography-meta font-medium text-foreground w-12">
|
||||||
{bashPermissionSummary.label}
|
{bashPermissionSummary.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -3041,7 +3086,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.webFetch')}</span>
|
<span className="typography-meta text-muted-foreground/80 w-16">{t('chat.modelControls.webFetch')}</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{renderEditModeIcon(webfetchPermissionSummary.mode, 'h-3.5 w-3.5')}
|
<EditModeIcon mode={webfetchPermissionSummary.mode} className="size-3.5" />
|
||||||
<span className="typography-meta font-medium text-foreground w-12">
|
<span className="typography-meta font-medium text-foreground w-12">
|
||||||
{webfetchPermissionSummary.label}
|
{webfetchPermissionSummary.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -3052,7 +3097,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{hasCustomPrompt && (
|
{hasCustomPrompt && (
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
|
<span className="typography-meta text-muted-foreground/80">{t('chat.modelControls.customPrompt')}</span>
|
||||||
<Icon name="checkbox-circle" className="h-4 w-4 text-foreground" />
|
<Icon name="checkbox-circle" className="size-4 text-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -3125,7 +3170,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<DropdownMenuItem className="typography-meta" onSelect={() => handleVariantSelect(undefined)}>
|
<DropdownMenuItem className="typography-meta" onSelect={() => handleVariantSelect(undefined)}>
|
||||||
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
||||||
<span className="typography-meta font-medium text-foreground truncate min-w-0">{t('chat.modelControls.default')}</span>
|
<span className="typography-meta font-medium text-foreground truncate min-w-0">{t('chat.modelControls.default')}</span>
|
||||||
{isDefault && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />}
|
{isDefault && <Icon name="check" className="size-4 text-primary flex-shrink-0" />}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{availableVariants.length > 0 && <DropdownMenuSeparator />}
|
{availableVariants.length > 0 && <DropdownMenuSeparator />}
|
||||||
@@ -3140,7 +3185,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
||||||
<span className="typography-meta font-medium text-foreground truncate min-w-0">{label}</span>
|
<span className="typography-meta font-medium text-foreground truncate min-w-0">{label}</span>
|
||||||
{selected && <Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />}
|
{selected && <Icon name="check" className="size-4 text-primary flex-shrink-0" />}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
@@ -3213,7 +3258,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(280px,calc(100vw-2rem))] p-0 flex flex-col">
|
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(280px,calc(100vw-2rem))] p-0 flex flex-col">
|
||||||
<div className="p-2 border-b border-border/40">
|
<div className="p-2 border-b border-border/40">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('chat.modelControls.searchAgents')}
|
placeholder={t('chat.modelControls.searchAgents')}
|
||||||
@@ -3223,7 +3268,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="pl-8 h-8 typography-meta"
|
className="pl-8 h-8 typography-meta"
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3236,7 +3280,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
onSelect={() => handleAgentChange(defaultAgentName)}
|
onSelect={() => handleAgentChange(defaultAgentName)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Icon name="arrow-go-back" className="h-3.5 w-3.5 text-muted-foreground" />
|
<Icon name="arrow-go-back" className="size-3.5 text-muted-foreground" />
|
||||||
<span className="font-medium">{t('chat.modelControls.resetToDefault')}</span>
|
<span className="font-medium">{t('chat.modelControls.resetToDefault')}</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
Reference in New Issue
Block a user