Studio: refine tool call and reasoning trigger UI (#5873)
* Studio: refine tool call and reasoning trigger UI Tool call triggers: - Chevron fades in on hover or keyboard focus and sits next to the label instead of being pinned to the right edge, matching the other collapsible triggers. - Labels wrap instead of truncating so long tool names and search queries stay fully readable. - Smaller chevron for a lighter look. Reasoning trigger: - Smaller chevron to match. - Thinking box drops its bottom padding and raises the streaming max height so more of the thinking text is visible. * Studio: pointer cursors and sidebar 3-dots polish Collapsible triggers: - Pointer cursor on the reasoning, tool call, and tool group triggers so they read as clickable. Chat sidebar: - Swap the chat row 3-dots menu to the vertical more-vertical icon. - Pointer cursor on the chat row and its menu button. - Chat row right padding opens up on hover (pr-4 at rest, pr-8 on hover) so the title keeps a comfortable gap and clears the menu. * studio: refine tool-call spinner, chevron, and reasoning spacing - Use the lucide arc spinner for running tool calls and the app-wide Spinner, so loading states match the rest of the UI. - Collapse long tool-call labels to a single line with an ellipsis, reveal the full label when the row is expanded, and fix the clipped descenders. - Keep the collapse chevron next to the label and add top spacing above the reasoning trigger. - Remove the redundant nested spinner in the web search running state. * Studio: drop tool call group background fill The ghost tool call group used a translucent bg-muted/10 fill that read as a faint lighter box around every group in dark mode. Remove the fill and rounding so the group sits flush on the chat background. --------- Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
parent
2b51bec946
commit
9f1d029c18
10 changed files with 89 additions and 41 deletions
|
|
@ -33,7 +33,7 @@ import {
|
|||
const ANIMATION_DURATION = 200;
|
||||
const AUTO_SCROLL_THRESHOLD_PX = 24;
|
||||
|
||||
export const reasoningVariants = cva("aui-reasoning-root mb-4 w-full", {
|
||||
export const reasoningVariants = cva("aui-reasoning-root mt-3 mb-4 w-full", {
|
||||
variants: {
|
||||
variant: {
|
||||
outline: "rounded-lg border px-3 py-2",
|
||||
|
|
@ -121,7 +121,7 @@ function ReasoningTrigger({
|
|||
<CollapsibleTrigger
|
||||
data-slot="reasoning-trigger"
|
||||
className={cn(
|
||||
"aui-reasoning-trigger group/trigger flex min-w-0 flex-1 items-center gap-2 py-1 text-muted-foreground text-sm transition-colors hover:text-foreground",
|
||||
"aui-reasoning-trigger group/trigger flex min-w-0 flex-1 cursor-pointer items-center gap-2 py-1 text-muted-foreground text-sm transition-colors hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -140,7 +140,7 @@ function ReasoningTrigger({
|
|||
<ChevronDownIcon
|
||||
data-slot="reasoning-trigger-chevron"
|
||||
className={cn(
|
||||
"aui-reasoning-trigger-chevron mt-0.5 size-4 shrink-0",
|
||||
"aui-reasoning-trigger-chevron mt-0.5 size-3.5 shrink-0",
|
||||
"transition-transform duration-(--animation-duration) ease-out",
|
||||
"group-data-[state=closed]/trigger:-rotate-90",
|
||||
"group-data-[state=open]/trigger:rotate-0",
|
||||
|
|
@ -241,8 +241,8 @@ function ReasoningText({
|
|||
ref={scrollRef}
|
||||
data-slot="reasoning-text"
|
||||
className={cn(
|
||||
"aui-reasoning-text relative z-0 overflow-y-auto pt-2 pb-2 pl-0 leading-relaxed",
|
||||
streaming ? "max-h-32" : "max-h-64",
|
||||
"aui-reasoning-text relative z-0 overflow-y-auto pt-2 pb-0 pl-0 leading-relaxed",
|
||||
streaming ? "max-h-64" : "",
|
||||
"transform-gpu transition-[transform,opacity]",
|
||||
"group-data-[state=open]/collapsible-content:animate-in",
|
||||
"group-data-[state=closed]/collapsible-content:animate-out",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"use client";
|
||||
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Spinner shown while a tool call is running: a clean circular arc with a
|
||||
* rounded cap (lucide Loader2 / LoaderCircle), animated. Inherits the current
|
||||
* text color so it matches the surrounding font.
|
||||
*/
|
||||
export function ToolCallSpinner({ className }: { className?: string }) {
|
||||
return (
|
||||
<Loader2Icon
|
||||
aria-label="Loading"
|
||||
className={cn("size-4 shrink-0 animate-spin", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
|
||||
import { useCollapseScrollLock } from "@/hooks/use-collapse-scroll-lock";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
|
|
@ -135,16 +136,13 @@ function ToolFallbackTrigger({
|
|||
<CollapsibleTrigger
|
||||
data-slot="tool-fallback-trigger"
|
||||
className={cn(
|
||||
"aui-tool-fallback-trigger group/trigger flex w-full items-center gap-2 py-1.5 text-sm transition-colors",
|
||||
"aui-tool-fallback-trigger group/trigger flex w-full cursor-pointer items-center gap-2 py-1.5 text-sm transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{isRunning ? (
|
||||
<StatusIcon
|
||||
data-slot="tool-fallback-trigger-icon"
|
||||
className="aui-tool-fallback-trigger-icon size-4 shrink-0 animate-spin"
|
||||
/>
|
||||
<ToolCallSpinner className="aui-tool-fallback-trigger-icon" />
|
||||
) : ToolIcon ? (
|
||||
<ToolIcon
|
||||
data-slot="tool-fallback-trigger-icon"
|
||||
|
|
@ -165,11 +163,16 @@ function ToolFallbackTrigger({
|
|||
<span
|
||||
data-slot="tool-fallback-trigger-label"
|
||||
className={cn(
|
||||
"aui-tool-fallback-trigger-label-wrapper relative min-w-0 grow text-left leading-none text-muted-foreground",
|
||||
"aui-tool-fallback-trigger-label-wrapper relative min-w-0 text-left leading-none text-muted-foreground",
|
||||
isCancelled && "text-muted-foreground line-through",
|
||||
)}
|
||||
>
|
||||
<span className="block truncate">
|
||||
<span
|
||||
className={cn(
|
||||
"block truncate leading-normal",
|
||||
"group-data-[state=open]/trigger:overflow-visible group-data-[state=open]/trigger:whitespace-normal group-data-[state=open]/trigger:break-words",
|
||||
)}
|
||||
>
|
||||
{label}:{" "}
|
||||
<span className="font-medium text-foreground/85">{displayName}</span>
|
||||
</span>
|
||||
|
|
@ -177,7 +180,10 @@ function ToolFallbackTrigger({
|
|||
<span
|
||||
aria-hidden={true}
|
||||
data-slot="tool-fallback-trigger-shimmer"
|
||||
className="aui-tool-fallback-trigger-shimmer shimmer pointer-events-none absolute inset-0 block truncate motion-reduce:animate-none"
|
||||
className={cn(
|
||||
"aui-tool-fallback-trigger-shimmer shimmer pointer-events-none absolute inset-0 block truncate leading-normal motion-reduce:animate-none",
|
||||
"group-data-[state=open]/trigger:overflow-visible group-data-[state=open]/trigger:whitespace-normal group-data-[state=open]/trigger:break-words",
|
||||
)}
|
||||
>
|
||||
{label}:{" "}
|
||||
<span className="font-medium text-foreground/85">{displayName}</span>
|
||||
|
|
@ -187,8 +193,8 @@ function ToolFallbackTrigger({
|
|||
<ChevronDownIcon
|
||||
data-slot="tool-fallback-trigger-chevron"
|
||||
className={cn(
|
||||
"aui-tool-fallback-trigger-chevron size-4 shrink-0",
|
||||
"transition-transform duration-(--animation-duration) ease-out",
|
||||
"aui-tool-fallback-trigger-chevron mr-1 size-3.5 shrink-0 self-center",
|
||||
"transition-[transform,opacity] duration-(--animation-duration) ease-out",
|
||||
"group-data-[state=closed]/trigger:-rotate-90",
|
||||
"group-data-[state=open]/trigger:rotate-0",
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
type PropsWithChildren,
|
||||
} from "react";
|
||||
import { useAuiState } from "@assistant-ui/react";
|
||||
import { ChevronDownIcon, LoaderIcon } from "lucide-react";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { Wrench01Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from "@/components/ui/collapsible";
|
||||
import { useCollapseScrollLock } from "@/hooks/use-collapse-scroll-lock";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
|
||||
|
||||
const ANIMATION_DURATION = 200;
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ const toolGroupVariants = cva("aui-tool-group-root group/tool-group w-full", {
|
|||
variants: {
|
||||
variant: {
|
||||
outline: "corner-squircle rounded-lg border py-3",
|
||||
ghost: "rounded-lg bg-muted/10 py-2",
|
||||
ghost: "py-2",
|
||||
muted:
|
||||
"corner-squircle rounded-lg border border-muted-foreground/30 bg-muted/30 py-3",
|
||||
},
|
||||
|
|
@ -113,7 +114,7 @@ function ToolGroupTrigger({
|
|||
<CollapsibleTrigger
|
||||
data-slot="tool-group-trigger"
|
||||
className={cn(
|
||||
"aui-tool-group-trigger group/trigger flex w-full items-center gap-2 text-sm transition-colors",
|
||||
"aui-tool-group-trigger group/trigger flex w-full cursor-pointer items-center gap-2 text-sm transition-colors",
|
||||
"group-data-[variant=outline]/tool-group-root:px-4",
|
||||
"group-data-[variant=muted]/tool-group-root:px-4",
|
||||
"group-data-[variant=ghost]/tool-group-root:px-0",
|
||||
|
|
@ -122,10 +123,7 @@ function ToolGroupTrigger({
|
|||
{...props}
|
||||
>
|
||||
{active ? (
|
||||
<LoaderIcon
|
||||
data-slot="tool-group-trigger-loader"
|
||||
className="aui-tool-group-trigger-loader size-4 shrink-0 animate-spin"
|
||||
/>
|
||||
<ToolCallSpinner className="aui-tool-group-trigger-loader" />
|
||||
) : (
|
||||
<HugeiconsIcon
|
||||
icon={Wrench01Icon}
|
||||
|
|
@ -137,7 +135,7 @@ function ToolGroupTrigger({
|
|||
<span
|
||||
data-slot="tool-group-trigger-label"
|
||||
className={cn(
|
||||
"aui-tool-group-trigger-label-wrapper relative inline-block grow text-left font-medium leading-none",
|
||||
"aui-tool-group-trigger-label-wrapper relative inline-block text-left font-medium leading-none",
|
||||
)}
|
||||
>
|
||||
<span>{label}</span>
|
||||
|
|
@ -154,7 +152,7 @@ function ToolGroupTrigger({
|
|||
<ChevronDownIcon
|
||||
data-slot="tool-group-trigger-chevron"
|
||||
className={cn(
|
||||
"aui-tool-group-trigger-chevron size-4 shrink-0",
|
||||
"aui-tool-group-trigger-chevron size-3.5 shrink-0",
|
||||
"transition-transform duration-(--animation-duration) ease-out",
|
||||
"group-data-[state=closed]/trigger:-rotate-90",
|
||||
"group-data-[state=open]/trigger:rotate-0",
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import {
|
|||
CheckIcon,
|
||||
CopyIcon,
|
||||
FileTextIcon,
|
||||
LoaderIcon,
|
||||
TerminalIcon,
|
||||
} from "lucide-react";
|
||||
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ToolFallbackContent,
|
||||
|
|
@ -185,7 +185,7 @@ const CodeExecutionToolUIImpl: ToolCallMessagePartComponent = ({
|
|||
<ToolFallbackContent>
|
||||
{isRunning ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderIcon className="size-3.5 animate-spin" />
|
||||
<ToolCallSpinner className="size-3.5" />
|
||||
<span>{runningLabel}</span>
|
||||
</div>
|
||||
) : resultText ? (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard";
|
|||
import { getAuthToken } from "@/features/auth/session";
|
||||
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
||||
import { code as codePlugin } from "@streamdown/code";
|
||||
import { CheckIcon, CodeIcon, CopyIcon, LoaderIcon } from "lucide-react";
|
||||
import { CheckIcon, CodeIcon, CopyIcon } from "lucide-react";
|
||||
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
import {
|
||||
|
|
@ -148,7 +149,7 @@ const PythonToolUIImpl: ToolCallMessagePartComponent = ({
|
|||
{/* Output */}
|
||||
{isRunning ? (
|
||||
<div className="mt-2 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderIcon className="size-3.5 animate-spin" />
|
||||
<ToolCallSpinner className="size-3.5" />
|
||||
<span>Running…</span>
|
||||
</div>
|
||||
) : output ? (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
import { copyToClipboard } from "@/lib/copy-to-clipboard";
|
||||
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
||||
import { CheckIcon, CopyIcon, LoaderIcon, TerminalIcon } from "lucide-react";
|
||||
import { CheckIcon, CopyIcon, TerminalIcon } from "lucide-react";
|
||||
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
|
||||
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ToolFallbackContent,
|
||||
|
|
@ -86,7 +87,7 @@ const TerminalToolUIImpl: ToolCallMessagePartComponent = ({
|
|||
<div className="border-l-2 border-muted-foreground/20 pl-2">
|
||||
{isRunning ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderIcon className="size-3.5 animate-spin" />
|
||||
<ToolCallSpinner className="size-3.5" />
|
||||
<span>Running…</span>
|
||||
</div>
|
||||
) : output ? (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"use client";
|
||||
|
||||
import { type ToolCallMessagePartComponent, useAuiState } from "@assistant-ui/react";
|
||||
import { GlobeIcon, LoaderIcon } from "lucide-react";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { Source, SourceIcon, SourceTitle } from "./sources";
|
||||
import {
|
||||
|
|
@ -116,8 +116,7 @@ const WebSearchToolUIImpl: ToolCallMessagePartComponent = ({
|
|||
/>
|
||||
<ToolFallbackContent>
|
||||
{isRunning ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderIcon className="size-3.5 animate-spin" />
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<span>
|
||||
{isUrlFetch
|
||||
? <>Reading {displayDomain || "page"}…</>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
"use client";
|
||||
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* App-wide spinner: a clean circular arc with a rounded cap (lucide
|
||||
* Loader2 / LoaderCircle), animated, inheriting the current text color.
|
||||
*/
|
||||
function Spinner({ className }: { className?: string }) {
|
||||
return (
|
||||
<span
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cn(
|
||||
"inline-block size-4 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent",
|
||||
className,
|
||||
)}
|
||||
className={cn("size-4 shrink-0 animate-spin", className)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Spinner }
|
||||
export { Spinner };
|
||||
|
|
|
|||
|
|
@ -2012,3 +2012,21 @@
|
|||
transition-duration: 250ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Tool-call chevron visibility:
|
||||
* - A solo tool call (not wrapped in a multi-tool group) always shows its
|
||||
* collapsible chevron.
|
||||
* - When the tool call is part of a multi-tool group (.aui-tool-group-root),
|
||||
* the chevron stays hidden and only reveals on hover / keyboard focus.
|
||||
*/
|
||||
.aui-tool-fallback-trigger-chevron {
|
||||
opacity: 1;
|
||||
}
|
||||
.aui-tool-group-root .aui-tool-fallback-trigger-chevron {
|
||||
opacity: 0;
|
||||
}
|
||||
.aui-tool-group-root .aui-tool-fallback-trigger:hover .aui-tool-fallback-trigger-chevron,
|
||||
.aui-tool-group-root .aui-tool-fallback-trigger:focus-visible .aui-tool-fallback-trigger-chevron {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue