fix(ui): avoid follow-up suggestion overlap (#1777)

* fix(ui): avoid follow-up suggestion overlap

* fix(ui): address followup review feedback

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Admire
2026-04-03 15:48:41 +08:00
committed by GitHub
parent 48565664e0
commit 9735d73b83
4 changed files with 111 additions and 39 deletions
+79 -34
View File
@@ -109,6 +109,7 @@ export function InputBox({
threadId,
initialValue,
onContextChange,
onFollowupsVisibilityChange,
onSubmit,
onStop,
...props
@@ -136,6 +137,7 @@ export function InputBox({
reasoning_effort?: "minimal" | "low" | "medium" | "high";
},
) => void;
onFollowupsVisibilityChange?: (visible: boolean) => void;
onSubmit?: (message: PromptInputMessage) => void;
onStop?: () => void;
}) {
@@ -186,6 +188,8 @@ export function InputBox({
return models.find((m) => m.name === context.model_name) ?? models[0];
}, [context.model_name, models]);
const resolvedModelName = selectedModel?.name;
const supportThinking = useMemo(
() => selectedModel?.supports_thinking ?? false,
[selectedModel],
@@ -253,9 +257,33 @@ export function InputBox({
setFollowups([]);
setFollowupsHidden(false);
setFollowupsLoading(false);
// Guard against submitting before the initial model auto-selection
// effect has flushed thread settings to storage/state.
if (resolvedModelName && context.model_name !== resolvedModelName) {
onContextChange?.({
...context,
model_name: resolvedModelName,
mode: getResolvedMode(
context.mode,
selectedModel?.supports_thinking ?? false,
),
});
setTimeout(() => onSubmit?.(message), 0);
return;
}
onSubmit?.(message);
},
[onSubmit, onStop, status],
[
context,
onContextChange,
onSubmit,
onStop,
resolvedModelName,
selectedModel?.supports_thinking,
status,
],
);
const requestFormSubmit = useCallback(() => {
@@ -309,6 +337,26 @@ export function InputBox({
setTimeout(() => requestFormSubmit(), 0);
}, [pendingSuggestion, requestFormSubmit, textInput]);
const showFollowups =
!disabled &&
!isNewThread &&
!followupsHidden &&
(followupsLoading || followups.length > 0);
const followupsVisibilityChangeRef = useRef(onFollowupsVisibilityChange);
useEffect(() => {
followupsVisibilityChangeRef.current = onFollowupsVisibilityChange;
}, [onFollowupsVisibilityChange]);
useEffect(() => {
followupsVisibilityChangeRef.current?.(showFollowups);
}, [showFollowups]);
useEffect(() => {
return () => followupsVisibilityChangeRef.current?.(false);
}, []);
useEffect(() => {
const streaming = status === "streaming";
const wasStreaming = wasStreamingRef.current;
@@ -769,40 +817,37 @@ export function InputBox({
)}
</PromptInput>
{!disabled &&
!isNewThread &&
!followupsHidden &&
(followupsLoading || followups.length > 0) && (
<div className="absolute -top-20 right-0 left-0 z-20 flex items-center justify-center">
<div className="flex items-center gap-2">
{followupsLoading ? (
<div className="text-muted-foreground bg-background/80 rounded-full border px-4 py-2 text-xs backdrop-blur-sm">
{t.inputBox.followupLoading}
</div>
) : (
<Suggestions className="min-h-16 w-fit items-start">
{followups.map((s) => (
<Suggestion
key={s}
suggestion={s}
onClick={() => handleFollowupClick(s)}
/>
))}
<Button
aria-label={t.common.close}
className="text-muted-foreground cursor-pointer rounded-full px-3 text-xs font-normal"
variant="outline"
size="sm"
type="button"
onClick={() => setFollowupsHidden(true)}
>
<XIcon className="size-4" />
</Button>
</Suggestions>
)}
</div>
{showFollowups && (
<div className="absolute -top-20 right-0 left-0 z-20 flex items-center justify-center">
<div className="flex items-center gap-2">
{followupsLoading ? (
<div className="text-muted-foreground bg-background/80 rounded-full border px-4 py-2 text-xs backdrop-blur-sm">
{t.inputBox.followupLoading}
</div>
) : (
<Suggestions className="min-h-16 w-fit items-start">
{followups.map((s) => (
<Suggestion
key={s}
suggestion={s}
onClick={() => handleFollowupClick(s)}
/>
))}
<Button
aria-label={t.common.close}
className="text-muted-foreground cursor-pointer rounded-full px-3 text-xs font-normal"
variant="outline"
size="sm"
type="button"
onClick={() => setFollowupsHidden(true)}
>
<XIcon className="size-4" />
</Button>
</Suggestions>
)}
</div>
)}
</div>
)}
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<DialogContent>
@@ -29,11 +29,14 @@ import { MessageListItem } from "./message-list-item";
import { MessageListSkeleton } from "./skeleton";
import { SubtaskCard } from "./subtask-card";
export const MESSAGE_LIST_DEFAULT_PADDING_BOTTOM = 160;
export const MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM = 80;
export function MessageList({
className,
threadId,
thread,
paddingBottom = 160,
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
}: {
className?: string;
threadId: string;