From 1380db85cb41a7f89e36a233fb3eeedfbe052990 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 6 Jul 2025 12:26:03 +0200 Subject: [PATCH] feat: Add common types and interfaces for accessibility, agents, artifacts, assistants, and tools --- client/src/common/a11y.ts | 6 + client/src/common/agents-types.ts | 33 ++ client/src/common/artifacts.ts | 27 ++ client/src/common/assistants-types.ts | 31 ++ client/src/common/index.ts | 8 + client/src/common/mcp.ts | 26 ++ client/src/common/menus.ts | 23 + client/src/common/selector.ts | 23 + client/src/common/tools.ts | 6 + client/src/common/types.ts | 624 ++++++++++++++++++++++++++ 10 files changed, 807 insertions(+) create mode 100644 client/src/common/a11y.ts create mode 100644 client/src/common/agents-types.ts create mode 100644 client/src/common/artifacts.ts create mode 100644 client/src/common/assistants-types.ts create mode 100644 client/src/common/index.ts create mode 100644 client/src/common/mcp.ts create mode 100644 client/src/common/menus.ts create mode 100644 client/src/common/selector.ts create mode 100644 client/src/common/tools.ts create mode 100644 client/src/common/types.ts diff --git a/client/src/common/a11y.ts b/client/src/common/a11y.ts new file mode 100644 index 0000000000..0a0e56eab2 --- /dev/null +++ b/client/src/common/a11y.ts @@ -0,0 +1,6 @@ +export interface AnnounceOptions { + message: string; + isStatus?: boolean; +} + +export const MESSAGE_UPDATE_INTERVAL = 7000; diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts new file mode 100644 index 0000000000..7a6c25d642 --- /dev/null +++ b/client/src/common/agents-types.ts @@ -0,0 +1,33 @@ +import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider'; +import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider'; +import type { OptionWithIcon, ExtendedFile } from './types'; + +export type TAgentOption = OptionWithIcon & + Agent & { + knowledge_files?: Array<[string, ExtendedFile]>; + context_files?: Array<[string, ExtendedFile]>; + code_files?: Array<[string, ExtendedFile]>; + }; + +export type TAgentCapabilities = { + [AgentCapabilities.web_search]: boolean; + [AgentCapabilities.file_search]: boolean; + [AgentCapabilities.execute_code]: boolean; + [AgentCapabilities.end_after_tools]?: boolean; + [AgentCapabilities.hide_sequential_outputs]?: boolean; +}; + +export type AgentForm = { + agent?: TAgentOption; + id: string; + name: string | null; + description: string | null; + instructions: string | null; + model: string | null; + model_parameters: AgentModelParameters; + tools?: string[]; + provider?: AgentProvider | OptionWithIcon; + agent_ids?: string[]; + [AgentCapabilities.artifacts]?: ArtifactModes | string; + recursion_limit?: number; +} & TAgentCapabilities; diff --git a/client/src/common/artifacts.ts b/client/src/common/artifacts.ts new file mode 100644 index 0000000000..168b0d56e3 --- /dev/null +++ b/client/src/common/artifacts.ts @@ -0,0 +1,27 @@ +export interface CodeBlock { + id: string; + language: string; + content: string; +} + +export interface Artifact { + id: string; + lastUpdateTime: number; + index?: number; + messageId?: string; + identifier?: string; + language?: string; + content?: string; + title?: string; + type?: string; +} + +export type ArtifactFiles = + | { + 'App.tsx': string; + 'index.tsx': string; + '/components/ui/MermaidDiagram.tsx': string; + } + | Partial<{ + [x: string]: string | undefined; + }>; diff --git a/client/src/common/assistants-types.ts b/client/src/common/assistants-types.ts new file mode 100644 index 0000000000..f54a841690 --- /dev/null +++ b/client/src/common/assistants-types.ts @@ -0,0 +1,31 @@ +import { Capabilities, EModelEndpoint } from 'librechat-data-provider'; +import type { Assistant, AssistantsEndpoint } from 'librechat-data-provider'; +import type { Option, ExtendedFile } from './types'; + +export type ActionsEndpoint = AssistantsEndpoint | EModelEndpoint.agents; + +export type TAssistantOption = + | string + | (Option & + Assistant & { + files?: Array<[string, ExtendedFile]>; + code_files?: Array<[string, ExtendedFile]>; + }); + +export type Actions = { + [Capabilities.code_interpreter]: boolean; + [Capabilities.image_vision]: boolean; + [Capabilities.retrieval]: boolean; +}; + +export type AssistantForm = { + assistant: TAssistantOption; + id: string; + name: string | null; + description: string | null; + instructions: string | null; + conversation_starters: string[]; + model: string; + functions: string[]; + append_current_datetime: boolean; +} & Actions; diff --git a/client/src/common/index.ts b/client/src/common/index.ts new file mode 100644 index 0000000000..e1a3ab0a05 --- /dev/null +++ b/client/src/common/index.ts @@ -0,0 +1,8 @@ +export * from './a11y'; +export * from './artifacts'; +export * from './types'; +export * from './menus'; +export * from './tools'; +export * from './selector'; +export * from './assistants-types'; +export * from './agents-types'; diff --git a/client/src/common/mcp.ts b/client/src/common/mcp.ts new file mode 100644 index 0000000000..b4f44a1f94 --- /dev/null +++ b/client/src/common/mcp.ts @@ -0,0 +1,26 @@ +import { + AuthorizationTypeEnum, + AuthTypeEnum, + TokenExchangeMethodEnum, +} from 'librechat-data-provider'; +import { MCPForm } from '~/common/types'; + +export const defaultMCPFormValues: MCPForm = { + type: AuthTypeEnum.None, + saved_auth_fields: false, + api_key: '', + authorization_type: AuthorizationTypeEnum.Basic, + custom_auth_header: '', + oauth_client_id: '', + oauth_client_secret: '', + authorization_url: '', + client_url: '', + scope: '', + token_exchange_method: TokenExchangeMethodEnum.DefaultPost, + name: '', + description: '', + url: '', + tools: [], + icon: '', + trust: false, +}; diff --git a/client/src/common/menus.ts b/client/src/common/menus.ts new file mode 100644 index 0000000000..4d70f282c2 --- /dev/null +++ b/client/src/common/menus.ts @@ -0,0 +1,23 @@ +export type RenderProp< + P = React.HTMLAttributes & { + ref?: React.Ref; + }, +> = (props: P) => React.ReactNode; + +export interface MenuItemProps { + id?: string; + label?: string; + onClick?: (e: React.MouseEvent) => void; + icon?: React.ReactNode; + kbd?: string; + show?: boolean; + disabled?: boolean; + separate?: boolean; + hideOnClick?: boolean; + dialog?: React.ReactElement; + ref?: React.Ref; + render?: + | RenderProp & { ref?: React.Ref | undefined }> + | React.ReactElement> + | undefined; +} diff --git a/client/src/common/selector.ts b/client/src/common/selector.ts new file mode 100644 index 0000000000..619d8e8f80 --- /dev/null +++ b/client/src/common/selector.ts @@ -0,0 +1,23 @@ +import React from 'react'; +import { TModelSpec, TStartupConfig } from 'librechat-data-provider'; + +export interface Endpoint { + value: string; + label: string; + hasModels: boolean; + models?: Array<{ name: string; isGlobal?: boolean }>; + icon: React.ReactNode; + agentNames?: Record; + assistantNames?: Record; + modelIcons?: Record; +} + +export interface SelectedValues { + endpoint: string | null; + model: string | null; + modelSpec: string | null; +} + +export interface ModelSelectorProps { + startupConfig: TStartupConfig | undefined; +} diff --git a/client/src/common/tools.ts b/client/src/common/tools.ts new file mode 100644 index 0000000000..140f5678c1 --- /dev/null +++ b/client/src/common/tools.ts @@ -0,0 +1,6 @@ +import type { AuthType } from 'librechat-data-provider'; + +export type ApiKeyFormData = { + apiKey: string; + authType?: string | AuthType; +}; diff --git a/client/src/common/types.ts b/client/src/common/types.ts new file mode 100644 index 0000000000..214dc349b5 --- /dev/null +++ b/client/src/common/types.ts @@ -0,0 +1,624 @@ +import { RefObject } from 'react'; +import { FileSources, EModelEndpoint } from 'librechat-data-provider'; +import type { UseMutationResult } from '@tanstack/react-query'; +import type * as InputNumberPrimitive from 'rc-input-number'; +import type { SetterOrUpdater, RecoilState } from 'recoil'; +import type { ColumnDef } from '@tanstack/react-table'; +import type * as t from 'librechat-data-provider'; +import type { LucideIcon } from 'lucide-react'; +import type { TranslationKeys } from '~/hooks'; + +export type CodeBarProps = { + lang: string; + error?: boolean; + plugin?: boolean; + blockIndex?: number; + allowExecution?: boolean; + codeRef: RefObject; +}; + +export enum PromptsEditorMode { + SIMPLE = 'simple', + ADVANCED = 'advanced', +} + +export enum STTEndpoints { + browser = 'browser', + external = 'external', +} + +export enum TTSEndpoints { + browser = 'browser', + external = 'external', +} + +export type AudioChunk = { + audio: string; + isFinal: boolean; + alignment: { + char_start_times_ms: number[]; + chars_durations_ms: number[]; + chars: string[]; + }; + normalizedAlignment: { + char_start_times_ms: number[]; + chars_durations_ms: number[]; + chars: string[]; + }; +}; + +export type BadgeItem = { + id: string; + icon: React.ComponentType; + label: string; + atom: RecoilState; + isAvailable: boolean; +}; + +export type AssistantListItem = { + id: string; + name: string; + metadata: t.Assistant['metadata']; + model: string; +}; + +export type AgentListItem = { + id: string; + name: string; + avatar: t.Agent['avatar']; +}; + +export type TPluginMap = Record; + +export type GenericSetter = (value: T | ((currentValue: T) => T)) => void; + +export type LastSelectedModels = Record; + +export type LocalizeFunction = ( + phraseKey: TranslationKeys, + options?: Record, +) => string; + +export type ChatFormValues = { text: string }; + +export const mainTextareaId = 'prompt-textarea'; +export const globalAudioId = 'global-audio'; + +export enum IconContext { + landing = 'landing', + menuItem = 'menu-item', + nav = 'nav', + message = 'message', +} + +export type IconMapProps = { + className?: string; + iconURL?: string; + context?: 'landing' | 'menu-item' | 'nav' | 'message'; + endpoint?: string | null; + endpointType?: string; + assistantName?: string; + agentName?: string; + avatar?: string; + size?: number; +}; + +export type IconComponent = React.ComponentType; +export type AgentIconComponent = React.ComponentType; +export type IconComponentTypes = IconComponent | AgentIconComponent; +export type IconsRecord = { + [key in t.EModelEndpoint | 'unknown' | string]: IconComponentTypes | null | undefined; +}; + +export type AgentIconMapProps = IconMapProps & { agentName?: string }; + +export type NavLink = { + title: TranslationKeys; + label?: string; + icon: LucideIcon | React.FC; + Component?: React.ComponentType; + onClick?: (e?: React.MouseEvent) => void; + variant?: 'default' | 'ghost'; + id: string; +}; + +export interface NavProps { + isCollapsed: boolean; + links: NavLink[]; + resize?: (size: number) => void; + defaultActive?: string; +} + +export interface DataColumnMeta { + meta: + | { + size: number | string; + } + | undefined; +} + +export enum Panel { + advanced = 'advanced', + builder = 'builder', + actions = 'actions', + model = 'model', + version = 'version', + mcp = 'mcp', +} + +export type FileSetter = + | SetterOrUpdater> + | React.Dispatch>>; + +export type ActionAuthForm = { + /* General */ + type: t.AuthTypeEnum; + saved_auth_fields: boolean; + /* API key */ + api_key: string; // not nested + authorization_type: t.AuthorizationTypeEnum; + custom_auth_header: string; + /* OAuth */ + oauth_client_id: string; // not nested + oauth_client_secret: string; // not nested + authorization_url: string; + client_url: string; + scope: string; + token_exchange_method: t.TokenExchangeMethodEnum; +}; + +export type MCPForm = ActionAuthForm & { + name?: string; + description?: string; + url?: string; + tools?: string[]; + icon?: string; + trust?: boolean; +}; + +export type ActionWithNullableMetadata = Omit & { + metadata: t.ActionMetadata | null; +}; + +export type AssistantPanelProps = { + index?: number; + action?: ActionWithNullableMetadata; + actions?: t.Action[]; + assistant_id?: string; + activePanel?: string; + endpoint: t.AssistantsEndpoint; + version: number | string; + documentsMap: Map | null; + setAction: React.Dispatch>; + setCurrentAssistantId: React.Dispatch>; + setActivePanel: React.Dispatch>; +}; + +export type AgentPanelProps = { + index?: number; + agent_id?: string; + activePanel?: string; + mcp?: t.MCP; + mcps?: t.MCP[]; + action?: t.Action; + actions?: t.Action[]; + createMutation: UseMutationResult; + setActivePanel: React.Dispatch>; + setMcp: React.Dispatch>; + setAction: React.Dispatch>; + endpointsConfig?: t.TEndpointsConfig; + setCurrentAgentId: React.Dispatch>; + agentsConfig?: t.TAgentsEndpoint | null; +}; + +export type AgentPanelContextType = { + action?: t.Action; + actions?: t.Action[]; + setAction: React.Dispatch>; + mcp?: t.MCP; + mcps?: t.MCP[]; + setMcp: React.Dispatch>; + setMcps: React.Dispatch>; + groupedTools: Record; + tools: t.AgentToolType[]; + activePanel?: string; + setActivePanel: React.Dispatch>; + setCurrentAgentId: React.Dispatch>; + agent_id?: string; +}; + +export type AgentModelPanelProps = { + agent_id?: string; + providers: Option[]; + models: Record; + setActivePanel: React.Dispatch>; +}; + +export type AugmentedColumnDef = ColumnDef & DataColumnMeta; + +export type TSetOption = t.TSetOption; + +export type TSetExample = ( + i: number, + type: string, + newValue: number | string | boolean | null, +) => void; + +export type OnInputNumberChange = InputNumberPrimitive.InputNumberProps['onChange']; + +export const defaultDebouncedDelay = 450; + +export enum ESide { + Top = 'top', + Right = 'right', + Bottom = 'bottom', + Left = 'left', +} + +export enum NotificationSeverity { + INFO = 'info', + SUCCESS = 'success', + WARNING = 'warning', + ERROR = 'error', +} + +export type TShowToast = { + message: string; + severity?: NotificationSeverity; + showIcon?: boolean; + duration?: number; + status?: 'error' | 'success' | 'warning' | 'info'; +}; + +export type TBaseSettingsProps = { + conversation: t.TConversation | t.TPreset | null; + className?: string; + isPreset?: boolean; + readonly?: boolean; +}; + +export type TSettingsProps = TBaseSettingsProps & { + setOption: TSetOption; +}; + +export type TModels = { + models: string[]; + showAbove?: boolean; + popover?: boolean; +}; + +export type TModelSelectProps = TSettingsProps & TModels; + +export type TEditPresetProps = { + open: boolean; + onOpenChange: React.Dispatch>; + preset: t.TPreset; + title?: string; +}; + +export type TSetOptions = (options: Record) => void; +export type TSetOptionsPayload = { + setOption: TSetOption; + setExample: TSetExample; + addExample: () => void; + removeExample: () => void; + setAgentOption: TSetOption; + // getConversation: () => t.TConversation | t.TPreset | null; + checkPluginSelection: (value: string) => boolean; + setTools: (newValue: string, remove?: boolean) => void; + setOptions?: TSetOptions; +}; + +export type TPresetItemProps = { + preset: t.TPreset; + value: t.TPreset; + onSelect: (preset: t.TPreset) => void; + onChangePreset: (preset: t.TPreset) => void; + onDeletePreset: (preset: t.TPreset) => void; +}; + +export type TOnClick = (e: React.MouseEvent) => void; + +export type TGenButtonProps = { + onClick: TOnClick; +}; + +export type TAskProps = { + text: string; + overrideConvoId?: string; + overrideUserMessageId?: string; + parentMessageId?: string | null; + conversationId?: string | null; + messageId?: string | null; + clientTimestamp?: string; +}; + +export type TOptions = { + editedMessageId?: string | null; + editedText?: string | null; + isRegenerate?: boolean; + isContinued?: boolean; + isEdited?: boolean; + overrideMessages?: t.TMessage[]; + /** This value is only true when the user submits a message with "Save & Submit" for a user-created message */ + isResubmission?: boolean; + /** Currently only utilized when `isResubmission === true`, uses that message's currently attached files */ + overrideFiles?: t.TMessage['files']; +}; + +export type TAskFunction = (props: TAskProps, options?: TOptions) => void; + +export type TMessageProps = { + conversation?: t.TConversation | null; + messageId?: string | null; + message?: t.TMessage; + messagesTree?: t.TMessage[]; + currentEditId: string | number | null; + isSearchView?: boolean; + siblingIdx?: number; + siblingCount?: number; + setCurrentEditId?: React.Dispatch> | null; + setSiblingIdx?: ((value: number) => void | React.Dispatch>) | null; +}; + +export type TMessageIcon = { endpoint?: string | null; isCreatedByUser?: boolean } & Pick< + t.TConversation, + 'modelLabel' +> & + Pick; + +export type TInitialProps = { + text: string; + edit: boolean; + error: boolean; + unfinished: boolean; + isSubmitting: boolean; + isLast: boolean; +}; +export type TAdditionalProps = { + ask: TAskFunction; + message: t.TMessage; + isCreatedByUser: boolean; + siblingIdx: number; + enterEdit: (cancel: boolean) => void; + setSiblingIdx: (value: number) => void; +}; + +export type TMessageContentProps = TInitialProps & TAdditionalProps; + +export type TText = Pick & { className?: string }; +export type TEditProps = Pick & + Omit & { + text?: string; + index?: number; + siblingIdx: number | null; + }; +export type TDisplayProps = TText & + Pick & { + showCursor?: boolean; + }; + +export type TConfigProps = { + userKey: string; + setUserKey: React.Dispatch>; + endpoint: t.EModelEndpoint | string; +}; + +export type TDangerButtonProps = { + id: string; + confirmClear: boolean; + className?: string; + disabled?: boolean; + showText?: boolean; + mutation?: UseMutationResult; + onClick: () => void; + infoTextCode: TranslationKeys; + actionTextCode: TranslationKeys; + dataTestIdInitial: string; + dataTestIdConfirm: string; + infoDescriptionCode?: TranslationKeys; + confirmActionTextCode?: TranslationKeys; +}; + +export type TDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; +}; + +export type TPluginStoreDialogProps = { + isOpen: boolean; + setIsOpen: (open: boolean) => void; +}; + +export type TResError = { + response: { data: { message: string } }; + message: string; +}; + +export type TAuthContext = { + user: t.TUser | undefined; + token: string | undefined; + isAuthenticated: boolean; + error: string | undefined; + login: (data: t.TLoginUser) => void; + logout: (redirect?: string) => void; + setError: React.Dispatch>; + roles?: Record; +}; + +export type TUserContext = { + user?: t.TUser | undefined; + token: string | undefined; + isAuthenticated: boolean; + redirect?: string; +}; + +export type TAuthConfig = { + loginRedirect: string; + test?: boolean; +}; + +export type IconProps = Pick & + Pick & { + size?: number; + button?: boolean; + iconURL?: string; + message?: boolean; + className?: string; + iconClassName?: string; + endpoint?: t.EModelEndpoint | string | null; + endpointType?: t.EModelEndpoint | null; + assistantName?: string; + agentName?: string; + error?: boolean; + }; + +export type Option = Record & { + label?: string; + value: string | number | null; +}; + +export type StringOption = Option & { value: string | null }; + +export type VoiceOption = { + value: string; + label: string; +}; + +export type TMessageAudio = { + isLast?: boolean; + index: number; + messageId: string; + content: string; + className?: string; + renderButton?: (props: { + onClick: (e?: React.MouseEvent) => void; + title: string; + icon: React.ReactNode; + isActive?: boolean; + isVisible?: boolean; + isDisabled?: boolean; + className?: string; + }) => React.ReactNode; +}; + +export type OptionWithIcon = Option & { icon?: React.ReactNode }; +export type DropdownValueSetter = (value: string | Option | OptionWithIcon) => void; +export type MentionOption = OptionWithIcon & { + type: string; + value: string; + description?: string; +}; +export type PromptOption = MentionOption & { + id: string; +}; + +export type TOptionSettings = { + showExamples?: boolean; + isCodeChat?: boolean; +}; + +export interface ExtendedFile { + file?: File; + file_id: string; + temp_file_id?: string; + type?: string; + filepath?: string; + filename?: string; + width?: number; + height?: number; + size: number; + preview?: string; + progress: number; + source?: FileSources; + attached?: boolean; + embedded?: boolean; + tool_resource?: string; + metadata?: t.TFile['metadata']; +} + +export interface ModelItemProps { + modelName: string; + endpoint: EModelEndpoint; + isSelected: boolean; + onSelect: () => void; + onNavigateBack: () => void; + icon?: JSX.Element; + className?: string; +} + +export type ContextType = { + navVisible: boolean; + setNavVisible: React.Dispatch>; +}; + +export interface SwitcherProps { + endpoint?: t.EModelEndpoint | null; + endpointKeyProvided: boolean; + isCollapsed: boolean; +} +export type TLoginLayoutContext = { + startupConfig: t.TStartupConfig | null; + startupConfigError: unknown; + isFetching: boolean; + error: string | null; + setError: React.Dispatch>; + headerText: string; + setHeaderText: React.Dispatch>; +}; + +export type NewConversationParams = { + template?: Partial; + preset?: Partial; + modelsData?: t.TModelsConfig; + buildDefault?: boolean; + keepLatestMessage?: boolean; + keepAddedConvos?: boolean; + disableParams?: boolean; +}; + +export type ConvoGenerator = (params: NewConversationParams) => void | t.TConversation; + +export type TBaseResData = { + plugin?: t.TResPlugin; + final?: boolean; + initial?: boolean; + previousMessages?: t.TMessage[]; + conversation: t.TConversation; + conversationId?: string; + runMessages?: t.TMessage[]; +}; + +export type TResData = TBaseResData & { + requestMessage: t.TMessage; + responseMessage: t.TMessage; +}; + +export type TFinalResData = Omit & { + conversation: Partial & Pick; + requestMessage?: t.TMessage; + responseMessage?: t.TMessage; +}; + +export type TVectorStore = { + _id: string; + object: 'vector_store'; + created_at: string | Date; + name: string; + bytes?: number; + file_counts?: { + in_progress: number; + completed: number; + failed: number; + cancelled: number; + total: number; + }; +}; + +export type TThread = { id: string; createdAt: string }; + +declare global { + interface Window { + google_tag_manager?: unknown; + } +}