From a288ad1d9ca711cf1d1a5eeffe19c60c75f16b9e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 4 Jul 2025 13:23:37 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=84=20feat:=20Artifacts=20Badge=20&=20?= =?UTF-8?q?Optimize=20Ephemeral=20Agent=20State=20(#8252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 fix: Update type annotations in useEventHandlers for better type safety * 🔧 refactor: `useToolToggle` for improved localStorage synchronization and allow string/falsy values for setting to storage * ✨ feat: Implement Artifacts badge to BadgeRow with toggle options and UI components - Added Artifacts component to manage artifacts state and options. - Introduced ArtifactsSubMenu for additional settings related to artifacts. - Integrated artifacts functionality into BadgeRow and ToolsDropdown components. - Updated localStorage handling for artifacts state persistence. - Enhanced localization for artifacts-related strings in translation files. - Refactored Agent model to include artifacts in the ephemeral agent response. * fix: set ephemeral agent state for conversation on finalization * chore: remove beta settings dialog tab * refactor: improve Ephemeral Agent statefulness * fix: update setValue parameter to use 'value' instead of 'isChecked' in CheckboxButton * refactor: update color classes for Artifact toggle and order of dropdown components * chore: remove unused i18n localization --- api/models/Agent.js | 7 +- client/src/Providers/BadgeRowContext.tsx | 95 ++++++++++- .../src/components/Chat/Input/Artifacts.tsx | 152 ++++++++++++++++++ .../Chat/Input/ArtifactsSubMenu.tsx | 147 +++++++++++++++++ client/src/components/Chat/Input/BadgeRow.tsx | 6 +- client/src/components/Chat/Input/ChatForm.tsx | 1 + .../components/Chat/Input/ToolsDropdown.tsx | 60 ++++++- client/src/components/Nav/Settings.tsx | 4 - .../components/Nav/SettingsTabs/Beta/Beta.tsx | 18 --- .../Nav/SettingsTabs/Beta/ChatBadges.tsx | 22 --- .../Nav/SettingsTabs/Beta/CodeArtifacts.tsx | 95 ----------- .../src/components/Nav/SettingsTabs/index.ts | 1 - .../components/SidePanel/Agents/Artifacts.tsx | 2 +- client/src/components/ui/CheckboxButton.tsx | 7 +- client/src/hooks/Chat/useChatFunctions.ts | 8 - client/src/hooks/Plugins/useToolToggle.ts | 64 +++++--- client/src/hooks/SSE/useEventHandlers.ts | 47 ++++-- client/src/locales/en/translation.json | 7 +- client/src/store/agents.ts | 9 +- client/src/store/settings.ts | 3 - client/src/utils/artifacts.ts | 21 +-- packages/data-provider/src/config.ts | 2 + packages/data-provider/src/types.ts | 1 - 23 files changed, 547 insertions(+), 232 deletions(-) create mode 100644 client/src/components/Chat/Input/Artifacts.tsx create mode 100644 client/src/components/Chat/Input/ArtifactsSubMenu.tsx delete mode 100644 client/src/components/Nav/SettingsTabs/Beta/Beta.tsx delete mode 100644 client/src/components/Nav/SettingsTabs/Beta/ChatBadges.tsx delete mode 100644 client/src/components/Nav/SettingsTabs/Beta/CodeArtifacts.tsx diff --git a/api/models/Agent.js b/api/models/Agent.js index 04ba8b020..dcb646f03 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -90,7 +90,7 @@ const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _ } const instructions = req.body.promptPrefix; - return { + const result = { id: agent_id, instructions, provider: endpoint, @@ -98,6 +98,11 @@ const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _ model, tools, }; + + if (ephemeralAgent?.artifacts != null && ephemeralAgent.artifacts) { + result.artifacts = ephemeralAgent.artifacts; + } + return result; }; /** diff --git a/client/src/Providers/BadgeRowContext.tsx b/client/src/Providers/BadgeRowContext.tsx index 01590b194..266d0895b 100644 --- a/client/src/Providers/BadgeRowContext.tsx +++ b/client/src/Providers/BadgeRowContext.tsx @@ -1,7 +1,9 @@ -import React, { createContext, useContext } from 'react'; -import { Tools, LocalStorageKeys } from 'librechat-data-provider'; +import React, { createContext, useContext, useEffect, useRef } from 'react'; +import { Tools, LocalStorageKeys, AgentCapabilities, Constants } from 'librechat-data-provider'; import { useMCPSelect, useToolToggle, useCodeApiKeyForm, useSearchApiKeyForm } from '~/hooks'; import { useGetStartupConfig } from '~/data-provider'; +import { useSetRecoilState } from 'recoil'; +import { ephemeralAgentByConvoId } from '~/store'; interface BadgeRowContextType { conversationId?: string | null; @@ -9,6 +11,7 @@ interface BadgeRowContextType { webSearch: ReturnType; codeInterpreter: ReturnType; fileSearch: ReturnType; + artifacts: ReturnType; codeApiKeyForm: ReturnType; searchApiKeyForm: ReturnType; startupConfig: ReturnType['data']; @@ -26,10 +29,87 @@ export function useBadgeRowContext() { interface BadgeRowProviderProps { children: React.ReactNode; + isSubmitting?: boolean; conversationId?: string | null; } -export default function BadgeRowProvider({ children, conversationId }: BadgeRowProviderProps) { +export default function BadgeRowProvider({ + children, + isSubmitting, + conversationId, +}: BadgeRowProviderProps) { + const hasInitializedRef = useRef(false); + const lastKeyRef = useRef(''); + const key = conversationId ?? Constants.NEW_CONVO; + const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(key)); + + /** Initialize ephemeralAgent from localStorage on mount and when conversation changes */ + useEffect(() => { + if (isSubmitting) { + return; + } + // Check if this is a new conversation or the first load + if (!hasInitializedRef.current || lastKeyRef.current !== key) { + hasInitializedRef.current = true; + lastKeyRef.current = key; + + // Load all localStorage values + const codeToggleKey = `${LocalStorageKeys.LAST_CODE_TOGGLE_}${key}`; + const webSearchToggleKey = `${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${key}`; + const fileSearchToggleKey = `${LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_}${key}`; + const artifactsToggleKey = `${LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_}${key}`; + + const codeToggleValue = localStorage.getItem(codeToggleKey); + const webSearchToggleValue = localStorage.getItem(webSearchToggleKey); + const fileSearchToggleValue = localStorage.getItem(fileSearchToggleKey); + const artifactsToggleValue = localStorage.getItem(artifactsToggleKey); + + const initialValues: Record = {}; + + if (codeToggleValue !== null) { + try { + initialValues[Tools.execute_code] = JSON.parse(codeToggleValue); + } catch (e) { + console.error('Failed to parse code toggle value:', e); + } + } + + if (webSearchToggleValue !== null) { + try { + initialValues[Tools.web_search] = JSON.parse(webSearchToggleValue); + } catch (e) { + console.error('Failed to parse web search toggle value:', e); + } + } + + if (fileSearchToggleValue !== null) { + try { + initialValues[Tools.file_search] = JSON.parse(fileSearchToggleValue); + } catch (e) { + console.error('Failed to parse file search toggle value:', e); + } + } + + if (artifactsToggleValue !== null) { + try { + initialValues[AgentCapabilities.artifacts] = JSON.parse(artifactsToggleValue); + } catch (e) { + console.error('Failed to parse artifacts toggle value:', e); + } + } + + // Always set values for all tools (use defaults if not in localStorage) + // If ephemeralAgent is null, create a new object with just our tool values + setEphemeralAgent((prev) => ({ + ...(prev || {}), + [Tools.execute_code]: initialValues[Tools.execute_code] ?? false, + [Tools.web_search]: initialValues[Tools.web_search] ?? false, + [Tools.file_search]: initialValues[Tools.file_search] ?? false, + [AgentCapabilities.artifacts]: initialValues[AgentCapabilities.artifacts] ?? false, + })); + } + }, [key, isSubmitting, setEphemeralAgent]); + /** Startup config */ const { data: startupConfig } = useGetStartupConfig(); @@ -74,10 +154,19 @@ export default function BadgeRowProvider({ children, conversationId }: BadgeRowP isAuthenticated: true, }); + /** Artifacts hook - using a custom key since it's not a Tool but a capability */ + const artifacts = useToolToggle({ + conversationId, + toolKey: AgentCapabilities.artifacts, + localStorageKey: LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_, + isAuthenticated: true, + }); + const value: BadgeRowContextType = { mcpSelect, webSearch, fileSearch, + artifacts, startupConfig, conversationId, codeApiKeyForm, diff --git a/client/src/components/Chat/Input/Artifacts.tsx b/client/src/components/Chat/Input/Artifacts.tsx new file mode 100644 index 000000000..eb2c49572 --- /dev/null +++ b/client/src/components/Chat/Input/Artifacts.tsx @@ -0,0 +1,152 @@ +import React, { memo, useState, useCallback, useMemo } from 'react'; +import * as Ariakit from '@ariakit/react'; +import { ArtifactModes } from 'librechat-data-provider'; +import { WandSparkles, ChevronDown } from 'lucide-react'; +import CheckboxButton from '~/components/ui/CheckboxButton'; +import { useBadgeRowContext } from '~/Providers'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils'; + +interface ArtifactsToggleState { + enabled: boolean; + mode: string; +} + +function Artifacts() { + const localize = useLocalize(); + const { artifacts } = useBadgeRowContext(); + const { toggleState, debouncedChange, isPinned } = artifacts; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const currentState = useMemo(() => { + if (typeof toggleState === 'string' && toggleState) { + return { enabled: true, mode: toggleState }; + } + return { enabled: false, mode: '' }; + }, [toggleState]); + + const isEnabled = currentState.enabled; + const isShadcnEnabled = currentState.mode === ArtifactModes.SHADCNUI; + const isCustomEnabled = currentState.mode === ArtifactModes.CUSTOM; + + const handleToggle = useCallback(() => { + if (isEnabled) { + debouncedChange({ value: '' }); + } else { + debouncedChange({ value: ArtifactModes.DEFAULT }); + } + }, [isEnabled, debouncedChange]); + + const handleShadcnToggle = useCallback(() => { + if (isShadcnEnabled) { + debouncedChange({ value: ArtifactModes.DEFAULT }); + } else { + debouncedChange({ value: ArtifactModes.SHADCNUI }); + } + }, [isShadcnEnabled, debouncedChange]); + + const handleCustomToggle = useCallback(() => { + if (isCustomEnabled) { + debouncedChange({ value: ArtifactModes.DEFAULT }); + } else { + debouncedChange({ value: ArtifactModes.CUSTOM }); + } + }, [isCustomEnabled, debouncedChange]); + + if (!isEnabled && !isPinned) { + return null; + } + + return ( +
+ } + /> + + {isEnabled && ( + + e.stopPropagation()} + > + + + + +
+
+ {localize('com_ui_artifacts_options')} +
+ + {/* Include shadcn/ui Option */} + { + event.preventDefault(); + event.stopPropagation(); + handleShadcnToggle(); + }} + disabled={isCustomEnabled} + className={cn( + 'mb-1 flex items-center justify-between rounded-lg px-2 py-2', + 'cursor-pointer outline-none transition-colors', + 'hover:bg-black/[0.075] dark:hover:bg-white/10', + 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + isCustomEnabled && 'cursor-not-allowed opacity-50', + )} + > +
+ + {localize('com_ui_include_shadcnui' as any)} +
+
+ + {/* Custom Prompt Mode Option */} + { + event.preventDefault(); + event.stopPropagation(); + handleCustomToggle(); + }} + className={cn( + 'flex items-center justify-between rounded-lg px-2 py-2', + 'cursor-pointer outline-none transition-colors', + 'hover:bg-black/[0.075] dark:hover:bg-white/10', + 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + )} + > +
+ + {localize('com_ui_custom_prompt_mode' as any)} +
+
+
+
+
+ )} +
+ ); +} + +export default memo(Artifacts); diff --git a/client/src/components/Chat/Input/ArtifactsSubMenu.tsx b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx new file mode 100644 index 000000000..944ecb66c --- /dev/null +++ b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import * as Ariakit from '@ariakit/react'; +import { ChevronRight, WandSparkles } from 'lucide-react'; +import { ArtifactModes } from 'librechat-data-provider'; +import { PinIcon } from '~/components/svg'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils'; + +interface ArtifactsSubMenuProps { + isArtifactsPinned: boolean; + setIsArtifactsPinned: (value: boolean) => void; + artifactsMode: string; + handleArtifactsToggle: () => void; + handleShadcnToggle: () => void; + handleCustomToggle: () => void; +} + +const ArtifactsSubMenu = ({ + isArtifactsPinned, + setIsArtifactsPinned, + artifactsMode, + handleArtifactsToggle, + handleShadcnToggle, + handleCustomToggle, + ...props +}: ArtifactsSubMenuProps) => { + const localize = useLocalize(); + + const menuStore = Ariakit.useMenuStore({ + focusLoop: true, + showTimeout: 100, + placement: 'right', + }); + + const isEnabled = artifactsMode !== '' && artifactsMode !== undefined; + const isShadcnEnabled = artifactsMode === ArtifactModes.SHADCNUI; + const isCustomEnabled = artifactsMode === ArtifactModes.CUSTOM; + + return ( + + ) => { + e.stopPropagation(); + handleArtifactsToggle(); + }} + onMouseEnter={() => { + if (isEnabled) { + menuStore.show(); + } + }} + className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover" + /> + } + > +
+ + {localize('com_ui_artifacts')} + {isEnabled && } +
+ +
+ + {isEnabled && ( + +
+
+ {localize('com_ui_artifacts_options')} +
+ + {/* Include shadcn/ui Option */} + { + event.preventDefault(); + event.stopPropagation(); + handleShadcnToggle(); + }} + disabled={isCustomEnabled} + className={cn( + 'mb-1 flex items-center justify-between rounded-lg px-2 py-2', + 'cursor-pointer text-text-primary outline-none transition-colors', + 'hover:bg-black/[0.075] dark:hover:bg-white/10', + 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + isCustomEnabled && 'cursor-not-allowed opacity-50', + )} + > +
+ + {localize('com_ui_include_shadcnui' as any)} +
+
+ + {/* Custom Prompt Mode Option */} + { + event.preventDefault(); + event.stopPropagation(); + handleCustomToggle(); + }} + className={cn( + 'flex items-center justify-between rounded-lg px-2 py-2', + 'cursor-pointer text-text-primary outline-none transition-colors', + 'hover:bg-black/[0.075] dark:hover:bg-white/10', + 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + )} + > +
+ + {localize('com_ui_custom_prompt_mode' as any)} +
+
+
+
+ )} +
+ ); +}; + +export default React.memo(ArtifactsSubMenu); diff --git a/client/src/components/Chat/Input/BadgeRow.tsx b/client/src/components/Chat/Input/BadgeRow.tsx index 14f98b452..d77fc5426 100644 --- a/client/src/components/Chat/Input/BadgeRow.tsx +++ b/client/src/components/Chat/Input/BadgeRow.tsx @@ -18,6 +18,7 @@ import { useChatBadges } from '~/hooks'; import { Badge } from '~/components/ui'; import ToolDialogs from './ToolDialogs'; import FileSearch from './FileSearch'; +import Artifacts from './Artifacts'; import MCPSelect from './MCPSelect'; import WebSearch from './WebSearch'; import store from '~/store'; @@ -27,6 +28,7 @@ interface BadgeRowProps { onChange: (badges: Pick[]) => void; onToggle?: (badgeId: string, currentActive: boolean) => void; conversationId?: string | null; + isSubmitting?: boolean; isInChat: boolean; } @@ -140,6 +142,7 @@ const dragReducer = (state: DragState, action: DragAction): DragState => { function BadgeRow({ showEphemeralBadges, conversationId, + isSubmitting, onChange, onToggle, isInChat, @@ -317,7 +320,7 @@ function BadgeRow({ }, [dragState.draggedBadge, handleMouseMove, handleMouseUp]); return ( - +
{showEphemeralBadges === true && } {tempBadges.map((badge, index) => ( @@ -364,6 +367,7 @@ function BadgeRow({ + )} diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 23bece362..0ca644809 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -305,6 +305,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
{ const { webSearch, mcpSelect, + artifacts, fileSearch, startupConfig, codeApiKeyForm, @@ -42,6 +44,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { authData: codeAuthData, } = codeInterpreter; const { isPinned: isFileSearchPinned, setIsPinned: setIsFileSearchPinned } = fileSearch; + const { isPinned: isArtifactsPinned, setIsPinned: setIsArtifactsPinned } = artifacts; const { mcpValues, mcpServerNames, @@ -72,19 +75,46 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { const handleWebSearchToggle = useCallback(() => { const newValue = !webSearch.toggleState; - webSearch.debouncedChange({ isChecked: newValue }); + webSearch.debouncedChange({ value: newValue }); }, [webSearch]); const handleCodeInterpreterToggle = useCallback(() => { const newValue = !codeInterpreter.toggleState; - codeInterpreter.debouncedChange({ isChecked: newValue }); + codeInterpreter.debouncedChange({ value: newValue }); }, [codeInterpreter]); const handleFileSearchToggle = useCallback(() => { const newValue = !fileSearch.toggleState; - fileSearch.debouncedChange({ isChecked: newValue }); + fileSearch.debouncedChange({ value: newValue }); }, [fileSearch]); + const handleArtifactsToggle = useCallback(() => { + const currentState = artifacts.toggleState; + if (!currentState || currentState === '') { + artifacts.debouncedChange({ value: ArtifactModes.DEFAULT }); + } else { + artifacts.debouncedChange({ value: '' }); + } + }, [artifacts]); + + const handleShadcnToggle = useCallback(() => { + const currentState = artifacts.toggleState; + if (currentState === ArtifactModes.SHADCNUI) { + artifacts.debouncedChange({ value: ArtifactModes.DEFAULT }); + } else { + artifacts.debouncedChange({ value: ArtifactModes.SHADCNUI }); + } + }, [artifacts]); + + const handleCustomToggle = useCallback(() => { + const currentState = artifacts.toggleState; + if (currentState === ArtifactModes.CUSTOM) { + artifacts.debouncedChange({ value: ArtifactModes.DEFAULT }); + } else { + artifacts.debouncedChange({ value: ArtifactModes.CUSTOM }); + } + }, [artifacts]); + const handleMCPToggle = useCallback( (serverName: string) => { const currentValues = mcpSelect.mcpValues ?? []; @@ -238,6 +268,22 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { }); } + // Add Artifacts option + items.push({ + hideOnClick: false, + render: (props) => ( + + ), + }); + if (mcpServerNames && mcpServerNames.length > 0) { items.push({ hideOnClick: false, @@ -271,15 +317,21 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { handleMCPToggle, showCodeSettings, setIsSearchPinned, + handleShadcnToggle, + handleCustomToggle, isFileSearchPinned, + isArtifactsPinned, codeMenuTriggerRef, setIsCodeDialogOpen, searchMenuTriggerRef, showWebSearchSettings, setIsFileSearchPinned, + artifacts.toggleState, + setIsArtifactsPinned, handleWebSearchToggle, setIsSearchDialogOpen, handleFileSearchToggle, + handleArtifactsToggle, handleCodeInterpreterToggle, ]); diff --git a/client/src/components/Nav/Settings.tsx b/client/src/components/Nav/Settings.tsx index 23d7a456b..c78a9a1e6 100644 --- a/client/src/components/Nav/Settings.tsx +++ b/client/src/components/Nav/Settings.tsx @@ -17,7 +17,6 @@ import { General, Chat, Speech, - Beta, Commands, Data, Account, @@ -233,9 +232,6 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { - - - diff --git a/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx b/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx deleted file mode 100644 index 8aeaf554c..000000000 --- a/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { memo } from 'react'; -import CodeArtifacts from './CodeArtifacts'; -import ChatBadges from './ChatBadges'; - -function Beta() { - return ( -
-
- -
- {/*
- -
*/} -
- ); -} - -export default memo(Beta); diff --git a/client/src/components/Nav/SettingsTabs/Beta/ChatBadges.tsx b/client/src/components/Nav/SettingsTabs/Beta/ChatBadges.tsx deleted file mode 100644 index 8e5eddb13..000000000 --- a/client/src/components/Nav/SettingsTabs/Beta/ChatBadges.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useSetRecoilState } from 'recoil'; -import { Button } from '~/components/ui'; -import { useLocalize } from '~/hooks'; -import store from '~/store'; - -export default function ChatBadges() { - const setIsEditing = useSetRecoilState(store.isEditingBadges); - const localize = useLocalize(); - - const handleEditChatBadges = () => { - setIsEditing(true); - }; - - return ( -
-
{localize('com_nav_edit_chat_badges')}
- -
- ); -} diff --git a/client/src/components/Nav/SettingsTabs/Beta/CodeArtifacts.tsx b/client/src/components/Nav/SettingsTabs/Beta/CodeArtifacts.tsx deleted file mode 100644 index dd985a86a..000000000 --- a/client/src/components/Nav/SettingsTabs/Beta/CodeArtifacts.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useRecoilState } from 'recoil'; -import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings'; -import { Switch } from '~/components/ui'; -import { useLocalize } from '~/hooks'; -import store from '~/store'; - -export default function CodeArtifacts() { - const [codeArtifacts, setCodeArtifacts] = useRecoilState(store.codeArtifacts); - const [includeShadcnui, setIncludeShadcnui] = useRecoilState(store.includeShadcnui); - const [customPromptMode, setCustomPromptMode] = useRecoilState(store.customPromptMode); - const localize = useLocalize(); - - const handleCodeArtifactsChange = (value: boolean) => { - setCodeArtifacts(value); - if (!value) { - setIncludeShadcnui(false); - setCustomPromptMode(false); - } - }; - - const handleIncludeShadcnuiChange = (value: boolean) => { - setIncludeShadcnui(value); - }; - - const handleCustomPromptModeChange = (value: boolean) => { - setCustomPromptMode(value); - if (value) { - setIncludeShadcnui(false); - } - }; - - return ( -
-

{localize('com_ui_artifacts')}

-
- - - -
-
- ); -} - -function SwitchItem({ - id, - label, - checked, - onCheckedChange, - hoverCardText, - disabled = false, -}: { - id: string; - label: string; - checked: boolean; - onCheckedChange: (value: boolean) => void; - hoverCardText: string; - disabled?: boolean; -}) { - return ( -
-
-
{label}
- -
- -
- ); -} diff --git a/client/src/components/Nav/SettingsTabs/index.ts b/client/src/components/Nav/SettingsTabs/index.ts index b3398431f..9eab047c8 100644 --- a/client/src/components/Nav/SettingsTabs/index.ts +++ b/client/src/components/Nav/SettingsTabs/index.ts @@ -1,7 +1,6 @@ export { default as General } from './General/General'; export { default as Chat } from './Chat/Chat'; export { default as Data } from './Data/Data'; -export { default as Beta } from './Beta/Beta'; export { default as Commands } from './Commands/Commands'; export { RevokeKeysButton } from './Data/RevokeKeysButton'; export { default as Account } from './Account/Account'; diff --git a/client/src/components/SidePanel/Agents/Artifacts.tsx b/client/src/components/SidePanel/Agents/Artifacts.tsx index 2a814cc7f..a8b0bba7c 100644 --- a/client/src/components/SidePanel/Agents/Artifacts.tsx +++ b/client/src/components/SidePanel/Agents/Artifacts.tsx @@ -60,7 +60,7 @@ export default function Artifacts() { /> ; isChecked: boolean }) => void; + setValue?: (values: { + e?: React.ChangeEvent; + value: boolean | string; + }) => void; } >(({ icon, label, setValue, className, checked, defaultChecked, isCheckedClassName }, ref) => { const checkbox = useCheckboxStore(); @@ -22,7 +25,7 @@ const CheckboxButton = React.forwardRef< if (typeof isChecked !== 'boolean') { return; } - setValue?.({ e, isChecked: !isChecked }); + setValue?.({ e, value: !isChecked }); }; // Sync with controlled checked prop diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index cd6a71620..6bb35b001 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -25,7 +25,6 @@ import type { TAskFunction, ExtendedFile } from '~/common'; import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete'; import useGetSender from '~/hooks/Conversations/useGetSender'; import store, { useGetEphemeralAgent } from '~/store'; -import { getArtifactsMode } from '~/utils/artifacts'; import { getEndpointField, logger } from '~/utils'; import useUserKey from '~/hooks/Input/useUserKey'; import { useNavigate } from 'react-router-dom'; @@ -68,9 +67,6 @@ export default function useChatFunctions({ const setFilesToDelete = useSetFilesToDelete(); const getEphemeralAgent = useGetEphemeralAgent(); const isTemporary = useRecoilValue(store.isTemporary); - const codeArtifacts = useRecoilValue(store.codeArtifacts); - const includeShadcnui = useRecoilValue(store.includeShadcnui); - const customPromptMode = useRecoilValue(store.customPromptMode); const { getExpiry } = useUserKey(immutableConversation?.endpoint ?? ''); const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index)); const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1)); @@ -187,10 +183,6 @@ export default function useChatFunctions({ endpointType, overrideConvoId, overrideUserMessageId, - artifacts: - endpoint !== EModelEndpoint.agents - ? getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode }) - : undefined, }, convo, ) as TEndpointOption; diff --git a/client/src/hooks/Plugins/useToolToggle.ts b/client/src/hooks/Plugins/useToolToggle.ts index 27b1ff284..f95577d4c 100644 --- a/client/src/hooks/Plugins/useToolToggle.ts +++ b/client/src/hooks/Plugins/useToolToggle.ts @@ -1,6 +1,6 @@ -import { useRef, useEffect, useCallback, useMemo } from 'react'; -import { useRecoilState } from 'recoil'; +import { useCallback, useMemo, useEffect } from 'react'; import debounce from 'lodash/debounce'; +import { useRecoilState } from 'recoil'; import { Constants, LocalStorageKeys } from 'librechat-data-provider'; import type { VerifyToolAuthResponse } from 'librechat-data-provider'; import type { UseQueryOptions } from '@tanstack/react-query'; @@ -19,9 +19,11 @@ const storageCondition = (value: unknown, rawCurrentValue?: string | null) => { console.error(e); } } - return value !== undefined && value !== null && value !== '' && value !== false; + return value !== undefined && value !== null; }; +type ToolValue = boolean | string; + interface UseToolToggleOptions { conversationId?: string | null; toolKey: string; @@ -60,36 +62,52 @@ export function useToolToggle({ [externalIsAuthenticated, authConfig, authQuery.data?.authenticated], ); - const isToolEnabled = useMemo(() => { - return ephemeralAgent?.[toolKey] ?? false; - }, [ephemeralAgent, toolKey]); - - /** Track previous value to prevent infinite loops */ - const prevIsToolEnabled = useRef(isToolEnabled); - - const [toggleState, setToggleState] = useLocalStorage( + // Keep localStorage in sync + const [, setLocalStorageValue] = useLocalStorage( `${localStorageKey}${key}`, - isToolEnabled, + false, undefined, storageCondition, ); + // The actual current value comes from ephemeralAgent + const toolValue = useMemo(() => { + return ephemeralAgent?.[toolKey] ?? false; + }, [ephemeralAgent, toolKey]); + + const isToolEnabled = useMemo(() => { + // For backward compatibility, treat truthy string values as enabled + if (typeof toolValue === 'string') { + return toolValue.length > 0; + } + return toolValue === true; + }, [toolValue]); + + // Sync to localStorage when ephemeralAgent changes + useEffect(() => { + const value = ephemeralAgent?.[toolKey]; + if (value !== undefined) { + setLocalStorageValue(value); + } + }, [ephemeralAgent, toolKey, setLocalStorageValue]); + const [isPinned, setIsPinned] = useLocalStorage(`${localStorageKey}pinned`, false); const handleChange = useCallback( - ({ e, isChecked }: { e?: React.ChangeEvent; isChecked: boolean }) => { + ({ e, value }: { e?: React.ChangeEvent; value: ToolValue }) => { if (isAuthenticated !== undefined && !isAuthenticated && setIsDialogOpen) { setIsDialogOpen(true); e?.preventDefault?.(); return; } - setToggleState(isChecked); + + // Update ephemeralAgent (localStorage will sync automatically via effect) setEphemeralAgent((prev) => ({ - ...prev, - [toolKey]: isChecked, + ...(prev || {}), + [toolKey]: value, })); }, - [setToggleState, setIsDialogOpen, isAuthenticated, setEphemeralAgent, toolKey], + [setIsDialogOpen, isAuthenticated, setEphemeralAgent, toolKey], ); const debouncedChange = useMemo( @@ -97,18 +115,12 @@ export function useToolToggle({ [handleChange], ); - useEffect(() => { - if (prevIsToolEnabled.current !== isToolEnabled) { - setToggleState(isToolEnabled); - } - prevIsToolEnabled.current = isToolEnabled; - }, [isToolEnabled, setToggleState]); - return { - toggleState, + toggleState: toolValue, // Return the actual value from ephemeralAgent handleChange, isToolEnabled, - setToggleState, + toolValue, + setToggleState: (value: ToolValue) => handleChange({ value }), // Adapter for direct setting ephemeralAgent, debouncedChange, setEphemeralAgent, diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts index b5e132109..46cc37eed 100644 --- a/client/src/hooks/SSE/useEventHandlers.ts +++ b/client/src/hooks/SSE/useEventHandlers.ts @@ -68,7 +68,7 @@ const createErrorMessage = ({ errorMetadata?: Partial; submission: EventSubmission; error?: Error | unknown; -}) => { +}): TMessage => { const currentMessages = getMessages(); const latestMessage = currentMessages?.[currentMessages.length - 1]; let errorMessage: TMessage; @@ -123,7 +123,7 @@ const createErrorMessage = ({ error: true, }; } - return tMessageSchema.parse(errorMessage); + return tMessageSchema.parse(errorMessage) as TMessage; }; export const getConvoTitle = ({ @@ -374,9 +374,6 @@ export default function useEventHandlers({ }); let update = {} as TConversation; - if (conversationId) { - applyAgentTemplate(conversationId, submission.conversation.conversationId); - } if (setConversation && !isAddedRequest) { setConversation((prevState) => { const parentId = isRegenerate ? userMessage.overrideParentMessageId : parentMessageId; @@ -411,6 +408,14 @@ export default function useEventHandlers({ }); } + if (conversationId) { + applyAgentTemplate( + conversationId, + submission.conversation.conversationId, + submission.ephemeralAgent, + ); + } + if (resetLatestMessage) { resetLatestMessage(); } @@ -513,6 +518,15 @@ export default function useEventHandlers({ } return update; }); + + if (conversation.conversationId && submission.ephemeralAgent) { + applyAgentTemplate( + conversation.conversationId, + submissionConvo.conversationId, + submission.ephemeralAgent, + ); + } + if (location.pathname === '/c/new') { navigate(`/c/${conversation.conversationId}`, { replace: true }); } @@ -521,18 +535,19 @@ export default function useEventHandlers({ setIsSubmitting(false); }, [ - setShowStopButton, - setCompleted, - getMessages, - announcePolite, + navigate, genTitle, - setConversation, - isAddedRequest, - setIsSubmitting, + getMessages, setMessages, queryClient, + setCompleted, + isAddedRequest, + announcePolite, + setConversation, + setIsSubmitting, + setShowStopButton, location.pathname, - navigate, + applyAgentTemplate, ], ); @@ -550,7 +565,7 @@ export default function useEventHandlers({ queryClient.setQueryData([QueryKeys.messages, convoId], finalMessages); }; - const parseErrorResponse = (data: TResData | Partial) => { + const parseErrorResponse = (data: TResData | Partial): TMessage => { const metadata = data['responseMessage'] ?? data; const errorMessage: Partial = { ...initialResponse, @@ -563,7 +578,7 @@ export default function useEventHandlers({ errorMessage.messageId = v4(); } - return tMessageSchema.parse(errorMessage); + return tMessageSchema.parse(errorMessage) as TMessage; }; if (!data) { @@ -613,7 +628,7 @@ export default function useEventHandlers({ ...data, error: true, parentMessageId: userMessage.messageId, - }); + }) as TMessage; setErrorMessages(receivedConvoId, errorResponse); if (receivedConvoId && paramId === Constants.NEW_CONVO && newConversation) { diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index c784e6468..83930f3a9 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -367,7 +367,6 @@ "com_nav_delete_cache_storage": "Delete TTS cache storage", "com_nav_delete_data_info": "All your data will be deleted.", "com_nav_delete_warning": "WARNING: This will permanently delete your account.", - "com_nav_edit_chat_badges": "Edit Chat Badges", "com_nav_enable_cache_tts": "Enable cache TTS", "com_nav_enable_cloud_browser_voice": "Use cloud-based voices", "com_nav_enabled": "Enabled", @@ -578,6 +577,7 @@ "com_ui_artifacts": "Artifacts", "com_ui_artifacts_toggle": "Toggle Artifacts UI", "com_ui_artifacts_toggle_agent": "Enable Artifacts", + "com_ui_artifacts_options": "Artifacts Options", "com_ui_ascending": "Asc", "com_ui_assistant": "Assistant", "com_ui_assistant_delete_error": "There was an error deleting the assistant", @@ -819,8 +819,7 @@ "com_ui_import_conversation_file_type_error": "Unsupported import type", "com_ui_import_conversation_info": "Import conversations from a JSON file", "com_ui_import_conversation_success": "Conversations imported successfully", - "com_ui_include_shadcnui": "Include shadcn/ui components instructions", - "com_ui_include_shadcnui_agent": "Include shadcn/ui instructions", + "com_ui_include_shadcnui": "Include shadcn/ui", "com_ui_input": "Input", "com_ui_instructions": "Instructions", "com_ui_key": "Key", @@ -1075,4 +1074,4 @@ "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", "com_user_message": "You" -} \ No newline at end of file +} diff --git a/client/src/store/agents.ts b/client/src/store/agents.ts index c539345ec..a62fae604 100644 --- a/client/src/store/agents.ts +++ b/client/src/store/agents.ts @@ -23,7 +23,11 @@ export const ephemeralAgentByConvoId = atomFamily - async (targetId: string, _sourceId: string | null = Constants.NEW_CONVO) => { + async ( + targetId: string, + _sourceId: string | null = Constants.NEW_CONVO, + ephemeralAgentState?: TEphemeralAgent | null, + ) => { const sourceId = _sourceId || Constants.NEW_CONVO; logger.log('agents', `Attempting to apply template from "${sourceId}" to "${targetId}"`); @@ -35,7 +39,8 @@ export function useApplyNewAgentTemplate() { try { // 1. Get the current agent state from the "new" conversation template using snapshot // getPromise reads the value without subscribing - const agentTemplate = await snapshot.getPromise(ephemeralAgentByConvoId(sourceId)); + const agentTemplate = + ephemeralAgentState ?? (await snapshot.getPromise(ephemeralAgentByConvoId(sourceId))); // 2. Check if a template state actually exists if (agentTemplate) { diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index 8fbce1b3d..0fe4dccd2 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -43,9 +43,6 @@ const localStorageAtoms = { // Beta features settings modularChat: atomWithLocalStorage('modularChat', true), LaTeXParsing: atomWithLocalStorage('LaTeXParsing', true), - codeArtifacts: atomWithLocalStorage('codeArtifacts', false), - includeShadcnui: atomWithLocalStorage('includeShadcnui', false), - customPromptMode: atomWithLocalStorage('customPromptMode', false), centerFormOnLanding: atomWithLocalStorage('centerFormOnLanding', true), showFooter: atomWithLocalStorage('showFooter', true), diff --git a/client/src/utils/artifacts.ts b/client/src/utils/artifacts.ts index 55658d07c..0f052dbda 100644 --- a/client/src/utils/artifacts.ts +++ b/client/src/utils/artifacts.ts @@ -1,29 +1,10 @@ import dedent from 'dedent'; -import { ArtifactModes, shadcnComponents } from 'librechat-data-provider'; +import { shadcnComponents } from 'librechat-data-provider'; import type { SandpackProviderProps, SandpackPredefinedTemplate, } from '@codesandbox/sandpack-react'; -export const getArtifactsMode = ({ - codeArtifacts, - includeShadcnui, - customPromptMode, -}: { - codeArtifacts: boolean; - includeShadcnui: boolean; - customPromptMode: boolean; -}): ArtifactModes | undefined => { - if (!codeArtifacts) { - return undefined; - } else if (customPromptMode) { - return ArtifactModes.CUSTOM; - } else if (includeShadcnui) { - return ArtifactModes.SHADCNUI; - } - return ArtifactModes.DEFAULT; -}; - const artifactFilename = { 'application/vnd.mermaid': 'App.tsx', 'application/vnd.react': 'App.tsx', diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 4075d9f20..7ee2efbf4 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1459,6 +1459,8 @@ export enum LocalStorageKeys { LAST_WEB_SEARCH_TOGGLE_ = 'LAST_WEB_SEARCH_TOGGLE_', /** Last checked toggle for File Search per conversation ID */ LAST_FILE_SEARCH_TOGGLE_ = 'LAST_FILE_SEARCH_TOGGLE_', + /** Last checked toggle for Artifacts per conversation ID */ + LAST_ARTIFACTS_TOGGLE_ = 'LAST_ARTIFACTS_TOGGLE_', /** Key for the last selected agent provider */ LAST_AGENT_PROVIDER = 'lastAgentProvider', /** Key for the last selected agent model */ diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index b5fdecab3..3c8ccc870 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -117,7 +117,6 @@ export type TPayload = Partial & }; export type TSubmission = { - artifacts?: string; plugin?: TResPlugin; plugins?: TResPlugin[]; userMessage: TMessage;