feat(webui): switch model presets from the composer (#5077)

This commit is contained in:
chengyongru
2026-07-24 17:34:26 +08:00
committed by GitHub
parent 9aab94c766
commit 5be176a6a0
16 changed files with 991 additions and 138 deletions
+17 -91
View File
@@ -60,6 +60,10 @@ import {
WorkspaceAccessMenu,
WorkspaceProjectPicker,
} from "@/components/thread/WorkspaceControls";
import {
ModelPresetBadge,
type ModelPresetOption,
} from "@/components/thread/ModelPresetBadge";
import {
ACCEPT_ATTR,
MAX_ATTACHMENTS_PER_MESSAGE,
@@ -87,9 +91,7 @@ import type {
WorkspacesPayload,
} from "@/lib/types";
import {
inferProviderFromModelName,
logoFallbackUrls,
providerBrand,
} from "@/lib/provider-brand";
import {
isSideChannelLifecycle,
@@ -168,6 +170,10 @@ interface ThreadComposerProps {
placeholder?: string;
isStreaming?: boolean;
modelLabel?: string | null;
modelDetail?: string | null;
modelPreset?: string | null;
modelPresets?: ModelPresetOption[];
onModelPresetChange?: (name: string) => void;
modelProvider?: string | null;
modelProviderLabel?: string | null;
modelNeedsSetup?: boolean;
@@ -814,6 +820,10 @@ export function ThreadComposer({
placeholder,
isStreaming = false,
modelLabel = null,
modelDetail = null,
modelPreset = null,
modelPresets = [],
onModelPresetChange,
modelProvider = null,
modelProviderLabel = null,
modelNeedsSetup = false,
@@ -2084,8 +2094,12 @@ export function ThreadComposer({
)}
>
{modelLabel && !voiceRecorder.isRecording ? (
<ComposerModelBadge
<ModelPresetBadge
label={modelLabel}
modelDetail={modelDetail}
modelPreset={modelPreset}
modelPresets={modelPresets}
onPresetChange={onModelPresetChange}
provider={modelProvider}
providerLabel={modelProviderLabel}
needsSetup={modelNeedsSetup}
@@ -2373,94 +2387,6 @@ function QueuedPromptRow({
);
}
function ComposerModelBadge({
label,
provider,
providerLabel,
needsSetup,
fallbackModelName,
isHero,
onClick,
}: {
label: string;
provider?: string | null;
providerLabel?: string | null;
needsSetup?: boolean;
fallbackModelName?: string | null;
isHero: boolean;
onClick?: () => void;
}) {
const inferredProvider = needsSetup ? null : provider || inferProviderFromModelName(label);
const brand = providerBrand(inferredProvider);
const { logoUrl, onLogoError, onLogoLoad } = useLogoFallback(brand?.logoUrls);
const showLogo = !!logoUrl;
const title = providerLabel ? `${label} · ${providerLabel}` : label;
const interactive = Boolean(onClick);
const Container = interactive ? "button" : "span";
return (
<Container
data-fallback={fallbackModelName ? "true" : undefined}
title={fallbackModelName || title}
aria-label={label}
type={interactive ? "button" : undefined}
onClick={onClick}
className={cn(
"composer-model-badge thread-composer-model-badge inline-flex min-w-0 items-center rounded-full border border-border/55 bg-card font-medium text-foreground/82",
"shadow-[0_2px_8px_rgba(15,23,42,0.045)]",
interactive && "cursor-pointer hover:bg-accent/55 hover:text-foreground",
needsSetup && "border-amber-500/35 bg-amber-50/70 text-amber-900 dark:bg-amber-500/10 dark:text-amber-200",
isHero
? "h-8 max-w-[min(12.5rem,44vw)] gap-1.5 px-2 text-[11.5px]"
: "h-9 max-w-[min(12rem,44vw)] gap-2 px-2.5 text-[12px]",
)}
>
<span
data-testid={needsSetup ? "composer-model-setup-icon" : inferredProvider ? `composer-model-logo-${inferredProvider}` : "composer-model-logo"}
className={cn(
"grid shrink-0 place-items-center overflow-hidden",
needsSetup
? "text-amber-800 dark:text-amber-200"
: "rounded-full border bg-background",
isHero ? "h-[18px] w-[18px]" : "h-5 w-5",
)}
style={{
borderColor: !needsSetup && brand ? `${brand.color}28` : undefined,
boxShadow: !needsSetup && brand ? `inset 0 0 0 1px ${brand.color}18` : undefined,
}}
aria-hidden
>
{needsSetup ? (
<CircleHelp className={cn(isHero ? "h-3 w-3" : "h-3.5 w-3.5")} strokeWidth={1.8} />
) : showLogo ? (
<img
src={logoUrl}
alt=""
decoding="async"
loading="lazy"
className={cn("object-contain", isHero ? "h-3 w-3" : "h-3.5 w-3.5")}
onLoad={onLogoLoad}
onError={onLogoError}
/>
) : brand ? (
<span
className={cn(
"grid h-full w-full place-items-center rounded-full text-white",
isHero ? "text-[7.5px]" : "text-[8px]",
)}
style={{ backgroundColor: brand.color }}
>
{brand.initials.slice(0, 2)}
</span>
) : (
<Sparkles className={cn("text-muted-foreground/65", isHero ? "h-3 w-3" : "h-3 w-3")} />
)}
</span>
<span className="thread-composer-model-label truncate">{label}</span>
</Container>
);
}
function ComposerCliMentionOverlay({
segments,
isHero,