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:
parent
48565664e0
commit
9735d73b83
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { BotIcon, PlusSquare } from "lucide-react";
|
import { BotIcon, PlusSquare } from "lucide-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -11,7 +11,11 @@ import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
||||||
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
|
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
|
||||||
import { ExportTrigger } from "@/components/workspace/export-trigger";
|
import { ExportTrigger } from "@/components/workspace/export-trigger";
|
||||||
import { InputBox } from "@/components/workspace/input-box";
|
import { InputBox } from "@/components/workspace/input-box";
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
import {
|
||||||
|
MessageList,
|
||||||
|
MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
|
||||||
|
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM,
|
||||||
|
} from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||||
import { TodoList } from "@/components/workspace/todo-list";
|
import { TodoList } from "@/components/workspace/todo-list";
|
||||||
|
|
@ -28,6 +32,7 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function AgentChatPage() {
|
export default function AgentChatPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const [showFollowups, setShowFollowups] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { agent_name } = useParams<{
|
const { agent_name } = useParams<{
|
||||||
|
|
@ -81,6 +86,11 @@ export default function AgentChatPage() {
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [thread]);
|
}, [thread]);
|
||||||
|
|
||||||
|
const messageListPaddingBottom = showFollowups
|
||||||
|
? MESSAGE_LIST_DEFAULT_PADDING_BOTTOM +
|
||||||
|
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread }}>
|
<ThreadContext.Provider value={{ thread }}>
|
||||||
<ChatBox threadId={threadId}>
|
<ChatBox threadId={threadId}>
|
||||||
|
|
@ -128,6 +138,7 @@ export default function AgentChatPage() {
|
||||||
className={cn("size-full", !isNewThread && "pt-10")}
|
className={cn("size-full", !isNewThread && "pt-10")}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
thread={thread}
|
thread={thread}
|
||||||
|
paddingBottom={messageListPaddingBottom}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -173,6 +184,7 @@ export default function AgentChatPage() {
|
||||||
}
|
}
|
||||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
|
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
|
||||||
onContextChange={(context) => setSettings("context", context)}
|
onContextChange={(context) => setSettings("context", context)}
|
||||||
|
onFollowupsVisibilityChange={setShowFollowups}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
||||||
|
|
@ -11,7 +11,11 @@ import {
|
||||||
} from "@/components/workspace/chats";
|
} from "@/components/workspace/chats";
|
||||||
import { ExportTrigger } from "@/components/workspace/export-trigger";
|
import { ExportTrigger } from "@/components/workspace/export-trigger";
|
||||||
import { InputBox } from "@/components/workspace/input-box";
|
import { InputBox } from "@/components/workspace/input-box";
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
import {
|
||||||
|
MessageList,
|
||||||
|
MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
|
||||||
|
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM,
|
||||||
|
} from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||||
import { TodoList } from "@/components/workspace/todo-list";
|
import { TodoList } from "@/components/workspace/todo-list";
|
||||||
|
|
@ -27,6 +31,7 @@ import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const [showFollowups, setShowFollowups] = useState(false);
|
||||||
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
||||||
const [settings, setSettings] = useThreadSettings(threadId);
|
const [settings, setSettings] = useThreadSettings(threadId);
|
||||||
useSpecificChatMode();
|
useSpecificChatMode();
|
||||||
|
|
@ -70,6 +75,11 @@ export default function ChatPage() {
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [thread]);
|
}, [thread]);
|
||||||
|
|
||||||
|
const messageListPaddingBottom = showFollowups
|
||||||
|
? MESSAGE_LIST_DEFAULT_PADDING_BOTTOM +
|
||||||
|
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread, isMock }}>
|
<ThreadContext.Provider value={{ thread, isMock }}>
|
||||||
<ChatBox threadId={threadId}>
|
<ChatBox threadId={threadId}>
|
||||||
|
|
@ -97,6 +107,7 @@ export default function ChatPage() {
|
||||||
className={cn("size-full", !isNewThread && "pt-10")}
|
className={cn("size-full", !isNewThread && "pt-10")}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
thread={thread}
|
thread={thread}
|
||||||
|
paddingBottom={messageListPaddingBottom}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
||||||
|
|
@ -141,6 +152,7 @@ export default function ChatPage() {
|
||||||
isUploading
|
isUploading
|
||||||
}
|
}
|
||||||
onContextChange={(context) => setSettings("context", context)}
|
onContextChange={(context) => setSettings("context", context)}
|
||||||
|
onFollowupsVisibilityChange={setShowFollowups}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ export function InputBox({
|
||||||
threadId,
|
threadId,
|
||||||
initialValue,
|
initialValue,
|
||||||
onContextChange,
|
onContextChange,
|
||||||
|
onFollowupsVisibilityChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onStop,
|
onStop,
|
||||||
...props
|
...props
|
||||||
|
|
@ -136,6 +137,7 @@ export function InputBox({
|
||||||
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
||||||
},
|
},
|
||||||
) => void;
|
) => void;
|
||||||
|
onFollowupsVisibilityChange?: (visible: boolean) => void;
|
||||||
onSubmit?: (message: PromptInputMessage) => void;
|
onSubmit?: (message: PromptInputMessage) => void;
|
||||||
onStop?: () => void;
|
onStop?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -186,6 +188,8 @@ export function InputBox({
|
||||||
return models.find((m) => m.name === context.model_name) ?? models[0];
|
return models.find((m) => m.name === context.model_name) ?? models[0];
|
||||||
}, [context.model_name, models]);
|
}, [context.model_name, models]);
|
||||||
|
|
||||||
|
const resolvedModelName = selectedModel?.name;
|
||||||
|
|
||||||
const supportThinking = useMemo(
|
const supportThinking = useMemo(
|
||||||
() => selectedModel?.supports_thinking ?? false,
|
() => selectedModel?.supports_thinking ?? false,
|
||||||
[selectedModel],
|
[selectedModel],
|
||||||
|
|
@ -253,9 +257,33 @@ export function InputBox({
|
||||||
setFollowups([]);
|
setFollowups([]);
|
||||||
setFollowupsHidden(false);
|
setFollowupsHidden(false);
|
||||||
setFollowupsLoading(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?.(message);
|
||||||
},
|
},
|
||||||
[onSubmit, onStop, status],
|
[
|
||||||
|
context,
|
||||||
|
onContextChange,
|
||||||
|
onSubmit,
|
||||||
|
onStop,
|
||||||
|
resolvedModelName,
|
||||||
|
selectedModel?.supports_thinking,
|
||||||
|
status,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestFormSubmit = useCallback(() => {
|
const requestFormSubmit = useCallback(() => {
|
||||||
|
|
@ -309,6 +337,26 @@ export function InputBox({
|
||||||
setTimeout(() => requestFormSubmit(), 0);
|
setTimeout(() => requestFormSubmit(), 0);
|
||||||
}, [pendingSuggestion, requestFormSubmit, textInput]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const streaming = status === "streaming";
|
const streaming = status === "streaming";
|
||||||
const wasStreaming = wasStreamingRef.current;
|
const wasStreaming = wasStreamingRef.current;
|
||||||
|
|
@ -769,40 +817,37 @@ export function InputBox({
|
||||||
)}
|
)}
|
||||||
</PromptInput>
|
</PromptInput>
|
||||||
|
|
||||||
{!disabled &&
|
{showFollowups && (
|
||||||
!isNewThread &&
|
<div className="absolute -top-20 right-0 left-0 z-20 flex items-center justify-center">
|
||||||
!followupsHidden &&
|
<div className="flex items-center gap-2">
|
||||||
(followupsLoading || followups.length > 0) && (
|
{followupsLoading ? (
|
||||||
<div className="absolute -top-20 right-0 left-0 z-20 flex items-center justify-center">
|
<div className="text-muted-foreground bg-background/80 rounded-full border px-4 py-2 text-xs backdrop-blur-sm">
|
||||||
<div className="flex items-center gap-2">
|
{t.inputBox.followupLoading}
|
||||||
{followupsLoading ? (
|
</div>
|
||||||
<div className="text-muted-foreground bg-background/80 rounded-full border px-4 py-2 text-xs backdrop-blur-sm">
|
) : (
|
||||||
{t.inputBox.followupLoading}
|
<Suggestions className="min-h-16 w-fit items-start">
|
||||||
</div>
|
{followups.map((s) => (
|
||||||
) : (
|
<Suggestion
|
||||||
<Suggestions className="min-h-16 w-fit items-start">
|
key={s}
|
||||||
{followups.map((s) => (
|
suggestion={s}
|
||||||
<Suggestion
|
onClick={() => handleFollowupClick(s)}
|
||||||
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"
|
||||||
<Button
|
variant="outline"
|
||||||
aria-label={t.common.close}
|
size="sm"
|
||||||
className="text-muted-foreground cursor-pointer rounded-full px-3 text-xs font-normal"
|
type="button"
|
||||||
variant="outline"
|
onClick={() => setFollowupsHidden(true)}
|
||||||
size="sm"
|
>
|
||||||
type="button"
|
<XIcon className="size-4" />
|
||||||
onClick={() => setFollowupsHidden(true)}
|
</Button>
|
||||||
>
|
</Suggestions>
|
||||||
<XIcon className="size-4" />
|
)}
|
||||||
</Button>
|
|
||||||
</Suggestions>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,14 @@ import { MessageListItem } from "./message-list-item";
|
||||||
import { MessageListSkeleton } from "./skeleton";
|
import { MessageListSkeleton } from "./skeleton";
|
||||||
import { SubtaskCard } from "./subtask-card";
|
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({
|
export function MessageList({
|
||||||
className,
|
className,
|
||||||
threadId,
|
threadId,
|
||||||
thread,
|
thread,
|
||||||
paddingBottom = 160,
|
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue