From 46adefa1488bc44419fcce30b27aea217ae0bcd5 Mon Sep 17 00:00:00 2001 From: wasimysaid Date: Mon, 25 May 2026 18:29:49 +0200 Subject: [PATCH] Studio: mount chat artifact panel and overlay --- .../frontend/src/features/chat/chat-page.tsx | 184 +++++++++++++----- 1 file changed, 136 insertions(+), 48 deletions(-) diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index ce02b0da18..8bb218c201 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -10,6 +10,11 @@ import { ModelSelector, } from "@/components/assistant-ui/model-selector"; import { Thread } from "@/components/assistant-ui/thread"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; import { useSidebar } from "@/components/ui/sidebar"; import { Tooltip, TooltipContent } from "@/components/ui/tooltip"; import { NativeModelChip } from "@/features/native-intents/components/native-model-chip"; @@ -76,6 +81,12 @@ import { } from "./stores/chat-runtime-store"; import { useExternalProvidersStore } from "./stores/external-providers-store"; import { buildChatTourSteps } from "./tour"; +import { ArtifactSurface } from "./artifacts/artifact-surface"; +import { + useChatArtifactsStore, + useSelectedChatArtifact, +} from "./artifacts/store"; +import type { ChatArtifact, ChatArtifactSurface } from "./artifacts/types"; import type { ChatView, MessageRecord } from "./types"; import { getStoredChatThread, @@ -154,16 +165,58 @@ function messageHasImage(message: MessageRecord): boolean { const SingleContent = memo(function SingleContent({ threadId, newThreadNonce, -}: { threadId?: string; newThreadNonce?: string }): ReactElement { + artifact, + artifactSurface, + onCloseArtifact, +}: { + threadId?: string; + newThreadNonce?: string; + artifact?: ChatArtifact | null; + artifactSurface: ChatArtifactSurface; + onCloseArtifact: () => void; +}): ReactElement { + const openArtifact = useChatArtifactsStore((state) => state.openArtifact); + const showArtifactPanel = Boolean( + artifact && + artifactSurface === "panel" && + (!artifact.threadId || !threadId || artifact.threadId === threadId), + ); + + const threadPane = ( +
+ +
+ ); + return ( -
- -
+ {showArtifactPanel && artifact ? ( + + + {threadPane} + + + + + openArtifact(artifact, { surface: "overlay" }) + } + /> + + + ) : ( + threadPane + )}
); }); @@ -329,15 +382,17 @@ const LoraCompareContent = memo(function LoraCompareContent({ useEffect(() => { let isActive = true; - listStoredChatThreads({ pairId }).then((threads) => { - if (!isActive) return; - setBaseThreadId(threads.find((t) => t.modelType === "base")?.id); - setLoraThreadId(threads.find((t) => t.modelType === "lora")?.id); - }).catch((error) => { - if (!isExpectedBackgroundChatStorageError(error)) { - throw error; - } - }); + listStoredChatThreads({ pairId }) + .then((threads) => { + if (!isActive) return; + setBaseThreadId(threads.find((t) => t.modelType === "base")?.id); + setLoraThreadId(threads.find((t) => t.modelType === "lora")?.id); + }) + .catch((error) => { + if (!isExpectedBackgroundChatStorageError(error)) { + throw error; + } + }); return () => { isActive = false; }; @@ -478,21 +533,25 @@ const GeneralCompareContent = memo(function GeneralCompareContent({ useEffect(() => { let isActive = true; - listStoredChatThreads({ pairId }).then((threads) => { - if (!isActive) return; - setModel1ThreadId( - threads.find((t) => t.modelType === "model1" || t.modelType === "base") - ?.id, - ); - setModel2ThreadId( - threads.find((t) => t.modelType === "model2" || t.modelType === "lora") - ?.id, - ); - }).catch((error) => { - if (!isExpectedBackgroundChatStorageError(error)) { - throw error; - } - }); + listStoredChatThreads({ pairId }) + .then((threads) => { + if (!isActive) return; + setModel1ThreadId( + threads.find( + (t) => t.modelType === "model1" || t.modelType === "base", + )?.id, + ); + setModel2ThreadId( + threads.find( + (t) => t.modelType === "model2" || t.modelType === "lora", + )?.id, + ); + }) + .catch((error) => { + if (!isExpectedBackgroundChatStorageError(error)) { + throw error; + } + }); return () => { isActive = false; }; @@ -645,8 +704,7 @@ export function ChatPage(): ReactElement { } = useChatModelRuntime(); const prevConnectionsEnabledRef = useRef(connectionsEnabled); useEffect(() => { - const turnedOff = - prevConnectionsEnabledRef.current && !connectionsEnabled; + const turnedOff = prevConnectionsEnabledRef.current && !connectionsEnabled; if (!connectionsEnabled && isExternalModelId(inferenceParams.checkpoint)) { clearCheckpoint(); if (turnedOff) { @@ -656,11 +714,7 @@ export function ChatPage(): ReactElement { } } prevConnectionsEnabledRef.current = connectionsEnabled; - }, [ - clearCheckpoint, - connectionsEnabled, - inferenceParams.checkpoint, - ]); + }, [clearCheckpoint, connectionsEnabled, inferenceParams.checkpoint]); const pendingNativeModelIntent = useNativeIntentStore( (state) => state.pendingModelIntent, ); @@ -679,17 +733,19 @@ export function ChatPage(): ReactElement { const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled); const reasoningStyle = useChatRuntimeStore((s) => s.reasoningStyle); const reasoningEffort = useChatRuntimeStore((s) => s.reasoningEffort); - const supportsReasoningOff = useChatRuntimeStore((s) => s.supportsReasoningOff); + const supportsReasoningOff = useChatRuntimeStore( + (s) => s.supportsReasoningOff, + ); const activeExternalProvider = useMemo(() => { const selection = parseExternalModelId(inferenceParams.checkpoint); if (!selection) return null; return ( - externalProvidersForChat.find( - (p) => p.id === selection.providerId, - ) ?? null + externalProvidersForChat.find((p) => p.id === selection.providerId) ?? + null ); }, [externalProvidersForChat, inferenceParams.checkpoint]); - const activeExternalProviderType = activeExternalProvider?.providerType ?? null; + const activeExternalProviderType = + activeExternalProvider?.providerType ?? null; const activeProviderCapabilities = useMemo(() => { const selection = parseExternalModelId(inferenceParams.checkpoint); if (!selection) return null; @@ -797,7 +853,9 @@ export function ChatPage(): ReactElement { (provider?.providerType === "anthropic" || provider?.providerType === "openai"); const storedToolsEnabled = loadOptionalBool(CHAT_TOOLS_ENABLED_KEY); - const storedCodeToolsEnabled = loadOptionalBool(CHAT_CODE_TOOLS_ENABLED_KEY); + const storedCodeToolsEnabled = loadOptionalBool( + CHAT_CODE_TOOLS_ENABLED_KEY, + ); const storedImageToolsEnabled = loadOptionalBool( CHAT_IMAGE_TOOLS_ENABLED_KEY, ); @@ -867,6 +925,20 @@ export function ChatPage(): ReactElement { return { mode: "single" }; }, [search.thread, search.compare, search.new, activeThreadId]); + const selectedArtifact = useSelectedChatArtifact(); + const artifactSurface = useChatArtifactsStore((state) => state.surface); + const closeArtifactSurface = useChatArtifactsStore( + (state) => state.closeArtifactSurface, + ); + const artifactViewKey = + view.mode === "single" + ? `single:${view.threadId ?? view.newThreadNonce ?? "new"}` + : `compare:${view.pairId}`; + + useEffect(() => { + closeArtifactSurface(); + }, [artifactViewKey, closeArtifactSurface]); + const hasActiveModel = Boolean(inferenceParams.checkpoint); const loadNativeModelIntent = useCallback( async (intent: NativeIntent, loadingDescription: string) => { @@ -953,8 +1025,7 @@ export function ChatPage(): ReactElement { selectedProvider?.providerType, selectedExternal?.modelId, { - isReasoningProvider: - selectedProvider?.isReasoningModel === true, + isReasoningProvider: selectedProvider?.isReasoningModel === true, }, ); const preferredEffort = store.reasoningEffort; @@ -997,11 +1068,12 @@ export function ChatPage(): ReactElement { const supportsBuiltinWebSearch = providerSupportsBuiltinWebSearch( selectedProvider?.providerType, ); - const supportsBuiltinCodeExecution = providerSupportsBuiltinCodeExecution( - selectedProvider?.providerType, - selectedExternal?.modelId, - selectedProvider?.baseUrl, - ); + const supportsBuiltinCodeExecution = + providerSupportsBuiltinCodeExecution( + selectedProvider?.providerType, + selectedExternal?.modelId, + selectedProvider?.baseUrl, + ); const supportsBuiltinImageGeneration = providerSupportsBuiltinImageGeneration( selectedProvider?.providerType, @@ -1416,6 +1488,11 @@ export function ChatPage(): ReactElement { return () => window.clearTimeout(timeoutId); }, [modelSelectorLocked, tour.open]); + const showArtifactOverlay = Boolean( + selectedArtifact && + (view.mode === "compare" || artifactSurface === "overlay"), + ); + return (
@@ -1535,6 +1612,9 @@ export function ChatPage(): ReactElement { key={view.threadId ?? "single"} threadId={view.threadId} newThreadNonce={view.newThreadNonce} + artifact={selectedArtifact} + artifactSurface={artifactSurface} + onCloseArtifact={closeArtifactSurface} /> ) : ( )} + + {showArtifactOverlay && selectedArtifact ? ( + + ) : null}