fix(webui): nudge model setup on blocked submit

This commit is contained in:
Xubin Ren
2026-09-02 12:21:53 +08:00
parent 38ba66d943
commit 0e8354fe6a
4 changed files with 52 additions and 3 deletions
@@ -90,6 +90,7 @@ interface ModelPresetBadgeProps {
provider?: string | null;
providerLabel?: string | null;
needsSetup?: boolean;
attentionRequest?: number;
fallbackModelName?: string | null;
isHero: boolean;
onClick?: () => void;
@@ -106,6 +107,7 @@ export function ModelPresetBadge({
provider,
providerLabel,
needsSetup = false,
attentionRequest = 0,
fallbackModelName,
isHero,
onClick,
@@ -272,11 +274,13 @@ export function ModelPresetBadge({
const pill = (
<PresetPill
key={needsSetup ? attentionRequest : undefined}
label={displayLabel}
modelDetail={displayModelDetail}
provider={displayProvider}
providerLabel={fallbackModelName ? null : providerLabel}
needsSetup={needsSetup}
needsAttention={needsSetup && attentionRequest > 0}
fallbackModelName={fallbackModelName}
fallbackFromLabel={fallbackModelName ? label : null}
isHero={isHero}
@@ -493,6 +497,7 @@ function PresetPill({
provider,
providerLabel,
needsSetup = false,
needsAttention = false,
fallbackModelName,
fallbackFromLabel,
isHero,
@@ -504,6 +509,7 @@ function PresetPill({
provider?: string | null;
providerLabel?: string | null;
needsSetup?: boolean;
needsAttention?: boolean;
fallbackModelName?: string | null;
fallbackFromLabel?: string | null;
isHero: boolean;
@@ -542,6 +548,7 @@ function PresetPill({
"transition-[color,background-color,border-color,transform] duration-150 ease-out group-focus-visible:ring-2 group-focus-visible:ring-ring/45",
isHero ? "gap-1.5 px-2.5 text-[12px]" : "gap-2 px-3 text-[12.5px]",
needsSetup && "composer-model-pill-setup",
needsAttention && "composer-model-pill-setup-attention",
offset !== undefined && "composer-model-pill-dock",
)}
style={scale === undefined ? undefined : {
@@ -1036,6 +1036,7 @@ export function ThreadComposer({
} | null>(null);
const [inlineError, setInlineError] = useState<string | null>(null);
const [sendPending, setSendPending] = useState(false);
const [modelSetupAttentionRequest, setModelSetupAttentionRequest] = useState(0);
const interactionDisabled = !!disabled || sendPending;
const [voiceErrorFading, setVoiceErrorFading] = useState(false);
const [slashMenuDismissed, setSlashMenuDismissed] = useState(false);
@@ -2008,7 +2009,9 @@ export function ThreadComposer({
const submit = useCallback(() => {
if (modelNeedsSetup) {
onModelBadgeClick?.();
if (hasComposerContent) {
setModelSetupAttentionRequest((request) => request + 1);
}
return;
}
if (!canSend) return;
@@ -2112,11 +2115,11 @@ export function ThreadComposer({
clear,
clearComposerText,
hasTouchPrimaryPointer,
hasComposerContent,
handleStop,
isStreaming,
maxTextBytes,
modelNeedsSetup,
onModelBadgeClick,
onSend,
onStop,
onQuotedContextChange,
@@ -2546,6 +2549,7 @@ export function ThreadComposer({
provider={modelProvider}
providerLabel={modelProviderLabel}
needsSetup={modelNeedsSetup}
attentionRequest={modelSetupAttentionRequest}
fallbackModelName={fallbackModelName}
isHero={isHero}
onClick={modelNeedsSetup ? onModelBadgeClick : undefined}
+31
View File
@@ -765,6 +765,33 @@
0 2px 5px rgb(0 0 0 / 0.16);
}
@keyframes composer-model-pill-setup-attention {
0%,
100% {
transform: translateX(0);
}
22% {
transform: translateX(-3px);
}
46% {
transform: translateX(3px);
}
68% {
transform: translateX(-2px);
}
86% {
transform: translateX(1px);
}
}
.composer-model-pill-setup-attention {
animation: composer-model-pill-setup-attention 280ms cubic-bezier(0.22, 0.8, 0.32, 1);
}
@keyframes composer-model-pill-viewport-enter {
from {
transform: scale(0.9074);
@@ -817,6 +844,10 @@
transition: none;
}
.composer-model-pill-setup-attention {
animation: none;
}
.composer-model-pill-track[data-settling="true"],
.composer-model-pill-dock {
transition: none;
+8 -1
View File
@@ -895,11 +895,18 @@ describe("ThreadShell", () => {
expect(client.sendMessage).not.toHaveBeenCalled();
onOpenModelSettings.mockClear();
const firstSetupPill = badge.querySelector('[data-needs-setup="true"]');
fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
expect(onOpenModelSettings).toHaveBeenCalledTimes(1);
const secondSetupPill = badge.querySelector('[data-needs-setup="true"]');
expect(onOpenModelSettings).not.toHaveBeenCalled();
expect(secondSetupPill).not.toBe(firstSetupPill);
expect(secondSetupPill).toHaveClass("composer-model-pill-setup-attention");
expect(input).toHaveValue("hello");
expect(client.sendMessage).not.toHaveBeenCalled();
fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
expect(badge.querySelector('[data-needs-setup="true"]')).not.toBe(secondSetupPill);
});
it("keeps image generation controls out of the composer", async () => {