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