diff --git a/studio/frontend/src/components/assistant-ui/tool-group.tsx b/studio/frontend/src/components/assistant-ui/tool-group.tsx index e91da4cee2..47d2b6cd0e 100644 --- a/studio/frontend/src/components/assistant-ui/tool-group.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-group.tsx @@ -8,6 +8,7 @@ import { type FC, type PropsWithChildren, } from "react"; +import { useAuiState } from "@assistant-ui/react"; import { ChevronDownIcon, LoaderIcon } from "lucide-react"; import { Wrench01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; @@ -27,7 +28,8 @@ const toolGroupVariants = cva("aui-tool-group-root group/tool-group w-full", { variant: { outline: "corner-squircle rounded-lg border py-3", ghost: "rounded-lg bg-muted/10 py-2", - muted: "corner-squircle rounded-lg border border-muted-foreground/30 bg-muted/30 py-3", + muted: + "corner-squircle rounded-lg border border-muted-foreground/30 bg-muted/30 py-3", }, }, defaultVariants: { variant: "ghost" }, @@ -209,9 +211,17 @@ const ToolGroupImpl: FC< PropsWithChildren<{ startIndex: number; endIndex: number }> > = ({ children, startIndex, endIndex }) => { const toolCount = endIndex - startIndex + 1; + const containsArtifactTool = useAuiState(({ message }) => + message.parts + .slice(startIndex, endIndex + 1) + .some( + (part) => part.type === "tool-call" && part.toolName === "render_html", + ), + ); - // Single tool call — render directly without wrapper - if (toolCount <= 1) { + // Single tool calls and artifacts render directly so cards never hide inside + // a collapsed tool group. + if (toolCount <= 1 || containsArtifactTool) { return <>{children}; } diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-render-html.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-render-html.tsx index f8b9887b09..710bf88dda 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-render-html.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-render-html.tsx @@ -4,6 +4,8 @@ "use client"; import { ArtifactCard } from "@/features/chat"; +import { BrowserIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; import { type ToolCallMessagePartComponent, useToolArgsStatus, @@ -17,19 +19,8 @@ type RenderHtmlArgs = Record & { title?: string; }; -function formatToolResult(result: unknown): string { - if (typeof result === "string") return result; - if (result == null) return ""; - try { - return JSON.stringify(result, null, 2); - } catch { - return String(result); - } -} - const RenderHtmlToolUIImpl: ToolCallMessagePartComponent = ({ args, - result, status, toolCallId, }) => { @@ -39,7 +30,6 @@ const RenderHtmlToolUIImpl: ToolCallMessagePartComponent = ({ const hasCode = code.trim().length > 0; const title = typeof parsedArgs.title === "string" ? parsedArgs.title : "HTML artifact"; - const resultText = formatToolResult(result); const isRunning = status?.type === "running"; const codeIsStreaming = propStatus.code === "streaming"; @@ -57,26 +47,25 @@ const RenderHtmlToolUIImpl: ToolCallMessagePartComponent = ({ } return ( -
-
-
-

- Generating artifact… -

-

- {resultText || "Waiting for HTML source"} -

-
- +
+
+ + + + Generating artifact + + + HTML artifact + + + Generating
-
-
-
); }; diff --git a/studio/frontend/src/components/ui/resizable.tsx b/studio/frontend/src/components/ui/resizable.tsx index 519a1192da..de6309cae9 100644 --- a/studio/frontend/src/components/ui/resizable.tsx +++ b/studio/frontend/src/components/ui/resizable.tsx @@ -1,54 +1,54 @@ // 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 type * as React from "react"; -import * as ResizablePrimitive from "react-resizable-panels"; - -import { cn } from "@/lib/utils"; - -function ResizablePanelGroup({ - className, - ...props -}: React.ComponentProps): React.ReactElement { - return ( - - ); -} - -function ResizablePanel({ - ...props -}: React.ComponentProps): React.ReactElement { - return ; -} - -function ResizableHandle({ - withHandle, - className, - ...props -}: React.ComponentProps & { - withHandle?: boolean; -}): React.ReactElement { - return ( - div]:rotate-90", - className, - )} - {...props} - > - {withHandle && ( -
- )} - - ); -} - -export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; +import type * as React from "react"; +import * as ResizablePrimitive from "react-resizable-panels"; + +import { cn } from "@/lib/utils"; + +function ResizablePanelGroup({ + className, + ...props +}: React.ComponentProps): React.ReactElement { + return ( + + ); +} + +function ResizablePanel({ + ...props +}: React.ComponentProps): React.ReactElement { + return ; +} + +function ResizableHandle({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean; +}): React.ReactElement { + return ( + div]:rotate-90", + className, + )} + {...props} + > + {withHandle && ( +
+ )} + + ); +} + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/studio/frontend/src/features/chat/artifacts/artifact-card.tsx b/studio/frontend/src/features/chat/artifacts/artifact-card.tsx index 080fd8f050..a9122ac8bf 100644 --- a/studio/frontend/src/features/chat/artifacts/artifact-card.tsx +++ b/studio/frontend/src/features/chat/artifacts/artifact-card.tsx @@ -3,11 +3,11 @@ "use client"; -import { copyToClipboard } from "@/lib/copy-to-clipboard"; import { cn } from "@/lib/utils"; +import { BrowserIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; import { useAuiState } from "@assistant-ui/react"; -import { CheckIcon, CopyIcon, DownloadIcon } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo } from "react"; import { useChatRuntimeStore } from "../stores/chat-runtime-store"; import { hasAutoOpenedArtifact, @@ -18,49 +18,8 @@ import { type ChatArtifact, type ChatArtifactSource, createChatArtifact, - getArtifactFilename, } from "./types"; -const COPY_RESET_MS = 2000; - -function downloadTextFile(filename: string, text: string): void { - const blob = new Blob([text], { type: "text/html;charset=utf-8" }); - const url = URL.createObjectURL(blob); - const anchor = document.createElement("a"); - anchor.href = url; - anchor.download = filename; - document.body.appendChild(anchor); - anchor.click(); - document.body.removeChild(anchor); - window.setTimeout(() => URL.revokeObjectURL(url), 0); -} - -function useCopiedState() { - const [copied, setCopied] = useState(false); - const resetTimeoutRef = useRef | null>(null); - - useEffect(() => { - return () => { - if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current); - }; - }, []); - - const showCopied = () => { - setCopied(true); - if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current); - resetTimeoutRef.current = setTimeout(() => { - setCopied(false); - resetTimeoutRef.current = null; - }, COPY_RESET_MS); - }; - - return { copied, showCopied }; -} - -function artifactDisplayTitle(title: string): string { - return /artifact/i.test(title) ? title : `${title} Artifact`; -} - export function ArtifactCard({ code, title, @@ -91,7 +50,6 @@ export function ArtifactCard({ const selectedArtifactId = useChatArtifactsStore( (state) => state.selectedArtifactId, ); - const { copied, showCopied } = useCopiedState(); const artifact = useMemo( () => createChatArtifact({ @@ -114,10 +72,7 @@ export function ArtifactCard({ title, ], ); - const filename = getArtifactFilename(artifact); const surface = artifactThreadId ? "panel" : "overlay"; - const lineCount = artifact.code.split("\n").length; - const displayTitle = artifactDisplayTitle(artifact.title); useEffect(() => { if (!autoOpen) return; @@ -139,71 +94,36 @@ export function ArtifactCard({ ]); return ( -
openArtifact(artifact, { surface })} + aria-label={`Open ${artifact.title}`} > -
- -
- - -
+
+ + + + {artifact.title} + + + HTML artifact + + + {isStreaming ? ( + + Generating + + ) : null}
-
+ ); } diff --git a/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx b/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx index d62a5ca0e2..6faff03139 100644 --- a/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx +++ b/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx @@ -4,6 +4,7 @@ "use client"; import { createCodePlugin } from "@/components/assistant-ui/code-plugin"; +import { CodeToggleIcon } from "@/components/assistant-ui/code-toggle-icon"; import { unslothDarkTheme, unslothLightTheme, @@ -15,7 +16,7 @@ import { CheckIcon, CopyIcon, DownloadIcon, - FileTextIcon, + EyeIcon, Maximize2Icon, XIcon, } from "lucide-react"; @@ -176,19 +177,42 @@ export function ArtifactSurface({ )} aria-label={`${artifact.title} artifact`} > -
-
- -
-
-

- {artifact.title} -

-

- HTML artifact ·{" "} - {artifact.source === "tool" ? "tool call" : "fenced fallback"} - {artifact.isStreaming ? " · streaming" : ""} -

+
+
+ {(["preview", "source"] as const).map((mode) => { + const isPreview = mode === "preview"; + const Icon = isPreview ? EyeIcon : CodeToggleIcon; + return ( + + ); + })}
-
- {(["preview", "source"] as const).map((mode) => ( - - ))} -
-
{effectiveViewMode === "preview" ? (