feat: introduce BadgeRowContext and BadgeRowProvider for managing conversation state, refactor related components to utilize context

This commit is contained in:
Danny Avila 2025-06-21 23:12:41 -04:00
parent d247e48212
commit 7a190ee33a
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
7 changed files with 119 additions and 80 deletions

View file

@ -0,0 +1,28 @@
import React, { createContext, useContext } from 'react';
interface BadgeRowContextType {
conversationId?: string | null;
}
const BadgeRowContext = createContext<BadgeRowContextType | undefined>(undefined);
export function useBadgeRowContext() {
const context = useContext(BadgeRowContext);
if (context === undefined) {
throw new Error('useBadgeRowContext must be used within a BadgeRowProvider');
}
return context;
}
interface BadgeRowProviderProps {
children: React.ReactNode;
conversationId?: string | null;
}
export default function BadgeRowProvider({ children, conversationId }: BadgeRowProviderProps) {
const value: BadgeRowContextType = {
conversationId,
};
return <BadgeRowContext.Provider value={value}>{children}</BadgeRowContext.Provider>;
}

View file

@ -22,3 +22,5 @@ export * from './CodeBlockContext';
export * from './ToolCallsMapContext'; export * from './ToolCallsMapContext';
export * from './SetConvoContext'; export * from './SetConvoContext';
export * from './SearchContext'; export * from './SearchContext';
export * from './BadgeRowContext';
export { default as BadgeRowProvider } from './BadgeRowContext';

View file

@ -11,10 +11,11 @@ import React, {
import { useRecoilValue, useRecoilCallback } from 'recoil'; import { useRecoilValue, useRecoilCallback } from 'recoil';
import type { LucideIcon } from 'lucide-react'; import type { LucideIcon } from 'lucide-react';
import CodeInterpreter from './CodeInterpreter'; import CodeInterpreter from './CodeInterpreter';
import { BadgeRowProvider } from '~/Providers';
import ToolsDropdown from './ToolsDropdown';
import type { BadgeItem } from '~/common'; import type { BadgeItem } from '~/common';
import { useChatBadges } from '~/hooks'; import { useChatBadges } from '~/hooks';
import { Badge } from '~/components/ui'; import { Badge } from '~/components/ui';
import ToolsDropdown from './ToolsDropdown';
import MCPSelect from './MCPSelect'; import MCPSelect from './MCPSelect';
import WebSearch from './WebSearch'; import WebSearch from './WebSearch';
import store from '~/store'; import store from '~/store';
@ -314,79 +315,81 @@ function BadgeRow({
}, [dragState.draggedBadge, handleMouseMove, handleMouseUp]); }, [dragState.draggedBadge, handleMouseMove, handleMouseUp]);
return ( return (
<div ref={containerRef} className="relative flex flex-wrap items-center gap-1"> <BadgeRowProvider conversationId={conversationId}>
<ToolsDropdown conversationId={conversationId} /> <div ref={containerRef} className="relative flex flex-wrap items-center gap-2">
{tempBadges.map((badge, index) => ( <ToolsDropdown />
<React.Fragment key={badge.id}> {tempBadges.map((badge, index) => (
{dragState.draggedBadge && dragState.insertIndex === index && ghostBadge && ( <React.Fragment key={badge.id}>
<div className="badge-icon h-full"> {dragState.draggedBadge && dragState.insertIndex === index && ghostBadge && (
<Badge <div className="badge-icon h-full">
id={ghostBadge.id} <Badge
icon={ghostBadge.icon as LucideIcon} id={ghostBadge.id}
label={ghostBadge.label} icon={ghostBadge.icon as LucideIcon}
isActive={dragState.draggedBadgeActive} label={ghostBadge.label}
isEditing={isEditing} isActive={dragState.draggedBadgeActive}
isAvailable={ghostBadge.isAvailable} isEditing={isEditing}
isInChat={isInChat} isAvailable={ghostBadge.isAvailable}
/> isInChat={isInChat}
</div> />
)} </div>
<BadgeWrapper )}
badge={badge} <BadgeWrapper
isEditing={isEditing} badge={badge}
isInChat={isInChat} isEditing={isEditing}
onToggle={handleBadgeToggle} isInChat={isInChat}
onDelete={handleDelete} onToggle={handleBadgeToggle}
onMouseDown={handleMouseDown} onDelete={handleDelete}
badgeRefs={badgeRefs} onMouseDown={handleMouseDown}
/> badgeRefs={badgeRefs}
</React.Fragment> />
))} </React.Fragment>
{dragState.draggedBadge && dragState.insertIndex === tempBadges.length && ghostBadge && ( ))}
<div className="badge-icon h-full"> {dragState.draggedBadge && dragState.insertIndex === tempBadges.length && ghostBadge && (
<Badge <div className="badge-icon h-full">
id={ghostBadge.id} <Badge
icon={ghostBadge.icon as LucideIcon} id={ghostBadge.id}
label={ghostBadge.label} icon={ghostBadge.icon as LucideIcon}
isActive={dragState.draggedBadgeActive} label={ghostBadge.label}
isEditing={isEditing} isActive={dragState.draggedBadgeActive}
isAvailable={ghostBadge.isAvailable} isEditing={isEditing}
isInChat={isInChat} isAvailable={ghostBadge.isAvailable}
/> isInChat={isInChat}
</div> />
)} </div>
{showEphemeralBadges === true && ( )}
<> {showEphemeralBadges === true && (
<WebSearch conversationId={conversationId} /> <>
<CodeInterpreter conversationId={conversationId} /> <WebSearch />
<MCPSelect conversationId={conversationId} /> <CodeInterpreter />
</> <MCPSelect />
)} </>
{ghostBadge && ( )}
<div {ghostBadge && (
className="ghost-badge h-full" <div
style={{ className="ghost-badge h-full"
position: 'absolute', style={{
top: 0, position: 'absolute',
left: 0, top: 0,
transform: `translateX(${dragState.mouseX - dragState.offsetX - (containerRectRef.current?.left || 0)}px)`, left: 0,
zIndex: 10, transform: `translateX(${dragState.mouseX - dragState.offsetX - (containerRectRef.current?.left || 0)}px)`,
pointerEvents: 'none', zIndex: 10,
}} pointerEvents: 'none',
> }}
<Badge >
id={ghostBadge.id} <Badge
icon={ghostBadge.icon as LucideIcon} id={ghostBadge.id}
label={ghostBadge.label} icon={ghostBadge.icon as LucideIcon}
isActive={dragState.draggedBadgeActive} label={ghostBadge.label}
isAvailable={ghostBadge.isAvailable} isActive={dragState.draggedBadgeActive}
isInChat={isInChat} isAvailable={ghostBadge.isAvailable}
isEditing isInChat={isInChat}
isDragging isEditing
/> isDragging
</div> />
)} </div>
</div> )}
</div>
</BadgeRowProvider>
); );
} }

View file

@ -11,10 +11,12 @@ import ApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog';
import { useLocalize, useHasAccess, useCodeApiKeyForm, useToolToggle } from '~/hooks'; import { useLocalize, useHasAccess, useCodeApiKeyForm, useToolToggle } from '~/hooks';
import CheckboxButton from '~/components/ui/CheckboxButton'; import CheckboxButton from '~/components/ui/CheckboxButton';
import { useVerifyAgentToolAuth } from '~/data-provider'; import { useVerifyAgentToolAuth } from '~/data-provider';
import { useBadgeRowContext } from '~/Providers';
function CodeInterpreter({ conversationId }: { conversationId?: string | null }) { function CodeInterpreter() {
const triggerRef = useRef<HTMLInputElement>(null); const triggerRef = useRef<HTMLInputElement>(null);
const localize = useLocalize(); const localize = useLocalize();
const { conversationId } = useBadgeRowContext();
const canRunCode = useHasAccess({ const canRunCode = useHasAccess({
permissionType: PermissionTypes.RUN_CODE, permissionType: PermissionTypes.RUN_CODE,

View file

@ -5,11 +5,11 @@ import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-quer
import { Constants, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider'; import { Constants, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import type { TPlugin, TPluginAuthConfig, TUpdateUserPlugins } from 'librechat-data-provider'; import type { TPlugin, TPluginAuthConfig, TUpdateUserPlugins } from 'librechat-data-provider';
import MCPConfigDialog, { type ConfigFieldDetail } from '~/components/ui/MCPConfigDialog'; import MCPConfigDialog, { type ConfigFieldDetail } from '~/components/ui/MCPConfigDialog';
import { useToastContext, useBadgeRowContext } from '~/Providers';
import { useAvailableToolsQuery } from '~/data-provider'; import { useAvailableToolsQuery } from '~/data-provider';
import useLocalStorage from '~/hooks/useLocalStorageAlt'; import useLocalStorage from '~/hooks/useLocalStorageAlt';
import MultiSelect from '~/components/ui/MultiSelect'; import MultiSelect from '~/components/ui/MultiSelect';
import { ephemeralAgentByConvoId } from '~/store'; import { ephemeralAgentByConvoId } from '~/store';
import { useToastContext } from '~/Providers';
import MCPIcon from '~/components/ui/MCPIcon'; import MCPIcon from '~/components/ui/MCPIcon';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
@ -40,9 +40,10 @@ const storageCondition = (value: unknown, rawCurrentValue?: string | null) => {
return Array.isArray(value) && value.length > 0; return Array.isArray(value) && value.length > 0;
}; };
function MCPSelect({ conversationId }: { conversationId?: string | null }) { function MCPSelect() {
const localize = useLocalize(); const localize = useLocalize();
const { showToast } = useToastContext(); const { showToast } = useToastContext();
const { conversationId } = useBadgeRowContext();
const key = conversationId ?? Constants.NEW_CONVO; const key = conversationId ?? Constants.NEW_CONVO;
const hasSetFetched = useRef<string | null>(null); const hasSetFetched = useRef<string | null>(null);
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);

View file

@ -2,16 +2,17 @@ import React, { useState, useMemo } from 'react';
import * as Ariakit from '@ariakit/react'; import * as Ariakit from '@ariakit/react';
import { Settings2, Search, ImageIcon, Globe, PenTool } from 'lucide-react'; import { Settings2, Search, ImageIcon, Globe, PenTool } from 'lucide-react';
import { TooltipAnchor, DropdownPopup } from '~/components'; import { TooltipAnchor, DropdownPopup } from '~/components';
import { useBadgeRowContext } from '~/Providers';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';
interface ToolsDropdownProps { interface ToolsDropdownProps {
conversationId?: string | null;
disabled?: boolean; disabled?: boolean;
} }
const ToolsDropdown = ({ disabled, conversationId }: ToolsDropdownProps) => { const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
const localize = useLocalize(); const localize = useLocalize();
const { conversationId } = useBadgeRowContext();
const isDisabled = disabled ?? false; const isDisabled = disabled ?? false;
const [isPopoverActive, setIsPopoverActive] = useState(false); const [isPopoverActive, setIsPopoverActive] = useState(false);

View file

@ -5,10 +5,12 @@ import { useLocalize, useHasAccess, useSearchApiKeyForm, useToolToggle } from '~
import ApiKeyDialog from '~/components/SidePanel/Agents/Search/ApiKeyDialog'; import ApiKeyDialog from '~/components/SidePanel/Agents/Search/ApiKeyDialog';
import CheckboxButton from '~/components/ui/CheckboxButton'; import CheckboxButton from '~/components/ui/CheckboxButton';
import { useVerifyAgentToolAuth } from '~/data-provider'; import { useVerifyAgentToolAuth } from '~/data-provider';
import { useBadgeRowContext } from '~/Providers';
function WebSearch({ conversationId }: { conversationId?: string | null }) { function WebSearch() {
const triggerRef = useRef<HTMLInputElement>(null); const triggerRef = useRef<HTMLInputElement>(null);
const localize = useLocalize(); const localize = useLocalize();
const { conversationId } = useBadgeRowContext();
const canUseWebSearch = useHasAccess({ const canUseWebSearch = useHasAccess({
permissionType: PermissionTypes.WEB_SEARCH, permissionType: PermissionTypes.WEB_SEARCH,