mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-24 11:16:12 +01:00
🎉 feat: Code Interpreter API and Agents Release (#4860)
* feat: Code Interpreter API & File Search Agent Uploads chore: add back code files wip: first pass, abstract key dialog refactor: influence checkbox on key changes refactor: update localization keys for 'execute code' to 'run code' wip: run code button refactor: add throwError parameter to loadAuthValues and getUserPluginAuthValue functions feat: first pass, API tool calling fix: handle missing toolId in callTool function and return 404 for non-existent tools feat: show code outputs fix: improve error handling in callTool function and log errors fix: handle potential null value for filepath in attachment destructuring fix: normalize language before rendering and prevent null return fix: add loading indicator in RunCode component while executing code feat: add support for conditional code execution in Markdown components feat: attachments refactor: remove bash fix: pass abort signal to graph/run refactor: debounce and rate limit tool call refactor: increase debounce delay for execute function feat: set code output attachments feat: image attachments refactor: apply message context refactor: pass `partIndex` feat: toolCall schema/model/methods feat: block indexing feat: get tool calls chore: imports chore: typing chore: condense type imports feat: get tool calls fix: block indexing chore: typing refactor: update tool calls mapping to support multiple results fix: add unique key to nav link for rendering wip: first pass, tool call results refactor: update query cache from successful tool call mutation style: improve result switcher styling chore: note on using \`.toObject()\` feat: add agent_id field to conversation schema chore: typing refactor: rename agentMap to agentsMap for consistency feat: Agent Name as chat input placeholder chore: bump agents 📦 chore: update @langchain dependencies to latest versions to match agents package 📦 chore: update @librechat/agents dependency to version 1.8.0 fix: Aborting agent stream removes sender; fix(bedrock): completion removes preset name label refactor: remove direct file parameter to use req.file, add `processAgentFileUpload` for image uploads feat: upload menu feat: prime message_file resources feat: implement conversation access validation in chat route refactor: remove file parameter from processFileUpload and use req.file instead feat: add savedMessageIds set to track saved message IDs in BaseClient, to prevent unnecessary double-write to db feat: prevent duplicate message saves by checking savedMessageIds in AgentController refactor: skip legacy RAG API handling for agents feat: add files field to convoSchema refactor: update request type annotations from Express.Request to ServerRequest in file processing functions feat: track conversation files fix: resendFiles, addPreviousAttachments handling feat: add ID validation for session_id and file_id in download route feat: entity_id for code file uploads/downloads fix: code file edge cases feat: delete related tool calls feat: add stream rate handling for LLM configuration feat: enhance system content with attached file information fix: improve error logging in resource priming function * WIP: PoC, sequential agents WIP: PoC Sequential Agents, first pass content data + bump agents package fix: package-lock WIP: PoC, o1 support, refactor bufferString feat: convertJsonSchemaToZod fix: form issues and schema defining erroneous model fix: max length issue on agent form instructions, limit conversation messages to sequential agents feat: add abort signal support to createRun function and AgentClient feat: PoC, hide prior sequential agent steps fix: update parameter naming from config to metadata in event handlers for clarity, add model to usage data refactor: use only last contentData, track model for usage data chore: bump agents package fix: content parts issue refactor: filter contentParts to include tool calls and relevant indices feat: show function calls refactor: filter context messages to exclude tool calls when no tools are available to the agent fix: ensure tool call content is not undefined in formatMessages feat: add agent_id field to conversationPreset schema feat: hide sequential agents feat: increase upload toast duration to 10 seconds * refactor: tool context handling & update Code API Key Dialog feat: toolContextMap chore: skipSpecs -> useSpecs ci: fix handleTools tests feat: API Key Dialog * feat: Agent Permissions Admin Controls feat: replace label with button for prompt permission toggle feat: update agent permissions feat: enable experimental agents and streamline capability configuration feat: implement access control for agents and enhance endpoint menu items feat: add welcome message for agent selection in localization feat: add agents permission to access control and update version to 0.7.57 * fix: update types in useAssistantListMap and useMentions hooks for better null handling * feat: mention agents * fix: agent tool resource race conditions when deleting agent tool resource files * feat: add error handling for code execution with user feedback * refactor: rename AdminControls to AdminSettings for clarity * style: add gap to button in AdminSettings for improved layout * refactor: separate agent query hooks and check access to enable fetching * fix: remove unused provider from agent initialization options, creates issue with custom endpoints * refactor: remove redundant/deprecated modelOptions from AgentClient processes * chore: update @librechat/agents to version 1.8.5 in package.json and package-lock.json * fix: minor styling issues + agent panel uniformity * fix: agent edge cases when set endpoint is no longer defined * refactor: remove unused cleanup function call from AppService * fix: update link in ApiKeyDialog to point to pricing page * fix: improve type handling and layout calculations in SidePanel component * fix: add missing localization string for agent selection in SidePanel * chore: form styling and localizations for upload filesearch/code interpreter * fix: model selection placeholder logic in AgentConfig component * style: agent capabilities * fix: add localization for provider selection and improve dropdown styling in ModelPanel * refactor: use gpt-4o-mini > gpt-3.5-turbo * fix: agents configuration for loadDefaultInterface and update related tests * feat: DALLE Agents support
This commit is contained in:
parent
affcebd48c
commit
1a815f5e19
189 changed files with 5056 additions and 1815 deletions
34
client/src/Providers/CodeBlockContext.tsx
Normal file
34
client/src/Providers/CodeBlockContext.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { createContext, useContext, ReactNode, useCallback, useRef } from 'react';
|
||||
|
||||
type TCodeBlockContext = {
|
||||
getNextIndex: (skip: boolean) => number;
|
||||
resetCounter: () => void;
|
||||
// codeBlocks: Map<number, string>;
|
||||
};
|
||||
|
||||
export const CodeBlockContext = createContext<TCodeBlockContext>({} as TCodeBlockContext);
|
||||
export const useCodeBlockContext = () => useContext(CodeBlockContext);
|
||||
|
||||
export function CodeBlockProvider({ children }: { children: ReactNode }) {
|
||||
const counterRef = useRef(0);
|
||||
// const codeBlocks = useRef(new Map<number, string>()).current;
|
||||
|
||||
const getNextIndex = useCallback((skip: boolean) => {
|
||||
if (skip) {
|
||||
return counterRef.current;
|
||||
}
|
||||
const nextIndex = counterRef.current;
|
||||
counterRef.current += 1;
|
||||
return nextIndex;
|
||||
}, []);
|
||||
|
||||
const resetCounter = useCallback(() => {
|
||||
counterRef.current = 0;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CodeBlockContext.Provider value={{ getNextIndex, resetCounter }}>
|
||||
{children}
|
||||
</CodeBlockContext.Provider>
|
||||
);
|
||||
}
|
||||
9
client/src/Providers/MessageContext.tsx
Normal file
9
client/src/Providers/MessageContext.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
type MessageContext = {
|
||||
messageId: string;
|
||||
partIndex?: number;
|
||||
conversationId?: string | null;
|
||||
};
|
||||
|
||||
export const MessageContext = createContext<MessageContext>({} as MessageContext);
|
||||
export const useMessageContext = () => useContext(MessageContext);
|
||||
21
client/src/Providers/ToolCallsMapContext.tsx
Normal file
21
client/src/Providers/ToolCallsMapContext.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
import useToolCallsMap from '~/hooks/Plugins/useToolCallsMap';
|
||||
type ToolCallsMapContextType = ReturnType<typeof useToolCallsMap>;
|
||||
|
||||
export const ToolCallsMapContext = createContext<ToolCallsMapContextType>(
|
||||
{} as ToolCallsMapContextType,
|
||||
);
|
||||
export const useToolCallsMapContext = () => useContext(ToolCallsMapContext);
|
||||
|
||||
interface ToolCallsMapProviderProps {
|
||||
children: React.ReactNode;
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
export function ToolCallsMapProvider({ children, conversationId }: ToolCallsMapProviderProps) {
|
||||
const toolCallsMap = useToolCallsMap({ conversationId });
|
||||
|
||||
return (
|
||||
<ToolCallsMapContext.Provider value={toolCallsMap}>{children}</ToolCallsMapContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,9 +9,12 @@ export * from './FileMapContext';
|
|||
export * from './AddedChatContext';
|
||||
export * from './ChatFormContext';
|
||||
export * from './BookmarkContext';
|
||||
export * from './MessageContext';
|
||||
export * from './DashboardContext';
|
||||
export * from './AssistantsContext';
|
||||
export * from './AgentsContext';
|
||||
export * from './AssistantsMapContext';
|
||||
export * from './AnnouncerContext';
|
||||
export * from './AgentsMapContext';
|
||||
export * from './CodeBlockContext';
|
||||
export * from './ToolCallsMapContext';
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export type TAgentOption = OptionWithIcon &
|
|||
export type TAgentCapabilities = {
|
||||
[AgentCapabilities.execute_code]: boolean;
|
||||
[AgentCapabilities.file_search]: boolean;
|
||||
[AgentCapabilities.end_after_tools]?: boolean;
|
||||
[AgentCapabilities.hide_sequential_outputs]?: boolean;
|
||||
};
|
||||
|
||||
export type AgentForm = {
|
||||
|
|
@ -23,4 +25,5 @@ export type AgentForm = {
|
|||
model_parameters: AgentModelParameters;
|
||||
tools?: string[];
|
||||
provider?: AgentProvider | OptionWithIcon;
|
||||
agent_ids?: string[];
|
||||
} & TAgentCapabilities;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export * from './a11y';
|
||||
export * from './artifacts';
|
||||
export * from './types';
|
||||
export * from './tools';
|
||||
export * from './assistants-types';
|
||||
export * from './agents-types';
|
||||
|
|
|
|||
6
client/src/common/tools.ts
Normal file
6
client/src/common/tools.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import type { AuthType } from 'librechat-data-provider';
|
||||
|
||||
export type ApiKeyFormData = {
|
||||
apiKey: string;
|
||||
authType?: string | AuthType;
|
||||
};
|
||||
|
|
@ -1,36 +1,21 @@
|
|||
import React from 'react';
|
||||
import { RefObject } from 'react';
|
||||
import { FileSources } from 'librechat-data-provider';
|
||||
import type * as InputNumberPrimitive from 'rc-input-number';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import type { SetterOrUpdater } from 'recoil';
|
||||
import type {
|
||||
TRole,
|
||||
TUser,
|
||||
Agent,
|
||||
Action,
|
||||
TPreset,
|
||||
TPlugin,
|
||||
TMessage,
|
||||
Assistant,
|
||||
TResPlugin,
|
||||
TLoginUser,
|
||||
AuthTypeEnum,
|
||||
TModelsConfig,
|
||||
TConversation,
|
||||
TStartupConfig,
|
||||
EModelEndpoint,
|
||||
TEndpointsConfig,
|
||||
ActionMetadata,
|
||||
AssistantDocument,
|
||||
AssistantsEndpoint,
|
||||
TMessageContentParts,
|
||||
AuthorizationTypeEnum,
|
||||
TSetOption as SetOption,
|
||||
TokenExchangeMethodEnum,
|
||||
} from 'librechat-data-provider';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
export type CodeBarProps = {
|
||||
lang: string;
|
||||
error?: boolean;
|
||||
plugin?: boolean;
|
||||
blockIndex?: number;
|
||||
allowExecution?: boolean;
|
||||
codeRef: RefObject<HTMLElement>;
|
||||
};
|
||||
|
||||
export enum PromptsEditorMode {
|
||||
SIMPLE = 'simple',
|
||||
ADVANCED = 'advanced',
|
||||
|
|
@ -65,21 +50,21 @@ export type AudioChunk = {
|
|||
export type AssistantListItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
metadata: Assistant['metadata'];
|
||||
metadata: t.Assistant['metadata'];
|
||||
model: string;
|
||||
};
|
||||
|
||||
export type AgentListItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: Agent['avatar'];
|
||||
avatar: t.Agent['avatar'];
|
||||
};
|
||||
|
||||
export type TPluginMap = Record<string, TPlugin>;
|
||||
export type TPluginMap = Record<string, t.TPlugin>;
|
||||
|
||||
export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
|
||||
|
||||
export type LastSelectedModels = Record<EModelEndpoint, string>;
|
||||
export type LastSelectedModels = Record<t.EModelEndpoint, string>;
|
||||
|
||||
export type LocalizeFunction = (phraseKey: string, ...values: string[]) => string;
|
||||
|
||||
|
|
@ -145,11 +130,11 @@ export type FileSetter =
|
|||
|
||||
export type ActionAuthForm = {
|
||||
/* General */
|
||||
type: AuthTypeEnum;
|
||||
type: t.AuthTypeEnum;
|
||||
saved_auth_fields: boolean;
|
||||
/* API key */
|
||||
api_key: string; // not nested
|
||||
authorization_type: AuthorizationTypeEnum;
|
||||
authorization_type: t.AuthorizationTypeEnum;
|
||||
custom_auth_header: string;
|
||||
/* OAuth */
|
||||
oauth_client_id: string; // not nested
|
||||
|
|
@ -157,23 +142,23 @@ export type ActionAuthForm = {
|
|||
authorization_url: string;
|
||||
client_url: string;
|
||||
scope: string;
|
||||
token_exchange_method: TokenExchangeMethodEnum;
|
||||
token_exchange_method: t.TokenExchangeMethodEnum;
|
||||
};
|
||||
|
||||
export type ActionWithNullableMetadata = Omit<Action, 'metadata'> & {
|
||||
metadata: ActionMetadata | null;
|
||||
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
|
||||
metadata: t.ActionMetadata | null;
|
||||
};
|
||||
|
||||
export type AssistantPanelProps = {
|
||||
index?: number;
|
||||
action?: ActionWithNullableMetadata;
|
||||
actions?: Action[];
|
||||
actions?: t.Action[];
|
||||
assistant_id?: string;
|
||||
activePanel?: string;
|
||||
endpoint: AssistantsEndpoint;
|
||||
endpoint: t.AssistantsEndpoint;
|
||||
version: number | string;
|
||||
documentsMap: Map<string, AssistantDocument> | null;
|
||||
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
|
||||
documentsMap: Map<string, t.AssistantDocument> | null;
|
||||
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
|
||||
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||
};
|
||||
|
|
@ -182,11 +167,11 @@ export type AgentPanelProps = {
|
|||
index?: number;
|
||||
agent_id?: string;
|
||||
activePanel?: string;
|
||||
action?: Action;
|
||||
actions?: Action[];
|
||||
action?: t.Action;
|
||||
actions?: t.Action[];
|
||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
|
||||
endpointsConfig?: TEndpointsConfig;
|
||||
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
|
||||
endpointsConfig?: t.TEndpointsConfig;
|
||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
};
|
||||
|
||||
|
|
@ -199,7 +184,7 @@ export type AgentModelPanelProps = {
|
|||
|
||||
export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & DataColumnMeta;
|
||||
|
||||
export type TSetOption = SetOption;
|
||||
export type TSetOption = t.TSetOption;
|
||||
|
||||
export type TSetExample = (
|
||||
i: number,
|
||||
|
|
@ -234,7 +219,7 @@ export type TShowToast = {
|
|||
};
|
||||
|
||||
export type TBaseSettingsProps = {
|
||||
conversation: TConversation | TPreset | null;
|
||||
conversation: t.TConversation | t.TPreset | null;
|
||||
className?: string;
|
||||
isPreset?: boolean;
|
||||
readonly?: boolean;
|
||||
|
|
@ -255,7 +240,7 @@ export type TModelSelectProps = TSettingsProps & TModels;
|
|||
export type TEditPresetProps = {
|
||||
open: boolean;
|
||||
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
preset: TPreset;
|
||||
preset: t.TPreset;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
|
|
@ -266,18 +251,18 @@ export type TSetOptionsPayload = {
|
|||
addExample: () => void;
|
||||
removeExample: () => void;
|
||||
setAgentOption: TSetOption;
|
||||
// getConversation: () => TConversation | TPreset | null;
|
||||
// getConversation: () => t.TConversation | t.TPreset | null;
|
||||
checkPluginSelection: (value: string) => boolean;
|
||||
setTools: (newValue: string, remove?: boolean) => void;
|
||||
setOptions?: TSetOptions;
|
||||
};
|
||||
|
||||
export type TPresetItemProps = {
|
||||
preset: TPreset;
|
||||
value: TPreset;
|
||||
onSelect: (preset: TPreset) => void;
|
||||
onChangePreset: (preset: TPreset) => void;
|
||||
onDeletePreset: (preset: TPreset) => void;
|
||||
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<HTMLButtonElement>) => void;
|
||||
|
|
@ -302,16 +287,16 @@ export type TOptions = {
|
|||
isRegenerate?: boolean;
|
||||
isContinued?: boolean;
|
||||
isEdited?: boolean;
|
||||
overrideMessages?: TMessage[];
|
||||
overrideMessages?: t.TMessage[];
|
||||
};
|
||||
|
||||
export type TAskFunction = (props: TAskProps, options?: TOptions) => void;
|
||||
|
||||
export type TMessageProps = {
|
||||
conversation?: TConversation | null;
|
||||
conversation?: t.TConversation | null;
|
||||
messageId?: string | null;
|
||||
message?: TMessage;
|
||||
messagesTree?: TMessage[];
|
||||
message?: t.TMessage;
|
||||
messagesTree?: t.TMessage[];
|
||||
currentEditId: string | number | null;
|
||||
isSearchView?: boolean;
|
||||
siblingIdx?: number;
|
||||
|
|
@ -330,7 +315,7 @@ export type TInitialProps = {
|
|||
};
|
||||
export type TAdditionalProps = {
|
||||
ask: TAskFunction;
|
||||
message: TMessage;
|
||||
message: t.TMessage;
|
||||
isCreatedByUser: boolean;
|
||||
siblingIdx: number;
|
||||
enterEdit: (cancel: boolean) => void;
|
||||
|
|
@ -354,7 +339,7 @@ export type TDisplayProps = TText &
|
|||
export type TConfigProps = {
|
||||
userKey: string;
|
||||
setUserKey: React.Dispatch<React.SetStateAction<string>>;
|
||||
endpoint: EModelEndpoint | string;
|
||||
endpoint: t.EModelEndpoint | string;
|
||||
};
|
||||
|
||||
export type TDangerButtonProps = {
|
||||
|
|
@ -389,18 +374,18 @@ export type TResError = {
|
|||
};
|
||||
|
||||
export type TAuthContext = {
|
||||
user: TUser | undefined;
|
||||
user: t.TUser | undefined;
|
||||
token: string | undefined;
|
||||
isAuthenticated: boolean;
|
||||
error: string | undefined;
|
||||
login: (data: TLoginUser) => void;
|
||||
login: (data: t.TLoginUser) => void;
|
||||
logout: () => void;
|
||||
setError: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
roles?: Record<string, TRole | null | undefined>;
|
||||
roles?: Record<string, t.TRole | null | undefined>;
|
||||
};
|
||||
|
||||
export type TUserContext = {
|
||||
user?: TUser | undefined;
|
||||
user?: t.TUser | undefined;
|
||||
token: string | undefined;
|
||||
isAuthenticated: boolean;
|
||||
redirect?: string;
|
||||
|
|
@ -411,16 +396,16 @@ export type TAuthConfig = {
|
|||
test?: boolean;
|
||||
};
|
||||
|
||||
export type IconProps = Pick<TMessage, 'isCreatedByUser' | 'model'> &
|
||||
Pick<TConversation, 'chatGptLabel' | 'modelLabel' | 'jailbreak'> & {
|
||||
export type IconProps = Pick<t.TMessage, 'isCreatedByUser' | 'model'> &
|
||||
Pick<t.TConversation, 'chatGptLabel' | 'modelLabel' | 'jailbreak'> & {
|
||||
size?: number;
|
||||
button?: boolean;
|
||||
iconURL?: string;
|
||||
message?: boolean;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
endpoint?: EModelEndpoint | string | null;
|
||||
endpointType?: EModelEndpoint | null;
|
||||
endpoint?: t.EModelEndpoint | string | null;
|
||||
endpointType?: t.EModelEndpoint | null;
|
||||
assistantName?: string;
|
||||
agentName?: string;
|
||||
error?: boolean;
|
||||
|
|
@ -440,7 +425,7 @@ export type VoiceOption = {
|
|||
|
||||
export type TMessageAudio = {
|
||||
messageId?: string;
|
||||
content?: TMessageContentParts[] | string;
|
||||
content?: t.TMessageContentParts[] | string;
|
||||
className?: string;
|
||||
isLast: boolean;
|
||||
index: number;
|
||||
|
|
@ -482,12 +467,12 @@ export interface ExtendedFile {
|
|||
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };
|
||||
|
||||
export interface SwitcherProps {
|
||||
endpoint?: EModelEndpoint | null;
|
||||
endpoint?: t.EModelEndpoint | null;
|
||||
endpointKeyProvided: boolean;
|
||||
isCollapsed: boolean;
|
||||
}
|
||||
export type TLoginLayoutContext = {
|
||||
startupConfig: TStartupConfig | null;
|
||||
startupConfig: t.TStartupConfig | null;
|
||||
startupConfigError: unknown;
|
||||
isFetching: boolean;
|
||||
error: string | null;
|
||||
|
|
@ -497,34 +482,34 @@ export type TLoginLayoutContext = {
|
|||
};
|
||||
|
||||
export type NewConversationParams = {
|
||||
template?: Partial<TConversation>;
|
||||
preset?: Partial<TPreset>;
|
||||
modelsData?: TModelsConfig;
|
||||
template?: Partial<t.TConversation>;
|
||||
preset?: Partial<t.TPreset>;
|
||||
modelsData?: t.TModelsConfig;
|
||||
buildDefault?: boolean;
|
||||
keepLatestMessage?: boolean;
|
||||
keepAddedConvos?: boolean;
|
||||
};
|
||||
|
||||
export type ConvoGenerator = (params: NewConversationParams) => void | TConversation;
|
||||
export type ConvoGenerator = (params: NewConversationParams) => void | t.TConversation;
|
||||
|
||||
export type TBaseResData = {
|
||||
plugin?: TResPlugin;
|
||||
plugin?: t.TResPlugin;
|
||||
final?: boolean;
|
||||
initial?: boolean;
|
||||
previousMessages?: TMessage[];
|
||||
conversation: TConversation;
|
||||
previousMessages?: t.TMessage[];
|
||||
conversation: t.TConversation;
|
||||
conversationId?: string;
|
||||
runMessages?: TMessage[];
|
||||
runMessages?: t.TMessage[];
|
||||
};
|
||||
|
||||
export type TResData = TBaseResData & {
|
||||
requestMessage: TMessage;
|
||||
responseMessage: TMessage;
|
||||
requestMessage: t.TMessage;
|
||||
responseMessage: t.TMessage;
|
||||
};
|
||||
|
||||
export type TFinalResData = TBaseResData & {
|
||||
requestMessage?: TMessage;
|
||||
responseMessage?: TMessage;
|
||||
requestMessage?: t.TMessage;
|
||||
responseMessage?: t.TMessage;
|
||||
};
|
||||
|
||||
export type TVectorStore = {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { useChatContext, useAddedChatContext } from '~/Providers';
|
|||
import { TooltipAnchor } from '~/components';
|
||||
import { mainTextareaId } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
function AddMultiConvo() {
|
||||
const { conversation } = useChatContext();
|
||||
|
|
|
|||
100
client/src/components/Chat/Input/Files/AttachFileMenu.tsx
Normal file
100
client/src/components/Chat/Input/Files/AttachFileMenu.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import * as Ariakit from '@ariakit/react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
|
||||
import { EToolResources } from 'librechat-data-provider';
|
||||
import { FileUpload, TooltipAnchor, DropdownPopup } from '~/components/ui';
|
||||
import { AttachmentIcon } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface AttachFileProps {
|
||||
isRTL: boolean;
|
||||
disabled?: boolean | null;
|
||||
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
setToolResource?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
}
|
||||
|
||||
const AttachFile = ({ isRTL, disabled, setToolResource, handleFileChange }: AttachFileProps) => {
|
||||
const localize = useLocalize();
|
||||
const isUploadDisabled = disabled ?? false;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
|
||||
const handleUploadClick = (isImage?: boolean) => {
|
||||
if (!inputRef.current) {
|
||||
return;
|
||||
}
|
||||
inputRef.current.value = '';
|
||||
inputRef.current.accept = isImage === true ? 'image/*' : '';
|
||||
inputRef.current.click();
|
||||
inputRef.current.accept = '';
|
||||
};
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
label: localize('com_ui_upload_image_input'),
|
||||
onClick: () => {
|
||||
setToolResource?.(undefined);
|
||||
handleUploadClick(true);
|
||||
},
|
||||
icon: <ImageUpIcon className="icon-md" />,
|
||||
},
|
||||
{
|
||||
label: localize('com_ui_upload_file_search'),
|
||||
onClick: () => {
|
||||
setToolResource?.(EToolResources.file_search);
|
||||
handleUploadClick();
|
||||
},
|
||||
icon: <FileSearch className="icon-md" />,
|
||||
},
|
||||
{
|
||||
label: localize('com_ui_upload_code_files'),
|
||||
onClick: () => {
|
||||
setToolResource?.(EToolResources.execute_code);
|
||||
handleUploadClick();
|
||||
},
|
||||
icon: <TerminalSquareIcon className="icon-md" />,
|
||||
},
|
||||
];
|
||||
|
||||
const menuTrigger = (
|
||||
<TooltipAnchor
|
||||
render={
|
||||
<Ariakit.MenuButton
|
||||
disabled={isUploadDisabled}
|
||||
id="attach-file-menu-button"
|
||||
aria-label="Attach File Options"
|
||||
className={cn(
|
||||
'absolute flex size-[35px] items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
|
||||
isRTL ? 'bottom-2 right-2' : 'bottom-2 left-1 md:left-2',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<AttachmentIcon />
|
||||
</div>
|
||||
</Ariakit.MenuButton>
|
||||
}
|
||||
id="attach-file-menu-button"
|
||||
description={localize('com_sidepanel_attach_files')}
|
||||
disabled={isUploadDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<FileUpload ref={inputRef} handleFileChange={handleFileChange}>
|
||||
<div className="relative">
|
||||
<DropdownPopup
|
||||
menuId="attach-file-menu"
|
||||
isOpen={isPopoverActive}
|
||||
setIsOpen={setIsPopoverActive}
|
||||
modal={true}
|
||||
trigger={menuTrigger}
|
||||
items={dropdownItems}
|
||||
iconClassName="mr-0"
|
||||
/>
|
||||
</div>
|
||||
</FileUpload>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AttachFile);
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
import { memo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
supportsFiles,
|
||||
mergeFileConfig,
|
||||
isAgentsEndpoint,
|
||||
EndpointFileConfig,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import AttachFileMenu from './AttachFileMenu';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useFileHandling } from '~/hooks';
|
||||
import AttachFile from './AttachFile';
|
||||
|
|
@ -20,23 +22,46 @@ function FileFormWrapper({
|
|||
disableInputs: boolean;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const { handleFileChange, abortUpload } = useFileHandling();
|
||||
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
|
||||
|
||||
const { files, setFiles, conversation, setFilesLoading } = useChatContext();
|
||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
||||
const isAgents = useMemo(() => isAgentsEndpoint(_endpoint), [_endpoint]);
|
||||
|
||||
const { handleFileChange, abortUpload, setToolResource } = useFileHandling();
|
||||
|
||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
|
||||
const isRTL = chatDirection === 'rtl';
|
||||
|
||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
||||
const endpointFileConfig = fileConfig.endpoints[_endpoint ?? ''] as
|
||||
| EndpointFileConfig
|
||||
| undefined;
|
||||
|
||||
const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? _endpoint ?? ''] ?? false;
|
||||
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
|
||||
|
||||
const renderAttachFile = () => {
|
||||
if (isAgents) {
|
||||
return (
|
||||
<AttachFileMenu
|
||||
isRTL={isRTL}
|
||||
disabled={disableInputs}
|
||||
setToolResource={setToolResource}
|
||||
handleFileChange={handleFileChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (endpointSupportsFiles && !isUploadDisabled) {
|
||||
return (
|
||||
<AttachFile isRTL={isRTL} disabled={disableInputs} handleFileChange={handleFileChange} />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FileRow
|
||||
|
|
@ -50,9 +75,7 @@ function FileFormWrapper({
|
|||
)}
|
||||
/>
|
||||
{children}
|
||||
{endpointSupportsFiles && !isUploadDisabled && (
|
||||
<AttachFile isRTL={isRTL} disabled={disableInputs} handleFileChange={handleFileChange} />
|
||||
)}
|
||||
{renderAttachFile()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,15 @@ export default function Mention({
|
|||
}) {
|
||||
const localize = useLocalize();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const { options, presets, modelSpecs, modelsConfig, endpointsConfig, assistantListMap } =
|
||||
useMentions({ assistantMap: assistantMap || {}, includeAssistants });
|
||||
const {
|
||||
options,
|
||||
presets,
|
||||
modelSpecs,
|
||||
agentsList,
|
||||
modelsConfig,
|
||||
endpointsConfig,
|
||||
assistantListMap,
|
||||
} = useMentions({ assistantMap: assistantMap || {}, includeAssistants });
|
||||
const { onSelectMention } = useSelectMention({
|
||||
presets,
|
||||
modelSpecs,
|
||||
|
|
@ -62,18 +69,23 @@ export default function Mention({
|
|||
}
|
||||
};
|
||||
|
||||
if (mention.type === 'endpoint' && mention.value === EModelEndpoint.assistants) {
|
||||
if (mention.type === 'endpoint' && mention.value === EModelEndpoint.agents) {
|
||||
setSearchValue('');
|
||||
setInputOptions(assistantListMap[EModelEndpoint.assistants]);
|
||||
setInputOptions(agentsList ?? []);
|
||||
setActiveIndex(0);
|
||||
inputRef.current?.focus();
|
||||
} else if (mention.type === 'endpoint' && mention.value === EModelEndpoint.assistants) {
|
||||
setSearchValue('');
|
||||
setInputOptions(assistantListMap[EModelEndpoint.assistants] ?? []);
|
||||
setActiveIndex(0);
|
||||
inputRef.current?.focus();
|
||||
} else if (mention.type === 'endpoint' && mention.value === EModelEndpoint.azureAssistants) {
|
||||
setSearchValue('');
|
||||
setInputOptions(assistantListMap[EModelEndpoint.azureAssistants]);
|
||||
setInputOptions(assistantListMap[EModelEndpoint.azureAssistants] ?? []);
|
||||
setActiveIndex(0);
|
||||
inputRef.current?.focus();
|
||||
} else if (mention.type === 'endpoint') {
|
||||
const models = (modelsConfig?.[mention.value ?? ''] ?? []).map((model) => ({
|
||||
const models = (modelsConfig?.[mention.value || ''] ?? []).map((model) => ({
|
||||
value: mention.value,
|
||||
label: model,
|
||||
type: 'model',
|
||||
|
|
|
|||
|
|
@ -1,47 +1,57 @@
|
|||
import type { FC } from 'react';
|
||||
import { Close } from '@radix-ui/react-popover';
|
||||
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
alternateName,
|
||||
PermissionTypes,
|
||||
Permissions,
|
||||
} from 'librechat-data-provider';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import MenuSeparator from '../UI/MenuSeparator';
|
||||
import { getEndpointField } from '~/utils';
|
||||
import { useHasAccess } from '~/hooks';
|
||||
import MenuItem from './MenuItem';
|
||||
|
||||
const EndpointItems: FC<{
|
||||
endpoints: EModelEndpoint[];
|
||||
endpoints: Array<EModelEndpoint | undefined>;
|
||||
selected: EModelEndpoint | '';
|
||||
}> = ({ endpoints, selected }) => {
|
||||
}> = ({ endpoints = [], selected }) => {
|
||||
const hasAccessToAgents = useHasAccess({
|
||||
permissionType: PermissionTypes.AGENTS,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
return (
|
||||
<>
|
||||
{endpoints &&
|
||||
endpoints.map((endpoint, i) => {
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
} else if (!endpointsConfig?.[endpoint]) {
|
||||
return null;
|
||||
}
|
||||
const userProvidesKey: boolean | null | undefined = getEndpointField(
|
||||
endpointsConfig,
|
||||
endpoint,
|
||||
'userProvide',
|
||||
);
|
||||
return (
|
||||
<Close asChild key={`endpoint-${endpoint}`}>
|
||||
<div key={`endpoint-${endpoint}`}>
|
||||
<MenuItem
|
||||
key={`endpoint-item-${endpoint}`}
|
||||
title={alternateName[endpoint] || endpoint}
|
||||
value={endpoint}
|
||||
selected={selected === endpoint}
|
||||
data-testid={`endpoint-item-${endpoint}`}
|
||||
userProvidesKey={!!userProvidesKey}
|
||||
// description="With DALL·E, browsing and analysis"
|
||||
/>
|
||||
{i !== endpoints.length - 1 && <MenuSeparator />}
|
||||
</div>
|
||||
</Close>
|
||||
);
|
||||
})}
|
||||
{endpoints.map((endpoint, i) => {
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
} else if (!endpointsConfig?.[endpoint]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (endpoint === EModelEndpoint.agents && !hasAccessToAgents) {
|
||||
return null;
|
||||
}
|
||||
const userProvidesKey: boolean | null | undefined =
|
||||
getEndpointField(endpointsConfig, endpoint, 'userProvide') ?? false;
|
||||
return (
|
||||
<Close asChild key={`endpoint-${endpoint}`}>
|
||||
<div key={`endpoint-${endpoint}`}>
|
||||
<MenuItem
|
||||
key={`endpoint-item-${endpoint}`}
|
||||
title={alternateName[endpoint] || endpoint}
|
||||
value={endpoint}
|
||||
selected={selected === endpoint}
|
||||
data-testid={`endpoint-item-${endpoint}`}
|
||||
userProvidesKey={!!userProvidesKey}
|
||||
// description="With DALL·E, browsing and analysis"
|
||||
/>
|
||||
{i !== endpoints.length - 1 && <MenuSeparator />}
|
||||
</div>
|
||||
</Close>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import { ContentTypes } from 'librechat-data-provider';
|
|||
import type { TMessageContentParts, TAttachment, Agents } from 'librechat-data-provider';
|
||||
import EditTextPart from './Parts/EditTextPart';
|
||||
import { mapAttachments } from '~/utils/map';
|
||||
import { MessageContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
import Part from './Part';
|
||||
|
||||
type ContentPartsProps = {
|
||||
content: Array<TMessageContentParts | undefined> | undefined;
|
||||
messageId: string;
|
||||
conversationId?: string | null;
|
||||
attachments?: TAttachment[];
|
||||
isCreatedByUser: boolean;
|
||||
isLast: boolean;
|
||||
|
|
@ -27,6 +29,7 @@ const ContentParts = memo(
|
|||
({
|
||||
content,
|
||||
messageId,
|
||||
conversationId,
|
||||
attachments,
|
||||
isCreatedByUser,
|
||||
isLast,
|
||||
|
|
@ -79,15 +82,23 @@ const ContentParts = memo(
|
|||
const attachments = attachmentMap[toolCallId];
|
||||
|
||||
return (
|
||||
<Part
|
||||
part={part}
|
||||
isSubmitting={isSubmitting}
|
||||
attachments={attachments}
|
||||
key={`display-${messageId}-${idx}`}
|
||||
showCursor={idx === content.length - 1 && isLast}
|
||||
messageId={messageId}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
/>
|
||||
<MessageContext.Provider
|
||||
key={`provider-${messageId}-${idx}`}
|
||||
value={{
|
||||
messageId,
|
||||
conversationId,
|
||||
partIndex: idx,
|
||||
}}
|
||||
>
|
||||
<Part
|
||||
part={part}
|
||||
attachments={attachments}
|
||||
isSubmitting={isSubmitting}
|
||||
key={`part-${messageId}-${idx}`}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
showCursor={idx === content.length - 1 && isLast}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { memo, useMemo } from 'react';
|
||||
import React, { memo, useMemo, useRef, useEffect } from 'react';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import supersub from 'remark-supersub';
|
||||
|
|
@ -10,10 +10,10 @@ import remarkDirective from 'remark-directive';
|
|||
import type { Pluggable } from 'unified';
|
||||
import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact';
|
||||
import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils';
|
||||
import { useToastContext, CodeBlockProvider, useCodeBlockContext } from '~/Providers';
|
||||
import CodeBlock from '~/components/Messages/Content/CodeBlock';
|
||||
import { useFileDownload } from '~/data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
type TCodeProps = {
|
||||
|
|
@ -25,6 +25,32 @@ type TCodeProps = {
|
|||
export const code: React.ElementType = memo(({ className, children }: TCodeProps) => {
|
||||
const match = /language-(\w+)/.exec(className ?? '');
|
||||
const lang = match && match[1];
|
||||
const isMath = lang === 'math';
|
||||
const isSingleLine = typeof children === 'string' && children.split('\n').length === 1;
|
||||
|
||||
const { getNextIndex, resetCounter } = useCodeBlockContext();
|
||||
const blockIndex = useRef(getNextIndex(isMath || isSingleLine)).current;
|
||||
|
||||
useEffect(() => {
|
||||
resetCounter();
|
||||
}, [children, resetCounter]);
|
||||
|
||||
if (isMath) {
|
||||
return children;
|
||||
} else if (isSingleLine) {
|
||||
return (
|
||||
<code onDoubleClick={handleDoubleClick} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
} else {
|
||||
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} blockIndex={blockIndex} />;
|
||||
}
|
||||
});
|
||||
|
||||
export const codeNoExecution: React.ElementType = memo(({ className, children }: TCodeProps) => {
|
||||
const match = /language-(\w+)/.exec(className ?? '');
|
||||
const lang = match && match[1];
|
||||
|
||||
if (lang === 'math') {
|
||||
return children;
|
||||
|
|
@ -35,7 +61,7 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
|
|||
</code>
|
||||
);
|
||||
} else {
|
||||
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} />;
|
||||
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} allowExecution={false} />;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -45,7 +71,11 @@ export const a: React.ElementType = memo(
|
|||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
|
||||
const { file_id, filename, filepath } = useMemo(() => {
|
||||
const {
|
||||
file_id = '',
|
||||
filename = '',
|
||||
filepath,
|
||||
} = useMemo(() => {
|
||||
const pattern = new RegExp(`(?:files|outputs)/${user?.id}/([^\\s]+)`);
|
||||
const match = href.match(pattern);
|
||||
if (match && match[0]) {
|
||||
|
|
@ -164,25 +194,27 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
|
|||
: [supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]];
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={remarkPlugins}
|
||||
/* @ts-ignore */
|
||||
rehypePlugins={rehypePlugins}
|
||||
// linkTarget="_new"
|
||||
components={
|
||||
{
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
artifact: Artifact,
|
||||
} as {
|
||||
[nodeType: string]: React.ElementType;
|
||||
<CodeBlockProvider>
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={remarkPlugins}
|
||||
/* @ts-ignore */
|
||||
rehypePlugins={rehypePlugins}
|
||||
// linkTarget="_new"
|
||||
components={
|
||||
{
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
artifact: Artifact,
|
||||
} as {
|
||||
[nodeType: string]: React.ElementType;
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
{isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
|
||||
</ReactMarkdown>
|
||||
>
|
||||
{isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
|
||||
</ReactMarkdown>
|
||||
</CodeBlockProvider>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,40 +6,51 @@ import supersub from 'remark-supersub';
|
|||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import type { PluggableList } from 'unified';
|
||||
import { code, codeNoExecution, a, p } from './Markdown';
|
||||
import { CodeBlockProvider } from '~/Providers';
|
||||
import { langSubset } from '~/utils';
|
||||
import { code, a, p } from './Markdown';
|
||||
|
||||
const MarkdownLite = memo(({ content = '' }: { content?: string }) => {
|
||||
const rehypePlugins: PluggableList = [
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[
|
||||
rehypeHighlight,
|
||||
{
|
||||
detect: true,
|
||||
ignoreMissing: true,
|
||||
subset: langSubset,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
rehypePlugins={rehypePlugins}
|
||||
// linkTarget="_new"
|
||||
components={
|
||||
const MarkdownLite = memo(
|
||||
({ content = '', codeExecution = true }: { content?: string; codeExecution?: boolean }) => {
|
||||
const rehypePlugins: PluggableList = [
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[
|
||||
rehypeHighlight,
|
||||
{
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
} as {
|
||||
[nodeType: string]: React.ElementType;
|
||||
}
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
});
|
||||
detect: true,
|
||||
ignoreMissing: true,
|
||||
subset: langSubset,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return (
|
||||
<CodeBlockProvider>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[
|
||||
/** @ts-ignore */
|
||||
supersub,
|
||||
remarkGfm,
|
||||
[remarkMath, { singleDollarTextMath: true }],
|
||||
]}
|
||||
/** @ts-ignore */
|
||||
rehypePlugins={rehypePlugins}
|
||||
// linkTarget="_new"
|
||||
components={
|
||||
{
|
||||
code: codeExecution ? code : codeNoExecution,
|
||||
a,
|
||||
p,
|
||||
} as {
|
||||
[nodeType: string]: React.ElementType;
|
||||
}
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</CodeBlockProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default MarkdownLite;
|
||||
|
|
|
|||
|
|
@ -21,143 +21,130 @@ type PartProps = {
|
|||
part?: TMessageContentParts;
|
||||
isSubmitting: boolean;
|
||||
showCursor: boolean;
|
||||
messageId: string;
|
||||
isCreatedByUser: boolean;
|
||||
attachments?: TAttachment[];
|
||||
};
|
||||
|
||||
const Part = memo(
|
||||
({ part, isSubmitting, attachments, showCursor, messageId, isCreatedByUser }: PartProps) => {
|
||||
attachments && console.log(attachments);
|
||||
if (!part) {
|
||||
const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUser }: PartProps) => {
|
||||
if (!part) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (part.type === ContentTypes.ERROR) {
|
||||
return <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
|
||||
} else if (part.type === ContentTypes.TEXT) {
|
||||
const text = typeof part.text === 'string' ? part.text : part.text.value;
|
||||
|
||||
if (typeof text !== 'string') {
|
||||
return null;
|
||||
}
|
||||
if (part.tool_call_ids != null && !text) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
|
||||
</Container>
|
||||
);
|
||||
} else if (part.type === ContentTypes.TOOL_CALL) {
|
||||
const toolCall = part[ContentTypes.TOOL_CALL];
|
||||
|
||||
if (!toolCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (part.type === ContentTypes.ERROR) {
|
||||
return <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
|
||||
} else if (part.type === ContentTypes.TEXT) {
|
||||
const text = typeof part.text === 'string' ? part.text : part.text.value;
|
||||
|
||||
if (typeof text !== 'string') {
|
||||
return null;
|
||||
}
|
||||
if (part.tool_call_ids != null && !text) {
|
||||
return null;
|
||||
}
|
||||
const isToolCall =
|
||||
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
|
||||
if (isToolCall && toolCall.name === Tools.execute_code) {
|
||||
return (
|
||||
<Container>
|
||||
<Text
|
||||
text={text}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
messageId={messageId}
|
||||
showCursor={showCursor}
|
||||
/>
|
||||
</Container>
|
||||
<ExecuteCode
|
||||
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
} else if (part.type === ContentTypes.TOOL_CALL) {
|
||||
const toolCall = part[ContentTypes.TOOL_CALL];
|
||||
|
||||
if (!toolCall) {
|
||||
} else if (isToolCall) {
|
||||
return (
|
||||
<ToolCall
|
||||
args={toolCall.args ?? ''}
|
||||
name={toolCall.name || ''}
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
|
||||
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
|
||||
return (
|
||||
<CodeAnalyze
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
code={code_interpreter.input}
|
||||
outputs={code_interpreter.outputs ?? []}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
toolCall.type === ToolCallTypes.RETRIEVAL ||
|
||||
toolCall.type === ToolCallTypes.FILE_SEARCH
|
||||
) {
|
||||
return (
|
||||
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
|
||||
);
|
||||
} else if (
|
||||
toolCall.type === ToolCallTypes.FUNCTION &&
|
||||
ToolCallTypes.FUNCTION in toolCall &&
|
||||
imageGenTools.has(toolCall.function.name)
|
||||
) {
|
||||
return (
|
||||
<ImageGen
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
args={toolCall.function.arguments as string}
|
||||
/>
|
||||
);
|
||||
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
|
||||
if (isImageVisionTool(toolCall)) {
|
||||
if (isSubmitting && showCursor) {
|
||||
return (
|
||||
<Container>
|
||||
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const isToolCall =
|
||||
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
|
||||
if (isToolCall && toolCall.name === Tools.execute_code) {
|
||||
return (
|
||||
<ExecuteCode
|
||||
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
} else if (isToolCall) {
|
||||
return (
|
||||
<ToolCall
|
||||
args={toolCall.args ?? ''}
|
||||
name={toolCall.name ?? ''}
|
||||
output={toolCall.output ?? ''}
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
);
|
||||
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
|
||||
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
|
||||
return (
|
||||
<CodeAnalyze
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
code={code_interpreter.input}
|
||||
outputs={code_interpreter.outputs ?? []}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
toolCall.type === ToolCallTypes.RETRIEVAL ||
|
||||
toolCall.type === ToolCallTypes.FILE_SEARCH
|
||||
) {
|
||||
return (
|
||||
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
|
||||
);
|
||||
} else if (
|
||||
toolCall.type === ToolCallTypes.FUNCTION &&
|
||||
ToolCallTypes.FUNCTION in toolCall &&
|
||||
imageGenTools.has(toolCall.function.name)
|
||||
) {
|
||||
return (
|
||||
<ImageGen
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
args={toolCall.function.arguments as string}
|
||||
/>
|
||||
);
|
||||
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
|
||||
if (isImageVisionTool(toolCall)) {
|
||||
if (isSubmitting && showCursor) {
|
||||
return (
|
||||
<Container>
|
||||
<Text
|
||||
text={''}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
messageId={messageId}
|
||||
showCursor={showCursor}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolCall
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
args={toolCall.function.arguments as string}
|
||||
name={toolCall.function.name}
|
||||
output={toolCall.function.output}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (part.type === ContentTypes.IMAGE_FILE) {
|
||||
const imageFile = part[ContentTypes.IMAGE_FILE];
|
||||
const height = imageFile.height ?? 1920;
|
||||
const width = imageFile.width ?? 1080;
|
||||
return (
|
||||
<Image
|
||||
imagePath={imageFile.filepath}
|
||||
height={height}
|
||||
width={width}
|
||||
altText={imageFile.filename ?? 'Uploaded Image'}
|
||||
placeholderDimensions={{
|
||||
height: height + 'px',
|
||||
width: width + 'px',
|
||||
}}
|
||||
<ToolCall
|
||||
initialProgress={toolCall.progress ?? 0.1}
|
||||
isSubmitting={isSubmitting}
|
||||
args={toolCall.function.arguments as string}
|
||||
name={toolCall.function.name}
|
||||
output={toolCall.function.output}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (part.type === ContentTypes.IMAGE_FILE) {
|
||||
const imageFile = part[ContentTypes.IMAGE_FILE];
|
||||
const height = imageFile.height ?? 1920;
|
||||
const width = imageFile.width ?? 1080;
|
||||
return (
|
||||
<Image
|
||||
imagePath={imageFile.filepath}
|
||||
height={height}
|
||||
width={width}
|
||||
altText={imageFile.filename ?? 'Uploaded Image'}
|
||||
placeholderDimensions={{
|
||||
height: height + 'px',
|
||||
width: width + 'px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
export default Part;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { imageExtRegex } from 'librechat-data-provider';
|
||||
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
|
||||
import Image from '~/components/Chat/Messages/Content/Image';
|
||||
|
||||
export default function Attachment({ attachment }: { attachment?: TAttachment }) {
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
|
||||
const isImage =
|
||||
imageExtRegex.test(attachment.filename) && width != null && height != null && filepath != null;
|
||||
|
||||
if (isImage) {
|
||||
return (
|
||||
<Image altText={attachment.filename} imagePath={filepath} height={height} width={width} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { CodeInProgress } from './CodeProgress';
|
||||
import { imageExtRegex } from 'librechat-data-provider';
|
||||
import type { TFile, TAttachment, TAttachmentMetadata } from 'librechat-data-provider';
|
||||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
|
||||
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
|
||||
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
||||
import Image from '~/components/Chat/Messages/Content/Image';
|
||||
import { CodeInProgress } from './CodeProgress';
|
||||
import Attachment from './Attachment';
|
||||
import LogContent from './LogContent';
|
||||
import { useProgress } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
|
@ -86,7 +85,10 @@ export default function ExecuteCode({
|
|||
</div>
|
||||
{showCode && (
|
||||
<div className="code-analyze-block mb-3 mt-0.5 overflow-hidden rounded-xl bg-black">
|
||||
<MarkdownLite content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''} />
|
||||
<MarkdownLite
|
||||
content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''}
|
||||
codeExecution={false}
|
||||
/>
|
||||
{output.length > 0 && (
|
||||
<div className="bg-gray-700 p-4 text-xs">
|
||||
<div
|
||||
|
|
@ -103,25 +105,9 @@ export default function ExecuteCode({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{attachments?.map((attachment, index) => {
|
||||
const { width, height, filepath } = attachment as TFile & TAttachmentMetadata;
|
||||
const isImage =
|
||||
imageExtRegex.test(attachment.filename) &&
|
||||
width != null &&
|
||||
height != null &&
|
||||
filepath != null;
|
||||
if (isImage) {
|
||||
return (
|
||||
<Image
|
||||
key={index}
|
||||
altText={attachment.filename}
|
||||
imagePath={filepath}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{attachments?.map((attachment, index) => (
|
||||
<Attachment attachment={attachment} key={index} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
import { isAfter } from 'date-fns';
|
||||
import React, { useMemo } from 'react';
|
||||
import { imageExtRegex } from 'librechat-data-provider';
|
||||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import type { TFile, TAttachment, TAttachmentMetadata } from 'librechat-data-provider';
|
||||
import Image from '~/components/Chat/Messages/Content/Image';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import LogLink from './LogLink';
|
||||
|
||||
interface LogContentProps {
|
||||
output?: string;
|
||||
renderImages?: boolean;
|
||||
attachments?: TAttachment[];
|
||||
}
|
||||
|
||||
const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) => {
|
||||
type ImageAttachment = TFile &
|
||||
TAttachmentMetadata & {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
|
||||
const LogContent: React.FC<LogContentProps> = ({ output = '', renderImages, attachments }) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
const processedContent = useMemo(() => {
|
||||
if (!output) {
|
||||
return '';
|
||||
|
|
@ -21,8 +30,29 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) =>
|
|||
return parts[0].trim();
|
||||
}, [output]);
|
||||
|
||||
const nonImageAttachments =
|
||||
attachments?.filter((file) => !imageExtRegex.test(file.filename)) || [];
|
||||
const { imageAttachments, nonImageAttachments } = useMemo(() => {
|
||||
const imageAtts: ImageAttachment[] = [];
|
||||
const nonImageAtts: TAttachment[] = [];
|
||||
|
||||
attachments?.forEach((attachment) => {
|
||||
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
|
||||
const isImage =
|
||||
imageExtRegex.test(attachment.filename) &&
|
||||
width != null &&
|
||||
height != null &&
|
||||
filepath != null;
|
||||
if (isImage) {
|
||||
imageAtts.push(attachment as ImageAttachment);
|
||||
} else {
|
||||
nonImageAtts.push(attachment);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
imageAttachments: renderImages === true ? imageAtts : null,
|
||||
nonImageAttachments: nonImageAtts,
|
||||
};
|
||||
}, [attachments, renderImages]);
|
||||
|
||||
const renderAttachment = (file: TAttachment) => {
|
||||
const now = new Date();
|
||||
|
|
@ -59,6 +89,18 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) =>
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
{imageAttachments?.map((attachment, index) => {
|
||||
const { width, height, filepath } = attachment;
|
||||
return (
|
||||
<Image
|
||||
key={index}
|
||||
altText={attachment.filename}
|
||||
imagePath={filepath}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@ import { memo, useMemo, ReactElement } from 'react';
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
||||
import Markdown from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { useChatContext, useMessageContext } from '~/Providers';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
type TextPartProps = {
|
||||
text: string;
|
||||
isCreatedByUser: boolean;
|
||||
messageId: string;
|
||||
showCursor: boolean;
|
||||
isCreatedByUser: boolean;
|
||||
};
|
||||
|
||||
type ContentType =
|
||||
|
|
@ -18,7 +17,8 @@ type ContentType =
|
|||
| ReactElement<React.ComponentProps<typeof MarkdownLite>>
|
||||
| ReactElement;
|
||||
|
||||
const TextPart = memo(({ text, isCreatedByUser, messageId, showCursor }: TextPartProps) => {
|
||||
const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => {
|
||||
const { messageId } = useMessageContext();
|
||||
const { isSubmitting, latestMessage } = useChatContext();
|
||||
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
|
||||
const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useMemo } from 'react';
|
||||
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
|
||||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import ProgressCircle from './ProgressCircle';
|
||||
import InProgressCall from './InProgressCall';
|
||||
import Attachment from './Parts/Attachment';
|
||||
import CancelledIcon from './CancelledIcon';
|
||||
import ProgressText from './ProgressText';
|
||||
import FinishedIcon from './FinishedIcon';
|
||||
|
|
@ -18,12 +20,14 @@ export default function ToolCall({
|
|||
name,
|
||||
args: _args = '',
|
||||
output,
|
||||
attachments,
|
||||
}: {
|
||||
initialProgress: number;
|
||||
isSubmitting: boolean;
|
||||
name: string;
|
||||
args: string | Record<string, unknown>;
|
||||
output?: string | null;
|
||||
attachments?: TAttachment[];
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const progress = useProgress(initialProgress);
|
||||
|
|
@ -106,6 +110,9 @@ export default function ToolCall({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{attachments?.map((attachment, index) => (
|
||||
<Attachment attachment={attachment} key={index} />
|
||||
))}
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default function ToolPopover({
|
|||
<div tabIndex={-1}>
|
||||
<div className="bg-token-surface-primary max-w-sm rounded-md p-2 shadow-[0_0_24px_0_rgba(0,0,0,0.05),inset_0_0.5px_0_0_rgba(0,0,0,0.05),0_2px_8px_0_rgba(0,0,0,0.05)]">
|
||||
<div className="mb-2 text-sm font-medium dark:text-gray-100">
|
||||
{domain
|
||||
{domain != null && domain
|
||||
? localize('com_assistants_domain_info', domain)
|
||||
: localize('com_assistants_function_use', function_name)}
|
||||
</div>
|
||||
|
|
@ -42,7 +42,7 @@ export default function ToolPopover({
|
|||
<code className="!whitespace-pre-wrap ">{formatText(input)}</code>
|
||||
</div>
|
||||
</div>
|
||||
{output && (
|
||||
{output != null && output && (
|
||||
<>
|
||||
<div className="mb-2 mt-2 text-sm font-medium dark:text-gray-100">
|
||||
{localize('com_ui_result')}
|
||||
|
|
|
|||
|
|
@ -82,11 +82,12 @@ export default function Message(props: TMessageProps) {
|
|||
<div className="flex-col gap-1 md:gap-3">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
<ContentParts
|
||||
content={message.content as Array<TMessageContentParts | undefined>}
|
||||
messageId={message.messageId}
|
||||
isCreatedByUser={message.isCreatedByUser}
|
||||
isLast={isLast}
|
||||
isSubmitting={isSubmitting}
|
||||
messageId={message.messageId}
|
||||
isCreatedByUser={message.isCreatedByUser}
|
||||
conversationId={conversation?.conversationId}
|
||||
content={message.content as Array<TMessageContentParts | undefined>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import HoverButtons from '~/components/Chat/Messages/HoverButtons';
|
|||
import Icon from '~/components/Chat/Messages/MessageIcon';
|
||||
import { Plugin } from '~/components/Messages/Content';
|
||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||
import { MessageContext } from '~/Providers';
|
||||
import { useMessageActions } from '~/hooks';
|
||||
import { cn, logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
|
@ -59,9 +60,10 @@ const MessageRender = memo(
|
|||
const fontSize = useRecoilValue(store.fontSize);
|
||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||
const { isCreatedByUser, error, unfinished } = msg ?? {};
|
||||
const hasNoChildren = !(msg?.children?.length ?? 0);
|
||||
const isLast = useMemo(
|
||||
() => !msg?.children?.length && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
|
||||
[msg?.children, msg?.depth, latestMessage?.depth],
|
||||
() => hasNoChildren && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
|
||||
[hasNoChildren, msg?.depth, latestMessage?.depth],
|
||||
);
|
||||
|
||||
if (!msg) {
|
||||
|
|
@ -122,24 +124,31 @@ const MessageRender = memo(
|
|||
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
|
||||
<div className="flex-col gap-1 md:gap-3">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
{msg.plugin && <Plugin plugin={msg.plugin} />}
|
||||
<MessageContent
|
||||
ask={ask}
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
text={msg.text || ''}
|
||||
message={msg}
|
||||
enterEdit={enterEdit}
|
||||
error={!!(error ?? false)}
|
||||
isSubmitting={isSubmitting}
|
||||
unfinished={unfinished ?? false}
|
||||
isCreatedByUser={isCreatedByUser ?? true}
|
||||
siblingIdx={siblingIdx ?? 0}
|
||||
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
|
||||
/>
|
||||
<MessageContext.Provider
|
||||
value={{
|
||||
messageId: msg.messageId,
|
||||
conversationId: conversation?.conversationId,
|
||||
}}
|
||||
>
|
||||
{msg.plugin && <Plugin plugin={msg.plugin} />}
|
||||
<MessageContent
|
||||
ask={ask}
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
text={msg.text || ''}
|
||||
message={msg}
|
||||
enterEdit={enterEdit}
|
||||
error={!!(error ?? false)}
|
||||
isSubmitting={isSubmitting}
|
||||
unfinished={unfinished ?? false}
|
||||
isCreatedByUser={isCreatedByUser ?? true}
|
||||
siblingIdx={siblingIdx ?? 0}
|
||||
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
{!msg.children?.length && (isSubmittingFamily === true || isSubmitting) ? (
|
||||
{hasNoChildren && (isSubmittingFamily === true || isSubmitting) ? (
|
||||
<PlaceholderRow isCard={isCard} />
|
||||
) : (
|
||||
<SubRow classes="text-xs">
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
|
|||
createPresetMutation.mutate(_preset, {
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
message: `${toastTitle} ${localize('com_endpoint_preset_saved')}`,
|
||||
message: `${toastTitle} ${localize('com_ui_saved')}`,
|
||||
});
|
||||
onOpenChange(false); // Close the dialog on success
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,81 +1,133 @@
|
|||
import copy from 'copy-to-clipboard';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import React, { useRef, useState, RefObject } from 'react';
|
||||
import { Tools } from 'librechat-data-provider';
|
||||
import React, { useRef, useState, useMemo, useEffect } from 'react';
|
||||
import type { CodeBarProps } from '~/common';
|
||||
import LogContent from '~/components/Chat/Messages/Content/Parts/LogContent';
|
||||
import ResultSwitcher from '~/components/Messages/Content/ResultSwitcher';
|
||||
import { useToolCallsMapContext, useMessageContext } from '~/Providers';
|
||||
import RunCode from '~/components/Messages/Content/RunCode';
|
||||
import Clipboard from '~/components/svg/Clipboard';
|
||||
import CheckMark from '~/components/svg/CheckMark';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import cn from '~/utils/cn';
|
||||
|
||||
type CodeBarProps = {
|
||||
lang: string;
|
||||
codeRef: RefObject<HTMLElement>;
|
||||
plugin?: boolean;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
type CodeBlockProps = Pick<CodeBarProps, 'lang' | 'plugin' | 'error'> & {
|
||||
type CodeBlockProps = Pick<
|
||||
CodeBarProps,
|
||||
'lang' | 'plugin' | 'error' | 'allowExecution' | 'blockIndex'
|
||||
> & {
|
||||
codeChildren: React.ReactNode;
|
||||
classProp?: string;
|
||||
};
|
||||
|
||||
const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, error, plugin = null }) => {
|
||||
const localize = useLocalize();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
return (
|
||||
<div className="relative flex items-center rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
|
||||
<span className="">{lang}</span>
|
||||
{plugin === true ? (
|
||||
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'ml-auto flex gap-2',
|
||||
error === true ? 'h-4 w-4 items-start text-white/50' : '',
|
||||
)}
|
||||
onClick={async () => {
|
||||
const codeString = codeRef.current?.textContent;
|
||||
if (codeString != null) {
|
||||
setIsCopied(true);
|
||||
copy(codeString.trim(), { format: 'text/plain' });
|
||||
const CodeBar: React.FC<CodeBarProps> = React.memo(
|
||||
({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true }) => {
|
||||
const localize = useLocalize();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
return (
|
||||
<div className="relative flex items-center justify-between rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
|
||||
<span className="">{lang}</span>
|
||||
{plugin === true ? (
|
||||
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
{allowExecution === true && (
|
||||
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'ml-auto flex gap-2',
|
||||
error === true ? 'h-4 w-4 items-start text-white/50' : '',
|
||||
)}
|
||||
onClick={async () => {
|
||||
const codeString = codeRef.current?.textContent;
|
||||
if (codeString != null) {
|
||||
setIsCopied(true);
|
||||
copy(codeString.trim(), { format: 'text/plain' });
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 3000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<CheckMark className="h-[18px] w-[18px]" />
|
||||
{error === true ? '' : localize('com_ui_copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard />
|
||||
{error === true ? '' : localize('com_ui_copy_code')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 3000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<CheckMark className="h-[18px] w-[18px]" />
|
||||
{error === true ? '' : localize('com_ui_copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard />
|
||||
{error === true ? '' : localize('com_ui_copy_code')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({
|
||||
lang,
|
||||
blockIndex,
|
||||
codeChildren,
|
||||
classProp = '',
|
||||
allowExecution = true,
|
||||
plugin = null,
|
||||
error,
|
||||
}) => {
|
||||
const codeRef = useRef<HTMLElement>(null);
|
||||
const toolCallsMap = useToolCallsMapContext();
|
||||
const { messageId, partIndex } = useMessageContext();
|
||||
const key = allowExecution
|
||||
? `${messageId}_${partIndex ?? 0}_${blockIndex ?? 0}_${Tools.execute_code}`
|
||||
: '';
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
const fetchedToolCalls = toolCallsMap?.[key];
|
||||
const [toolCalls, setToolCalls] = useState(toolCallsMap?.[key] ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchedToolCalls) {
|
||||
setToolCalls(fetchedToolCalls);
|
||||
setCurrentIndex(fetchedToolCalls.length - 1);
|
||||
}
|
||||
}, [fetchedToolCalls]);
|
||||
|
||||
const currentToolCall = useMemo(() => toolCalls?.[currentIndex], [toolCalls, currentIndex]);
|
||||
|
||||
const next = () => {
|
||||
if (!toolCalls) {
|
||||
return;
|
||||
}
|
||||
if (currentIndex < toolCalls.length - 1) {
|
||||
setCurrentIndex(currentIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
if (currentIndex > 0) {
|
||||
setCurrentIndex(currentIndex - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const isNonCode = !!(plugin === true || error === true);
|
||||
const language = isNonCode ? 'json' : lang;
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-md bg-gray-900 text-xs text-white/80">
|
||||
<CodeBar lang={lang} codeRef={codeRef} plugin={plugin === true} error={error} />
|
||||
<CodeBar
|
||||
lang={lang}
|
||||
error={error}
|
||||
codeRef={codeRef}
|
||||
blockIndex={blockIndex}
|
||||
plugin={plugin === true}
|
||||
allowExecution={allowExecution}
|
||||
/>
|
||||
<div className={cn(classProp, 'overflow-y-auto p-4')}>
|
||||
<code
|
||||
ref={codeRef}
|
||||
|
|
@ -86,6 +138,34 @@ const CodeBlock: React.FC<CodeBlockProps> = ({
|
|||
{codeChildren}
|
||||
</code>
|
||||
</div>
|
||||
{allowExecution === true && toolCalls && toolCalls.length > 0 && (
|
||||
<>
|
||||
<div className="bg-gray-700 p-4 text-xs">
|
||||
<div
|
||||
className="prose flex flex-col-reverse text-white"
|
||||
style={{
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<pre className="shrink-0">
|
||||
<LogContent
|
||||
output={(currentToolCall?.result as string | undefined) ?? ''}
|
||||
attachments={currentToolCall?.attachments ?? []}
|
||||
renderImages={true}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
{toolCalls.length > 1 && (
|
||||
<ResultSwitcher
|
||||
currentIndex={currentIndex}
|
||||
totalCount={toolCalls.length}
|
||||
onPrevious={previous}
|
||||
onNext={next}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
69
client/src/components/Messages/Content/ResultSwitcher.tsx
Normal file
69
client/src/components/Messages/Content/ResultSwitcher.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
interface ResultSwitcherProps {
|
||||
currentIndex: number;
|
||||
totalCount: number;
|
||||
onPrevious: () => void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const ResultSwitcher: React.FC<ResultSwitcherProps> = ({
|
||||
currentIndex,
|
||||
totalCount,
|
||||
onPrevious,
|
||||
onNext,
|
||||
}) => {
|
||||
if (totalCount <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-start gap-1 self-center bg-gray-700 pb-2 text-xs">
|
||||
<button
|
||||
className="hover-button rounded-md p-1 text-gray-400 hover:bg-gray-700 hover:text-gray-200 disabled:hover:text-gray-400"
|
||||
type="button"
|
||||
onClick={onPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
</button>
|
||||
<span className="flex-shrink-0 tabular-nums">
|
||||
{currentIndex + 1} / {totalCount}
|
||||
</span>
|
||||
<button
|
||||
className="hover-button rounded-md p-1 text-gray-400 hover:bg-gray-700 hover:text-gray-200 disabled:hover:text-gray-400"
|
||||
type="button"
|
||||
onClick={onNext}
|
||||
disabled={currentIndex === totalCount - 1}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResultSwitcher;
|
||||
109
client/src/components/Messages/Content/RunCode.tsx
Normal file
109
client/src/components/Messages/Content/RunCode.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import { Tools, AuthType } from 'librechat-data-provider';
|
||||
import { TerminalSquareIcon, Loader } from 'lucide-react';
|
||||
import React, { useMemo, useCallback, useEffect } from 'react';
|
||||
import type { CodeBarProps } from '~/common';
|
||||
import { useVerifyAgentToolAuth, useToolCallMutation } from '~/data-provider';
|
||||
import ApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog';
|
||||
import { useLocalize, useCodeApiKeyForm } from '~/hooks';
|
||||
import { useMessageContext } from '~/Providers';
|
||||
import { cn, normalizeLanguage } from '~/utils';
|
||||
import { useToastContext } from '~/Providers';
|
||||
|
||||
const RunCode: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, blockIndex }) => {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const execute = useToolCallMutation(Tools.execute_code, {
|
||||
onError: () => {
|
||||
showToast({ message: localize('com_ui_run_code_error'), status: 'error' });
|
||||
},
|
||||
});
|
||||
|
||||
const { messageId, conversationId, partIndex } = useMessageContext();
|
||||
const normalizedLang = useMemo(() => normalizeLanguage(lang), [lang]);
|
||||
const { data } = useVerifyAgentToolAuth({ toolId: Tools.execute_code });
|
||||
const authType = useMemo(() => data?.message ?? false, [data?.message]);
|
||||
const isAuthenticated = useMemo(() => data?.authenticated ?? false, [data?.authenticated]);
|
||||
const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } =
|
||||
useCodeApiKeyForm({});
|
||||
|
||||
const handleExecute = useCallback(async () => {
|
||||
if (!isAuthenticated) {
|
||||
setIsDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
const codeString: string = codeRef.current?.textContent ?? '';
|
||||
if (
|
||||
typeof codeString !== 'string' ||
|
||||
codeString.length === 0 ||
|
||||
typeof normalizedLang !== 'string' ||
|
||||
normalizedLang.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
execute.mutate({
|
||||
partIndex,
|
||||
messageId,
|
||||
blockIndex,
|
||||
conversationId: conversationId ?? '',
|
||||
lang: normalizedLang,
|
||||
code: codeString,
|
||||
});
|
||||
}, [
|
||||
codeRef,
|
||||
execute,
|
||||
partIndex,
|
||||
messageId,
|
||||
blockIndex,
|
||||
conversationId,
|
||||
normalizedLang,
|
||||
setIsDialogOpen,
|
||||
isAuthenticated,
|
||||
]);
|
||||
|
||||
const debouncedExecute = useMemo(
|
||||
() => debounce(handleExecute, 1000, { leading: true }),
|
||||
[handleExecute],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedExecute.cancel();
|
||||
};
|
||||
}, [debouncedExecute]);
|
||||
|
||||
if (typeof normalizedLang !== 'string' || normalizedLang.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={cn('ml-auto flex gap-2')}
|
||||
onClick={debouncedExecute}
|
||||
disabled={execute.isLoading}
|
||||
>
|
||||
{execute.isLoading ? (
|
||||
<Loader className="animate-spin" size={18} />
|
||||
) : (
|
||||
<TerminalSquareIcon size={18} />
|
||||
)}
|
||||
{localize('com_ui_run_code')}
|
||||
</button>
|
||||
<ApiKeyDialog
|
||||
onSubmit={onSubmit}
|
||||
isOpen={isDialogOpen}
|
||||
register={methods.register}
|
||||
onRevoke={handleRevokeApiKey}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
handleSubmit={methods.handleSubmit}
|
||||
isToolAuthenticated={isAuthenticated}
|
||||
isUserProvided={authType === AuthType.USER_PROVIDED}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default RunCode;
|
||||
|
|
@ -129,16 +129,17 @@ const ContentRender = memo(
|
|||
<div className="flex-col gap-1 md:gap-3">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
<ContentParts
|
||||
content={msg.content as Array<TMessageContentParts | undefined>}
|
||||
messageId={msg.messageId}
|
||||
isCreatedByUser={msg.isCreatedByUser}
|
||||
isLast={isLast}
|
||||
isSubmitting={isSubmitting}
|
||||
edit={edit}
|
||||
isLast={isLast}
|
||||
enterEdit={enterEdit}
|
||||
siblingIdx={siblingIdx}
|
||||
messageId={msg.messageId}
|
||||
isSubmitting={isSubmitting}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
attachments={msg.attachments}
|
||||
isCreatedByUser={msg.isCreatedByUser}
|
||||
conversationId={conversation?.conversationId}
|
||||
content={msg.content as Array<TMessageContentParts | undefined>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,17 +29,19 @@ const LabelController: React.FC<LabelControllerProps> = ({
|
|||
setValue,
|
||||
}) => (
|
||||
<div className="mb-4 flex items-center justify-between gap-2">
|
||||
<label
|
||||
<button
|
||||
className="cursor-pointer select-none"
|
||||
htmlFor={promptPerm}
|
||||
type="button"
|
||||
// htmlFor={promptPerm}
|
||||
onClick={() =>
|
||||
setValue(promptPerm, !getValues(promptPerm), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</button>
|
||||
<Controller
|
||||
name={promptPerm}
|
||||
control={control}
|
||||
|
|
@ -48,7 +50,7 @@ const LabelController: React.FC<LabelControllerProps> = ({
|
|||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
value={field?.value?.toString()}
|
||||
value={field.value.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -61,7 +63,7 @@ const AdminSettings = () => {
|
|||
const { showToast } = useToastContext();
|
||||
const { mutate, isLoading } = useUpdatePromptPermissionsMutation({
|
||||
onSuccess: () => {
|
||||
showToast({ status: 'success', message: localize('com_endpoint_preset_saved') });
|
||||
showToast({ status: 'success', message: localize('com_ui_saved') });
|
||||
},
|
||||
onError: () => {
|
||||
showToast({ status: 'error', message: localize('com_ui_error_save_admin_settings') });
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import {
|
|||
replaceSpecialVars,
|
||||
extractVariableInfo,
|
||||
} from '~/utils';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
|
||||
import { TextareaAutosize, InputCombobox } from '~/components/ui';
|
||||
import { code } from '~/components/Chat/Messages/Content/Markdown';
|
||||
|
||||
type FieldType = 'text' | 'select';
|
||||
|
||||
|
|
@ -143,12 +143,16 @@ export default function VariableForm({
|
|||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="mb-6 max-h-screen max-w-[90vw] overflow-auto rounded-md bg-gray-100 p-4 text-text-secondary dark:bg-gray-700/50 sm:max-w-full md:max-h-80">
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
rehypePlugins={[
|
||||
/** @ts-ignore */
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
/** @ts-ignore */
|
||||
[rehypeHighlight, { ignoreMissing: true }],
|
||||
]}
|
||||
components={{ code }}
|
||||
/** @ts-ignore */
|
||||
components={{ code: codeNoExecution }}
|
||||
className="prose dark:prose-invert light dark:text-gray-70 my-1 max-h-[50vh] break-words"
|
||||
>
|
||||
{generateHighlightedMarkdown()}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import remarkMath from 'remark-math';
|
|||
import supersub from 'remark-supersub';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import { code } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import CategoryIcon from './Groups/CategoryIcon';
|
||||
import PromptVariables from './PromptVariables';
|
||||
|
|
@ -50,12 +50,20 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
|||
</h2>
|
||||
<div className="group relative min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600 sm:max-w-full">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
remarkPlugins={[
|
||||
/** @ts-ignore */
|
||||
supersub,
|
||||
remarkGfm,
|
||||
[remarkMath, { singleDollarTextMath: true }],
|
||||
]}
|
||||
rehypePlugins={[
|
||||
/** @ts-ignore */
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
/** @ts-ignore */
|
||||
[rehypeHighlight, { ignoreMissing: true }],
|
||||
]}
|
||||
components={{ p: PromptVariableGfm, code }}
|
||||
/** @ts-ignore */
|
||||
components={{ p: PromptVariableGfm, code: codeNoExecution }}
|
||||
className="prose dark:prose-invert light dark:text-gray-70 my-1"
|
||||
>
|
||||
{mainText}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import rehypeKatex from 'rehype-katex';
|
|||
import remarkMath from 'remark-math';
|
||||
import supersub from 'remark-supersub';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd';
|
||||
import { code } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { SaveIcon, CrossIcon } from '~/components/svg';
|
||||
import { TextareaAutosize } from '~/components/ui';
|
||||
import { PromptVariableGfm } from './Markdown';
|
||||
|
|
@ -75,7 +75,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
role="button"
|
||||
className={cn(
|
||||
'min-h-[8rem] w-full rounded-b-lg border border-border-medium p-4 transition-all duration-150',
|
||||
{ 'bg-surface-secondary-alt cursor-pointer hover:bg-surface-tertiary': !isEditing },
|
||||
{ 'cursor-pointer bg-surface-secondary-alt hover:bg-surface-tertiary': !isEditing },
|
||||
)}
|
||||
onClick={() => !isEditing && setIsEditing(true)}
|
||||
onKeyDown={(e) => {
|
||||
|
|
@ -107,9 +107,12 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
/>
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
/** @ts-ignore */
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={{ p: PromptVariableGfm, code }}
|
||||
/** @ts-ignore */
|
||||
components={{ p: PromptVariableGfm, code: codeNoExecution }}
|
||||
className="markdown prose dark:prose-invert light my-1 w-full break-words text-text-primary"
|
||||
>
|
||||
{field.value}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ const PromptVariables = ({
|
|||
) : (
|
||||
<div className="flex h-7 items-center">
|
||||
<span className="text-xs text-text-secondary md:text-sm">
|
||||
{/** @ts-ignore */}
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_variables_info')}
|
||||
</ReactMarkdown>
|
||||
|
|
@ -68,6 +69,7 @@ const PromptVariables = ({
|
|||
</span>
|
||||
{'\u00A0'}
|
||||
<span className="text-xs text-text-secondary md:text-sm">
|
||||
{/** @ts-ignore */}
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_special_variables_info')}
|
||||
</ReactMarkdown>
|
||||
|
|
@ -79,6 +81,7 @@ const PromptVariables = ({
|
|||
</span>
|
||||
{'\u00A0'}
|
||||
<span className="text-xs text-text-secondary md:text-sm">
|
||||
{/** @ts-ignore */}
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_dropdown_variables_info')}
|
||||
</ReactMarkdown>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import SearchContent from '~/components/Chat/Messages/Content/SearchContent';
|
|||
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
|
||||
import { Plugin } from '~/components/Messages/Content';
|
||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||
import { MessageContext } from '~/Providers';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import MultiMessage from './MultiMessage';
|
||||
import { cn } from '~/utils';
|
||||
|
|
@ -31,10 +32,10 @@ export default function Message(props: TMessageProps) {
|
|||
const {
|
||||
text = '',
|
||||
children,
|
||||
messageId = null,
|
||||
isCreatedByUser = true,
|
||||
error = false,
|
||||
messageId = '',
|
||||
unfinished = false,
|
||||
isCreatedByUser = true,
|
||||
} = message;
|
||||
|
||||
let messageLabel = '';
|
||||
|
|
@ -64,26 +65,33 @@ export default function Message(props: TMessageProps) {
|
|||
<div className={cn('select-none font-semibold', fontSize)}>{messageLabel}</div>
|
||||
<div className="flex-col gap-1 md:gap-3">
|
||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||
{/* Legacy Plugins */}
|
||||
{message.plugin && <Plugin plugin={message.plugin} />}
|
||||
{message.content ? (
|
||||
<SearchContent message={message} />
|
||||
) : (
|
||||
<MessageContent
|
||||
edit={false}
|
||||
error={error}
|
||||
isLast={false}
|
||||
ask={() => ({})}
|
||||
text={text}
|
||||
message={message}
|
||||
isSubmitting={false}
|
||||
enterEdit={() => ({})}
|
||||
unfinished={!!unfinished}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
siblingIdx={siblingIdx ?? 0}
|
||||
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
|
||||
/>
|
||||
)}
|
||||
<MessageContext.Provider
|
||||
value={{
|
||||
messageId,
|
||||
conversationId: conversation?.conversationId,
|
||||
}}
|
||||
>
|
||||
{/* Legacy Plugins */}
|
||||
{message.plugin && <Plugin plugin={message.plugin} />}
|
||||
{message.content ? (
|
||||
<SearchContent message={message} />
|
||||
) : (
|
||||
<MessageContent
|
||||
edit={false}
|
||||
error={error}
|
||||
isLast={false}
|
||||
ask={() => ({})}
|
||||
text={text || ''}
|
||||
message={message}
|
||||
isSubmitting={false}
|
||||
enterEdit={() => ({})}
|
||||
unfinished={unfinished}
|
||||
siblingIdx={siblingIdx ?? 0}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
|
||||
/>
|
||||
)}
|
||||
</MessageContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
<SubRow classes="text-xs">
|
||||
|
|
|
|||
163
client/src/components/SidePanel/Agents/AdminSettings.tsx
Normal file
163
client/src/components/SidePanel/Agents/AdminSettings.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { useMemo, useEffect } from 'react';
|
||||
import { ShieldEllipsis } from 'lucide-react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider';
|
||||
import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form';
|
||||
import { OGDialog, OGDialogTitle, OGDialogContent, OGDialogTrigger } from '~/components/ui';
|
||||
import { useUpdateAgentPermissionsMutation } from '~/data-provider';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import { Button, Switch } from '~/components/ui';
|
||||
import { useToastContext } from '~/Providers';
|
||||
|
||||
type FormValues = Record<Permissions, boolean>;
|
||||
|
||||
type LabelControllerProps = {
|
||||
label: string;
|
||||
agentPerm: Permissions;
|
||||
control: Control<FormValues, unknown, FormValues>;
|
||||
setValue: UseFormSetValue<FormValues>;
|
||||
getValues: UseFormGetValues<FormValues>;
|
||||
};
|
||||
|
||||
const defaultValues = roleDefaults[SystemRoles.USER];
|
||||
|
||||
const LabelController: React.FC<LabelControllerProps> = ({
|
||||
control,
|
||||
agentPerm,
|
||||
label,
|
||||
getValues,
|
||||
setValue,
|
||||
}) => (
|
||||
<div className="mb-4 flex items-center justify-between gap-2">
|
||||
<button
|
||||
className="cursor-pointer select-none"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setValue(agentPerm, !getValues(agentPerm), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<Controller
|
||||
name={agentPerm}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
value={field.value.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AdminSettings = () => {
|
||||
const localize = useLocalize();
|
||||
const { user, roles } = useAuthContext();
|
||||
const { showToast } = useToastContext();
|
||||
const { mutate, isLoading } = useUpdateAgentPermissionsMutation({
|
||||
onSuccess: () => {
|
||||
showToast({ status: 'success', message: localize('com_ui_saved') });
|
||||
},
|
||||
onError: () => {
|
||||
showToast({ status: 'error', message: localize('com_ui_error_save_admin_settings') });
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
control,
|
||||
setValue,
|
||||
getValues,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<FormValues>({
|
||||
mode: 'onChange',
|
||||
defaultValues: useMemo(() => {
|
||||
if (roles?.[SystemRoles.USER]) {
|
||||
return roles[SystemRoles.USER][PermissionTypes.AGENTS];
|
||||
}
|
||||
|
||||
return defaultValues[PermissionTypes.AGENTS];
|
||||
}, [roles]),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (roles?.[SystemRoles.USER]?.[PermissionTypes.AGENTS]) {
|
||||
reset(roles[SystemRoles.USER][PermissionTypes.AGENTS]);
|
||||
}
|
||||
}, [roles, reset]);
|
||||
|
||||
if (user?.role !== SystemRoles.ADMIN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelControllerData = [
|
||||
{
|
||||
agentPerm: Permissions.SHARED_GLOBAL,
|
||||
label: localize('com_ui_agents_allow_share_global'),
|
||||
},
|
||||
{
|
||||
agentPerm: Permissions.USE,
|
||||
label: localize('com_ui_agents_allow_use'),
|
||||
},
|
||||
{
|
||||
agentPerm: Permissions.CREATE,
|
||||
label: localize('com_ui_agents_allow_create'),
|
||||
},
|
||||
];
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
mutate({ roleName: SystemRoles.USER, updates: data });
|
||||
};
|
||||
|
||||
return (
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'outline'}
|
||||
className="btn btn-neutral border-token-border-light relative my-1 h-9 w-full rounded-lg font-medium"
|
||||
>
|
||||
<ShieldEllipsis className="cursor-pointer" />
|
||||
{localize('com_ui_admin_settings')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogContent className="w-1/4 bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300">
|
||||
<OGDialogTitle>{`${localize('com_ui_admin_settings')} - ${localize(
|
||||
'com_ui_agents',
|
||||
)}`}</OGDialogTitle>
|
||||
<form className="p-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="py-5">
|
||||
{labelControllerData.map(({ agentPerm, label }) => (
|
||||
<LabelController
|
||||
key={agentPerm}
|
||||
control={control}
|
||||
agentPerm={agentPerm}
|
||||
label={label}
|
||||
getValues={getValues}
|
||||
setValue={setValue}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || isLoading}
|
||||
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
||||
>
|
||||
{localize('com_ui_save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminSettings;
|
||||
|
|
@ -1,24 +1,32 @@
|
|||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Controller, useWatch, useFormContext } from 'react-hook-form';
|
||||
import { QueryKeys, AgentCapabilities, EModelEndpoint, SystemRoles } from 'librechat-data-provider';
|
||||
import {
|
||||
QueryKeys,
|
||||
SystemRoles,
|
||||
Permissions,
|
||||
EModelEndpoint,
|
||||
PermissionTypes,
|
||||
AgentCapabilities,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TConfig, TPlugin } from 'librechat-data-provider';
|
||||
import type { AgentForm, AgentPanelProps } from '~/common';
|
||||
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
|
||||
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
|
||||
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
|
||||
import { useToastContext, useFileMapContext } from '~/Providers';
|
||||
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||
import Action from '~/components/SidePanel/Builder/Action';
|
||||
import { ToolSelectDialog } from '~/components/Tools';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import { processAgentOption } from '~/utils';
|
||||
import AdminSettings from './AdminSettings';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import DeleteButton from './DeleteButton';
|
||||
import AgentAvatar from './AgentAvatar';
|
||||
import FileSearch from './FileSearch';
|
||||
import ShareAgent from './ShareAgent';
|
||||
import AgentTool from './AgentTool';
|
||||
// import CodeForm from './Code/Form';
|
||||
import CodeForm from './Code/Form';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
||||
|
|
@ -55,6 +63,11 @@ export default function AgentConfig({
|
|||
const tools = useWatch({ control, name: 'tools' });
|
||||
const agent_id = useWatch({ control, name: 'id' });
|
||||
|
||||
const hasAccessToShareAgents = useHasAccess({
|
||||
permissionType: PermissionTypes.AGENTS,
|
||||
permission: Permissions.SHARED_GLOBAL,
|
||||
});
|
||||
|
||||
const toolsEnabled = useMemo(
|
||||
() => agentsConfig?.capabilities?.includes(AgentCapabilities.tools),
|
||||
[agentsConfig],
|
||||
|
|
@ -263,7 +276,7 @@ export default function AgentConfig({
|
|||
/>
|
||||
</div>
|
||||
{/* Instructions */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-4">
|
||||
<label className={labelClass} htmlFor="instructions">
|
||||
{localize('com_ui_instructions')}
|
||||
</label>
|
||||
|
|
@ -275,7 +288,7 @@ export default function AgentConfig({
|
|||
<textarea
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
maxLength={32768}
|
||||
// maxLength={32768}
|
||||
className={cn(inputClass, 'min-h-[100px] resize-y')}
|
||||
id="instructions"
|
||||
placeholder={localize('com_agents_instructions_placeholder')}
|
||||
|
|
@ -297,7 +310,7 @@ export default function AgentConfig({
|
|||
/>
|
||||
</div>
|
||||
{/* Model and Provider */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-4">
|
||||
<label className={labelClass} htmlFor="provider">
|
||||
{localize('com_ui_model')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
|
|
@ -319,16 +332,23 @@ export default function AgentConfig({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<span>{model != null ? model : localize('com_ui_select_model')}</span>
|
||||
<span>{model != null && model ? model : localize('com_ui_select_model')}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/* Code Execution */}
|
||||
{/* {codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />} */}
|
||||
{/* File Search */}
|
||||
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
|
||||
{(codeEnabled || fileSearchEnabled) && (
|
||||
<div className="mb-4 flex w-full flex-col items-start gap-3">
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_capabilities')}
|
||||
</label>
|
||||
{/* Code Execution */}
|
||||
{codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />}
|
||||
{/* File Search */}
|
||||
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
|
||||
</div>
|
||||
)}
|
||||
{/* Agent Tools & Actions */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-4">
|
||||
<label className={labelClass}>
|
||||
{`${toolsEnabled === true ? localize('com_ui_tools') : ''}
|
||||
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
|
||||
|
|
@ -360,7 +380,7 @@ export default function AgentConfig({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToolDialog(true)}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
|
|
@ -373,7 +393,7 @@ export default function AgentConfig({
|
|||
type="button"
|
||||
disabled={!agent_id}
|
||||
onClick={handleAddActions}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
|
|
@ -384,6 +404,7 @@ export default function AgentConfig({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
|
||||
{/* Context Button */}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<DeleteButton
|
||||
|
|
@ -391,7 +412,8 @@ export default function AgentConfig({
|
|||
setCurrentAgentId={setCurrentAgentId}
|
||||
createMutation={create}
|
||||
/>
|
||||
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) && (
|
||||
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
|
||||
hasAccessToShareAgents && (
|
||||
<ShareAgent
|
||||
agent_id={agent_id}
|
||||
agentName={agent?.name ?? ''}
|
||||
|
|
@ -401,7 +423,7 @@ export default function AgentConfig({
|
|||
)}
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
|
||||
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
|
||||
type="submit"
|
||||
disabled={create.isLoading || update.isLoading}
|
||||
aria-busy={create.isLoading || update.isLoading}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,9 @@ export default function AgentPanel({
|
|||
model: _model,
|
||||
model_parameters,
|
||||
provider: _provider,
|
||||
agent_ids,
|
||||
end_after_tools,
|
||||
hide_sequential_outputs,
|
||||
} = data;
|
||||
|
||||
const model = _model ?? '';
|
||||
|
|
@ -143,6 +146,9 @@ export default function AgentPanel({
|
|||
tools,
|
||||
provider,
|
||||
model_parameters,
|
||||
agent_ids,
|
||||
end_after_tools,
|
||||
hide_sequential_outputs,
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
|
@ -163,6 +169,9 @@ export default function AgentPanel({
|
|||
tools,
|
||||
provider,
|
||||
model_parameters,
|
||||
agent_ids,
|
||||
end_after_tools,
|
||||
hide_sequential_outputs,
|
||||
});
|
||||
},
|
||||
[agent_id, create, update, showToast, localize],
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ export default function AgentSelect({
|
|||
const capabilities: TAgentCapabilities = {
|
||||
[AgentCapabilities.execute_code]: false,
|
||||
[AgentCapabilities.file_search]: false,
|
||||
[AgentCapabilities.end_after_tools]: false,
|
||||
[AgentCapabilities.hide_sequential_outputs]: false,
|
||||
};
|
||||
|
||||
const agentTools: string[] = [];
|
||||
|
|
|
|||
|
|
@ -1,38 +1,39 @@
|
|||
import { useState } from 'react';
|
||||
import { KeyRoundIcon } from 'lucide-react';
|
||||
import { AuthType, AgentCapabilities } from 'librechat-data-provider';
|
||||
import { useFormContext, Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import { useFormContext, Controller, useWatch } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
import {
|
||||
Input,
|
||||
OGDialog,
|
||||
Checkbox,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardPortal,
|
||||
HoverCardTrigger,
|
||||
Button,
|
||||
} from '~/components/ui';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useLocalize, useAuthCodeTool } from '~/hooks';
|
||||
import { useLocalize, useCodeApiKeyForm } from '~/hooks';
|
||||
import { CircleHelpIcon } from '~/components/svg';
|
||||
import ApiKeyDialog from './ApiKeyDialog';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
type ApiKeyFormData = {
|
||||
apiKey: string;
|
||||
authType?: string | AuthType;
|
||||
};
|
||||
|
||||
export default function Action({ authType = '', isToolAuthenticated = false }) {
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const { control, setValue, getValues } = methods;
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const {
|
||||
onSubmit,
|
||||
isDialogOpen,
|
||||
setIsDialogOpen,
|
||||
handleRevokeApiKey,
|
||||
methods: keyFormMethods,
|
||||
} = useCodeApiKeyForm({
|
||||
onSubmit: () => {
|
||||
setValue(AgentCapabilities.execute_code, true, { shouldDirty: true });
|
||||
},
|
||||
onRevoke: () => {
|
||||
setValue(AgentCapabilities.execute_code, false, { shouldDirty: true });
|
||||
},
|
||||
});
|
||||
|
||||
const runCodeIsEnabled = useWatch({ control, name: AgentCapabilities.execute_code });
|
||||
|
||||
const { installTool, removeTool } = useAuthCodeTool({ isEntityTool: true });
|
||||
|
||||
const { reset, register, handleSubmit } = useForm<ApiKeyFormData>();
|
||||
const isUserProvided = authType === AuthType.USER_PROVIDED;
|
||||
|
||||
const handleCheckboxChange = (checked: boolean) => {
|
||||
|
|
@ -45,18 +46,6 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
|
|||
}
|
||||
};
|
||||
|
||||
const onSubmit = (data: { apiKey: string }) => {
|
||||
reset();
|
||||
installTool(data.apiKey);
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleRevokeApiKey = () => {
|
||||
reset();
|
||||
removeTool();
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<HoverCard openDelay={50}>
|
||||
|
|
@ -87,7 +76,7 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
|
|||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||
htmlFor={AgentCapabilities.execute_code}
|
||||
>
|
||||
{localize('com_agents_execute_code')}
|
||||
{localize('com_ui_run_code')}
|
||||
</label>
|
||||
</button>
|
||||
<div className="ml-2 flex gap-2">
|
||||
|
|
@ -104,48 +93,23 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
|
|||
<HoverCardContent side={ESide.Top} className="w-80">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{/* // TODO: add a Code Interpreter description */}
|
||||
{localize('com_agents_code_interpreter')}
|
||||
</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</div>
|
||||
</HoverCard>
|
||||
<OGDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<OGDialogTemplate
|
||||
className="w-11/12 sm:w-1/4"
|
||||
title={localize('com_agents_tool_not_authenticated')}
|
||||
main={
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter API Key"
|
||||
autoComplete="one-time-code"
|
||||
readOnly={true}
|
||||
onFocus={(e) => (e.target.readOnly = false)}
|
||||
{...register('apiKey', { required: true })}
|
||||
/>
|
||||
</form>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: handleSubmit(onSubmit),
|
||||
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
|
||||
selectText: localize('com_ui_save'),
|
||||
}}
|
||||
buttons={
|
||||
isUserProvided &&
|
||||
isToolAuthenticated && (
|
||||
<Button
|
||||
onClick={handleRevokeApiKey}
|
||||
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
|
||||
>
|
||||
{localize('com_ui_revoke')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
showCancelButton={true}
|
||||
/>
|
||||
</OGDialog>
|
||||
<ApiKeyDialog
|
||||
isOpen={isDialogOpen}
|
||||
onSubmit={onSubmit}
|
||||
onRevoke={handleRevokeApiKey}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
register={keyFormMethods.register}
|
||||
isToolAuthenticated={isToolAuthenticated}
|
||||
handleSubmit={keyFormMethods.handleSubmit}
|
||||
isUserProvided={authType === AuthType.USER_PROVIDED}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
106
client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx
Normal file
106
client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import type { UseFormRegister, UseFormHandleSubmit } from 'react-hook-form';
|
||||
import type { ApiKeyFormData } from '~/common';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { Input, Button, OGDialog } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function ApiKeyDialog({
|
||||
isOpen,
|
||||
onSubmit,
|
||||
onRevoke,
|
||||
onOpenChange,
|
||||
isUserProvided,
|
||||
isToolAuthenticated,
|
||||
register,
|
||||
handleSubmit,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (data: { apiKey: string }) => void;
|
||||
onRevoke: () => void;
|
||||
isUserProvided: boolean;
|
||||
isToolAuthenticated: boolean;
|
||||
register: UseFormRegister<ApiKeyFormData>;
|
||||
handleSubmit: UseFormHandleSubmit<ApiKeyFormData>;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const languageIcons = [
|
||||
'python.svg',
|
||||
'nodedotjs.svg',
|
||||
'tsnode.svg',
|
||||
'rust.svg',
|
||||
'go.svg',
|
||||
'c.svg',
|
||||
'cplusplus.svg',
|
||||
'php.svg',
|
||||
'fortran.svg',
|
||||
];
|
||||
|
||||
return (
|
||||
<OGDialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<OGDialogTemplate
|
||||
className="w-11/12 sm:w-[450px]"
|
||||
title=""
|
||||
main={
|
||||
<>
|
||||
<div className="mb-4 text-center font-medium">
|
||||
{localize('com_ui_librechat_code_api_title')}
|
||||
</div>
|
||||
<div className="mb-4 text-center text-sm">
|
||||
{localize('com_ui_librechat_code_api_subtitle')}
|
||||
</div>
|
||||
{/* Language Icons Stack */}
|
||||
<div className="mb-6">
|
||||
<div className="mx-auto mb-4 flex max-w-[400px] flex-wrap justify-center gap-3">
|
||||
{languageIcons.map((icon) => (
|
||||
<div key={icon} className="h-6 w-6">
|
||||
<img
|
||||
src={`/assets/${icon}`}
|
||||
alt=""
|
||||
className="h-full w-full object-contain opacity-[0.85] dark:invert"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
href="https://code.librechat.ai/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-center text-[15px] font-medium text-blue-500 underline decoration-1 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{localize('com_ui_librechat_code_api_key')}
|
||||
</a>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={localize('com_ui_enter_api_key')}
|
||||
autoComplete="one-time-code"
|
||||
readOnly={true}
|
||||
onFocus={(e) => (e.target.readOnly = false)}
|
||||
{...register('apiKey', { required: true })}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: handleSubmit(onSubmit),
|
||||
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
|
||||
selectText: localize('com_ui_save'),
|
||||
}}
|
||||
buttons={
|
||||
isUserProvided &&
|
||||
isToolAuthenticated && (
|
||||
<Button
|
||||
onClick={onRevoke}
|
||||
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
|
||||
>
|
||||
{localize('com_ui_revoke')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
showCancelButton={true}
|
||||
/>
|
||||
</OGDialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import type { ExtendedFile, AgentForm } from '~/common';
|
|||
import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { AttachmentIcon } from '~/components/svg';
|
||||
import { useChatContext } from '~/Providers';
|
||||
|
||||
const tool_resource = EToolResources.execute_code;
|
||||
|
|
@ -68,8 +69,8 @@ export default function Files({
|
|||
|
||||
return (
|
||||
<div className="mb-2 w-full">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-token-text-tertiary rounded-lg text-xs">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="rounded-lg text-xs text-text-secondary">
|
||||
{localize('com_assistants_code_interpreter_files')}
|
||||
</div>
|
||||
<FileRow
|
||||
|
|
@ -85,10 +86,10 @@ export default function Files({
|
|||
<button
|
||||
type="button"
|
||||
disabled={!agent_id || codeChecked === false}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<div className="flex w-full items-center justify-center gap-1">
|
||||
<input
|
||||
multiple={true}
|
||||
type="file"
|
||||
|
|
@ -98,7 +99,8 @@ export default function Files({
|
|||
disabled={!agent_id || codeChecked === false}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{localize('com_ui_upload_files')}
|
||||
<AttachmentIcon className="text-token-text-primary h-4 w-4" />
|
||||
{localize('com_ui_upload_code_files')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,13 +16,18 @@ export default function CodeForm({
|
|||
const { data } = useVerifyAgentToolAuth({ toolId: Tools.execute_code });
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-1.5 flex items-center">
|
||||
<span>
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
{localize('com_assistants_capabilities')}
|
||||
</label>
|
||||
</span>
|
||||
<div className="w-full">
|
||||
<div className="mb-1.5 flex items-center gap-2">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-token-text-primary block font-medium">
|
||||
{localize('com_agents_code_interpreter_title')}
|
||||
</span>
|
||||
<span className="text-xs text-text-secondary">
|
||||
{localize('com_agents_by_librechat')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Action authType={data?.message} isToolAuthenticated={data?.authenticated} />
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export default function FileSearch({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="w-full">
|
||||
<div className="mb-1.5 flex items-center gap-2">
|
||||
<span>
|
||||
<label className="text-token-text-primary block font-medium">
|
||||
|
|
@ -76,12 +76,12 @@ export default function FileSearch({
|
|||
</span>
|
||||
</div>
|
||||
<FileSearchCheckbox />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!agent_id || fileSearchChecked === false}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-1">
|
||||
|
|
@ -95,13 +95,13 @@ export default function FileSearch({
|
|||
disabled={!agent_id || fileSearchChecked === false}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{localize('com_ui_upload_files')}
|
||||
{localize('com_ui_upload_file_search')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/* Disabled Message */}
|
||||
{agent_id ? null : (
|
||||
<div className="text-sm text-text-secondary">
|
||||
<div className="text-xs text-text-secondary">
|
||||
{localize('com_agents_file_search_disabled')}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -31,14 +31,17 @@ export default function Parameters({
|
|||
: (providerOption as StringOption | undefined)?.value;
|
||||
return value ?? '';
|
||||
}, [providerOption]);
|
||||
const models = useMemo(() => (provider ? modelsData[provider] : []), [modelsData, provider]);
|
||||
const models = useMemo(
|
||||
() => (provider ? modelsData[provider] ?? [] : []),
|
||||
[modelsData, provider],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const _model = model ?? '';
|
||||
if (provider && _model) {
|
||||
const modelExists = models.includes(_model);
|
||||
if (!modelExists) {
|
||||
const newModels = modelsData[provider];
|
||||
const newModels = modelsData[provider] ?? [];
|
||||
setValue('model', newModels[0] ?? '');
|
||||
}
|
||||
}
|
||||
|
|
@ -105,14 +108,16 @@ export default function Parameters({
|
|||
<SelectDropDown
|
||||
emptyTitle={true}
|
||||
value={field.value ?? ''}
|
||||
title={localize('com_ui_provider')}
|
||||
placeholder={localize('com_ui_select_provider')}
|
||||
searchPlaceholder={localize('com_ui_select_search_provider')}
|
||||
setValue={field.onChange}
|
||||
availableValues={providers}
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
|
||||
'flex h-9 w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
|
||||
(field.value === undefined || field.value === '') &&
|
||||
'border-2 border-yellow-400',
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { AgentCapabilities } from 'librechat-data-provider';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
import {
|
||||
Checkbox,
|
||||
HoverCard,
|
||||
// HoverCardContent,
|
||||
// HoverCardPortal,
|
||||
// HoverCardTrigger,
|
||||
} from '~/components/ui';
|
||||
// import { CircleHelpIcon } from '~/components/svg';
|
||||
// import { useLocalize } from '~/hooks';
|
||||
// import { ESide } from '~/common';
|
||||
|
||||
export default function HideSequential() {
|
||||
// const localize = useLocalize();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const { control, setValue, getValues } = methods;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HoverCard openDelay={50}>
|
||||
<div className="my-2 flex items-center">
|
||||
<Controller
|
||||
name={AgentCapabilities.hide_sequential_outputs}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() =>
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
setValue(
|
||||
AgentCapabilities.hide_sequential_outputs,
|
||||
!getValues(AgentCapabilities.hide_sequential_outputs),
|
||||
{
|
||||
shouldDirty: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="form-check-label text-token-text-primary w-full cursor-pointer"
|
||||
htmlFor={AgentCapabilities.hide_sequential_outputs}
|
||||
>
|
||||
Hide Sequential Agent Outputs except the last agent's
|
||||
</label>
|
||||
{/* <HoverCardTrigger>
|
||||
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
|
||||
</HoverCardTrigger> */}
|
||||
</button>
|
||||
{/* <HoverCardPortal>
|
||||
<HoverCardContent side={ESide.Top} className="w-80">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{localize('com_agents_ttg_info')}
|
||||
</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal> */}
|
||||
</div>
|
||||
</HoverCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { Plus, X } from 'lucide-react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
|
||||
import { TooltipAnchor } from '~/components/ui';
|
||||
import HideSequential from './HideSequential';
|
||||
|
||||
interface SequentialAgentsProps {
|
||||
field: {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
||||
const inputClass = cn(
|
||||
defaultTextProps,
|
||||
'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800 rounded-xl mb-2',
|
||||
removeFocusOutlines,
|
||||
);
|
||||
|
||||
const maxAgents = 5;
|
||||
|
||||
const SequentialAgents: React.FC<SequentialAgentsProps> = ({ field }) => {
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const nodeRef = useRef(null);
|
||||
const [newAgentId, setNewAgentId] = useState('');
|
||||
|
||||
const handleAddAgentId = () => {
|
||||
if (newAgentId.trim() && field.value.length < maxAgents) {
|
||||
const newValues = [...field.value, newAgentId];
|
||||
field.onChange(newValues);
|
||||
setNewAgentId('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAgentId = (index: number) => {
|
||||
const newValues = field.value.filter((_, i) => i !== index);
|
||||
field.onChange(newValues);
|
||||
};
|
||||
|
||||
const defaultStyle = {
|
||||
transition: 'opacity 200ms ease-in-out',
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
const triggerShake = (element: HTMLElement) => {
|
||||
element.classList.remove('shake');
|
||||
void element.offsetWidth;
|
||||
element.classList.add('shake');
|
||||
setTimeout(() => {
|
||||
element.classList.remove('shake');
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const transitionStyles = {
|
||||
entering: { opacity: 1 },
|
||||
entered: { opacity: 1 },
|
||||
exiting: { opacity: 0 },
|
||||
exited: { opacity: 0 },
|
||||
};
|
||||
|
||||
const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<label className={labelClass} htmlFor="agent_ids">
|
||||
Sequential Agents
|
||||
</label>
|
||||
<div className="mt-4 space-y-2">
|
||||
<HideSequential />
|
||||
{/* Display existing agents first */}
|
||||
{field.value.map((agentId, index) => (
|
||||
<div key={index} className="relative">
|
||||
<input
|
||||
ref={(el) => (inputRefs.current[index] = el)}
|
||||
value={agentId}
|
||||
onChange={(e) => {
|
||||
const newValue = [...field.value];
|
||||
newValue[index] = e.target.value;
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
className={`${inputClass} pr-10`}
|
||||
type="text"
|
||||
maxLength={64}
|
||||
/>
|
||||
<TooltipAnchor
|
||||
side="top"
|
||||
description={'Remove agent ID'}
|
||||
className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
|
||||
onClick={() => handleDeleteAgentId(index)}
|
||||
>
|
||||
<X className="size-4" />
|
||||
</TooltipAnchor>
|
||||
</div>
|
||||
))}
|
||||
{/* Input for new agent at the bottom */}
|
||||
<div className="relative">
|
||||
<input
|
||||
ref={(el) => (inputRefs.current[field.value.length] = el)}
|
||||
value={newAgentId}
|
||||
maxLength={64}
|
||||
className={`${inputClass} pr-10`}
|
||||
type="text"
|
||||
placeholder={hasReachedMax ? 'Max agents reached' : 'Enter agent ID (e.g. agent_1234)'}
|
||||
onChange={(e) => setNewAgentId(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (hasReachedMax) {
|
||||
triggerShake(e.currentTarget);
|
||||
} else {
|
||||
handleAddAgentId();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Transition
|
||||
nodeRef={nodeRef}
|
||||
in={field.value.length < Constants.MAX_CONVO_STARTERS}
|
||||
timeout={200}
|
||||
unmountOnExit
|
||||
>
|
||||
{(state: string) => (
|
||||
<div
|
||||
ref={nodeRef}
|
||||
style={{
|
||||
...defaultStyle,
|
||||
...transitionStyles[state as keyof typeof transitionStyles],
|
||||
transition: state === 'entering' ? 'none' : defaultStyle.transition,
|
||||
}}
|
||||
className="absolute right-1 top-1"
|
||||
>
|
||||
<TooltipAnchor
|
||||
side="top"
|
||||
description={hasReachedMax ? 'Max agents reached' : 'Add agent ID'}
|
||||
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
|
||||
onClick={handleAddAgentId}
|
||||
disabled={hasReachedMax}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</TooltipAnchor>
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SequentialAgents;
|
||||
|
|
@ -76,7 +76,7 @@ export default function CodeFiles({
|
|||
<button
|
||||
type="button"
|
||||
disabled={!assistant_id}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
|||
<TooltipAnchor
|
||||
description={localize(link.title)}
|
||||
side="left"
|
||||
key={`nav-link-${index}`}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import {
|
|||
useGetStartupConfig,
|
||||
useUserKeyQuery,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider';
|
||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
import type { TEndpointsConfig } from 'librechat-data-provider';
|
||||
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
|
||||
import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
|
||||
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
|
||||
|
|
@ -65,7 +65,7 @@ const SidePanel = ({
|
|||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const interfaceConfig = useMemo(
|
||||
() => startupConfig?.interface ?? defaultInterface,
|
||||
() => (startupConfig?.interface ?? defaultInterface) as Partial<TInterfaceConfig>,
|
||||
[startupConfig],
|
||||
);
|
||||
|
||||
|
|
@ -117,17 +117,17 @@ const SidePanel = ({
|
|||
});
|
||||
|
||||
const calculateLayout = useCallback(() => {
|
||||
if (!artifacts) {
|
||||
if (artifacts == null) {
|
||||
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
|
||||
return [100 - navSize, navSize];
|
||||
} else {
|
||||
const navSize = Math.max(minSize, navCollapsedSize);
|
||||
const navSize = 0;
|
||||
const remainingSpace = 100 - navSize;
|
||||
const newMainSize = Math.floor(remainingSpace / 2);
|
||||
const artifactsSize = remainingSpace - newMainSize;
|
||||
return [newMainSize, artifactsSize, navSize];
|
||||
}
|
||||
}, [artifacts, defaultLayout, minSize, navCollapsedSize]);
|
||||
}, [artifacts, defaultLayout]);
|
||||
|
||||
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
|
||||
|
||||
|
|
@ -261,7 +261,7 @@ const SidePanel = ({
|
|||
: 'opacity-100',
|
||||
)}
|
||||
>
|
||||
{interfaceConfig.modelSelect && (
|
||||
{interfaceConfig.modelSelect === true && (
|
||||
<div
|
||||
className={cn(
|
||||
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface DropdownProps {
|
||||
trigger: React.ReactNode;
|
||||
|
|
@ -15,11 +16,21 @@ interface DropdownProps {
|
|||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
anchor?: { x: string; y: string };
|
||||
modal?: boolean;
|
||||
menuId: string;
|
||||
}
|
||||
|
||||
const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsOpen, menuId }) => {
|
||||
const DropdownPopup: React.FC<DropdownProps> = ({
|
||||
trigger,
|
||||
items,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
menuId,
|
||||
modal,
|
||||
iconClassName,
|
||||
}) => {
|
||||
const menu = Ariakit.useMenuStore({ open: isOpen, setOpen: setIsOpen });
|
||||
|
||||
return (
|
||||
|
|
@ -27,8 +38,9 @@ const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsO
|
|||
{trigger}
|
||||
<Ariakit.Menu
|
||||
id={menuId}
|
||||
className="z-50 mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
|
||||
className="absolute z-50 mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
|
||||
gutter={8}
|
||||
modal={modal}
|
||||
>
|
||||
{items
|
||||
.filter((item) => item.show !== false)
|
||||
|
|
@ -49,7 +61,7 @@ const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsO
|
|||
}}
|
||||
>
|
||||
{item.icon != null && (
|
||||
<span className="mr-2 h-5 w-5" aria-hidden="true">
|
||||
<span className={cn('mr-2 h-5 w-5', iconClassName)} aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -81,13 +81,13 @@ function defaultGetStringKey(node: unknown): string {
|
|||
* @returns
|
||||
*/
|
||||
export function useMultiSearch<OptionsType extends unknown[]>({
|
||||
availableOptions,
|
||||
availableOptions = [] as unknown as OptionsType,
|
||||
placeholder,
|
||||
getTextKeyOverride,
|
||||
className,
|
||||
disabled = false,
|
||||
}: {
|
||||
availableOptions: OptionsType;
|
||||
availableOptions?: OptionsType;
|
||||
placeholder?: string;
|
||||
getTextKeyOverride?: (node: OptionsType[0]) => string;
|
||||
className?: string;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ type SelectDropDownProps = {
|
|||
value: string | null | Option | OptionWithIcon;
|
||||
setValue: DropdownValueSetter | ((value: string) => void);
|
||||
tabIndex?: number;
|
||||
availableValues: string[] | Option[] | OptionWithIcon[];
|
||||
availableValues?: string[] | Option[] | OptionWithIcon[];
|
||||
emptyTitle?: boolean;
|
||||
showAbove?: boolean;
|
||||
showLabel?: boolean;
|
||||
|
|
@ -89,18 +89,20 @@ function SelectDropDown({
|
|||
title = localize('com_ui_model');
|
||||
}
|
||||
|
||||
const values = availableValues ?? [];
|
||||
|
||||
// Detemine if we should to convert this component into a searchable select. If we have enough elements, a search
|
||||
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
|
||||
// reset once the component is unmounted (as per a normal search)
|
||||
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
|
||||
availableOptions: availableValues,
|
||||
availableOptions: values,
|
||||
placeholder: searchPlaceholder,
|
||||
getTextKeyOverride: (option) => getOptionText(option).toUpperCase(),
|
||||
className: searchClassName,
|
||||
disabled,
|
||||
});
|
||||
const hasSearchRender = searchRender != null;
|
||||
const options = hasSearchRender ? filteredValues : availableValues;
|
||||
const options = hasSearchRender ? filteredValues : values;
|
||||
|
||||
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
|||
description: string;
|
||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||
className?: string;
|
||||
focusable?: boolean;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
1
client/src/data-provider/Agents/index.ts
Normal file
1
client/src/data-provider/Agents/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './queries';
|
||||
76
client/src/data-provider/Agents/queries.ts
Normal file
76
client/src/data-provider/Agents/queries.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { QueryKeys, dataService, EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* AGENTS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hook for getting all available tools for A
|
||||
*/
|
||||
export const useAvailableAgentToolsQuery = (): QueryObserverResult<t.TPlugin[]> => {
|
||||
const queryClient = useQueryClient();
|
||||
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
|
||||
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
|
||||
return useQuery<t.TPlugin[]>([QueryKeys.tools], () => dataService.getAvailableAgentTools(), {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for listing all Agents, with optional parameters provided for pagination and sorting
|
||||
*/
|
||||
export const useListAgentsQuery = <TData = t.AgentListResponse>(
|
||||
params: t.AgentListParams = defaultOrderQuery,
|
||||
config?: UseQueryOptions<t.AgentListResponse, unknown, TData>,
|
||||
): QueryObserverResult<TData> => {
|
||||
const queryClient = useQueryClient();
|
||||
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
|
||||
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
|
||||
return useQuery<t.AgentListResponse, unknown, TData>(
|
||||
[QueryKeys.agents, params],
|
||||
() => dataService.listAgents(params),
|
||||
{
|
||||
// Example selector to sort them by created_at
|
||||
// select: (res) => {
|
||||
// return res.data.sort((a, b) => a.created_at - b.created_at);
|
||||
// },
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
retry: false,
|
||||
...config,
|
||||
enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for retrieving details about a single agent
|
||||
*/
|
||||
export const useGetAgentByIdQuery = (
|
||||
agent_id: string,
|
||||
config?: UseQueryOptions<t.Agent>,
|
||||
): QueryObserverResult<t.Agent> => {
|
||||
return useQuery<t.Agent>(
|
||||
[QueryKeys.agent, agent_id],
|
||||
() =>
|
||||
dataService.getAgentById({
|
||||
agent_id,
|
||||
}),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
retry: false,
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
export * from './queries';
|
||||
// export * from './mutations';
|
||||
export * from './mutations';
|
||||
|
|
|
|||
42
client/src/data-provider/Tools/mutations.ts
Normal file
42
client/src/data-provider/Tools/mutations.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { dataService, QueryKeys, Tools } from 'librechat-data-provider';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
|
||||
export const useToolCallMutation = <T extends t.ToolId>(
|
||||
toolId: T,
|
||||
options?: t.ToolCallMutationOptions<T>,
|
||||
): UseMutationResult<t.ToolCallResponse, Error, t.ToolParams<T>> => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
(toolParams: t.ToolParams<T>) => {
|
||||
return dataService.callTool({
|
||||
toolId,
|
||||
toolParams,
|
||||
});
|
||||
},
|
||||
{
|
||||
onMutate: (variables) => options?.onMutate?.(variables),
|
||||
onError: (error, variables, context) => options?.onError?.(error, variables, context),
|
||||
onSuccess: (response, variables, context) => {
|
||||
queryClient.setQueryData<t.ToolCallResults>(
|
||||
[QueryKeys.toolCalls, variables.conversationId],
|
||||
(prev) => [
|
||||
...(prev ?? []),
|
||||
{
|
||||
user: '',
|
||||
toolId: Tools.execute_code,
|
||||
partIndex: variables.partIndex,
|
||||
messageId: variables.messageId,
|
||||
blockIndex: variables.blockIndex,
|
||||
conversationId: variables.conversationId,
|
||||
result: response.result,
|
||||
attachments: response.attachments,
|
||||
},
|
||||
],
|
||||
);
|
||||
return options?.onSuccess?.(response, variables, context);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Constants, QueryKeys, dataService } from 'librechat-data-provider';
|
||||
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
|
||||
|
|
@ -18,3 +18,24 @@ export const useVerifyAgentToolAuth = (
|
|||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetToolCalls = <TData = t.ToolCallResults>(
|
||||
params: t.GetToolCallParams,
|
||||
config?: UseQueryOptions<t.ToolCallResults, unknown, TData>,
|
||||
): QueryObserverResult<TData, unknown> => {
|
||||
const { conversationId = '' } = params;
|
||||
return useQuery<t.ToolCallResults, unknown, TData>(
|
||||
[QueryKeys.toolCalls, conversationId],
|
||||
() => dataService.getToolCalls(params),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
enabled:
|
||||
conversationId.length > 0 &&
|
||||
conversationId !== Constants.NEW_CONVO &&
|
||||
conversationId !== Constants.SEARCH,
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './Agents';
|
||||
export * from './Files';
|
||||
export * from './Tools';
|
||||
export * from './connection';
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@ import type {
|
|||
AssistantListParams,
|
||||
AssistantListResponse,
|
||||
AssistantDocument,
|
||||
Agent,
|
||||
AgentListParams,
|
||||
AgentListResponse,
|
||||
TEndpointsConfig,
|
||||
TCheckUserKeyResponse,
|
||||
SharedLinkListParams,
|
||||
|
|
@ -370,78 +367,6 @@ export const useGetAssistantDocsQuery = <TData = AssistantDocument[]>(
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* AGENTS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hook for getting all available tools for A
|
||||
*/
|
||||
export const useAvailableAgentToolsQuery = (): QueryObserverResult<TPlugin[]> => {
|
||||
const queryClient = useQueryClient();
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
|
||||
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
|
||||
return useQuery<TPlugin[]>([QueryKeys.tools], () => dataService.getAvailableAgentTools(), {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for listing all Agents, with optional parameters provided for pagination and sorting
|
||||
*/
|
||||
export const useListAgentsQuery = <TData = AgentListResponse>(
|
||||
params: AgentListParams = defaultOrderQuery,
|
||||
config?: UseQueryOptions<AgentListResponse, unknown, TData>,
|
||||
): QueryObserverResult<TData> => {
|
||||
const queryClient = useQueryClient();
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
|
||||
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
|
||||
return useQuery<AgentListResponse, unknown, TData>(
|
||||
[QueryKeys.agents, params],
|
||||
() => dataService.listAgents(params),
|
||||
{
|
||||
// Example selector to sort them by created_at
|
||||
// select: (res) => {
|
||||
// return res.data.sort((a, b) => a.created_at - b.created_at);
|
||||
// },
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
retry: false,
|
||||
...config,
|
||||
enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for retrieving details about a single agent
|
||||
*/
|
||||
export const useGetAgentByIdQuery = (
|
||||
agent_id: string,
|
||||
config?: UseQueryOptions<Agent>,
|
||||
): QueryObserverResult<Agent> => {
|
||||
return useQuery<Agent>(
|
||||
[QueryKeys.agent, agent_id],
|
||||
() =>
|
||||
dataService.getAgentById({
|
||||
agent_id,
|
||||
}),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
retry: false,
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/** STT/TTS */
|
||||
|
||||
/* Text to speech voices */
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ export const useGetRole = (
|
|||
|
||||
export const useUpdatePromptPermissionsMutation = (
|
||||
options?: t.UpdatePromptPermOptions,
|
||||
): UseMutationResult<t.UpdatePromptPermResponse, t.TError, t.UpdatePromptPermVars, unknown> => {
|
||||
): UseMutationResult<
|
||||
t.UpdatePermResponse,
|
||||
t.TError | undefined,
|
||||
t.UpdatePromptPermVars,
|
||||
unknown
|
||||
> => {
|
||||
const queryClient = useQueryClient();
|
||||
const { onMutate, onSuccess, onError } = options ?? {};
|
||||
return useMutation(
|
||||
|
|
@ -38,7 +43,10 @@ export const useUpdatePromptPermissionsMutation = (
|
|||
}
|
||||
},
|
||||
onError: (...args) => {
|
||||
args[0] && console.error('Failed to update prompt permissions:', args[0]);
|
||||
const error = args[0];
|
||||
if (error != null) {
|
||||
console.error('Failed to update prompt permissions:', error);
|
||||
}
|
||||
if (onError) {
|
||||
onError(...args);
|
||||
}
|
||||
|
|
@ -47,3 +55,39 @@ export const useUpdatePromptPermissionsMutation = (
|
|||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateAgentPermissionsMutation = (
|
||||
options?: t.UpdateAgentPermOptions,
|
||||
): UseMutationResult<
|
||||
t.UpdatePermResponse,
|
||||
t.TError | undefined,
|
||||
t.UpdateAgentPermVars,
|
||||
unknown
|
||||
> => {
|
||||
const queryClient = useQueryClient();
|
||||
const { onMutate, onSuccess, onError } = options ?? {};
|
||||
return useMutation(
|
||||
(variables) => {
|
||||
promptPermissionsSchema.partial().parse(variables.updates);
|
||||
return dataService.updateAgentPermissions(variables);
|
||||
},
|
||||
{
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries([QueryKeys.roles, variables.roleName]);
|
||||
if (onSuccess != null) {
|
||||
onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (...args) => {
|
||||
const error = args[0];
|
||||
if (error != null) {
|
||||
console.error('Failed to update prompt permissions:', error);
|
||||
}
|
||||
if (onError != null) {
|
||||
onError(...args);
|
||||
}
|
||||
},
|
||||
onMutate,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function useAssistantListMap<T = AssistantListItem[] | null>(
|
|||
selector: (res: AssistantListResponse) => T = selectAssistantsResponse as (
|
||||
res: AssistantListResponse,
|
||||
) => T,
|
||||
): Record<AssistantsEndpoint, T> {
|
||||
): Record<AssistantsEndpoint, T | null> {
|
||||
const { data: assistantsList = null } = useListAssistantsQuery(
|
||||
EModelEndpoint.assistants,
|
||||
undefined,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export default function usePresets() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (presets && presets.length > 0 && user && presets[0].user !== user?.id) {
|
||||
if (presets && presets.length > 0 && user && presets[0].user !== user.id) {
|
||||
presetsQuery.refetch();
|
||||
return;
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ export default function usePresets() {
|
|||
}
|
||||
const previousPresets = presetsQuery.data ?? [];
|
||||
if (previousPresets) {
|
||||
setPresets(previousPresets.filter((p) => p.presetId !== preset?.presetId));
|
||||
setPresets(previousPresets.filter((p) => p.presetId !== preset.presetId));
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
@ -99,12 +99,12 @@ export default function usePresets() {
|
|||
const updatePreset = useUpdatePresetMutation({
|
||||
onSuccess: (data, preset) => {
|
||||
const toastTitle = data.title ? `"${data.title}"` : localize('com_endpoint_preset_title');
|
||||
let message = `${toastTitle} ${localize('com_endpoint_preset_saved')}`;
|
||||
let message = `${toastTitle} ${localize('com_ui_saved')}`;
|
||||
if (data.defaultPreset && data.presetId !== _defaultPreset?.presetId) {
|
||||
message = `${toastTitle} ${localize('com_endpoint_preset_default')}`;
|
||||
setDefaultPreset(data);
|
||||
newConversation({ preset: data });
|
||||
} else if (preset?.defaultPreset === false) {
|
||||
} else if (preset.defaultPreset === false) {
|
||||
setDefaultPreset(null);
|
||||
message = `${toastTitle} ${localize('com_endpoint_preset_default_removed')}`;
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ export default function usePresets() {
|
|||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
const fileName = filenamify(preset?.title || 'preset');
|
||||
const fileName = filenamify(preset.title || 'preset');
|
||||
exportFromJSON({
|
||||
data: cleanupPreset({ preset }),
|
||||
fileName,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const useDelayedUploadToast = () => {
|
|||
showToast({
|
||||
message,
|
||||
status: 'warning',
|
||||
duration: 7000,
|
||||
duration: 10000,
|
||||
});
|
||||
}, delay);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
EModelEndpoint,
|
||||
codeTypeMapping,
|
||||
mergeFileConfig,
|
||||
isAgentsEndpoint,
|
||||
isAssistantsEndpoint,
|
||||
defaultAssistantsVersion,
|
||||
fileConfig as defaultFileConfig,
|
||||
|
|
@ -38,6 +39,7 @@ const useFileHandling = (params?: UseFileHandling) => {
|
|||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const { startUploadTimer, clearUploadTimer } = useDelayedUploadToast();
|
||||
const [toolResource, setToolResource] = useState<string | undefined>();
|
||||
const { files, setFiles, setFilesLoading, conversation } = useChatContext();
|
||||
const setError = (error: string) => setErrors((prevErrors) => [...prevErrors, error]);
|
||||
const { addFile, replaceFile, updateFileById, deleteFileById } = useUpdateFiles(
|
||||
|
|
@ -147,6 +149,9 @@ const useFileHandling = (params?: UseFileHandling) => {
|
|||
: error?.response?.data?.message ?? 'com_error_files_upload';
|
||||
setError(errorMessage);
|
||||
},
|
||||
onMutate: () => {
|
||||
setToolResource(undefined);
|
||||
},
|
||||
},
|
||||
abortControllerRef.current?.signal,
|
||||
);
|
||||
|
|
@ -178,6 +183,18 @@ const useFileHandling = (params?: UseFileHandling) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (isAgentsEndpoint(endpoint)) {
|
||||
if (!agent_id) {
|
||||
formData.append('message_file', 'true');
|
||||
}
|
||||
if (toolResource != null) {
|
||||
formData.append('tool_resource', toolResource);
|
||||
}
|
||||
if (conversation?.agent_id != null && formData.get('agent_id') == null) {
|
||||
formData.append('agent_id', conversation.agent_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAssistantsEndpoint(endpoint)) {
|
||||
uploadFile.mutate(formData);
|
||||
return;
|
||||
|
|
@ -377,6 +394,7 @@ const useFileHandling = (params?: UseFileHandling) => {
|
|||
|
||||
return {
|
||||
handleFileChange,
|
||||
setToolResource,
|
||||
handleFiles,
|
||||
abortUpload,
|
||||
setFiles,
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ import {
|
|||
useGetEndpointsQuery,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import {
|
||||
getConfigDefaults,
|
||||
EModelEndpoint,
|
||||
alternateName,
|
||||
EModelEndpoint,
|
||||
getConfigDefaults,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type { AssistantsEndpoint, TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { MentionOption } from '~/common';
|
||||
import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
|
||||
import { useGetPresetsQuery, useListAgentsQuery } from '~/data-provider';
|
||||
import { mapEndpoints, getPresetTitle } from '~/utils';
|
||||
import { EndpointIcon } from '~/components/Endpoints';
|
||||
import { useGetPresetsQuery } from '~/data-provider';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ const assistantMapFn =
|
|||
assistantMap,
|
||||
endpointsConfig,
|
||||
}: {
|
||||
endpoint: AssistantsEndpoint;
|
||||
endpoint: EModelEndpoint | string;
|
||||
assistantMap: TAssistantsMap;
|
||||
endpointsConfig: TEndpointsConfig;
|
||||
}) =>
|
||||
|
|
@ -65,6 +65,27 @@ export default function useMentions({
|
|||
description,
|
||||
})),
|
||||
);
|
||||
const { data: agentsList = null } = useListAgentsQuery(undefined, {
|
||||
select: (res) => {
|
||||
const { data } = res;
|
||||
return data.map(({ id, name, avatar }) => ({
|
||||
value: id,
|
||||
label: name ?? '',
|
||||
type: EModelEndpoint.agents,
|
||||
icon: EndpointIcon({
|
||||
conversation: {
|
||||
agent_id: id,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
iconURL: avatar?.filepath,
|
||||
},
|
||||
containerClassName: 'shadow-stroke overflow-hidden rounded-full',
|
||||
endpointsConfig: endpointsConfig,
|
||||
context: 'menu-item',
|
||||
size: 20,
|
||||
}),
|
||||
}));
|
||||
},
|
||||
});
|
||||
const assistantListMap = useMemo(
|
||||
() => ({
|
||||
[EModelEndpoint.assistants]: listMap[EModelEndpoint.assistants]
|
||||
|
|
@ -101,7 +122,7 @@ export default function useMentions({
|
|||
validEndpoints = endpoints.filter((endpoint) => !isAssistantsEndpoint(endpoint));
|
||||
}
|
||||
const mentions = [
|
||||
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
|
||||
...(modelSpecs.length > 0 ? modelSpecs : []).map((modelSpec) => ({
|
||||
value: modelSpec.name,
|
||||
label: modelSpec.label,
|
||||
description: modelSpec.description,
|
||||
|
|
@ -116,9 +137,9 @@ export default function useMentions({
|
|||
}),
|
||||
type: 'modelSpec' as const,
|
||||
})),
|
||||
...(interfaceConfig.endpointsMenu ? validEndpoints : []).map((endpoint) => ({
|
||||
...(interfaceConfig.endpointsMenu === true ? validEndpoints : []).map((endpoint) => ({
|
||||
value: endpoint,
|
||||
label: alternateName[endpoint] ?? endpoint ?? '',
|
||||
label: alternateName[endpoint as string] ?? endpoint ?? '',
|
||||
type: 'endpoint' as const,
|
||||
icon: EndpointIcon({
|
||||
conversation: { endpoint },
|
||||
|
|
@ -127,13 +148,14 @@ export default function useMentions({
|
|||
size: 20,
|
||||
}),
|
||||
})),
|
||||
...(agentsList ?? []),
|
||||
...(endpointsConfig?.[EModelEndpoint.assistants] && includeAssistants
|
||||
? assistantListMap[EModelEndpoint.assistants] || []
|
||||
: []),
|
||||
...(endpointsConfig?.[EModelEndpoint.azureAssistants] && includeAssistants
|
||||
? assistantListMap[EModelEndpoint.azureAssistants] || []
|
||||
: []),
|
||||
...((interfaceConfig.presets ? presets : [])?.map((preset, index) => ({
|
||||
...((interfaceConfig.presets === true ? presets : [])?.map((preset, index) => ({
|
||||
value: preset.presetId ?? `preset-${index}`,
|
||||
label: preset.title ?? preset.modelLabel ?? preset.chatGptLabel ?? '',
|
||||
description: getPresetTitle(preset, true),
|
||||
|
|
@ -154,6 +176,7 @@ export default function useMentions({
|
|||
presets,
|
||||
endpoints,
|
||||
modelSpecs,
|
||||
agentsList,
|
||||
assistantMap,
|
||||
endpointsConfig,
|
||||
assistantListMap,
|
||||
|
|
@ -166,6 +189,7 @@ export default function useMentions({
|
|||
options,
|
||||
presets,
|
||||
modelSpecs,
|
||||
agentsList,
|
||||
modelsConfig,
|
||||
endpointsConfig,
|
||||
assistantListMap,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type {
|
||||
TPreset,
|
||||
TModelSpec,
|
||||
|
|
@ -64,7 +64,11 @@ export default function useSelectMention({
|
|||
preset.endpointType = newEndpointType;
|
||||
}
|
||||
|
||||
if (isAssistantsEndpoint(newEndpoint) && preset.assistant_id != null && !(preset.model ?? '')) {
|
||||
if (
|
||||
isAssistantsEndpoint(newEndpoint) &&
|
||||
preset.assistant_id != null &&
|
||||
!(preset.model ?? '')
|
||||
) {
|
||||
preset.model = assistantMap?.[newEndpoint]?.[preset.assistant_id]?.model;
|
||||
}
|
||||
|
||||
|
|
@ -94,11 +98,19 @@ export default function useSelectMention({
|
|||
keepAddedConvos: isModular,
|
||||
});
|
||||
},
|
||||
[conversation, getDefaultConversation, modularChat, newConversation, endpointsConfig, assistantMap],
|
||||
[
|
||||
conversation,
|
||||
getDefaultConversation,
|
||||
modularChat,
|
||||
newConversation,
|
||||
endpointsConfig,
|
||||
assistantMap,
|
||||
],
|
||||
);
|
||||
|
||||
type Kwargs = {
|
||||
model?: string;
|
||||
agent_id?: string;
|
||||
assistant_id?: string;
|
||||
};
|
||||
|
||||
|
|
@ -228,6 +240,10 @@ export default function useSelectMention({
|
|||
assistant_id: key,
|
||||
model: assistantMap?.[option.type]?.[key]?.model ?? '',
|
||||
});
|
||||
} else if (isAgentsEndpoint(option.type)) {
|
||||
onSelectEndpoint(option.type, {
|
||||
agent_id: key,
|
||||
});
|
||||
}
|
||||
},
|
||||
[modelSpecs, onSelectEndpoint, onSelectPreset, onSelectSpec, presets, assistantMap],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import type { TEndpointOption } from 'librechat-data-provider';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils';
|
||||
import { forceResize, insertTextAtCursor, getEntityName, getEntity } from '~/utils';
|
||||
import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext';
|
||||
import { useAgentsMapContext } from '~/Providers/AgentsMapContext';
|
||||
import useGetSender from '~/hooks/Conversations/useGetSender';
|
||||
import useFileHandling from '~/hooks/Files/useFileHandling';
|
||||
import { useInteractionHealthCheck } from '~/data-provider';
|
||||
|
|
@ -28,6 +29,7 @@ export default function useTextarea({
|
|||
const localize = useLocalize();
|
||||
const getSender = useGetSender();
|
||||
const isComposing = useRef(false);
|
||||
const agentsMap = useAgentsMapContext();
|
||||
const { handleFiles } = useFileHandling();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const checkHealth = useInteractionHealthCheck();
|
||||
|
|
@ -44,19 +46,25 @@ export default function useTextarea({
|
|||
} = useChatContext();
|
||||
const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index));
|
||||
|
||||
const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {};
|
||||
const { conversationId, jailbreak = false, endpoint = '' } = conversation || {};
|
||||
const { entity, isAgent, isAssistant } = getEntity({
|
||||
endpoint,
|
||||
agentsMap,
|
||||
assistantMap,
|
||||
agent_id: conversation?.agent_id,
|
||||
assistant_id: conversation?.assistant_id,
|
||||
});
|
||||
const entityName = entity?.name ?? '';
|
||||
|
||||
const isNotAppendable =
|
||||
((latestMessage?.unfinished && !isSubmitting) || latestMessage?.error) &&
|
||||
!isAssistantsEndpoint(endpoint);
|
||||
(((latestMessage?.unfinished ?? false) && !isSubmitting) || (latestMessage?.error ?? false)) &&
|
||||
!isAssistant;
|
||||
// && (conversationId?.length ?? 0) > 6; // also ensures that we don't show the wrong placeholder
|
||||
|
||||
const assistant =
|
||||
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint ?? '']?.[assistant_id ?? ''];
|
||||
const assistantName = (assistant && assistant.name) || '';
|
||||
|
||||
useEffect(() => {
|
||||
if (activePrompt && textAreaRef.current) {
|
||||
insertTextAtCursor(textAreaRef.current, activePrompt);
|
||||
const prompt = activePrompt ?? '';
|
||||
if (prompt && textAreaRef.current) {
|
||||
insertTextAtCursor(textAreaRef.current, prompt);
|
||||
forceResize(textAreaRef.current);
|
||||
setActivePrompt(undefined);
|
||||
}
|
||||
|
|
@ -64,16 +72,17 @@ export default function useTextarea({
|
|||
|
||||
// auto focus to input, when enter a conversation.
|
||||
useEffect(() => {
|
||||
if (!conversationId) {
|
||||
const convoId = conversationId ?? '';
|
||||
if (!convoId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevents Settings from not showing on new conversation, also prevents showing toneStyle change without jailbreak
|
||||
if (conversationId === 'new' || !jailbreak) {
|
||||
if (convoId === Constants.NEW_CONVO || !jailbreak) {
|
||||
setShowBingToneSetting(false);
|
||||
}
|
||||
|
||||
if (conversationId !== 'search') {
|
||||
if (convoId !== Constants.SEARCH) {
|
||||
textAreaRef.current?.focus();
|
||||
}
|
||||
// setShowBingToneSetting is a recoil setter, so it doesn't need to be in the dependency array
|
||||
|
|
@ -89,7 +98,8 @@ export default function useTextarea({
|
|||
}, [isSubmitting, textAreaRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (textAreaRef.current?.value) {
|
||||
const currentValue = textAreaRef.current?.value ?? '';
|
||||
if (currentValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -98,10 +108,13 @@ export default function useTextarea({
|
|||
return localize('com_endpoint_config_placeholder');
|
||||
}
|
||||
const currentEndpoint = conversation?.endpoint ?? '';
|
||||
const currentAgentId = conversation?.agent_id ?? '';
|
||||
const currentAssistantId = conversation?.assistant_id ?? '';
|
||||
if (
|
||||
isAssistantsEndpoint(currentEndpoint) &&
|
||||
(!currentAssistantId || !assistantMap?.[currentEndpoint]?.[currentAssistantId ?? ''])
|
||||
if (isAgent && (!currentAgentId || !agentsMap?.[currentAgentId])) {
|
||||
return localize('com_endpoint_agent_placeholder');
|
||||
} else if (
|
||||
isAssistant &&
|
||||
(!currentAssistantId || !assistantMap?.[currentEndpoint]?.[currentAssistantId])
|
||||
) {
|
||||
return localize('com_endpoint_assistant_placeholder');
|
||||
}
|
||||
|
|
@ -110,9 +123,10 @@ export default function useTextarea({
|
|||
return localize('com_endpoint_message_not_appendable');
|
||||
}
|
||||
|
||||
const sender = isAssistantsEndpoint(currentEndpoint)
|
||||
? getAssistantName({ name: assistantName, localize })
|
||||
: getSender(conversation as TEndpointOption);
|
||||
const sender =
|
||||
isAssistant || isAgent
|
||||
? getEntityName({ name: entityName, isAgent, localize })
|
||||
: getSender(conversation as TEndpointOption);
|
||||
|
||||
return `${localize('com_endpoint_message')} ${sender ? sender : 'AI'}`;
|
||||
};
|
||||
|
|
@ -137,15 +151,18 @@ export default function useTextarea({
|
|||
|
||||
return () => debouncedSetPlaceholder.cancel();
|
||||
}, [
|
||||
conversation,
|
||||
isAgent,
|
||||
localize,
|
||||
disabled,
|
||||
getSender,
|
||||
agentsMap,
|
||||
entityName,
|
||||
textAreaRef,
|
||||
isAssistant,
|
||||
assistantMap,
|
||||
conversation,
|
||||
latestMessage,
|
||||
isNotAppendable,
|
||||
localize,
|
||||
getSender,
|
||||
assistantName,
|
||||
textAreaRef,
|
||||
assistantMap,
|
||||
]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
|
|
@ -181,7 +198,7 @@ export default function useTextarea({
|
|||
}
|
||||
|
||||
if ((isNonShiftEnter || isCtrlEnter) && !isComposing.current) {
|
||||
const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement;
|
||||
const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement | undefined;
|
||||
if (globalAudio) {
|
||||
console.log('Unmuting global audio');
|
||||
globalAudio.muted = false;
|
||||
|
|
@ -207,14 +224,15 @@ export default function useTextarea({
|
|||
return;
|
||||
}
|
||||
|
||||
if (!e.clipboardData) {
|
||||
const clipboardData = e.clipboardData as DataTransfer | undefined;
|
||||
if (!clipboardData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.clipboardData.files.length > 0) {
|
||||
if (clipboardData.files.length > 0) {
|
||||
setFilesLoading(true);
|
||||
const timestampedFiles: File[] = [];
|
||||
for (const file of e.clipboardData.files) {
|
||||
for (const file of clipboardData.files) {
|
||||
const newFile = new File([file], `clipboard_${+new Date()}_${file.name}`, {
|
||||
type: file.type,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default function useMessageActions(props: TMessageActions) {
|
|||
[isMultiMessage, addedConvo, rootConvo],
|
||||
);
|
||||
|
||||
const agentMap = useAgentsMapContext();
|
||||
const agentsMap = useAgentsMapContext();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
|
||||
const { text, content, messageId = null, isCreatedByUser } = message ?? {};
|
||||
|
|
@ -68,20 +68,20 @@ export default function useMessageActions(props: TMessageActions) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (!agentMap) {
|
||||
if (!agentsMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const modelKey = message?.model ?? '';
|
||||
if (modelKey) {
|
||||
return agentMap[modelKey];
|
||||
return agentsMap[modelKey];
|
||||
}
|
||||
|
||||
const agentId = conversation?.agent_id ?? '';
|
||||
if (agentId) {
|
||||
return agentMap[agentId];
|
||||
return agentsMap[agentId];
|
||||
}
|
||||
}, [agentMap, conversation?.agent_id, conversation?.endpoint, message?.model]);
|
||||
}, [agentsMap, conversation?.agent_id, conversation?.endpoint, message?.model]);
|
||||
|
||||
const isSubmitting = useMemo(
|
||||
() => (isMultiMessage === true ? isSubmittingAdditional : isSubmittingRoot),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default function useMessageHelpers(props: TMessageProps) {
|
|||
setLatestMessage,
|
||||
} = useChatContext();
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
const agentMap = useAgentsMapContext();
|
||||
const agentsMap = useAgentsMapContext();
|
||||
|
||||
const { text, content, children, messageId = null, isCreatedByUser } = message ?? {};
|
||||
const edit = messageId === currentEditId;
|
||||
|
|
@ -102,8 +102,8 @@ export default function useMessageHelpers(props: TMessageProps) {
|
|||
|
||||
const modelKey = message?.model ?? '';
|
||||
|
||||
return agentMap ? agentMap[modelKey] : undefined;
|
||||
}, [agentMap, conversation?.endpoint, message?.model]);
|
||||
return agentsMap ? agentsMap[modelKey] : undefined;
|
||||
}, [agentsMap, conversation?.endpoint, message?.model]);
|
||||
|
||||
const regenerateMessage = () => {
|
||||
if ((isSubmitting && isCreatedByUser === true) || !message) {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import {
|
|||
} from 'librechat-data-provider';
|
||||
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
|
||||
import type { NavLink } from '~/common';
|
||||
import AgentPanelSwitch from '~/components/SidePanel/Agents/AgentPanelSwitch';
|
||||
import BookmarkPanel from '~/components/SidePanel/Bookmarks/BookmarkPanel';
|
||||
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
|
||||
import AgentPanelSwitch from '~/components/SidePanel/Agents/AgentPanelSwitch';
|
||||
import PromptsAccordion from '~/components/Prompts/PromptsAccordion';
|
||||
import Parameters from '~/components/SidePanel/Parameters/Panel';
|
||||
import FilesPanel from '~/components/SidePanel/Files/Panel';
|
||||
|
|
@ -44,6 +44,14 @@ export default function useSideNavLinks({
|
|||
permissionType: PermissionTypes.BOOKMARKS,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
const hasAccessToAgents = useHasAccess({
|
||||
permissionType: PermissionTypes.AGENTS,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
const hasAccessToCreateAgents = useHasAccess({
|
||||
permissionType: PermissionTypes.AGENTS,
|
||||
permission: Permissions.CREATE,
|
||||
});
|
||||
|
||||
const Links = useMemo(() => {
|
||||
const links: NavLink[] = [];
|
||||
|
|
@ -64,6 +72,8 @@ export default function useSideNavLinks({
|
|||
}
|
||||
|
||||
if (
|
||||
hasAccessToAgents &&
|
||||
hasAccessToCreateAgents &&
|
||||
isAgentsEndpoint(endpoint) &&
|
||||
agents &&
|
||||
// agents.disableBuilder !== true &&
|
||||
|
|
@ -137,8 +147,10 @@ export default function useSideNavLinks({
|
|||
endpointType,
|
||||
endpoint,
|
||||
agents,
|
||||
hasAccessToAgents,
|
||||
hasAccessToPrompts,
|
||||
hasAccessToBookmarks,
|
||||
hasAccessToCreateAgents,
|
||||
hidePanel,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { default as useAuthCodeTool } from './useAuthCodeTool';
|
||||
export { default as usePluginInstall } from './usePluginInstall';
|
||||
export { default as useCodeApiKeyForm } from './useCodeApiKeyForm';
|
||||
export { default as usePluginDialogHelpers } from './usePluginDialogHelpers';
|
||||
|
|
|
|||
43
client/src/hooks/Plugins/useCodeApiKeyForm.ts
Normal file
43
client/src/hooks/Plugins/useCodeApiKeyForm.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// client/src/hooks/Plugins/useCodeApiKeyForm.ts
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { ApiKeyFormData } from '~/common';
|
||||
import useAuthCodeTool from '~/hooks/Plugins/useAuthCodeTool';
|
||||
|
||||
export default function useCodeApiKeyForm({
|
||||
onSubmit,
|
||||
onRevoke,
|
||||
}: {
|
||||
onSubmit?: () => void;
|
||||
onRevoke?: () => void;
|
||||
}) {
|
||||
const methods = useForm<ApiKeyFormData>();
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const { installTool, removeTool } = useAuthCodeTool({ isEntityTool: true });
|
||||
const { reset } = methods;
|
||||
|
||||
const onSubmitHandler = useCallback(
|
||||
(data: { apiKey: string }) => {
|
||||
reset();
|
||||
installTool(data.apiKey);
|
||||
setIsDialogOpen(false);
|
||||
onSubmit?.();
|
||||
},
|
||||
[onSubmit, reset, installTool],
|
||||
);
|
||||
|
||||
const handleRevokeApiKey = useCallback(() => {
|
||||
reset();
|
||||
removeTool();
|
||||
setIsDialogOpen(false);
|
||||
onRevoke?.();
|
||||
}, [reset, onRevoke, removeTool]);
|
||||
|
||||
return {
|
||||
methods,
|
||||
isDialogOpen,
|
||||
setIsDialogOpen,
|
||||
handleRevokeApiKey,
|
||||
onSubmit: onSubmitHandler,
|
||||
};
|
||||
}
|
||||
28
client/src/hooks/Plugins/useToolCallsMap.ts
Normal file
28
client/src/hooks/Plugins/useToolCallsMap.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { ToolCallResult } from 'librechat-data-provider';
|
||||
import { useMemo } from 'react';
|
||||
import { useGetToolCalls } from '~/data-provider';
|
||||
import { mapToolCalls, logger } from '~/utils';
|
||||
|
||||
type ToolCallsMap = {
|
||||
[x: string]: ToolCallResult[] | undefined;
|
||||
};
|
||||
|
||||
export default function useToolCallsMap({
|
||||
conversationId,
|
||||
}: {
|
||||
conversationId: string;
|
||||
}): ToolCallsMap | undefined {
|
||||
const { data: toolCallsMap = null } = useGetToolCalls(
|
||||
{ conversationId },
|
||||
{
|
||||
select: (res) => mapToolCalls(res),
|
||||
},
|
||||
);
|
||||
|
||||
const result = useMemo<ToolCallsMap | undefined>(() => {
|
||||
return toolCallsMap !== null ? toolCallsMap : undefined;
|
||||
}, [toolCallsMap]);
|
||||
|
||||
logger.log('tools', 'tool calls map:', result);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -21,7 +21,14 @@ type TUseStepHandler = {
|
|||
|
||||
type TStepEvent = {
|
||||
event: string;
|
||||
data: Agents.MessageDeltaEvent | Agents.RunStep | Agents.ToolEndEvent;
|
||||
data:
|
||||
| Agents.MessageDeltaEvent
|
||||
| Agents.RunStep
|
||||
| Agents.ToolEndEvent
|
||||
| {
|
||||
runId?: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
type MessageDeltaUpdate = { type: ContentTypes.TEXT; text: string; tool_call_ids?: string[] };
|
||||
|
|
@ -166,6 +173,30 @@ export default function useStepHandler({
|
|||
}
|
||||
});
|
||||
}
|
||||
} else if (event === 'on_agent_update') {
|
||||
const { runId, message } = data as { runId?: string; message: string };
|
||||
const responseMessageId = runId ?? '';
|
||||
if (!responseMessageId) {
|
||||
console.warn('No message id found in agent update event');
|
||||
return;
|
||||
}
|
||||
|
||||
const responseMessage = messages[messages.length - 1] as TMessage;
|
||||
|
||||
const response = {
|
||||
...responseMessage,
|
||||
parentMessageId: userMessage.messageId,
|
||||
conversationId: userMessage.conversationId,
|
||||
messageId: responseMessageId,
|
||||
content: [
|
||||
{
|
||||
type: ContentTypes.TEXT,
|
||||
text: message,
|
||||
},
|
||||
],
|
||||
} as TMessage;
|
||||
|
||||
setMessages([...messages.slice(0, -1), response]);
|
||||
} else if (event === 'on_message_delta') {
|
||||
const messageDelta = data as Agents.MessageDeltaEvent;
|
||||
const runStep = stepMap.current.get(messageDelta.id);
|
||||
|
|
|
|||
|
|
@ -528,7 +528,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'الافتراضي:',
|
||||
com_endpoint_preset_default_none: 'لا يوجد إعداد مسبق افتراضي نشط.',
|
||||
com_endpoint_preset_title: 'إعداد مسبق',
|
||||
com_endpoint_preset_saved: 'تم الحفظ!',
|
||||
com_ui_saved: 'تم الحفظ!',
|
||||
com_endpoint_preset_default: 'أصبح الإعداد المسبق الافتراضي الآن.',
|
||||
com_endpoint_preset_selected: 'الإعداد المسبق نشط!',
|
||||
com_endpoint_preset_selected_title: 'مُحدَّد!',
|
||||
|
|
@ -644,7 +644,7 @@ export default {
|
|||
com_agents_file_search_info:
|
||||
'عند التمكين، سيتم إعلام الوكيل بأسماء الملفات المدرجة أدناه بالضبط، مما يتيح له استرجاع السياق ذي الصلة من هذه الملفات.',
|
||||
com_ui_agent_already_shared_to_all: 'هذا المساعد مشارك بالفعل مع جميع المستخدمين',
|
||||
com_agents_execute_code: 'تنفيذ الشفرة',
|
||||
com_ui_run_code: 'تنفيذ الشفرة',
|
||||
com_ui_no_changes: 'لا توجد تغييرات للتحديث',
|
||||
com_ui_agent_editing_allowed: 'يمكن للمستخدمين الآخرين تعديل هذا الوكيل بالفعل',
|
||||
com_ui_error_connection: 'خطأ في الاتصال بالخادم، حاول تحديث الصفحة.',
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'Padrão:',
|
||||
com_endpoint_preset_default_none: 'Nenhum preset padrão ativo.',
|
||||
com_endpoint_preset_title: 'Preset',
|
||||
com_endpoint_preset_saved: 'Salvo!',
|
||||
com_ui_saved: 'Salvo!',
|
||||
com_endpoint_preset_default: 'é agora o preset padrão.',
|
||||
com_endpoint_preset: 'preset',
|
||||
com_endpoint_presets: 'presets',
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'Standard:',
|
||||
com_endpoint_preset_default_none: 'Keine Standardvoreinstellung aktiv.',
|
||||
com_endpoint_preset_title: 'Voreinstellung',
|
||||
com_endpoint_preset_saved: 'Gespeichert!',
|
||||
com_ui_saved: 'Gespeichert!',
|
||||
com_endpoint_preset_default: 'ist jetzt die Standardvoreinstellung.',
|
||||
com_endpoint_preset: 'Voreinstellung',
|
||||
com_endpoint_presets: 'Voreinstellungen',
|
||||
|
|
@ -801,7 +801,7 @@ export default {
|
|||
com_ui_endpoint: 'Endpunkt',
|
||||
com_ui_region: 'Region',
|
||||
com_ui_model_parameters: 'Modell-Parameter',
|
||||
com_agents_execute_code: 'Code ausführen',
|
||||
com_ui_run_code: 'Code ausführen',
|
||||
com_ui_provider: 'Anbieter',
|
||||
com_ui_model_save_success: 'Modellparameter erfolgreich gespeichert',
|
||||
com_ui_select_region: 'Wähle eine Region',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets present in this file
|
||||
|
||||
export default {
|
||||
com_ui_enter_api_key: 'Enter API Key',
|
||||
com_ui_librechat_code_api_title: 'Run AI Code',
|
||||
com_ui_librechat_code_api_subtitle: 'Secure. Multi-language. Input/Output Files.',
|
||||
com_ui_librechat_code_api_key: 'Get your LibreChat Code Interpreter API key',
|
||||
com_nav_convo_menu_options: 'Conversation Menu Options',
|
||||
com_ui_artifacts: 'Artifacts',
|
||||
com_ui_artifacts_toggle: 'Toggle Artifacts UI',
|
||||
|
|
@ -102,6 +106,7 @@ export default {
|
|||
com_agents_description_placeholder: 'Optional: Describe your Agent here',
|
||||
com_agents_instructions_placeholder: 'The system instructions that the agent uses',
|
||||
com_agents_search_name: 'Search agents by name',
|
||||
com_sidepanel_select_agent: 'Select an Agent',
|
||||
com_agents_update_error: 'There was an error updating your agent.',
|
||||
com_agents_create_error: 'There was an error creating your agent.',
|
||||
com_agents_missing_provider_model: 'Please select a provider and model before creating an agent.',
|
||||
|
|
@ -111,8 +116,11 @@ export default {
|
|||
com_agents_enable_file_search: 'Enable File Search',
|
||||
com_agents_file_search_info:
|
||||
'When enabled, the agent will be informed of the exact filenames listed below, allowing it to retrieve relevant context from these files.',
|
||||
com_agents_code_interpreter_title: 'Code Interpreter',
|
||||
com_agents_by_librechat: 'by LibreChat',
|
||||
com_agents_code_interpreter:
|
||||
'When enabled, allows your agent to leverage the LibreChat Code Interpreter API to run generated code, including file processing, securely. Requires a valid API key.',
|
||||
com_agents_file_search_disabled: 'Agent must be created before uploading files for File Search.',
|
||||
com_agents_execute_code: 'Run Code',
|
||||
com_ui_agent_already_shared_to_all: 'This agent is already shared to all users',
|
||||
com_ui_agent_editing_allowed: 'Other users can already edit this agent',
|
||||
com_ui_no_changes: 'No changes to update',
|
||||
|
|
@ -177,6 +185,7 @@ export default {
|
|||
com_ui_select_provider: 'Select a provider',
|
||||
com_ui_select_provider_first: 'Select a provider first',
|
||||
com_ui_select_search_model: 'Search model by name',
|
||||
com_ui_select_search_provider: 'Search provider by name',
|
||||
com_ui_select_search_region: 'Search region by name',
|
||||
com_ui_select_search_plugin: 'Search plugin by name',
|
||||
com_ui_use_prompt: 'Use prompt',
|
||||
|
|
@ -184,6 +193,9 @@ export default {
|
|||
com_ui_next: 'Next',
|
||||
com_ui_stop: 'Stop',
|
||||
com_ui_upload_files: 'Upload files',
|
||||
com_ui_upload_image_input: 'Upload Image',
|
||||
com_ui_upload_file_search: 'Upload for File Search',
|
||||
com_ui_upload_code_files: 'Upload for Code Interpreter',
|
||||
com_ui_prompt: 'Prompt',
|
||||
com_ui_prompts: 'Prompts',
|
||||
com_ui_prompt_name: 'Prompt Name',
|
||||
|
|
@ -236,6 +248,8 @@ export default {
|
|||
com_ui_read_aloud: 'Read aloud',
|
||||
com_ui_copied: 'Copied!',
|
||||
com_ui_copy_code: 'Copy code',
|
||||
com_ui_run_code: 'Run Code',
|
||||
com_ui_run_code_error: 'There was an error running the code',
|
||||
com_ui_copy_to_clipboard: 'Copy to clipboard',
|
||||
com_ui_copied_to_clipboard: 'Copied to clipboard',
|
||||
com_ui_fork: 'Fork',
|
||||
|
|
@ -331,6 +345,9 @@ export default {
|
|||
com_ui_prompts_allow_share_global: 'Allow sharing Prompts to all users',
|
||||
com_ui_prompt_shared_to_all: 'This prompt is shared to all users',
|
||||
com_ui_prompt_update_error: 'There was an error updating the prompt',
|
||||
com_ui_agents_allow_share_global: 'Allow sharing Agents to all users',
|
||||
com_ui_agents_allow_use: 'Allow using Agents',
|
||||
com_ui_agents_allow_create: 'Allow creating Agents',
|
||||
com_ui_prompt_already_shared_to_all: 'This prompt is already shared to all users',
|
||||
com_ui_description_placeholder: 'Optional: Enter a description to display for the prompt',
|
||||
com_ui_command_placeholder: 'Optional: Enter a command for the prompt or name will be used.',
|
||||
|
|
@ -574,7 +591,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'Default:',
|
||||
com_endpoint_preset_default_none: 'No default preset active.',
|
||||
com_endpoint_preset_title: 'Preset',
|
||||
com_endpoint_preset_saved: 'Saved!',
|
||||
com_ui_saved: 'Saved!',
|
||||
com_endpoint_preset_default: 'is now the default preset.',
|
||||
com_endpoint_preset: 'preset',
|
||||
com_endpoint_presets: 'presets',
|
||||
|
|
@ -610,6 +627,7 @@ export default {
|
|||
com_endpoint_skip_hover:
|
||||
'Enable skipping the completion step, which reviews the final answer and generated steps',
|
||||
com_endpoint_config_key: 'Set API Key',
|
||||
com_endpoint_agent_placeholder: 'Please select an Agent',
|
||||
com_endpoint_assistant_placeholder: 'Please select an Assistant from the right-hand Side Panel',
|
||||
com_endpoint_config_placeholder: 'Set your Key in the Header menu to chat.',
|
||||
com_endpoint_config_key_for: 'Set API Key for',
|
||||
|
|
@ -651,6 +669,7 @@ export default {
|
|||
com_nav_font_size_lg: 'Large',
|
||||
com_nav_font_size_xl: 'Extra Large',
|
||||
com_nav_welcome_assistant: 'Please Select an Assistant',
|
||||
com_nav_welcome_agent: 'Please Select an Agent',
|
||||
com_nav_welcome_message: 'How can I help you today?',
|
||||
com_nav_auto_scroll: 'Auto-Scroll to latest message on chat open',
|
||||
com_nav_user_msg_markdown: 'Render user messages as markdown',
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'Predeterminado:',
|
||||
com_endpoint_preset_default_none: 'No hay configuración preestablecida predeterminada activa.',
|
||||
com_endpoint_preset_title: 'Configuración preestablecida',
|
||||
com_endpoint_preset_saved: '¡Guardado!',
|
||||
com_ui_saved: '¡Guardado!',
|
||||
com_endpoint_preset_default: 'es ahora la configuración preestablecida predeterminada.',
|
||||
com_endpoint_preset: 'configuración preestablecida',
|
||||
com_endpoint_presets: 'configuraciones preestablecidas',
|
||||
|
|
@ -716,7 +716,7 @@ export default {
|
|||
com_agents_file_search_disabled:
|
||||
'Es necesario crear el Agente antes de subir archivos para la Búsqueda de Archivos.',
|
||||
|
||||
com_agents_execute_code: 'Ejecutar código',
|
||||
com_ui_run_code: 'Ejecutar código',
|
||||
|
||||
com_ui_agent_already_shared_to_all: 'Este asistente ya está compartido con todos los usuarios',
|
||||
|
||||
|
|
|
|||
|
|
@ -489,7 +489,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'Oletus:',
|
||||
com_endpoint_preset_default_none: 'Oletus-esiasetusta ei ole käytössä',
|
||||
com_endpoint_preset_title: 'Esiasetus',
|
||||
com_endpoint_preset_saved: 'Tallennettu!',
|
||||
com_ui_saved: 'Tallennettu!',
|
||||
com_endpoint_preset_default: 'on nyt oletus-esiasetus.',
|
||||
com_endpoint_preset: 'esiasetus',
|
||||
com_endpoint_presets: 'esiasetukset',
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'Par défaut :',
|
||||
com_endpoint_preset_default_none: 'Aucun préréglage par défaut actif.',
|
||||
com_endpoint_preset_title: 'Préréglage',
|
||||
com_endpoint_preset_saved: 'Enregistré!',
|
||||
com_ui_saved: 'Enregistré!',
|
||||
com_endpoint_preset_default: 'est maintenant le préréglage par défaut.',
|
||||
com_endpoint_preset: 'préréglage',
|
||||
com_endpoint_presets: 'préréglages',
|
||||
|
|
@ -794,7 +794,7 @@ export default {
|
|||
com_agents_enable_file_search: 'Activer la recherche de fichiers',
|
||||
com_agents_file_search_info:
|
||||
'Lorsque cette option est activée, l\'agent sera informé des noms exacts des fichiers listés ci-dessous, lui permettant d\'extraire le contexte pertinent de ces fichiers.',
|
||||
com_agents_execute_code: 'Exécuter le code',
|
||||
com_ui_run_code: 'Exécuter le code',
|
||||
com_agents_file_search_disabled:
|
||||
'L\'agent doit être créé avant de pouvoir télécharger des fichiers pour la Recherche de Fichiers.',
|
||||
com_ui_agent_already_shared_to_all: 'Cet agent est déjà partagé avec tous les utilisateurs',
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'ברירת מחדל:',
|
||||
com_endpoint_preset_default_none: 'אין ברירת מחדל פעילה.',
|
||||
com_endpoint_preset_title: 'הגדרה מראש',
|
||||
com_endpoint_preset_saved: 'שמור!',
|
||||
com_ui_saved: 'שמור!',
|
||||
com_endpoint_preset_default: 'הוא כעת ברירת המחדל המוגדרת מראש.',
|
||||
com_endpoint_preset: 'preset',
|
||||
com_endpoint_presets: 'presets',
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'Default:',
|
||||
com_endpoint_preset_default_none: 'Tidak ada preset default yang aktif.',
|
||||
com_endpoint_preset_title: 'Preset',
|
||||
com_endpoint_preset_saved: 'Tersimpan!',
|
||||
com_ui_saved: 'Tersimpan!',
|
||||
com_endpoint_preset_default: 'sekarang menjadi preset default.',
|
||||
com_endpoint_preset: 'preset',
|
||||
com_endpoint_presets: 'presets',
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'Predefinita:',
|
||||
com_endpoint_preset_default_none: 'Nessuna preimpostazione predefinita attiva.',
|
||||
com_endpoint_preset_title: 'Preimpostazione',
|
||||
com_endpoint_preset_saved: 'Salvata!',
|
||||
com_ui_saved: 'Salvata!',
|
||||
com_endpoint_preset_default: 'è ora la preimpostazione predefinita.',
|
||||
com_endpoint_preset: 'preimpostazione',
|
||||
com_endpoint_presets: 'preimpostazioni',
|
||||
|
|
@ -676,7 +676,7 @@ export default {
|
|||
com_agents_enable_file_search: 'Abilita Ricerca File',
|
||||
com_agents_file_search_info:
|
||||
'Quando abilitato, l\'agente verrà informato dei nomi esatti dei file elencati di seguito, permettendogli di recuperare il contesto pertinente da questi file.',
|
||||
com_agents_execute_code: 'Esegui Codice',
|
||||
com_ui_run_code: 'Esegui Codice',
|
||||
com_agents_file_search_disabled:
|
||||
'L\'Agente deve essere creato prima di caricare file per la Ricerca File.',
|
||||
com_ui_agent_already_shared_to_all: 'Questo assistente è già condiviso con tutti gli utenti',
|
||||
|
|
|
|||
|
|
@ -541,7 +541,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'デフォルト:',
|
||||
com_endpoint_preset_default_none: '現在有効なプリセットはありません。',
|
||||
com_endpoint_preset_title: 'プリセット',
|
||||
com_endpoint_preset_saved: '保存しました!',
|
||||
com_ui_saved: '保存しました!',
|
||||
com_endpoint_preset_default: 'が有効化されました。',
|
||||
com_endpoint_preset: 'プリセット',
|
||||
com_endpoint_presets: 'プリセット',
|
||||
|
|
@ -834,7 +834,7 @@ export default {
|
|||
'ファイル検索用のファイルをアップロードする前に、エージェントを作成する必要があります。',
|
||||
com_agents_file_search_info:
|
||||
'有効にすると、エージェントは以下に表示されているファイル名を正確に認識し、それらのファイルから関連する情報を取得することができます。',
|
||||
com_agents_execute_code: 'コードを実行',
|
||||
com_ui_run_code: 'コードを実行',
|
||||
com_ui_agent_editing_allowed: 'このエージェントは他のユーザーが既に編集可能です',
|
||||
com_ui_agent_already_shared_to_all: 'このアシスタントは既に全ユーザーに共有されています',
|
||||
com_ui_no_changes: '更新する変更はありません',
|
||||
|
|
|
|||
|
|
@ -859,7 +859,7 @@ export default {
|
|||
com_endpoint_preset_default_item: '기본값:',
|
||||
com_endpoint_preset_default_none: '기본 프리셋이 설정되지 않았습니다.',
|
||||
com_endpoint_preset_title: '프리셋',
|
||||
com_endpoint_preset_saved: '저장되었습니다!',
|
||||
com_ui_saved: '저장되었습니다!',
|
||||
com_endpoint_preset_default: '이제 기본 프리셋입니다.',
|
||||
com_endpoint_preset_selected: '프리셋 활성화됨',
|
||||
com_endpoint_preset_selected_title: '활성화됨',
|
||||
|
|
@ -1020,7 +1020,7 @@ export default {
|
|||
com_agents_file_search_disabled:
|
||||
'파일 검색을 위해 파일을 업로드하기 전에 에이전트를 먼저 생성해야 합니다',
|
||||
|
||||
com_agents_execute_code: '코드 실행',
|
||||
com_ui_run_code: '코드 실행',
|
||||
|
||||
com_ui_agent_already_shared_to_all: '이 에이전트는 이미 모든 사용자와 공유되어 있습니다',
|
||||
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'По умолчанию:',
|
||||
com_endpoint_preset_default_none: 'Активных пресетов по умолчанию нет.',
|
||||
com_endpoint_preset_title: 'Пресет',
|
||||
com_endpoint_preset_saved: 'Сохранено!',
|
||||
com_ui_saved: 'Сохранено!',
|
||||
com_endpoint_preset_default: 'теперь пресет "По умолчаанию".',
|
||||
com_endpoint_preset: 'пресет',
|
||||
com_endpoint_presets: 'пресеты',
|
||||
|
|
@ -699,7 +699,7 @@ export default {
|
|||
|
||||
com_agents_file_search_disabled: 'Для загрузки файлов в Поиск необходимо сначала создать агента',
|
||||
|
||||
com_agents_execute_code: 'Выполнить код',
|
||||
com_ui_run_code: 'Выполнить код',
|
||||
|
||||
com_ui_agent_editing_allowed: 'Другие пользователи уже могут редактировать этого ассистента',
|
||||
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ export default {
|
|||
com_endpoint_preset_default_item: 'Varsayılan:',
|
||||
com_endpoint_preset_default_none: 'Aktif varsayılan hazır ayar yok.',
|
||||
com_endpoint_preset_title: 'Hazır Ayar',
|
||||
com_endpoint_preset_saved: 'Kaydedildi!',
|
||||
com_ui_saved: 'Kaydedildi!',
|
||||
com_endpoint_preset_default: 'şu anda varsayılan hazır ayar.',
|
||||
com_endpoint_preset: 'hazır ayar',
|
||||
com_endpoint_presets: 'hazır ayarlar',
|
||||
|
|
|
|||
|
|
@ -514,7 +514,7 @@ export default {
|
|||
com_endpoint_preset_default_item: '默认:',
|
||||
com_endpoint_preset_default_none: '无默认预设可用。',
|
||||
com_endpoint_preset_title: '预设',
|
||||
com_endpoint_preset_saved: '保存成功!',
|
||||
com_ui_saved: '保存成功!',
|
||||
com_endpoint_preset_default: '现在是默认预设。',
|
||||
com_endpoint_preset: '预设',
|
||||
com_endpoint_presets: '预设',
|
||||
|
|
@ -806,7 +806,7 @@ export default {
|
|||
com_agents_file_search_info:
|
||||
'启用后,系统会告知Agent以下列出的具体文件名,使其能够从这些文件中检索相关内容。',
|
||||
|
||||
com_agents_execute_code: '运行代码',
|
||||
com_ui_run_code: '运行代码',
|
||||
|
||||
com_agents_file_search_disabled: '必须先创建Agent,才能上传文件用于文件搜索。',
|
||||
|
||||
|
|
|
|||
|
|
@ -502,7 +502,7 @@ export default {
|
|||
com_endpoint_preset_default_item: '預設值',
|
||||
com_endpoint_preset_default_none: '無啟用的預設設定。',
|
||||
com_endpoint_preset_title: '預設項目',
|
||||
com_endpoint_preset_saved: '已儲存!',
|
||||
com_ui_saved: '已儲存!',
|
||||
com_endpoint_preset_default: '現在是預設的預設設定。',
|
||||
com_endpoint_preset_selected: '已選擇預設設定!',
|
||||
com_endpoint_preset_selected_title: '已選取!',
|
||||
|
|
@ -612,7 +612,7 @@ export default {
|
|||
com_agents_missing_provider_model: '請在建立代理前選擇供應商和模型。',
|
||||
com_agents_enable_file_search: '啟用檔案搜尋',
|
||||
com_agents_file_search_disabled: '必須先建立代理才能上傳檔案進行檔案搜尋。',
|
||||
com_agents_execute_code: '執行程式碼',
|
||||
com_ui_run_code: '執行程式碼',
|
||||
com_agents_no_access: '您沒有權限編輯此助理',
|
||||
com_ui_no_changes: '沒有需要更新的變更',
|
||||
com_ui_agent_already_shared_to_all: '此助理已與所有使用者共享',
|
||||
|
|
|
|||
|
|
@ -1063,7 +1063,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
|
|||
- **english**: Preset
|
||||
- **translated**: Predefinição
|
||||
|
||||
- **com_endpoint_preset_saved**:
|
||||
- **com_ui_saved**:
|
||||
- **english**: Saved!
|
||||
- **translated**: Salvo!
|
||||
|
||||
|
|
|
|||
|
|
@ -1091,7 +1091,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
|
|||
- **english**: Preset
|
||||
- **translated**: Voreinstellung
|
||||
|
||||
- **com_endpoint_preset_saved**:
|
||||
- **com_ui_saved**:
|
||||
- **english**: Saved!
|
||||
- **translated**: Gespeichert!
|
||||
|
||||
|
|
|
|||
|
|
@ -1063,7 +1063,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
|
|||
- **english**: Preset
|
||||
- **translated**: Configuración preestablecida
|
||||
|
||||
- **com_endpoint_preset_saved**:
|
||||
- **com_ui_saved**:
|
||||
- **english**: Saved!
|
||||
- **translated**: ¡Guardado!
|
||||
|
||||
|
|
|
|||
|
|
@ -731,7 +731,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
|
|||
- **english**: Preset
|
||||
- **translated**: Préréglage
|
||||
|
||||
- **com_endpoint_preset_saved**:
|
||||
- **com_ui_saved**:
|
||||
- **english**: Saved!
|
||||
- **translated**: Enregistré!
|
||||
|
||||
|
|
|
|||
|
|
@ -875,7 +875,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
|
|||
- **english**: Preset
|
||||
- **translated**: הגדרה מראש
|
||||
|
||||
- **com_endpoint_preset_saved**:
|
||||
- **com_ui_saved**:
|
||||
- **english**: Saved!
|
||||
- **translated**: שמור!
|
||||
|
||||
|
|
|
|||
|
|
@ -739,7 +739,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
|
|||
- **english**: Preset
|
||||
- **translated**: Preset
|
||||
|
||||
- **com_endpoint_preset_saved**:
|
||||
- **com_ui_saved**:
|
||||
- **english**: Saved!
|
||||
- **translated**: Tersimpan!
|
||||
|
||||
|
|
|
|||
|
|
@ -1219,7 +1219,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
|
|||
- **english**: Preset
|
||||
- **translated**: Preimpostazione
|
||||
|
||||
- **com_endpoint_preset_saved**:
|
||||
- **com_ui_saved**:
|
||||
- **english**: Saved!
|
||||
- **translated**: Salvata!
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue