From e4b5bec248dff9c84de29ae88831c67946e328da Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 10 Jun 2026 05:15:12 -0700 Subject: [PATCH] Studio chat: declutter the plus menu, session-only pre-selectable RAG (#6140) --- .../src/components/assistant-ui/thread.tsx | 160 ++++++++-------- ...-available.ts => use-rag-tool-disabled.ts} | 9 +- .../src/features/chat/shared-composer.tsx | 171 ++++++++++-------- .../chat/stores/chat-runtime-store.ts | 14 +- .../knowledge-base-composer-button.tsx | 12 +- .../rag/components/thread-documents-bar.tsx | 7 +- 6 files changed, 190 insertions(+), 183 deletions(-) rename studio/frontend/src/features/chat/hooks/{use-rag-tool-available.ts => use-rag-tool-disabled.ts} (60%) diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index 8146c4603b..ea660f7af8 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -64,7 +64,7 @@ import { NewProjectDialog } from "@/features/chat/components/new-project-dialog" import { parseExternalModelId } from "@/features/chat/external-providers"; import { McpComposerButton } from "@/features/chat/mcp-composer-button"; import { getExternalReasoningCapabilities } from "@/features/chat/provider-capabilities"; -import { useRagToolAvailable } from "@/features/chat/hooks/use-rag-tool-available"; +import { useRagToolDisabled } from "@/features/chat/hooks/use-rag-tool-disabled"; import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store"; import { useExternalProvidersStore } from "@/features/chat/stores/external-providers-store"; import { deleteThreadMessage } from "@/features/chat/utils/delete-thread-message"; @@ -876,12 +876,11 @@ const Composer: FC<{ const artifactsEnabled = useChatRuntimeStore((s) => s.artifactsEnabled); const mcpEnabledForChat = useChatRuntimeStore((s) => s.mcpEnabledForChat); const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled); - const ragToolAvailable = useRagToolAvailable(); // More than 4 pills: collapse to icons only. Search and Code always show; - // Images, Canvas and MCP are conditional. + // Images, RAG, Canvas and MCP are conditional. const pillsCompact = 2 + - (ragEnabled && ragToolAvailable ? 1 : 0) + + (ragEnabled ? 1 : 0) + (supportsBuiltinImageGeneration ? 1 : 0) + (artifactsEnabled ? 1 : 0) + (mcpEnabledForChat ? 1 : 0) > @@ -2017,8 +2016,8 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({ ); const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled); const setRagEnabled = useChatRuntimeStore((s) => s.setRagEnabled); - // Shared gate so the menu row agrees with the RAG pill and Add Files bar. - const ragAvailable = useRagToolAvailable(); + // Shared gate so the menu row agrees with the RAG pill. + const ragDisabled = useRagToolDisabled(); // Capability gating mirrors the visible pills so menu and pills agree on // what a loaded model supports (a tool the backend drops must not look on). const modelLoaded = useChatRuntimeStore( @@ -2180,10 +2179,11 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({ } onSelect={() => setCodeToolsEnabled(!codeToolsEnabled)} > + {/* Scale, not width: an oversized box pushed the label out of line. */} Code {codeToolsEnabled && !codeDisabled ? ( @@ -2217,15 +2217,15 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({ )} setRagEnabled(!ragEnabled)} > - RAG - {ragEnabled && ragAvailable ? ( + Chat with Files + {ragEnabled && !ragDisabled ? ( = ({ /> ) : null} - {/* Top-level so it stays one click away (not buried in More). */} - startCompare()}> - - Compare chat - - - - - Saved prompts - - - {recentPrompts.map((p) => ( - aui.composer().setText(p.text)} - > - {p.name} - - ))} - {recentPrompts.length > 0 ? : null} - setPromptStorageOpen(true)}> - All saved prompts… - - - - {/* Top-level: a third-level submenu collision-flips at narrow widths - and is awkward to reach. */} - - - - Export chat - - - { - if (!activeThreadId) return; - exportConversationRawJsonl(activeThreadId).catch(() => - toast.error("Export failed."), - ); - }} - > - Raw JSONL - - { - if (!activeThreadId) return; - exportConversationCsv(activeThreadId).catch(() => - toast.error("Export failed."), - ); - }} - > - CSV - - { - if (!activeThreadId) return; - exportConversationShareGPT(activeThreadId).catch(() => - toast.error("Export failed."), - ); - }} - > - ShareGPT JSONL - - - More + startCompare()}> + + Compare chat + + + + + Saved prompts + + + {recentPrompts.map((p) => ( + aui.composer().setText(p.text)} + > + {p.name} + + ))} + {recentPrompts.length > 0 ? : null} + setPromptStorageOpen(true)}> + All saved prompts… + + + + + + + Export chat + + + { + if (!activeThreadId) return; + exportConversationRawJsonl(activeThreadId).catch(() => + toast.error("Export failed."), + ); + }} + > + Raw JSONL + + { + if (!activeThreadId) return; + exportConversationCsv(activeThreadId).catch(() => + toast.error("Export failed."), + ); + }} + > + CSV + + { + if (!activeThreadId) return; + exportConversationShareGPT(activeThreadId).catch(() => + toast.error("Export failed."), + ); + }} + > + ShareGPT JSONL + + + !!s.params.checkpoint && !s.modelLoading, ); const checkpoint = useChatRuntimeStore((s) => s.params.checkpoint); const supportsTools = useChatRuntimeStore((s) => s.supportsTools); return ( - modelLoaded && parseExternalModelId(checkpoint) === null && supportsTools + modelLoaded && (parseExternalModelId(checkpoint) !== null || !supportsTools) ); } diff --git a/studio/frontend/src/features/chat/shared-composer.tsx b/studio/frontend/src/features/chat/shared-composer.tsx index f68ec6cc73..fe71adf4d7 100644 --- a/studio/frontend/src/features/chat/shared-composer.tsx +++ b/studio/frontend/src/features/chat/shared-composer.tsx @@ -656,8 +656,10 @@ export function SharedComposer({ // Fetch pill: Anthropic-only (web_fetch_20250910 / web_fetch_20260209). const webFetchDisabled = !modelLoaded || !supportsBuiltinWebFetch; const showWebFetchPill = supportsBuiltinWebFetch; - // Docs (RAG) pill is local-only: search_knowledge_base needs the local runtime. - const ragDisabled = !modelLoaded || isExternalModel || !supportsTools; + // Docs (RAG) is local-only: search_knowledge_base needs the local runtime. + // Disable only when a loaded model can't run it; with no model the toggle + // can still be pre-selected, matching Web search/Code/MCP. + const ragDisabled = modelLoaded && (isExternalModel || !supportsTools); const showRagPill = !isExternalModel; // Above 4 pills, collapse to icons only to cut clutter. Compare, Search and // Code always show; the rest are conditional. @@ -1323,10 +1325,12 @@ export function SharedComposer({ } onSelect={() => setCodeToolsEnabled(!codeToolsEnabled)} > + {/* Scale, not width: an oversized box pushed the label out of + line. */} Code {codeToolsEnabled && !codeDisabled ? ( @@ -1369,7 +1373,7 @@ export function SharedComposer({ onSelect={() => setRagEnabled(!ragEnabled)} > - RAG + Chat with Files {ragEnabled && !ragDisabled ? ( {/* RAG hidden temporarily */} - {/* Always active: this menu only renders in compare mode. Ticked - like Web search/Code; click toggles it off. */} - - - Compare chat - - - - - - Saved prompts - - - {recentPrompts.map((p) => ( - { - setText(p.text); - requestAnimationFrame(() => - textareaRef.current?.focus(), - ); - }} - > - {p.name} - - ))} - {recentPrompts.length > 0 ? : null} - setPromptStorageOpen(true)}> - All saved prompts… - - - - {/* Top-level: a third-level submenu collision-flips at narrow - widths and is awkward to reach. */} - - - - Export chat - - - {[ - { label: "Raw JSONL", fn: exportConversationRawJsonl }, - { label: "CSV", fn: exportConversationCsv }, - { - label: "ShareGPT JSONL", - fn: exportConversationShareGPT, - }, - ].map(({ label, fn }) => ( - { - if (!exportThreadIds.length) { - toast.error("No conversation to export yet."); - return; - } - Promise.all(exportThreadIds.map((id) => fn(id))).catch( - () => toast.error("Export failed."), - ); - }} - > - {label} - - ))} - - More + {/* Always active: this menu only renders in compare mode. Ticked + like Web search/Code; click toggles it off. */} + + + Compare chat + + + + + + Saved prompts + + + {recentPrompts.map((p) => ( + { + setText(p.text); + requestAnimationFrame(() => + textareaRef.current?.focus(), + ); + }} + > + {p.name} + + ))} + {recentPrompts.length > 0 ? ( + + ) : null} + setPromptStorageOpen(true)} + > + All saved prompts… + + + + + + + Export chat + + + {[ + { label: "Raw JSONL", fn: exportConversationRawJsonl }, + { label: "CSV", fn: exportConversationCsv }, + { + label: "ShareGPT JSONL", + fn: exportConversationShareGPT, + }, + ].map(({ label, fn }) => ( + { + if (!exportThreadIds.length) { + toast.error("No conversation to export yet."); + return; + } + Promise.all( + exportThreadIds.map((id) => fn(id)), + ).catch(() => toast.error("Export failed.")); + }} + > + {label} + + ))} + + void; setMcpEnabledForChat: (enabled: boolean) => void; setWebFetchToolsEnabled: (enabled: boolean) => void; - setRagEnabled: (enabled: boolean, options?: { persist?: boolean }) => void; + setRagEnabled: (enabled: boolean) => void; setRagSource: (source: RagSource) => void; setRagMode: (mode: RagMode) => void; setRagTopK: (topK: number) => void; @@ -746,7 +745,8 @@ export const useChatRuntimeStore = create((set, get) => ({ ), mcpEnabledForChat: loadBool(CHAT_MCP_ENABLED_KEY, false), webFetchToolsEnabled: loadBool(CHAT_WEB_FETCH_TOOLS_ENABLED_KEY, false), - ragEnabled: loadBool(CHAT_RAG_ENABLED_KEY, false), + // RAG is opt-in per session: always starts off, never restored from storage. + ragEnabled: false, ragSource: loadRagSource(), ragMode: loadRagMode(), ragTopK: loadRagTopK(), @@ -1072,13 +1072,7 @@ export const useChatRuntimeStore = create((set, get) => ({ saveBool(CHAT_WEB_FETCH_TOOLS_ENABLED_KEY, webFetchToolsEnabled); return { webFetchToolsEnabled }; }), - setRagEnabled: (ragEnabled, options) => - set(() => { - if (options?.persist !== false) { - saveBool(CHAT_RAG_ENABLED_KEY, ragEnabled); - } - return { ragEnabled }; - }), + setRagEnabled: (ragEnabled) => set(() => ({ ragEnabled })), setRagSource: (ragSource) => set(() => { saveRagSource(ragSource); diff --git a/studio/frontend/src/features/rag/components/knowledge-base-composer-button.tsx b/studio/frontend/src/features/rag/components/knowledge-base-composer-button.tsx index b703fca347..e040d1842a 100644 --- a/studio/frontend/src/features/rag/components/knowledge-base-composer-button.tsx +++ b/studio/frontend/src/features/rag/components/knowledge-base-composer-button.tsx @@ -14,7 +14,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { useRagToolAvailable } from "@/features/chat/hooks/use-rag-tool-available"; +import { useRagToolDisabled } from "@/features/chat/hooks/use-rag-tool-disabled"; import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store"; import { listKnowledgeBases } from "../api/rag-api"; @@ -38,8 +38,8 @@ const ArrowDownStandardIcon: FC<{ className?: string }> = ({ className }) => ( ); -// Picks the retrieval source. Only rendered when retrieval is on and the loaded -// model can run search_knowledge_base. +// Picks the retrieval source. Shown whenever retrieval is on; dims but stays +// interactive (so it can be turned off) while the loaded model can't run it. export function KnowledgeBaseComposerButton({ side = "bottom", }: { @@ -47,7 +47,7 @@ export function KnowledgeBaseComposerButton({ } = {}) { const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled); const setRagEnabled = useChatRuntimeStore((s) => s.setRagEnabled); - const ragAvailable = useRagToolAvailable(); + const ragDisabled = useRagToolDisabled(); const ragSource = useChatRuntimeStore((s) => s.ragSource); const setRagSource = useChatRuntimeStore((s) => s.setRagSource); @@ -85,7 +85,7 @@ export function KnowledgeBaseComposerButton({ } }, [kbs, kbsLoaded, ragSource, setRagSource]); - if (!ragEnabled || !ragAvailable) return null; + if (!ragEnabled) return null; return ( <> @@ -100,7 +100,7 @@ export function KnowledgeBaseComposerButton({