From 94d62f86a029531dc28c484b437dbcc13cdb3a65 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:21:53 +0800 Subject: [PATCH] fix(webui): nudge model setup on blocked submit --- .../components/thread/ModelPresetBadge.tsx | 7 +++++ .../src/components/thread/ThreadComposer.tsx | 8 +++-- webui/src/globals.css | 31 +++++++++++++++++++ webui/src/tests/thread-shell.test.tsx | 9 +++++- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/webui/src/components/thread/ModelPresetBadge.tsx b/webui/src/components/thread/ModelPresetBadge.tsx index de0dbc5bc..51c2492ae 100644 --- a/webui/src/components/thread/ModelPresetBadge.tsx +++ b/webui/src/components/thread/ModelPresetBadge.tsx @@ -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 = ( 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 : { diff --git a/webui/src/components/thread/ThreadComposer.tsx b/webui/src/components/thread/ThreadComposer.tsx index 3bd2b9ebd..71d1576d9 100644 --- a/webui/src/components/thread/ThreadComposer.tsx +++ b/webui/src/components/thread/ThreadComposer.tsx @@ -1036,6 +1036,7 @@ export function ThreadComposer({ } | null>(null); const [inlineError, setInlineError] = useState(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} diff --git a/webui/src/globals.css b/webui/src/globals.css index a83f98c0a..9c0eae90b 100644 --- a/webui/src/globals.css +++ b/webui/src/globals.css @@ -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; diff --git a/webui/src/tests/thread-shell.test.tsx b/webui/src/tests/thread-shell.test.tsx index e11099461..8868482e3 100644 --- a/webui/src/tests/thread-shell.test.tsx +++ b/webui/src/tests/thread-shell.test.tsx @@ -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 () => {