mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
🪄 feat: Artifacts Badge & Optimize Ephemeral Agent State (#8252)
* 🔧 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
This commit is contained in:
parent
458580ec87
commit
a288ad1d9c
23 changed files with 547 additions and 232 deletions
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<typeof useToolToggle>;
|
||||
codeInterpreter: ReturnType<typeof useToolToggle>;
|
||||
fileSearch: ReturnType<typeof useToolToggle>;
|
||||
artifacts: ReturnType<typeof useToolToggle>;
|
||||
codeApiKeyForm: ReturnType<typeof useCodeApiKeyForm>;
|
||||
searchApiKeyForm: ReturnType<typeof useSearchApiKeyForm>;
|
||||
startupConfig: ReturnType<typeof useGetStartupConfig>['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<string>('');
|
||||
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<string, any> = {};
|
||||
|
||||
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,
|
||||
|
|
152
client/src/components/Chat/Input/Artifacts.tsx
Normal file
152
client/src/components/Chat/Input/Artifacts.tsx
Normal file
|
@ -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<ArtifactsToggleState>(() => {
|
||||
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 (
|
||||
<div className="flex">
|
||||
<CheckboxButton
|
||||
className={cn('max-w-fit', isEnabled && 'rounded-r-none border-r-0')}
|
||||
checked={isEnabled}
|
||||
setValue={handleToggle}
|
||||
label={localize('com_ui_artifacts')}
|
||||
isCheckedClassName="border-amber-600/40 bg-amber-500/10 hover:bg-amber-700/10"
|
||||
icon={<WandSparkles className="icon-md" />}
|
||||
/>
|
||||
|
||||
{isEnabled && (
|
||||
<Ariakit.MenuProvider open={isPopoverOpen} setOpen={setIsPopoverOpen}>
|
||||
<Ariakit.MenuButton
|
||||
className={cn(
|
||||
'w-7 rounded-l-none rounded-r-full border-b border-l-0 border-r border-t border-border-light md:w-6',
|
||||
'border-amber-600/40 bg-amber-500/10 hover:bg-amber-700/10',
|
||||
'transition-colors',
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ChevronDown className="ml-1 h-4 w-4 text-text-secondary md:ml-0" />
|
||||
</Ariakit.MenuButton>
|
||||
|
||||
<Ariakit.Menu
|
||||
gutter={8}
|
||||
className={cn(
|
||||
'animate-popover z-50 flex max-h-[300px]',
|
||||
'flex-col overflow-auto overscroll-contain rounded-xl',
|
||||
'bg-surface-secondary px-1.5 py-1 text-text-primary shadow-lg',
|
||||
'border border-border-light',
|
||||
'min-w-[250px] outline-none',
|
||||
)}
|
||||
portal
|
||||
>
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="mb-2 text-xs font-medium text-text-secondary">
|
||||
{localize('com_ui_artifacts_options')}
|
||||
</div>
|
||||
|
||||
{/* Include shadcn/ui Option */}
|
||||
<Ariakit.MenuItem
|
||||
hideOnClick={false}
|
||||
onClick={(event) => {
|
||||
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',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Ariakit.MenuItemCheck checked={isShadcnEnabled} />
|
||||
<span className="text-sm">{localize('com_ui_include_shadcnui' as any)}</span>
|
||||
</div>
|
||||
</Ariakit.MenuItem>
|
||||
|
||||
{/* Custom Prompt Mode Option */}
|
||||
<Ariakit.MenuItem
|
||||
hideOnClick={false}
|
||||
onClick={(event) => {
|
||||
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',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Ariakit.MenuItemCheck checked={isCustomEnabled} />
|
||||
<span className="text-sm">{localize('com_ui_custom_prompt_mode' as any)}</span>
|
||||
</div>
|
||||
</Ariakit.MenuItem>
|
||||
</div>
|
||||
</Ariakit.Menu>
|
||||
</Ariakit.MenuProvider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Artifacts);
|
147
client/src/components/Chat/Input/ArtifactsSubMenu.tsx
Normal file
147
client/src/components/Chat/Input/ArtifactsSubMenu.tsx
Normal file
|
@ -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 (
|
||||
<Ariakit.MenuProvider store={menuStore}>
|
||||
<Ariakit.MenuItem
|
||||
{...props}
|
||||
hideOnClick={false}
|
||||
render={
|
||||
<Ariakit.MenuButton
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
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"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<WandSparkles className="icon-md" />
|
||||
<span>{localize('com_ui_artifacts')}</span>
|
||||
{isEnabled && <ChevronRight className="ml-auto h-3 w-3" />}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsArtifactsPinned(!isArtifactsPinned);
|
||||
}}
|
||||
className={cn(
|
||||
'rounded p-1 transition-all duration-200',
|
||||
'hover:bg-surface-tertiary hover:shadow-sm',
|
||||
!isArtifactsPinned && 'text-text-secondary hover:text-text-primary',
|
||||
)}
|
||||
aria-label={isArtifactsPinned ? 'Unpin' : 'Pin'}
|
||||
>
|
||||
<div className="h-4 w-4">
|
||||
<PinIcon unpin={isArtifactsPinned} />
|
||||
</div>
|
||||
</button>
|
||||
</Ariakit.MenuItem>
|
||||
|
||||
{isEnabled && (
|
||||
<Ariakit.Menu
|
||||
portal={true}
|
||||
unmountOnHide={true}
|
||||
className={cn(
|
||||
'animate-popover-left z-50 ml-3 flex min-w-[250px] flex-col rounded-xl',
|
||||
'border border-border-light bg-surface-secondary px-1.5 py-1 shadow-lg',
|
||||
)}
|
||||
>
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="mb-2 text-xs font-medium text-text-secondary">
|
||||
{localize('com_ui_artifacts_options')}
|
||||
</div>
|
||||
|
||||
{/* Include shadcn/ui Option */}
|
||||
<Ariakit.MenuItem
|
||||
hideOnClick={false}
|
||||
onClick={(event) => {
|
||||
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',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Ariakit.MenuItemCheck checked={isShadcnEnabled} />
|
||||
<span className="text-sm">{localize('com_ui_include_shadcnui' as any)}</span>
|
||||
</div>
|
||||
</Ariakit.MenuItem>
|
||||
|
||||
{/* Custom Prompt Mode Option */}
|
||||
<Ariakit.MenuItem
|
||||
hideOnClick={false}
|
||||
onClick={(event) => {
|
||||
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',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Ariakit.MenuItemCheck checked={isCustomEnabled} />
|
||||
<span className="text-sm">{localize('com_ui_custom_prompt_mode' as any)}</span>
|
||||
</div>
|
||||
</Ariakit.MenuItem>
|
||||
</div>
|
||||
</Ariakit.Menu>
|
||||
)}
|
||||
</Ariakit.MenuProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ArtifactsSubMenu);
|
|
@ -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<BadgeItem, 'id'>[]) => 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 (
|
||||
<BadgeRowProvider conversationId={conversationId}>
|
||||
<BadgeRowProvider conversationId={conversationId} isSubmitting={isSubmitting}>
|
||||
<div ref={containerRef} className="relative flex flex-wrap items-center gap-2">
|
||||
{showEphemeralBadges === true && <ToolsDropdown />}
|
||||
{tempBadges.map((badge, index) => (
|
||||
|
@ -364,6 +367,7 @@ function BadgeRow({
|
|||
<WebSearch />
|
||||
<CodeInterpreter />
|
||||
<FileSearch />
|
||||
<Artifacts />
|
||||
<MCPSelect />
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -305,6 +305,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
</div>
|
||||
<BadgeRow
|
||||
showEphemeralBadges={!isAgentsEndpoint(endpoint) && !isAssistantsEndpoint(endpoint)}
|
||||
isSubmitting={isSubmitting || isSubmittingAdded}
|
||||
conversationId={conversationId}
|
||||
onChange={setBadges}
|
||||
isInChat={
|
||||
|
|
|
@ -2,8 +2,9 @@ import React, { useState, useMemo, useCallback } from 'react';
|
|||
import * as Ariakit from '@ariakit/react';
|
||||
import { Globe, Settings, Settings2, TerminalSquareIcon } from 'lucide-react';
|
||||
import type { MenuItemProps } from '~/common';
|
||||
import { Permissions, PermissionTypes, AuthType } from 'librechat-data-provider';
|
||||
import { Permissions, PermissionTypes, AuthType, ArtifactModes } from 'librechat-data-provider';
|
||||
import { TooltipAnchor, DropdownPopup } from '~/components';
|
||||
import ArtifactsSubMenu from '~/components/Chat/Input/ArtifactsSubMenu';
|
||||
import MCPSubMenu from '~/components/Chat/Input/MCPSubMenu';
|
||||
import { PinIcon, VectorIcon } from '~/components/svg';
|
||||
import { useLocalize, useHasAccess } from '~/hooks';
|
||||
|
@ -21,6 +22,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
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) => (
|
||||
<ArtifactsSubMenu
|
||||
{...props}
|
||||
isArtifactsPinned={isArtifactsPinned}
|
||||
setIsArtifactsPinned={setIsArtifactsPinned}
|
||||
artifactsMode={artifacts.toggleState as string}
|
||||
handleArtifactsToggle={handleArtifactsToggle}
|
||||
handleShadcnToggle={handleShadcnToggle}
|
||||
handleCustomToggle={handleCustomToggle}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
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,
|
||||
]);
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
General,
|
||||
Chat,
|
||||
Speech,
|
||||
Beta,
|
||||
Commands,
|
||||
Data,
|
||||
Account,
|
||||
|
@ -233,9 +232,6 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
<Tabs.Content value={SettingsTabValues.CHAT}>
|
||||
<Chat />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value={SettingsTabValues.BETA}>
|
||||
<Beta />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value={SettingsTabValues.COMMANDS}>
|
||||
<Commands />
|
||||
</Tabs.Content>
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import { memo } from 'react';
|
||||
import CodeArtifacts from './CodeArtifacts';
|
||||
import ChatBadges from './ChatBadges';
|
||||
|
||||
function Beta() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||
<div className="pb-3">
|
||||
<CodeArtifacts />
|
||||
</div>
|
||||
{/* <div className="pb-3">
|
||||
<ChatBadges />
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Beta);
|
|
@ -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<boolean>(store.isEditingBadges);
|
||||
const localize = useLocalize();
|
||||
|
||||
const handleEditChatBadges = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>{localize('com_nav_edit_chat_badges')}</div>
|
||||
<Button variant="outline" onClick={handleEditChatBadges}>
|
||||
{localize('com_ui_edit')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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<boolean>(store.codeArtifacts);
|
||||
const [includeShadcnui, setIncludeShadcnui] = useRecoilState<boolean>(store.includeShadcnui);
|
||||
const [customPromptMode, setCustomPromptMode] = useRecoilState<boolean>(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 (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">{localize('com_ui_artifacts')}</h3>
|
||||
<div className="space-y-2">
|
||||
<SwitchItem
|
||||
id="codeArtifacts"
|
||||
label={localize('com_ui_artifacts_toggle')}
|
||||
checked={codeArtifacts}
|
||||
onCheckedChange={handleCodeArtifactsChange}
|
||||
hoverCardText="com_nav_info_code_artifacts"
|
||||
/>
|
||||
<SwitchItem
|
||||
id="includeShadcnui"
|
||||
label={localize('com_ui_include_shadcnui')}
|
||||
checked={includeShadcnui}
|
||||
onCheckedChange={handleIncludeShadcnuiChange}
|
||||
hoverCardText="com_nav_info_include_shadcnui"
|
||||
disabled={!codeArtifacts || customPromptMode}
|
||||
/>
|
||||
<SwitchItem
|
||||
id="customPromptMode"
|
||||
label={localize('com_ui_custom_prompt_mode')}
|
||||
checked={customPromptMode}
|
||||
onCheckedChange={handleCustomPromptModeChange}
|
||||
hoverCardText="com_nav_info_custom_prompt_mode"
|
||||
disabled={!codeArtifacts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SwitchItem({
|
||||
id,
|
||||
label,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
hoverCardText,
|
||||
disabled = false,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onCheckedChange: (value: boolean) => void;
|
||||
hoverCardText: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={disabled ? 'text-gray-400' : ''}>{label}</div>
|
||||
<HoverCardSettings side="bottom" text={hoverCardText} />
|
||||
</div>
|
||||
<Switch
|
||||
id={id}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
className="ml-4"
|
||||
data-testid={id}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -60,7 +60,7 @@ export default function Artifacts() {
|
|||
/>
|
||||
<SwitchItem
|
||||
id="includeShadcnui"
|
||||
label={localize('com_ui_include_shadcnui_agent')}
|
||||
label={localize('com_ui_include_shadcnui')}
|
||||
checked={isShadcnEnabled}
|
||||
onCheckedChange={handleShadcnuiChange}
|
||||
hoverCardText={localize('com_nav_info_include_shadcnui')}
|
||||
|
|
|
@ -12,7 +12,10 @@ const CheckboxButton = React.forwardRef<
|
|||
checked?: boolean;
|
||||
defaultChecked?: boolean;
|
||||
isCheckedClassName?: string;
|
||||
setValue?: (values: { e?: React.ChangeEvent<HTMLInputElement>; isChecked: boolean }) => void;
|
||||
setValue?: (values: {
|
||||
e?: React.ChangeEvent<HTMLInputElement>;
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<boolean>(
|
||||
// Keep localStorage in sync
|
||||
const [, setLocalStorageValue] = useLocalStorage<ToolValue>(
|
||||
`${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<boolean>(`${localStorageKey}pinned`, false);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ e, isChecked }: { e?: React.ChangeEvent<HTMLInputElement>; isChecked: boolean }) => {
|
||||
({ e, value }: { e?: React.ChangeEvent<HTMLInputElement>; 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,
|
||||
|
|
|
@ -68,7 +68,7 @@ const createErrorMessage = ({
|
|||
errorMetadata?: Partial<TMessage>;
|
||||
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<TMessage[]>([QueryKeys.messages, convoId], finalMessages);
|
||||
};
|
||||
|
||||
const parseErrorResponse = (data: TResData | Partial<TMessage>) => {
|
||||
const parseErrorResponse = (data: TResData | Partial<TMessage>): TMessage => {
|
||||
const metadata = data['responseMessage'] ?? data;
|
||||
const errorMessage: Partial<TMessage> = {
|
||||
...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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -23,7 +23,11 @@ export const ephemeralAgentByConvoId = atomFamily<TEphemeralAgent | null, string
|
|||
export function useApplyNewAgentTemplate() {
|
||||
const applyTemplate = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
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) {
|
||||
|
|
|
@ -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),
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -117,7 +117,6 @@ export type TPayload = Partial<TMessage> &
|
|||
};
|
||||
|
||||
export type TSubmission = {
|
||||
artifacts?: string;
|
||||
plugin?: TResPlugin;
|
||||
plugins?: TResPlugin[];
|
||||
userMessage: TMessage;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue