mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-07 09:58:34 +00:00
fix(webui): tighten interactive motion
This commit is contained in:
parent
a95fd0ee82
commit
223b911e7e
@ -307,7 +307,7 @@ export function AgentActivityCluster({
|
||||
}
|
||||
if (!wasStreaming || userToggledOuter) return undefined;
|
||||
setCompletionHoldOpen(true);
|
||||
const timeout = window.setTimeout(() => setCompletionHoldOpen(false), 900);
|
||||
const timeout = window.setTimeout(() => setCompletionHoldOpen(false), 300);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [isTurnStreaming, userToggledOuter]);
|
||||
|
||||
|
||||
@ -2226,7 +2226,7 @@ export function ThreadComposer({
|
||||
role="alert"
|
||||
className={cn(
|
||||
"mx-3 mb-1 max-h-10 overflow-hidden rounded-md border border-destructive/40 bg-destructive/8 px-2.5 py-1",
|
||||
"text-[11.5px] font-medium text-destructive transition-[max-height,margin,padding,opacity] duration-500 ease-out",
|
||||
"text-[11.5px] font-medium text-destructive transition-[max-height,margin,padding,opacity] [transition-duration:220ms] ease-out motion-reduce:transition-none",
|
||||
voiceErrorFading && "mb-0 max-h-0 border-transparent py-0 opacity-0",
|
||||
)}
|
||||
>
|
||||
|
||||
@ -1455,7 +1455,7 @@ export function ThreadShell({
|
||||
{t("thread.loadingConversation")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center text-center animate-in fade-in-0 slide-in-from-bottom-2 duration-500">
|
||||
<div className="flex w-full flex-col items-center text-center animate-in fade-in-0 slide-in-from-bottom-2 [animation-duration:220ms] motion-reduce:animate-none">
|
||||
<HeroGreeting text={t(heroGreetingKey)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -97,6 +97,11 @@ function isKeyboardEditableElement(element: Element | null): element is HTMLElem
|
||||
].includes(element.type);
|
||||
}
|
||||
|
||||
function isThreadDisclosureTarget(target: EventTarget | null): boolean {
|
||||
return target instanceof Element
|
||||
&& target.closest("[data-thread-disclosure]") !== null;
|
||||
}
|
||||
|
||||
type ThreadScrollDirection = "backward" | "forward";
|
||||
|
||||
const KEYBOARD_SCROLL_DIRECTIONS: Readonly<
|
||||
@ -572,7 +577,12 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
handleDirectionalInput(directionFromDelta(event.deltaY));
|
||||
};
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (event.button === 0 && event.target === el) yieldCameraToUser();
|
||||
if (
|
||||
event.button === 0
|
||||
&& (event.target === el || isThreadDisclosureTarget(event.target))
|
||||
) {
|
||||
yieldCameraToUser();
|
||||
}
|
||||
};
|
||||
let lastTouchY: number | null = null;
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
@ -600,6 +610,13 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(event.key === "Enter" || event.key === " ")
|
||||
&& isThreadDisclosureTarget(event.target)
|
||||
) {
|
||||
yieldCameraToUser();
|
||||
return;
|
||||
}
|
||||
handleDirectionalInput(keyboardScrollDirection(event));
|
||||
};
|
||||
el.addEventListener("scroll", handleScroll, { passive: true });
|
||||
|
||||
@ -41,7 +41,7 @@ function ReasoningMarker({ streaming }: { streaming: boolean }) {
|
||||
useEffect(() => {
|
||||
if (wasStreamingRef.current && !streaming) {
|
||||
setJustCompleted(true);
|
||||
const timeout = window.setTimeout(() => setJustCompleted(false), 650);
|
||||
const timeout = window.setTimeout(() => setJustCompleted(false), 300);
|
||||
wasStreamingRef.current = streaming;
|
||||
return () => window.clearTimeout(timeout);
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ export function ThinkingReasoningShell({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-thread-disclosure=""
|
||||
className="group inline-flex min-h-5 items-center self-start gap-1.5 bg-transparent p-0"
|
||||
onClick={onToggle}
|
||||
aria-expanded={expanded}
|
||||
@ -51,8 +52,8 @@ export function ThinkingReasoningShell({
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex shrink-0 transition-transform [transition-duration:600ms] ease-out",
|
||||
"motion-reduce:[transition-duration:220ms]",
|
||||
"inline-flex shrink-0 transition-transform [transition-duration:220ms] ease-out",
|
||||
"motion-reduce:transition-none",
|
||||
expanded && "rotate-180",
|
||||
)}
|
||||
>
|
||||
@ -69,7 +70,7 @@ export function ThinkingReasoningShell({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"grid transition-[grid-template-rows,opacity] [transition-duration:600ms] ease-out motion-reduce:[transition-duration:220ms]",
|
||||
"grid transition-[grid-template-rows,opacity] [transition-duration:220ms] ease-out motion-reduce:transition-none",
|
||||
expanded
|
||||
? "grid-rows-[1fr] opacity-100"
|
||||
: "pointer-events-none grid-rows-[0fr] opacity-0",
|
||||
|
||||
@ -142,7 +142,7 @@
|
||||
pointer-events: none;
|
||||
background-color: rgb(236 141 49);
|
||||
opacity: 0;
|
||||
transition: opacity 600ms ease-in-out;
|
||||
transition: opacity 220ms ease-out;
|
||||
}
|
||||
|
||||
.composer-model-badge[data-fallback="true"]::before {
|
||||
@ -352,8 +352,7 @@
|
||||
*/
|
||||
.thread-layout {
|
||||
grid-template-rows: minmax(min-content, 1fr) auto 0fr;
|
||||
transition:
|
||||
grid-template-rows 900ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||
transition: grid-template-rows 220ms ease-out;
|
||||
}
|
||||
.thread-layout[data-layout="thread"] {
|
||||
grid-template-rows: minmax(0, 1fr) auto 0fr;
|
||||
@ -404,7 +403,7 @@
|
||||
animation: composer-status-strip-exit 180ms ease-in both;
|
||||
}
|
||||
.composer-status-drawer {
|
||||
--composer-status-drawer-duration: 600ms;
|
||||
--composer-status-drawer-duration: 220ms;
|
||||
--composer-status-drawer-easing: ease-out;
|
||||
|
||||
display: grid;
|
||||
@ -501,14 +500,14 @@
|
||||
animation: none;
|
||||
}
|
||||
.composer-status-drawer {
|
||||
transition-duration: 220ms;
|
||||
transition-duration: 0.01ms;
|
||||
}
|
||||
.composer-status-drawer-content {
|
||||
transform: translateY(2px);
|
||||
transition-duration: 140ms, 220ms;
|
||||
transform: none;
|
||||
transition-duration: 120ms, 0.01ms;
|
||||
}
|
||||
.thread-layout {
|
||||
transition-duration: 360ms;
|
||||
transition-duration: 0.01ms;
|
||||
}
|
||||
.run-pulse-icon,
|
||||
.run-pulse-icon * {
|
||||
|
||||
@ -385,7 +385,7 @@ describe("AgentActivityCluster", () => {
|
||||
|
||||
expect(screen.getByTestId("agent-activity-scroll")).toBeInTheDocument();
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(901);
|
||||
vi.advanceTimersByTime(301);
|
||||
});
|
||||
expect(screen.queryByTestId("agent-activity-scroll")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Thought" })).toHaveAttribute(
|
||||
@ -413,12 +413,13 @@ describe("AgentActivityCluster", () => {
|
||||
);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Thought" });
|
||||
expect(button).toHaveAttribute("data-thread-disclosure");
|
||||
const chevron = button.querySelector("svg");
|
||||
expect(chevron).toBeInTheDocument();
|
||||
expect(chevron).toHaveClass("transition-colors", "duration-200");
|
||||
expect(chevron?.parentElement).toHaveClass(
|
||||
"transition-transform",
|
||||
"[transition-duration:600ms]",
|
||||
"[transition-duration:220ms]",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -215,6 +215,34 @@ function ViewportWithPromptNavigator({ messages }: { messages: UIMessage[] }) {
|
||||
}
|
||||
|
||||
describe("ThreadViewport", () => {
|
||||
it("keeps reasoning disclosure anchored for pointer and keyboard toggles", () => {
|
||||
const takeUserControl = vi.spyOn(
|
||||
ThreadMotionCoordinator.prototype,
|
||||
"takeUserControl",
|
||||
);
|
||||
render(
|
||||
<ThreadViewport
|
||||
messages={[{
|
||||
id: "reasoning-1",
|
||||
role: "assistant",
|
||||
content: "",
|
||||
reasoning: "A completed thought",
|
||||
createdAt: 1,
|
||||
}]}
|
||||
isStreaming={false}
|
||||
composer={<div>composer</div>}
|
||||
/>,
|
||||
);
|
||||
|
||||
const disclosure = screen.getByRole("button", { name: "Thought" });
|
||||
fireEvent.pointerDown(disclosure, { button: 0 });
|
||||
expect(takeUserControl).toHaveBeenCalledTimes(1);
|
||||
|
||||
takeUserControl.mockClear();
|
||||
fireEvent.keyDown(disclosure, { key: "Enter" });
|
||||
expect(takeUserControl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("top-aligns short threads in the message rendering area", () => {
|
||||
render(
|
||||
<ThreadViewport
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user