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 './SetConvoContext';
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 type { LucideIcon } from 'lucide-react';
import CodeInterpreter from './CodeInterpreter';
import { BadgeRowProvider } from '~/Providers';
import ToolsDropdown from './ToolsDropdown';
import type { BadgeItem } from '~/common';
import { useChatBadges } from '~/hooks';
import { Badge } from '~/components/ui';
import ToolsDropdown from './ToolsDropdown';
import MCPSelect from './MCPSelect';
import WebSearch from './WebSearch';
import store from '~/store';
@ -314,79 +315,81 @@ function BadgeRow({
}, [dragState.draggedBadge, handleMouseMove, handleMouseUp]);
return (
<div ref={containerRef} className="relative flex flex-wrap items-center gap-1">
<ToolsDropdown conversationId={conversationId} />
{tempBadges.map((badge, index) => (
<React.Fragment key={badge.id}>
{dragState.draggedBadge && dragState.insertIndex === index && ghostBadge && (
<div className="badge-icon h-full">
<Badge
id={ghostBadge.id}
icon={ghostBadge.icon as LucideIcon}
label={ghostBadge.label}
isActive={dragState.draggedBadgeActive}
isEditing={isEditing}
isAvailable={ghostBadge.isAvailable}
isInChat={isInChat}
/>
</div>
)}
<BadgeWrapper
badge={badge}
isEditing={isEditing}
isInChat={isInChat}
onToggle={handleBadgeToggle}
onDelete={handleDelete}
onMouseDown={handleMouseDown}
badgeRefs={badgeRefs}
/>
</React.Fragment>
))}
{dragState.draggedBadge && dragState.insertIndex === tempBadges.length && ghostBadge && (
<div className="badge-icon h-full">
<Badge
id={ghostBadge.id}
icon={ghostBadge.icon as LucideIcon}
label={ghostBadge.label}
isActive={dragState.draggedBadgeActive}
isEditing={isEditing}
isAvailable={ghostBadge.isAvailable}
isInChat={isInChat}
/>
</div>
)}
{showEphemeralBadges === true && (
<>
<WebSearch conversationId={conversationId} />
<CodeInterpreter conversationId={conversationId} />
<MCPSelect conversationId={conversationId} />
</>
)}
{ghostBadge && (
<div
className="ghost-badge h-full"
style={{
position: 'absolute',
top: 0,
left: 0,
transform: `translateX(${dragState.mouseX - dragState.offsetX - (containerRectRef.current?.left || 0)}px)`,
zIndex: 10,
pointerEvents: 'none',
}}
>
<Badge
id={ghostBadge.id}
icon={ghostBadge.icon as LucideIcon}
label={ghostBadge.label}
isActive={dragState.draggedBadgeActive}
isAvailable={ghostBadge.isAvailable}
isInChat={isInChat}
isEditing
isDragging
/>
</div>
)}
</div>
<BadgeRowProvider conversationId={conversationId}>
<div ref={containerRef} className="relative flex flex-wrap items-center gap-2">
<ToolsDropdown />
{tempBadges.map((badge, index) => (
<React.Fragment key={badge.id}>
{dragState.draggedBadge && dragState.insertIndex === index && ghostBadge && (
<div className="badge-icon h-full">
<Badge
id={ghostBadge.id}
icon={ghostBadge.icon as LucideIcon}
label={ghostBadge.label}
isActive={dragState.draggedBadgeActive}
isEditing={isEditing}
isAvailable={ghostBadge.isAvailable}
isInChat={isInChat}
/>
</div>
)}
<BadgeWrapper
badge={badge}
isEditing={isEditing}
isInChat={isInChat}
onToggle={handleBadgeToggle}
onDelete={handleDelete}
onMouseDown={handleMouseDown}
badgeRefs={badgeRefs}
/>
</React.Fragment>
))}
{dragState.draggedBadge && dragState.insertIndex === tempBadges.length && ghostBadge && (
<div className="badge-icon h-full">
<Badge
id={ghostBadge.id}
icon={ghostBadge.icon as LucideIcon}
label={ghostBadge.label}
isActive={dragState.draggedBadgeActive}
isEditing={isEditing}
isAvailable={ghostBadge.isAvailable}
isInChat={isInChat}
/>
</div>
)}
{showEphemeralBadges === true && (
<>
<WebSearch />
<CodeInterpreter />
<MCPSelect />
</>
)}
{ghostBadge && (
<div
className="ghost-badge h-full"
style={{
position: 'absolute',
top: 0,
left: 0,
transform: `translateX(${dragState.mouseX - dragState.offsetX - (containerRectRef.current?.left || 0)}px)`,
zIndex: 10,
pointerEvents: 'none',
}}
>
<Badge
id={ghostBadge.id}
icon={ghostBadge.icon as LucideIcon}
label={ghostBadge.label}
isActive={dragState.draggedBadgeActive}
isAvailable={ghostBadge.isAvailable}
isInChat={isInChat}
isEditing
isDragging
/>
</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 CheckboxButton from '~/components/ui/CheckboxButton';
import { useVerifyAgentToolAuth } from '~/data-provider';
import { useBadgeRowContext } from '~/Providers';
function CodeInterpreter({ conversationId }: { conversationId?: string | null }) {
function CodeInterpreter() {
const triggerRef = useRef<HTMLInputElement>(null);
const localize = useLocalize();
const { conversationId } = useBadgeRowContext();
const canRunCode = useHasAccess({
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 type { TPlugin, TPluginAuthConfig, TUpdateUserPlugins } from 'librechat-data-provider';
import MCPConfigDialog, { type ConfigFieldDetail } from '~/components/ui/MCPConfigDialog';
import { useToastContext, useBadgeRowContext } from '~/Providers';
import { useAvailableToolsQuery } from '~/data-provider';
import useLocalStorage from '~/hooks/useLocalStorageAlt';
import MultiSelect from '~/components/ui/MultiSelect';
import { ephemeralAgentByConvoId } from '~/store';
import { useToastContext } from '~/Providers';
import MCPIcon from '~/components/ui/MCPIcon';
import { useLocalize } from '~/hooks';
@ -40,9 +40,10 @@ const storageCondition = (value: unknown, rawCurrentValue?: string | null) => {
return Array.isArray(value) && value.length > 0;
};
function MCPSelect({ conversationId }: { conversationId?: string | null }) {
function MCPSelect() {
const localize = useLocalize();
const { showToast } = useToastContext();
const { conversationId } = useBadgeRowContext();
const key = conversationId ?? Constants.NEW_CONVO;
const hasSetFetched = useRef<string | null>(null);
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);

View file

@ -2,16 +2,17 @@ import React, { useState, useMemo } from 'react';
import * as Ariakit from '@ariakit/react';
import { Settings2, Search, ImageIcon, Globe, PenTool } from 'lucide-react';
import { TooltipAnchor, DropdownPopup } from '~/components';
import { useBadgeRowContext } from '~/Providers';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
interface ToolsDropdownProps {
conversationId?: string | null;
disabled?: boolean;
}
const ToolsDropdown = ({ disabled, conversationId }: ToolsDropdownProps) => {
const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
const localize = useLocalize();
const { conversationId } = useBadgeRowContext();
const isDisabled = disabled ?? 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 CheckboxButton from '~/components/ui/CheckboxButton';
import { useVerifyAgentToolAuth } from '~/data-provider';
import { useBadgeRowContext } from '~/Providers';
function WebSearch({ conversationId }: { conversationId?: string | null }) {
function WebSearch() {
const triggerRef = useRef<HTMLInputElement>(null);
const localize = useLocalize();
const { conversationId } = useBadgeRowContext();
const canUseWebSearch = useHasAccess({
permissionType: PermissionTypes.WEB_SEARCH,