mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-20 01:06:11 +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
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue