🪨 feat: AWS Bedrock support (#3935)

* feat: Add BedrockIcon component to SVG library

* feat: EModelEndpoint.bedrock

* feat: first pass, bedrock chat. note: AgentClient is returning `agents` as conversation.endpoint

* fix: declare endpoint in initialization step

* chore: Update @librechat/agents dependency to version 1.4.5

* feat: backend content aggregation for agents/bedrock

* feat: abort agent requests

* feat: AWS Bedrock icons

* WIP: agent provider schema parsing

* chore: Update EditIcon props type

* refactor(useGenerationsByLatest): make agents and bedrock editable

* refactor: non-assistant message content, parts

* fix: Bedrock response `sender`

* fix: use endpointOption.model_parameters not endpointOption.modelOptions

* fix: types for step handler

* refactor: Update Agents.ToolCallDelta type

* refactor: Remove unnecessary assignment of parentMessageId in AskController

* refactor: remove unnecessary assignment of parentMessageId (agent request handler)

* fix(bedrock/agents): message regeneration

* refactor: dynamic form elements using react-hook-form Controllers

* fix: agent icons/labels for messages

* fix: agent actions

* fix: use of new dynamic tags causing application crash

* refactor: dynamic settings touch-ups

* refactor: update Slider component to allow custom track class name

* refactor: update DynamicSlider component styles

* refactor: use Constants value for GLOBAL_PROJECT_NAME (enum)

* feat: agent share global methods/controllers

* fix: agents query

* fix: `getResponseModel`

* fix: share prompt a11y issue

* refactor: update SharePrompt dialog theme styles

* refactor: explicit typing for SharePrompt

* feat: add agent roles/permissions

* chore: update @librechat/agents dependency to version 1.4.7 for tool_call_ids edge case

* fix(Anthropic): messages.X.content.Y.tool_use.input: Input should be a valid dictionary

* fix: handle text parts with tool_call_ids and empty text

* fix: role initialization

* refactor: don't make instructions required

* refactor: improve typing of Text part

* fix: setShowStopButton for agents route

* chore: remove params for now

* fix: add streamBuffer and streamRate to help prevent 'Overloaded' errors from Anthropic API

* refactor: remove console.log statement in ContentRender component

* chore: typing, rename Context to Delete Button

* chore(DeleteButton): logging

* refactor(Action): make accessible

* style(Action): improve a11y again

* refactor: remove use/mention of mongoose sessions

* feat: first pass, sharing agents

* feat: visual indicator for global agent, remove author when serving to non-author

* wip: params

* chore: fix typing issues

* fix(schemas): typing

* refactor: improve accessibility of ListCard component and fix console React warning

* wip: reset templates for non-legacy new convos

* Revert "wip: params"

This reverts commit f8067e91d4.

* Revert "refactor: dynamic form elements using react-hook-form Controllers"

This reverts commit 2150c4815d.

* fix(Parameters): types and parameter effect update to only update local state to parameters

* refactor: optimize useDebouncedInput hook for better performance

* feat: first pass, anthropic bedrock params

* chore: paramEndpoints check for endpointType too

* fix: maxTokens to use coerceNumber.optional(),

* feat: extra chat model params

* chore: reduce code repetition

* refactor: improve preset title handling in SaveAsPresetDialog component

* refactor: improve preset handling in HeaderOptions component

* chore: improve typing, replace legacy dialog for SaveAsPresetDialog

* feat: save as preset from parameters panel

* fix: multi-search in select dropdown when using Option type

* refactor: update default showDefault value to false in Dynamic components

* feat: Bedrock presets settings

* chore: config, fix agents schema, update config version

* refactor: update AWS region variable name in bedrock options endpoint to BEDROCK_AWS_DEFAULT_REGION

* refactor: update baseEndpointSchema in config.ts to include baseURL property

* refactor: update createRun function to include req parameter and set streamRate based on provider

* feat: availableRegions via config

* refactor: remove unused demo agent controller file

* WIP: title

* Update @librechat/agents to version 1.5.0

* chore: addTitle.js to handle empty responseText

* feat: support images and titles

* feat: context token updates

* Refactor BaseClient test to use expect.objectContaining

* refactor: add model select, remove header options params, move side panel params below prompts

* chore: update models list, catch title error

* feat: model service for bedrock models (env)

* chore: Remove verbose debug log in AgentClient class following stream

* feat(bedrock): track token spend; fix: token rates, value key mapping for AWS models

* refactor: handle streamRate in `handleLLMNewToken` callback

* chore: AWS Bedrock example config in `.env.example`

* refactor: Rename bedrockMeta to bedrockGeneral in settings.ts and use for AI21 and Amazon Bedrock providers

* refactor: Update `.env.example` with AWS Bedrock model IDs URL and additional notes

* feat: titleModel support for bedrock

* refactor: Update `.env.example` with additional notes for AWS Bedrock model IDs
This commit is contained in:
Danny Avila 2024-09-09 12:06:59 -04:00 committed by GitHub
parent 8c14360263
commit d59b62174f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
134 changed files with 3684 additions and 1213 deletions

View file

@ -1,8 +1,8 @@
import { Capabilities } from 'librechat-data-provider';
import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider';
import type { Option, ExtendedFile } from './types';
import type { OptionWithIcon, ExtendedFile } from './types';
export type TAgentOption = Option &
export type TAgentOption = OptionWithIcon &
Agent & {
files?: Array<[string, ExtendedFile]>;
code_files?: Array<[string, ExtendedFile]>;
@ -23,5 +23,5 @@ export type AgentForm = {
model: string | null;
model_parameters: AgentModelParameters;
tools?: string[];
provider?: AgentProvider | Option;
provider?: AgentProvider | OptionWithIcon;
} & AgentCapabilities;

View file

@ -2,7 +2,7 @@ import { useRecoilState } from 'recoil';
import { Settings2 } from 'lucide-react';
import { Root, Anchor } from '@radix-ui/react-popover';
import { useState, useEffect, useMemo } from 'react';
import { tPresetUpdateSchema, EModelEndpoint } from 'librechat-data-provider';
import { tPresetUpdateSchema, EModelEndpoint, paramEndpoints } from 'librechat-data-provider';
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
import { ModelSelect } from '~/components/Input/ModelSelect';
@ -12,7 +12,6 @@ import PopoverButtons from './PopoverButtons';
import { useSetIndexOptions } from '~/hooks';
import { useChatContext } from '~/Providers';
import { Button } from '~/components/ui';
import { cn, cardStyle } from '~/utils/';
import store from '~/store';
export default function HeaderOptions({
@ -29,10 +28,10 @@ export default function HeaderOptions({
useChatContext();
const { setOption } = useSetIndexOptions();
const { endpoint, conversationId, jailbreak } = conversation ?? {};
const { endpoint, conversationId, jailbreak = false } = conversation ?? {};
const altConditions: { [key: string]: boolean } = {
bingAI: !!(latestMessage && conversation?.jailbreak && endpoint === 'bingAI'),
bingAI: !!(latestMessage && jailbreak && endpoint === 'bingAI'),
};
const altSettings: { [key: string]: () => void } = {
@ -74,7 +73,7 @@ export default function HeaderOptions({
<div className="my-auto lg:max-w-2xl xl:max-w-3xl">
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
<div className="z-[61] flex w-full items-center justify-center gap-2">
{interfaceConfig?.modelSelect && (
{interfaceConfig?.modelSelect === true && (
<ModelSelect
conversation={conversation}
setOption={setOption}
@ -82,7 +81,9 @@ export default function HeaderOptions({
popover={true}
/>
)}
{!noSettings[endpoint] && interfaceConfig?.parameters && (
{!noSettings[endpoint] &&
interfaceConfig?.parameters === true &&
!paramEndpoints.has(endpoint) && (
<Button
aria-label="Settings/parameters"
id="parameters-button"
@ -96,11 +97,11 @@ export default function HeaderOptions({
</Button>
)}
</div>
{interfaceConfig?.parameters && (
{interfaceConfig?.parameters === true && !paramEndpoints.has(endpoint) && (
<OptionsPopover
visible={showPopover}
saveAsPreset={saveAsPreset}
presetsDisabled={!interfaceConfig.presets}
presetsDisabled={!(interfaceConfig.presets ?? false)}
PopoverButtons={<PopoverButtons />}
closePopover={() => setShowPopover(false)}
>
@ -114,7 +115,7 @@ export default function HeaderOptions({
</div>
</OptionsPopover>
)}
{interfaceConfig?.presets && (
{interfaceConfig?.presets === true && (
<SaveAsPresetDialog
open={saveAsDialogShow}
onOpenChange={setSaveAsDialogShow}
@ -125,7 +126,7 @@ export default function HeaderOptions({
}
/>
)}
{interfaceConfig?.parameters && (
{interfaceConfig?.parameters === true && (
<PluginStoreDialog
isOpen={showPluginStoreDialog}
setIsOpen={setShowPluginStoreDialog}

View file

@ -11,6 +11,7 @@ import {
CustomMinimalIcon,
AssistantIcon,
LightningIcon,
BedrockIcon,
Sparkles,
} from '~/components/svg';
import UnknownIcon from './UnknownIcon';
@ -52,6 +53,10 @@ const AgentAvatar = ({ className = '', agentName, avatar, size }: AgentIconMapPr
return <BrainCircuit className={cn(agentName === '' ? 'icon-2xl' : '', className)} />;
};
const Bedrock = ({ className = '' }: IconMapProps) => {
return <BedrockIcon className={cn(className, 'h-full w-full')} />;
};
export const icons = {
[EModelEndpoint.azureOpenAI]: AzureMinimalIcon,
[EModelEndpoint.openAI]: GPTIcon,
@ -64,5 +69,6 @@ export const icons = {
[EModelEndpoint.assistants]: AssistantAvatar,
[EModelEndpoint.azureAssistants]: AssistantAvatar,
[EModelEndpoint.agents]: AgentAvatar,
[EModelEndpoint.bedrock]: Bedrock,
unknown: UnknownIcon,
};

View file

@ -1,12 +1,12 @@
import { TMessage } from 'librechat-data-provider';
import Files from './Files';
const Container = ({ children, message }: { children: React.ReactNode; message: TMessage }) => (
const Container = ({ children, message }: { children: React.ReactNode; message?: TMessage }) => (
<div
className="text-message flex min-h-[20px] flex-col items-start gap-3 overflow-x-auto [.text-message+&]:mt-5"
dir="auto"
>
{message.isCreatedByUser && <Files message={message} />}
{message?.isCreatedByUser === true && <Files message={message} />}
{children}
</div>
);

View file

@ -1,51 +1,34 @@
import { Suspense } from 'react';
import { memo } from 'react';
import type { TMessageContentParts } from 'librechat-data-provider';
import { UnfinishedMessage } from './MessageContent';
import { DelayedRender } from '~/components/ui';
import Part from './Part';
const ContentParts = ({
error,
unfinished,
isSubmitting,
isLast,
content,
...props
}: // eslint-disable-next-line @typescript-eslint/no-explicit-any
any) => {
if (error) {
// return <ErrorMessage text={text} />;
} else {
const { message } = props;
const { messageId } = message;
type ContentPartsProps = {
content: Array<TMessageContentParts | undefined>;
messageId: string;
isCreatedByUser: boolean;
isLast: boolean;
isSubmitting: boolean;
};
const ContentParts = memo(
({ content, messageId, isCreatedByUser, isLast, isSubmitting }: ContentPartsProps) => {
return (
<>
{content
.filter((part: TMessageContentParts | undefined) => part)
.map((part: TMessageContentParts | undefined, idx: number) => {
const showCursor = idx === content.length - 1 && isLast;
return (
<Part
key={`display-${messageId}-${idx}`}
showCursor={showCursor === true && isSubmitting}
isSubmitting={isSubmitting}
part={part}
{...props}
/>
);
})}
{/* Temporarily remove this */}
{/* {!isSubmitting && unfinished && (
<Suspense>
<DelayedRender delay={250}>
<UnfinishedMessage message={message} key={`unfinished-${messageId}`} />
</DelayedRender>
</Suspense>
)} */}
.filter((part) => part)
.map((part, idx) => (
<Part
key={`display-${messageId}-${idx}`}
part={part}
isSubmitting={isSubmitting}
showCursor={idx === content.length - 1 && isLast}
messageId={messageId}
isCreatedByUser={isCreatedByUser}
/>
))}
</>
);
}
};
},
);
export default ContentParts;

View file

@ -3,7 +3,7 @@ import type { TFile, TMessage } from 'librechat-data-provider';
import FileContainer from '~/components/Chat/Input/Files/FileContainer';
import Image from './Image';
const Files = ({ message }: { message: TMessage }) => {
const Files = ({ message }: { message?: TMessage }) => {
const imageFiles = useMemo(() => {
return message?.files?.filter((file) => file.type?.startsWith('image/')) || [];
}, [message?.files]);
@ -20,7 +20,7 @@ const Files = ({ message }: { message: TMessage }) => {
imageFiles.map((file) => (
<Image
key={file.file_id}
imagePath={file?.preview ?? file.filepath ?? ''}
imagePath={file.preview ?? file.filepath ?? ''}
height={file.height ?? 1920}
width={file.width ?? 1080}
altText={file.filename ?? 'Uploaded Image'}

View file

@ -15,7 +15,9 @@ export const ErrorMessage = ({
text,
message,
className = '',
}: Pick<TDisplayProps, 'text' | 'className' | 'message'>) => {
}: Pick<TDisplayProps, 'text' | 'className'> & {
message?: TMessage;
}) => {
const localize = useLocalize();
if (text === 'Error connecting to server, try refreshing the page.') {
console.log('error message', message);

View file

@ -4,81 +4,47 @@ import {
imageGenTools,
isImageVisionTool,
} from 'librechat-data-provider';
import { useMemo } from 'react';
import type { TMessageContentParts, TMessage } from 'librechat-data-provider';
import type { TDisplayProps } from '~/common';
import { memo } from 'react';
import type { TMessageContentParts } from 'librechat-data-provider';
import { ErrorMessage } from './MessageContent';
import { useChatContext } from '~/Providers';
import RetrievalCall from './RetrievalCall';
import CodeAnalyze from './CodeAnalyze';
import Container from './Container';
import ToolCall from './ToolCall';
import Markdown from './Markdown';
import ImageGen from './ImageGen';
import { cn } from '~/utils';
import Text from './Parts/Text';
import Image from './Image';
// Display Message Component
const DisplayMessage = ({ text, isCreatedByUser = false, message, showCursor }: TDisplayProps) => {
const { isSubmitting, latestMessage } = useChatContext();
const showCursorState = useMemo(
() => showCursor === true && isSubmitting,
[showCursor, isSubmitting],
);
const isLatestMessage = useMemo(
() => message.messageId === latestMessage?.messageId,
[message.messageId, latestMessage?.messageId],
);
// Note: for testing purposes
// isSubmitting && isLatestMessage && logger.log('message_stream', { text, isCreatedByUser, isSubmitting, showCursorState });
return (
<div
className={cn(
isSubmitting ? 'submitting' : '',
showCursorState && !!text.length ? 'result-streaming' : '',
'markdown prose message-content dark:prose-invert light w-full break-words',
isCreatedByUser ? 'whitespace-pre-wrap dark:text-gray-20' : 'dark:text-gray-70',
)}
>
{!isCreatedByUser ? (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
) : (
<>{text}</>
)}
</div>
);
};
export default function Part({
part,
showCursor,
isSubmitting,
message,
}: {
part: TMessageContentParts | undefined;
type PartProps = {
part?: TMessageContentParts;
isSubmitting: boolean;
showCursor: boolean;
message: TMessage;
}) {
messageId: string;
isCreatedByUser: boolean;
};
const Part = memo(({ part, isSubmitting, showCursor, messageId, isCreatedByUser }: PartProps) => {
if (!part) {
return null;
}
if (part.type === ContentTypes.ERROR) {
return <ErrorMessage message={message} text={part[ContentTypes.TEXT].value} className="my-2" />;
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 message={message}>
<DisplayMessage
<Container>
<Text
text={text}
isCreatedByUser={message.isCreatedByUser}
message={message}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
@ -93,7 +59,7 @@ export default function Part({
if ('args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL)) {
return (
<ToolCall
args={toolCall.args}
args={toolCall.args ?? ''}
name={toolCall.name ?? ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
@ -132,11 +98,11 @@ export default function Part({
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container message={message}>
<DisplayMessage
<Container>
<Text
text={''}
isCreatedByUser={message.isCreatedByUser}
message={message}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
@ -174,4 +140,6 @@ export default function Part({
}
return null;
}
});
export default Part;

View file

@ -0,0 +1,39 @@
import { memo, useMemo } from 'react';
import { useChatContext } from '~/Providers';
import Markdown from '~/components/Chat/Messages/Content/Markdown';
import { cn } from '~/utils';
type TextPartProps = {
text: string;
isCreatedByUser: boolean;
messageId: string;
showCursor: boolean;
};
const TextPart = memo(({ text, isCreatedByUser, messageId, showCursor }: TextPartProps) => {
const { isSubmitting, latestMessage } = useChatContext();
const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]);
const isLatestMessage = useMemo(
() => messageId === latestMessage?.messageId,
[messageId, latestMessage?.messageId],
);
return (
<div
className={cn(
isSubmitting ? 'submitting' : '',
showCursorState && !!text.length ? 'result-streaming' : '',
'markdown prose message-content dark:prose-invert light w-full break-words',
isCreatedByUser ? 'whitespace-pre-wrap dark:text-gray-20' : 'dark:text-gray-70',
)}
>
{!isCreatedByUser ? (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
) : (
<>{text}</>
)}
</div>
);
});
export default TextPart;

View file

@ -1,4 +1,5 @@
import { useRecoilValue } from 'recoil';
import type { TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import Icon from '~/components/Chat/Messages/MessageIcon';
import { useMessageHelpers, useLocalize } from '~/hooks';
@ -17,7 +18,6 @@ export default function Message(props: TMessageProps) {
props;
const {
ask,
edit,
index,
agent,
@ -33,7 +33,7 @@ export default function Message(props: TMessageProps) {
regenerateMessage,
} = useMessageHelpers(props);
const fontSize = useRecoilValue(store.fontSize);
const { content, children, messageId = null, isCreatedByUser, error, unfinished } = message ?? {};
const { children, messageId = null, isCreatedByUser } = message ?? {};
if (!message) {
return null;
@ -82,24 +82,11 @@ 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
ask={ask}
edit={edit}
content={message.content as Array<TMessageContentParts | undefined>}
messageId={message.messageId}
isCreatedByUser={message.isCreatedByUser}
isLast={isLast}
content={content ?? []}
message={message}
messageId={messageId}
enterEdit={enterEdit}
error={!!(error ?? false)}
isSubmitting={isSubmitting}
unfinished={unfinished ?? false}
isCreatedByUser={isCreatedByUser ?? true}
siblingIdx={siblingIdx ?? 0}
setSiblingIdx={
setSiblingIdx ??
(() => {
return;
})
}
/>
</div>
</div>

View file

@ -1,10 +1,14 @@
import { useRecoilState } from 'recoil';
import { useEffect, useCallback } from 'react';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
// eslint-disable-next-line import/no-cycle
import Message from './Message';
import MessageContent from '~/components/Messages/MessageContent';
// eslint-disable-next-line import/no-cycle
import MessageParts from './MessageParts';
// eslint-disable-next-line import/no-cycle
import Message from './Message';
import store from '~/store';
export default function MultiMessage({
@ -30,22 +34,22 @@ export default function MultiMessage({
}, [messagesTree?.length]);
useEffect(() => {
if (messagesTree?.length && siblingIdx >= messagesTree?.length) {
if (messagesTree?.length && siblingIdx >= messagesTree.length) {
setSiblingIdx(0);
}
}, [siblingIdx, messagesTree?.length, setSiblingIdx]);
if (!(messagesTree && messagesTree?.length)) {
if (!(messagesTree && messagesTree.length)) {
return null;
}
const message = messagesTree[messagesTree.length - siblingIdx - 1];
const message = messagesTree[messagesTree.length - siblingIdx - 1] as TMessage | undefined;
if (!message) {
return null;
}
if (message.content) {
if (isAssistantsEndpoint(message.endpoint) && message.content) {
return (
<MessageParts
key={message.messageId}
@ -57,6 +61,18 @@ export default function MultiMessage({
setSiblingIdx={setSiblingIdxRev}
/>
);
} else if (message.content) {
return (
<MessageContent
key={message.messageId}
message={message}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}
siblingIdx={messagesTree.length - siblingIdx - 1}
siblingCount={messagesTree.length}
setSiblingIdx={setSiblingIdxRev}
/>
);
}
return (

View file

@ -1,4 +1,4 @@
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAssistantsEndpoint, alternateName } from 'librechat-data-provider';
import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon';
import { BrainCircuit } from 'lucide-react';
import {
@ -7,6 +7,7 @@ import {
PaLMIcon,
CodeyIcon,
GeminiIcon,
BedrockIcon,
AssistantIcon,
AnthropicIcon,
AzureMinimalIcon,
@ -16,11 +17,31 @@ import {
import { IconProps } from '~/common';
import { cn } from '~/utils';
function getGoogleIcon(model: string | null | undefined, size: number) {
if (model?.toLowerCase().includes('code') === true) {
return <CodeyIcon size={size * 0.75} />;
} else if (model?.toLowerCase().includes('gemini') === true) {
return <GeminiIcon size={size * 0.7} />;
} else {
return <PaLMIcon size={size * 0.7} />;
}
}
function getGoogleModelName(model: string | null | undefined) {
if (model?.toLowerCase().includes('code') === true) {
return 'Codey';
} else if (model?.toLowerCase().includes('gemini') === true) {
return 'Gemini';
} else {
return 'PaLM2';
}
}
const MessageEndpointIcon: React.FC<IconProps> = (props) => {
const {
error,
button,
iconURL,
iconURL = '',
endpoint,
jailbreak,
size = 30,
@ -30,7 +51,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
} = props;
const assistantsIcon = {
icon: props.iconURL ? (
icon: iconURL ? (
<div className="relative flex h-6 w-6 items-center justify-center">
<div
title={assistantName}
@ -42,7 +63,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
>
<img
className="shadow-stroke h-full w-full object-cover"
src={props.iconURL}
src={iconURL}
alt={assistantName}
style={{ height: '80', width: '80' }}
/>
@ -59,7 +80,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
};
const agentsIcon = {
icon: props.iconURL ? (
icon: iconURL ? (
<div className="relative flex h-6 w-6 items-center justify-center">
<div
title={agentName}
@ -71,7 +92,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
>
<img
className="shadow-stroke h-full w-full object-cover"
src={props.iconURL}
src={iconURL}
alt={agentName}
style={{ height: '80', width: '80' }}
/>
@ -104,42 +125,38 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
},
[EModelEndpoint.gptPlugins]: {
icon: <Plugin size={size * 0.7} />,
bg: `rgba(69, 89, 164, ${button ? 0.75 : 1})`,
bg: `rgba(69, 89, 164, ${button === true ? 0.75 : 1})`,
name: 'Plugins',
},
[EModelEndpoint.google]: {
icon: model?.toLowerCase()?.includes('code') ? (
<CodeyIcon size={size * 0.75} />
) : model?.toLowerCase()?.includes('gemini') ? (
<GeminiIcon size={size * 0.7} />
) : (
<PaLMIcon size={size * 0.7} />
),
name: model?.toLowerCase()?.includes('code')
? 'Codey'
: model?.toLowerCase()?.includes('gemini')
? 'Gemini'
: 'PaLM2',
icon: getGoogleIcon(model, size),
name: getGoogleModelName(model),
},
[EModelEndpoint.anthropic]: {
icon: <AnthropicIcon size={size * 0.5555555555555556} />,
bg: '#d09a74',
name: 'Claude',
},
[EModelEndpoint.bedrock]: {
icon: <BedrockIcon className="icon-xl text-white" />,
bg: '#268672',
name: alternateName[EModelEndpoint.bedrock],
},
[EModelEndpoint.bingAI]: {
icon: jailbreak ? (
<img src="/assets/bingai-jb.png" alt="Bing Icon" />
) : (
<img src="/assets/bingai.png" alt="Sydney Icon" />
),
name: jailbreak ? 'Sydney' : 'BingAI',
icon:
jailbreak === true ? (
<img src="/assets/bingai-jb.png" alt="Bing Icon" />
) : (
<img src="/assets/bingai.png" alt="Sydney Icon" />
),
name: jailbreak === true ? 'Sydney' : 'BingAI',
},
[EModelEndpoint.chatGPTBrowser]: {
icon: <GPTIcon size={size * 0.5555555555555556} />,
bg:
typeof model === 'string' && model.toLowerCase().includes('gpt-4')
? '#AB68FF'
: `rgba(0, 163, 255, ${button ? 0.75 : 1})`,
: `rgba(0, 163, 255, ${button === true ? 0.75 : 1})`,
name: 'ChatGPT',
},
[EModelEndpoint.custom]: {
@ -152,7 +169,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
<div className="h-6 w-6">
<div className="overflow-hidden rounded-full">
<UnknownIcon
iconURL={props.iconURL}
iconURL={iconURL}
endpoint={endpoint ?? ''}
className="h-full w-full object-contain"
context="message"
@ -185,11 +202,11 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
}}
className={cn(
'relative flex h-9 w-9 items-center justify-center rounded-sm p-1 text-white',
props.className || '',
props.className ?? '',
)}
>
{icon}
{error && (
{error === true && (
<span className="absolute right-0 top-[20px] -mr-2 flex h-3 w-3 items-center justify-center rounded-full border border-white bg-red-500 text-[10px] text-white">
!
</span>

View file

@ -1,4 +1,4 @@
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
import { BrainCircuit } from 'lucide-react';
import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon';
import {
@ -10,6 +10,7 @@ import {
GoogleMinimalIcon,
CustomMinimalIcon,
AnthropicIcon,
BedrockIcon,
Sparkles,
} from '~/components/svg';
import { cn } from '~/utils';
@ -27,17 +28,17 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
const endpointIcons = {
[EModelEndpoint.azureOpenAI]: {
icon: <AzureMinimalIcon className={iconClassName} />,
name: props.chatGptLabel || 'ChatGPT',
name: props.chatGptLabel ?? 'ChatGPT',
},
[EModelEndpoint.openAI]: {
icon: <OpenAIMinimalIcon className={iconClassName} />,
name: props.chatGptLabel || 'ChatGPT',
name: props.chatGptLabel ?? 'ChatGPT',
},
[EModelEndpoint.gptPlugins]: { icon: <MinimalPlugin />, name: 'Plugins' },
[EModelEndpoint.google]: { icon: <GoogleMinimalIcon />, name: props.modelLabel || 'Google' },
[EModelEndpoint.google]: { icon: <GoogleMinimalIcon />, name: props.modelLabel ?? 'Google' },
[EModelEndpoint.anthropic]: {
icon: <AnthropicIcon className="icon-md shrink-0 dark:text-white" />,
name: props.modelLabel || 'Claude',
name: props.modelLabel ?? 'Claude',
},
[EModelEndpoint.custom]: {
icon: <CustomMinimalIcon />,
@ -47,7 +48,14 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
[EModelEndpoint.chatGPTBrowser]: { icon: <LightningIcon />, name: 'ChatGPT' },
[EModelEndpoint.assistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.azureAssistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.agents]: { icon: <BrainCircuit className="icon-sm" />, name: 'Agent' },
[EModelEndpoint.agents]: {
icon: <BrainCircuit className="icon-sm" />,
name: props.modelLabel ?? alternateName[EModelEndpoint.agents],
},
[EModelEndpoint.bedrock]: {
icon: <BedrockIcon className="icon-xl text-text-primary" />,
name: props.modelLabel ?? alternateName[EModelEndpoint.bedrock],
},
default: {
icon: (
<UnknownIcon
@ -76,11 +84,11 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
}}
className={cn(
'relative flex items-center justify-center rounded-sm text-black dark:text-white',
props.className || '',
props.className ?? '',
)}
>
{icon}
{error && (
{error === true && (
<span className="absolute right-0 top-[20px] -mr-2 flex h-4 w-4 items-center justify-center rounded-full border border-white bg-red-500 text-[10px] text-black dark:text-white">
!
</span>

View file

@ -2,14 +2,14 @@ import React, { useEffect, useState } from 'react';
import { useCreatePresetMutation } from 'librechat-data-provider/react-query';
import type { TEditPresetProps } from '~/common';
import { cn, removeFocusOutlines, cleanupPreset, defaultTextProps } from '~/utils/';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { Dialog, Input, Label } from '~/components/ui/';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { OGDialog, Input, Label } from '~/components/ui/';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => {
const [title, setTitle] = useState<string>(preset.title || 'My Preset');
const [title, setTitle] = useState<string>(preset.title ?? 'My Preset');
const createPresetMutation = useCreatePresetMutation();
const { showToast } = useToastContext();
const localize = useLocalize();
@ -22,15 +22,15 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
},
});
const toastTitle = _preset.title
? `\`${_preset.title}\``
: localize('com_endpoint_preset_title');
const toastTitle =
_preset.title ?? '' ? `\`${_preset.title}\`` : localize('com_endpoint_preset_title');
createPresetMutation.mutate(_preset, {
onSuccess: () => {
showToast({
message: `${toastTitle} ${localize('com_endpoint_preset_saved')}`,
});
onOpenChange(false); // Close the dialog on success
},
onError: () => {
showToast({
@ -42,27 +42,38 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
};
useEffect(() => {
setTitle(preset.title || localize('com_endpoint_my_preset'));
setTitle(preset.title ?? localize('com_endpoint_my_preset'));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
// Handle Enter key press
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
submitPreset();
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
<OGDialog open={open} onOpenChange={onOpenChange}>
<OGDialogTemplate
title={localize('com_endpoint_save_as_preset')}
className="w-11/12 sm:w-1/4"
className="z-[90] w-11/12 sm:w-1/4"
overlayClassName="z-[80]"
showCloseButton={false}
main={
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="dialog-preset-name" className="text-left text-sm font-medium">
<Label htmlFor="preset-custom-name" className="text-left text-sm font-medium">
{localize('com_endpoint_preset_name')}
</Label>
<Input
id="preset-custom-name"
value={title || ''}
onChange={(e) => setTitle(e.target.value || '')}
placeholder="Set a custom name for this preset"
onKeyDown={handleKeyDown}
placeholder={localize('com_endpoint_preset_custom_name_placeholder')}
aria-label={localize('com_endpoint_preset_name')}
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none border-gray-100 px-3 py-2 dark:border-gray-600',
@ -78,7 +89,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
selectText: localize('com_ui_save'),
}}
/>
</Dialog>
</OGDialog>
);
};

View file

@ -0,0 +1,59 @@
import { useMemo } from 'react';
import { getSettingsKeys } from 'librechat-data-provider';
import type { SettingDefinition } from 'librechat-data-provider';
import type { TModelSelectProps } from '~/common';
import { componentMapping } from '~/components/SidePanel/Parameters/components';
import { presetSettings } from '~/components/SidePanel/Parameters/settings';
export default function BedrockSettings({
conversation,
setOption,
models,
readonly,
}: TModelSelectProps) {
const parameters = useMemo(() => {
const [combinedKey, endpointKey] = getSettingsKeys(
conversation?.endpoint ?? '',
conversation?.model ?? '',
);
return presetSettings[combinedKey] ?? presetSettings[endpointKey];
}, [conversation]);
if (!parameters) {
return null;
}
const renderComponent = (setting: SettingDefinition) => {
const Component = componentMapping[setting.component];
const { key, default: defaultValue, ...rest } = setting;
const props = {
key,
settingKey: key,
defaultValue,
...rest,
readonly,
setOption,
conversation,
};
if (key === 'model') {
return <Component {...props} options={models} />;
}
return <Component {...props} />;
};
return (
<div className="h-auto max-w-full overflow-x-hidden p-3">
<div className="grid grid-cols-1 gap-6 md:grid-cols-5">
<div className="flex flex-col gap-6 md:col-span-3">
{parameters.col1.map(renderComponent)}
</div>
<div className="flex flex-col gap-6 md:col-span-2">
{parameters.col2.map(renderComponent)}
</div>
</div>
</div>
);
}

View file

@ -18,8 +18,7 @@ import {
HoverCardTrigger,
} from '~/components/ui';
import { cn, defaultTextProps, optionText, removeFocusOutlines, removeFocusRings } from '~/utils';
import OptionHoverAlt from '~/components/SidePanel/Parameters/OptionHover';
import { DynamicTags } from '~/components/SidePanel/Parameters';
import { OptionHoverAlt, DynamicTags } from '~/components/SidePanel/Parameters';
import { useLocalize, useDebouncedInput } from '~/hooks';
import OptionHover from './OptionHover';
import { ESide } from '~/common';

View file

@ -1,5 +1,6 @@
export { default as Advanced } from './Advanced';
export { default as AssistantsSettings } from './Assistants';
export { default as BedrockSettings } from './Bedrock';
export { default as OpenAISettings } from './OpenAI';
export { default as BingAISettings } from './BingAI';
export { default as GoogleSettings } from './Google';

View file

@ -4,6 +4,7 @@ import type { TModelSelectProps } from '~/common';
import { GoogleSettings, PluginSettings } from './MultiView';
import AssistantsSettings from './Assistants';
import AnthropicSettings from './Anthropic';
import BedrockSettings from './Bedrock';
import BingAISettings from './BingAI';
import OpenAISettings from './OpenAI';
@ -16,6 +17,7 @@ const settings: { [key: string]: FC<TModelSelectProps> } = {
[EModelEndpoint.azureOpenAI]: OpenAISettings,
[EModelEndpoint.bingAI]: BingAISettings,
[EModelEndpoint.anthropic]: AnthropicSettings,
[EModelEndpoint.bedrock]: BedrockSettings,
};
export const getSettings = () => {

View file

@ -12,6 +12,7 @@ import PluginsByIndex from './PluginsByIndex';
export const options: { [key: string]: FC<TModelSelectProps> } = {
[EModelEndpoint.openAI]: OpenAI,
[EModelEndpoint.custom]: OpenAI,
[EModelEndpoint.bedrock]: OpenAI,
[EModelEndpoint.azureOpenAI]: OpenAI,
[EModelEndpoint.bingAI]: BingAI,
[EModelEndpoint.google]: Google,

View file

@ -0,0 +1,170 @@
import { useRecoilValue } from 'recoil';
import { useCallback, useMemo, memo } from 'react';
import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import ContentParts from '~/components/Chat/Messages/Content/ContentParts';
import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon';
import SubRow from '~/components/Chat/Messages/SubRow';
import { useMessageActions } from '~/hooks';
import { cn, logger } from '~/utils';
import store from '~/store';
type ContentRenderProps = {
message?: TMessage;
isCard?: boolean;
isMultiMessage?: boolean;
isSubmittingFamily?: boolean;
} & Pick<
TMessageProps,
'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount'
>;
const ContentRender = memo(
({
isCard,
siblingIdx,
siblingCount,
message: msg,
setSiblingIdx,
currentEditId,
isMultiMessage,
setCurrentEditId,
isSubmittingFamily,
}: ContentRenderProps) => {
const {
// ask,
edit,
index,
agent,
assistant,
enterEdit,
conversation,
messageLabel,
isSubmitting,
latestMessage,
handleContinue,
copyToClipboard,
setLatestMessage,
regenerateMessage,
} = useMessageActions({
message: msg,
currentEditId,
isMultiMessage,
setCurrentEditId,
});
const fontSize = useRecoilValue(store.fontSize);
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
// const { isCreatedByUser, error, unfinished } = msg ?? {};
const isLast = useMemo(
() =>
!(msg?.children?.length ?? 0) && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
[msg?.children, msg?.depth, latestMessage?.depth],
);
if (!msg) {
return null;
}
const isLatestMessage = msg.messageId === latestMessage?.messageId;
const showCardRender = isLast && !(isSubmittingFamily === true) && isCard === true;
const isLatestCard = isCard === true && !(isSubmittingFamily === true) && isLatestMessage;
const clickHandler =
showCardRender && !isLatestMessage
? () => {
logger.log(`Message Card click: Setting ${msg.messageId} as latest message`);
logger.dir(msg);
setLatestMessage(msg);
}
: undefined;
return (
<div
aria-label={`message-${msg.depth}-${msg.messageId}`}
className={cn(
'final-completion group mx-auto flex flex-1 gap-3',
isCard === true
? 'relative w-full gap-1 rounded-lg border border-border-medium bg-surface-primary-alt p-2 md:w-1/2 md:gap-3 md:p-4'
: 'md:max-w-3xl md:px-5 lg:max-w-[40rem] lg:px-1 xl:max-w-[48rem] xl:px-5',
isLatestCard === true ? 'bg-surface-secondary' : '',
showCardRender ? 'cursor-pointer transition-colors duration-300' : '',
'focus:outline-none focus:ring-2 focus:ring-border-xheavy',
)}
onClick={clickHandler}
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && clickHandler) {
clickHandler();
}
}}
role={showCardRender ? 'button' : undefined}
tabIndex={showCardRender ? 0 : undefined}
>
{isLatestCard === true && (
<div className="absolute right-0 top-0 m-2 h-3 w-3 rounded-full bg-text-primary" />
)}
<div className="relative flex flex-shrink-0 flex-col items-end">
<div>
<div className="pt-0.5">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon
message={msg}
conversation={conversation}
assistant={assistant}
agent={agent}
/>
</div>
</div>
</div>
</div>
<div
className={cn(
'relative flex w-11/12 flex-col',
msg.isCreatedByUser === true ? '' : 'agent-turn',
)}
>
<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">
<ContentParts
content={msg.content as Array<TMessageContentParts | undefined>}
messageId={msg.messageId}
isCreatedByUser={msg.isCreatedByUser}
isLast={isLast}
isSubmitting={isSubmitting}
/>
</div>
</div>
{!(msg.children?.length ?? 0) && (isSubmittingFamily === true || isSubmitting) ? (
<PlaceholderRow isCard={isCard} />
) : (
<SubRow classes="text-xs">
<SiblingSwitch
siblingIdx={siblingIdx}
siblingCount={siblingCount}
setSiblingIdx={setSiblingIdx}
/>
<HoverButtons
index={index}
isEditing={edit}
message={msg}
enterEdit={enterEdit}
isSubmitting={isSubmitting}
conversation={conversation ?? null}
regenerate={handleRegenerateMessage}
copyToClipboard={copyToClipboard}
handleContinue={handleContinue}
latestMessage={latestMessage}
isLast={isLast}
/>
</SubRow>
)}
</div>
</div>
);
},
);
export default ContentRender;

View file

@ -0,0 +1,82 @@
import React from 'react';
import { useMessageProcess } from '~/hooks';
import type { TMessageProps } from '~/common';
// eslint-disable-next-line import/no-cycle
import MultiMessage from '~/components/Chat/Messages/MultiMessage';
import ContentRender from './ContentRender';
const MessageContainer = React.memo(
({
handleScroll,
children,
}: {
handleScroll: (event?: unknown) => void;
children: React.ReactNode;
}) => {
return (
<div
className="text-token-text-primary w-full border-0 bg-transparent dark:border-0 dark:bg-transparent"
onWheel={handleScroll}
onTouchMove={handleScroll}
>
{children}
</div>
);
},
);
export default function MessageContent(props: TMessageProps) {
const {
showSibling,
conversation,
handleScroll,
siblingMessage,
latestMultiMessage,
isSubmittingFamily,
} = useMessageProcess({ message: props.message });
const { message, currentEditId, setCurrentEditId } = props;
if (!message || typeof message !== 'object') {
return null;
}
const { children, messageId = null } = message;
return (
<>
<MessageContainer handleScroll={handleScroll}>
{showSibling ? (
<div className="m-auto my-2 flex justify-center p-4 py-2 md:gap-6">
<div className="flex w-full flex-row flex-wrap justify-between gap-1 md:max-w-5xl md:flex-nowrap md:gap-2 lg:max-w-5xl xl:max-w-6xl">
<ContentRender
{...props}
message={message}
isSubmittingFamily={isSubmittingFamily}
isCard
/>
<ContentRender
{...props}
isMultiMessage
isCard
message={siblingMessage ?? latestMultiMessage ?? undefined}
isSubmittingFamily={isSubmittingFamily}
/>
</div>
</div>
) : (
<div className="m-auto justify-center p-4 py-2 md:gap-6 ">
<ContentRender {...props} />
</div>
)}
</MessageContainer>
<MultiMessage
key={messageId}
messageId={messageId}
conversation={conversation}
messagesTree={children ?? []}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}
/>
</>
);
}

View file

@ -1,3 +1,4 @@
import React from 'react';
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
export default function ListCard({
@ -10,19 +11,33 @@ export default function ListCard({
category: string;
name: string;
snippet: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
onClick?: React.MouseEventHandler<HTMLDivElement | HTMLButtonElement>;
children?: React.ReactNode;
}) {
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement | HTMLButtonElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onClick?.(event as unknown as React.MouseEvent<HTMLDivElement | HTMLButtonElement>);
}
};
return (
<button
<div
onClick={onClick}
onKeyDown={handleKeyDown}
className="relative my-2 flex w-full cursor-pointer flex-col gap-2 rounded-2xl border border-border-light px-3 pb-4 pt-3 text-start
align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-all duration-300 ease-in-out hover:bg-surface-tertiary"
role="button"
tabIndex={0}
aria-labelledby={`card-title-${name}`}
>
<div className="flex w-full justify-between">
<div className="flex flex-row gap-2">
<CategoryIcon category={category} className="icon-md" />
<h3 className="break-word select-none text-balance text-sm font-semibold text-text-primary">
<CategoryIcon category={category} className="icon-md" aria-hidden="true" />
<h3
id={`card-title-${name}`}
className="break-word select-none text-balance text-sm font-semibold text-text-primary"
>
{name}
</h3>
</div>
@ -31,6 +46,6 @@ export default function ListCard({
<div className="ellipsis max-w-full select-none text-balance text-sm text-text-secondary">
{snippet}
</div>
</button>
</div>
);
}

View file

@ -31,7 +31,7 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
const { data: startupConfig = {} as TStartupConfig, isFetching } = useGetStartupConfig();
const { instanceProjectId } = startupConfig;
const groupIsGlobal = useMemo(
() => !!group?.projectIds?.includes(instanceProjectId),
() => !!(group?.projectIds ?? []).includes(instanceProjectId),
[group, instanceProjectId],
);
@ -57,7 +57,8 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
}
const onSubmit = (data: FormValues) => {
if (!group._id || !instanceProjectId) {
const groupId = group._id ?? '';
if (!groupId || !instanceProjectId) {
return;
}
@ -70,7 +71,7 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
}
updateGroup.mutate({
id: group._id,
id: groupId,
payload,
});
};
@ -87,24 +88,38 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
<Share2Icon className="cursor-pointer text-white " />
</Button>
</OGDialogTrigger>
<OGDialogContent className="bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300">
<OGDialogContent className="border-border-light bg-surface-primary-alt text-text-secondary">
<OGDialogTitle>{localize('com_ui_share_var', `"${group.name}"`)}</OGDialogTitle>
<form className="p-2" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-4 flex items-center justify-between gap-2 py-4">
<label
className="cursor-pointer select-none"
htmlFor={Permissions.SHARED_GLOBAL}
onClick={() =>
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
shouldDirty: true,
})
}
>
{localize('com_ui_share_to_all_users')}
{groupIsGlobal && (
<span className="ml-2 text-xs">{localize('com_ui_prompt_shared_to_all')}</span>
)}
</label>
<div className="flex items-center">
<button
type="button"
className="mr-2 cursor-pointer"
onClick={() =>
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
shouldDirty: true,
})
}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
shouldDirty: true,
});
}
}}
aria-checked={getValues(Permissions.SHARED_GLOBAL)}
role="checkbox"
>
{localize('com_ui_share_to_all_users')}
</button>
<label htmlFor={Permissions.SHARED_GLOBAL} className="select-none">
{groupIsGlobal && (
<span className="ml-2 text-xs">{localize('com_ui_prompt_shared_to_all')}</span>
)}
</label>
</div>
<Controller
name={Permissions.SHARED_GLOBAL}
control={control}
@ -126,7 +141,7 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
{...field}
checked={field.value}
onCheckedChange={field.onChange}
value={field?.value?.toString()}
value={field.value.toString()}
/>
)}
/>

View file

@ -11,9 +11,10 @@ import Action from '~/components/SidePanel/Builder/Action';
import { useLocalize } from '~/hooks';
import { ToolSelectDialog } from '~/components/Tools';
import { useToastContext } from '~/Providers';
import ContextButton from './ContextButton';
import { Spinner } from '~/components/svg';
import DeleteButton from './DeleteButton';
import AgentAvatar from './AgentAvatar';
import ShareAgent from './ShareAgent';
import AgentTool from './AgentTool';
import { Panel } from '~/common';
@ -57,14 +58,14 @@ export default function AgentConfig({
() => agentsConfig?.capabilities?.includes(Capabilities.actions),
[agentsConfig],
);
const retrievalEnabled = useMemo(
() => agentsConfig?.capabilities?.includes(Capabilities.retrieval),
[agentsConfig],
);
const codeEnabled = useMemo(
() => agentsConfig?.capabilities?.includes(Capabilities.code_interpreter),
[agentsConfig],
);
// const retrievalEnabled = useMemo(
// () => agentsConfig?.capabilities?.includes(Capabilities.retrieval),
// [agentsConfig],
// );
// const codeEnabled = useMemo(
// () => agentsConfig?.capabilities?.includes(Capabilities.code_interpreter),
// [agentsConfig],
// );
/* Mutations */
const update = useUpdateAgentMutation({
@ -190,7 +191,7 @@ export default function AgentConfig({
name="id"
control={control}
render={({ field }) => (
<p className="h-3 text-xs italic text-gray-600" aria-live="polite">
<p className="h-3 text-xs italic text-text-secondary" aria-live="polite">
{field.value}
</p>
)}
@ -221,12 +222,11 @@ export default function AgentConfig({
{/* Instructions */}
<div className="mb-6">
<label className={labelClass} htmlFor="instructions">
{localize('com_ui_instructions')} <span className="text-red-500">*</span>
{localize('com_ui_instructions')}
</label>
<Controller
name="instructions"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => (
<>
<textarea
@ -276,16 +276,16 @@ export default function AgentConfig({
/>
</div>
)}
<span>{model ? model : localize('com_ui_select_model')}</span>
<span>{model != null ? model : localize('com_ui_select_model')}</span>
</div>
</button>
</div>
{/* Agent Tools & Actions */}
<div className="mb-6">
<label className={labelClass}>
{`${toolsEnabled ? localize('com_assistants_tools') : ''}
${toolsEnabled && actionsEnabled ? ' + ' : ''}
${actionsEnabled ? localize('com_assistants_actions') : ''}`}
{`${toolsEnabled === true ? localize('com_assistants_tools') : ''}
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
${actionsEnabled === true ? localize('com_assistants_actions') : ''}`}
</label>
<div className="space-y-2">
{tools?.map((func, i) => (
@ -339,11 +339,16 @@ export default function AgentConfig({
</div>
{/* Context Button */}
<div className="flex items-center justify-end gap-2">
<ContextButton
<DeleteButton
agent_id={agent_id}
setCurrentAgentId={setCurrentAgentId}
createMutation={create}
/>
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
/>
{/* 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"

View file

@ -1,10 +1,11 @@
import { Plus } from 'lucide-react';
import { Plus, EarthIcon } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { Capabilities, defaultAgentFormValues } from 'librechat-data-provider';
import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common';
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type { UseFormReset } from 'react-hook-form';
import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common';
import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils';
import { useListAgentsQuery, useGetAgentByIdQuery } from '~/data-provider';
import SelectDropDown from '~/components/ui/SelectDropDown';
@ -16,7 +17,7 @@ const keys = new Set(Object.keys(defaultAgentFormValues));
export default function AgentSelect({
reset,
value: currentAgentValue,
selectedAgentId,
selectedAgentId = null,
setCurrentAgentId,
createMutation,
}: {
@ -31,21 +32,33 @@ export default function AgentSelect({
// const fileMap = useFileMapContext();
const lastSelectedAgent = useRef<string | null>(null);
const { data: agents = [] } = useListAgentsQuery(undefined, {
select: (res) => res.data.map((agent) => processAgentOption(agent /*, fileMap */)),
const { data: startupConfig } = useGetStartupConfig();
const { data: agents = null } = useListAgentsQuery(undefined, {
select: (res) =>
res.data.map((agent) =>
processAgentOption({
agent,
instanceProjectId: startupConfig?.instanceProjectId,
/* fileMap */
}),
),
});
const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', {
enabled: !!selectedAgentId,
enabled: !!(selectedAgentId ?? ''),
});
const resetAgentForm = useCallback(
(fullAgent: Agent) => {
const { instanceProjectId } = startupConfig ?? {};
const isGlobal =
(instanceProjectId != null && fullAgent.projectIds?.includes(instanceProjectId)) ?? false;
const update = {
...fullAgent,
provider: createProviderOption(fullAgent.provider),
label: fullAgent.name ?? '',
value: fullAgent.id ?? '',
value: fullAgent.id || '',
icon: isGlobal ? <EarthIcon className={'icon-lg text-green-400'} /> : null,
};
const actions: AgentCapabilities = {
@ -84,7 +97,7 @@ export default function AgentSelect({
const onSelect = useCallback(
(selectedId: string) => {
const agentExists = !!(selectedId
? agents.find((agent) => agent.id === selectedId)
? (agents ?? []).find((agent) => agent.id === selectedId)
: undefined);
createMutation.reset();
@ -120,7 +133,7 @@ export default function AgentSelect({
return;
}
if (selectedAgentId && agents) {
if (selectedAgentId != null && selectedAgentId !== '' && agents) {
timerId = setTimeout(() => {
lastSelectedAgent.current = selectedAgentId;
onSelect(selectedAgentId);
@ -136,8 +149,9 @@ export default function AgentSelect({
const createAgent = localize('com_ui_create') + ' ' + localize('com_ui_agent');
const hasAgentValue = !!(typeof currentAgentValue === 'object'
? currentAgentValue.value
: currentAgentValue);
? currentAgentValue.value != null && currentAgentValue.value !== ''
: typeof currentAgentValue !== 'undefined');
return (
<SelectDropDown
value={!hasAgentValue ? createAgent : (currentAgentValue as TAgentOption)}
@ -151,9 +165,11 @@ export default function AgentSelect({
]
}
iconSide="left"
optionIconSide="right"
showAbove={false}
showLabel={false}
emptyTitle={true}
showOptionIcon={true}
containerClassName="flex-grow"
searchClassName="dark:from-gray-850"
searchPlaceholder={localize('com_agents_search_name')}

View file

@ -4,11 +4,11 @@ import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
import { useChatContext, useToastContext } from '~/Providers';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useSetIndexOptions } from '~/hooks';
import { cn, removeFocusOutlines, logger } from '~/utils';
import { useDeleteAgentMutation } from '~/data-provider';
import { cn, removeFocusOutlines } from '~/utils/';
import { TrashIcon } from '~/components/svg';
export default function ContextButton({
export default function DeleteButton({
agent_id,
setCurrentAgentId,
createMutation,
@ -34,8 +34,8 @@ export default function ContextButton({
status: 'success',
});
if (createMutation.data?.id) {
console.log('[deleteAgent] resetting createMutation');
if (createMutation.data?.id ?? '') {
logger.log('agents', 'resetting createMutation');
createMutation.reset();
}
@ -49,7 +49,7 @@ export default function ContextButton({
return setOption('agent_id')(firstAgent.id);
}
const currentAgent = updatedList?.find((agent) => agent.id === conversation?.agent_id);
const currentAgent = updatedList.find((agent) => agent.id === conversation?.agent_id);
if (currentAgent) {
setCurrentAgentId(currentAgent.id);
@ -78,6 +78,7 @@ export default function ContextButton({
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
removeFocusOutlines,
)}
aria-label={localize('com_ui_delete') + ' ' + localize('com_ui_agent')}
type="button"
>
<div className="flex w-full items-center justify-center gap-2 text-red-500">

View file

@ -0,0 +1,206 @@
import React, { useEffect, useMemo } from 'react';
import { Share2Icon } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import { Permissions } from 'librechat-data-provider';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { TStartupConfig, AgentUpdateParams } from 'librechat-data-provider';
import {
Switch,
OGDialog,
OGDialogTitle,
OGDialogClose,
OGDialogContent,
OGDialogTrigger,
} from '~/components/ui';
import { useUpdateAgentMutation } from '~/data-provider';
import { cn, removeFocusOutlines } from '~/utils';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
type FormValues = {
[Permissions.SHARED_GLOBAL]: boolean;
};
export default function ShareAgent({
agent_id = '',
agentName,
projectIds = [],
}: {
agent_id?: string;
agentName?: string;
projectIds?: string[];
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
const { data: startupConfig = {} as TStartupConfig, isFetching } = useGetStartupConfig();
const { instanceProjectId } = startupConfig;
const agentIsGlobal = useMemo(
() => !!projectIds.includes(instanceProjectId),
[projectIds, instanceProjectId],
);
const {
control,
setValue,
getValues,
handleSubmit,
formState: { isSubmitting },
} = useForm<FormValues>({
mode: 'onChange',
defaultValues: {
[Permissions.SHARED_GLOBAL]: agentIsGlobal,
},
});
useEffect(() => {
setValue(Permissions.SHARED_GLOBAL, agentIsGlobal);
}, [agentIsGlobal, setValue]);
const updateAgent = useUpdateAgentMutation({
onSuccess: (data) => {
showToast({
message: `${localize('com_assistants_update_success')} ${
data.name ?? localize('com_ui_agent')
}`,
status: 'success',
});
},
onError: (err) => {
const error = err as Error;
showToast({
message: `${localize('com_agents_update_error')}${
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
}`,
status: 'error',
});
},
});
if (!agent_id || !instanceProjectId) {
return null;
}
const onSubmit = (data: FormValues) => {
if (!agent_id || !instanceProjectId) {
return;
}
const payload = {} as AgentUpdateParams;
if (data[Permissions.SHARED_GLOBAL]) {
payload.projectIds = [startupConfig.instanceProjectId];
} else {
payload.removeProjectIds = [startupConfig.instanceProjectId];
}
updateAgent.mutate({
agent_id,
data: payload,
});
};
return (
<OGDialog>
<OGDialogTrigger asChild>
<button
className={cn(
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
removeFocusOutlines,
)}
aria-label={localize(
'com_ui_share_var',
agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent'),
)}
type="button"
>
<div className="flex w-full items-center justify-center gap-2 text-blue-500">
<Share2Icon className="icon-md h-4 w-4" />
</div>
</button>
</OGDialogTrigger>
<OGDialogContent className="border-border-light bg-surface-primary-alt text-text-secondary">
<OGDialogTitle>
{localize(
'com_ui_share_var',
agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent'),
)}
</OGDialogTitle>
<form
className="p-2"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
}}
>
<div className="mb-4 flex items-center justify-between gap-2 py-4">
<div className="flex items-center">
<button
type="button"
className="mr-2 cursor-pointer"
onClick={() =>
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
shouldDirty: true,
})
}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
shouldDirty: true,
});
}
}}
aria-checked={getValues(Permissions.SHARED_GLOBAL)}
role="checkbox"
>
{localize('com_ui_share_to_all_users')}
</button>
<label htmlFor={Permissions.SHARED_GLOBAL} className="select-none">
{agentIsGlobal && (
<span className="ml-2 text-xs">{localize('com_ui_agent_shared_to_all')}</span>
)}
</label>
</div>
<Controller
name={Permissions.SHARED_GLOBAL}
control={control}
disabled={isFetching || updateAgent.isLoading || !instanceProjectId}
rules={{
validate: (value) => {
const isValid = !(value && agentIsGlobal);
if (!isValid) {
showToast({
message: localize('com_ui_agent_already_shared_to_all'),
status: 'warning',
});
}
return isValid;
},
}}
render={({ field }) => (
<Switch
{...field}
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
/>
)}
/>
</div>
<div className="flex justify-end">
<OGDialogClose asChild>
<button
type="submit"
disabled={isSubmitting || isFetching}
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
>
{localize('com_ui_save')}
</button>
</OGDialogClose>
</div>
</form>
</OGDialogContent>
</OGDialog>
);
}

View file

@ -1,32 +1,40 @@
import { useState } from 'react';
import type { Action } from 'librechat-data-provider';
import GearIcon from '~/components/svg/GearIcon';
import { cn } from '~/utils';
export default function Action({ action, onClick }: { action: Action; onClick: () => void }) {
const [isHovering, setIsHovering] = useState(false);
return (
<div>
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onClick();
}
}}
className="group flex w-full rounded-lg border border-border-medium text-sm hover:cursor-pointer focus:outline-none focus:ring-2 focus:ring-text-primary"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
aria-label={`Action for ${action.metadata.domain}`}
>
<div
onClick={onClick}
className="flex w-full rounded-lg text-sm hover:cursor-pointer"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className="h-9 grow overflow-hidden text-ellipsis whitespace-nowrap px-3 py-2"
style={{ wordBreak: 'break-all' }}
>
<div
className="h-9 grow whitespace-nowrap px-3 py-2"
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
>
{action.metadata.domain}
</div>
{isHovering && (
<button
type="button"
className="transition-colors flex h-9 w-9 min-w-9 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
>
<GearIcon className="icon-sm" />
</button>
{action.metadata.domain}
</div>
<div
className={cn(
'h-9 w-9 min-w-9 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-tertiary focus:outline-none focus:ring-2 focus:ring-text-primary group-focus:flex',
isHovering ? 'flex' : 'hidden',
)}
aria-label="Settings"
>
<GearIcon className="icon-sm" aria-hidden="true" />
</div>
</div>
);

View file

@ -9,17 +9,17 @@ import OptionHover from './OptionHover';
import { ESide } from '~/common';
function DynamicCheckbox({
label,
label = '',
settingKey,
defaultValue,
description,
description = '',
columnSpan,
setOption,
optionType,
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
showDefault = false,
labelCode = false,
descriptionCode = false,
conversation,
}: DynamicSettingProps) {
const localize = useLocalize();
@ -57,7 +57,7 @@ function DynamicCheckbox({
return (
<div
className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`}
>
<HoverCard openDelay={300}>
@ -67,11 +67,11 @@ function DynamicCheckbox({
htmlFor={`${settingKey}-dynamic-checkbox`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}:{' '}
{defaultValue ? localize('com_ui_yes') : localize('com_ui_no')})
{defaultValue != null ? localize('com_ui_yes') : localize('com_ui_no')})
</small>
)}
</Label>
@ -86,7 +86,7 @@ function DynamicCheckbox({
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={descriptionCode ? localize(description) ?? description : description}
side={ESide.Left}
/>
)}

View file

@ -0,0 +1,132 @@
import { useMemo, useState, useCallback } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, HoverCard, HoverCardTrigger } from '~/components/ui';
import ControlCombobox from '~/components/ui/ControlCombobox';
import { useLocalize, useParameterEffects } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
import { cn } from '~/utils';
function DynamicCombobox({
label = '',
settingKey,
defaultValue,
description = '',
columnSpan,
setOption,
optionType,
options: _options,
items: _items,
showLabel = true,
showDefault = false,
labelCode = false,
descriptionCode = false,
searchPlaceholderCode = false,
selectPlaceholderCode = false,
conversation,
isCollapsed = false,
SelectIcon = null,
selectPlaceholder = '',
searchPlaceholder = '',
}: DynamicSettingProps & { isCollapsed?: boolean; SelectIcon?: React.ReactNode }) {
const localize = useLocalize();
const { preset } = useChatContext();
const [inputValue, setInputValue] = useState<string | null>(null);
const selectedValue = useMemo(() => {
if (optionType === OptionTypes.Custom) {
return inputValue;
}
return conversation?.[settingKey] ?? defaultValue;
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
const items = useMemo(() => {
if (_items != null) {
return _items;
}
return (_options ?? []).map((option) => ({
label: option,
value: option,
}));
}, [_options, _items]);
const handleChange = useCallback(
(value: string) => {
if (optionType === OptionTypes.Custom) {
setInputValue(value);
} else {
setOption(settingKey)(value);
}
},
[optionType, setOption, settingKey],
);
useParameterEffects({
preset,
settingKey,
defaultValue,
conversation,
inputValue,
setInputValue,
preventDelayedUpdate: true,
});
const options = items ?? _options ?? [];
if (options.length === 0) {
return null;
}
return (
<div
className={cn(
'flex flex-col items-center justify-start gap-6',
columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full',
)}
>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
{showLabel === true && (
<div className="flex w-full justify-between">
<Label
htmlFor={`${settingKey}-dynamic-combobox`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label) ?? label : label || settingKey}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue})
</small>
)}
</Label>
</div>
)}
<ControlCombobox
displayValue={selectedValue}
selectPlaceholder={
selectPlaceholderCode === true ? localize(selectPlaceholder) : selectPlaceholder
}
searchPlaceholder={
searchPlaceholderCode === true ? localize(searchPlaceholder) : searchPlaceholder
}
isCollapsed={isCollapsed}
ariaLabel={settingKey}
selectedValue={selectedValue ?? ''}
setValue={handleChange}
items={items}
SelectIcon={SelectIcon}
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) ?? description : description}
side={ESide.Left}
/>
)}
</HoverCard>
</div>
);
}
export default DynamicCombobox;

View file

@ -9,19 +9,22 @@ import { ESide } from '~/common';
import { cn } from '~/utils';
function DynamicDropdown({
label,
label = '',
settingKey,
defaultValue,
description,
description = '',
columnSpan,
setOption,
optionType,
options,
// type: _type,
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
showLabel = true,
showDefault = false,
labelCode = false,
descriptionCode = false,
placeholder = '',
placeholderCode = false,
conversation,
}: DynamicSettingProps) {
const localize = useLocalize();
@ -64,24 +67,26 @@ function DynamicDropdown({
<div
className={cn(
'flex flex-col items-center justify-start gap-6',
columnSpan ? `col-span-${columnSpan}` : 'col-span-full',
columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full',
)}
>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex w-full justify-between">
<Label
htmlFor={`${settingKey}-dynamic-dropdown`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue})
</small>
)}
</Label>
</div>
{showLabel === true && (
<div className="flex w-full justify-between">
<Label
htmlFor={`${settingKey}-dynamic-dropdown`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label) ?? label : label || settingKey}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue})
</small>
)}
</Label>
</div>
)}
<SelectDropDown
showLabel={false}
emptyTitle={true}
@ -91,11 +96,12 @@ function DynamicDropdown({
availableValues={options}
containerClassName="w-full"
id={`${settingKey}-dynamic-dropdown`}
placeholder={placeholderCode ? localize(placeholder) ?? placeholder : placeholder}
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={descriptionCode ? localize(description) ?? description : description}
side={ESide.Left}
/>
)}

View file

@ -9,25 +9,25 @@ import OptionHover from './OptionHover';
import { ESide } from '~/common';
function DynamicInput({
label,
label = '',
settingKey,
defaultValue,
description,
description = '',
columnSpan,
setOption,
optionType,
placeholder,
placeholder = '',
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
placeholderCode,
showDefault = false,
labelCode = false,
descriptionCode = false,
placeholderCode = false,
conversation,
}: DynamicSettingProps) {
const localize = useLocalize();
const { preset } = useChatContext();
const [setInputValue, inputValue] = useDebouncedInput<string | null>({
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<string | null>({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
initialValue:
optionType !== OptionTypes.Custom
@ -43,13 +43,13 @@ function DynamicInput({
defaultValue: typeof defaultValue === 'undefined' ? '' : defaultValue,
conversation,
inputValue,
setInputValue,
setInputValue: setLocalValue,
});
return (
<div
className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`}
>
<HoverCard openDelay={300}>
@ -59,11 +59,11 @@ function DynamicInput({
htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
(
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
{typeof defaultValue === 'undefined' || !(defaultValue as string).length
? localize('com_endpoint_default_blank')
: `${localize('com_endpoint_default')}: ${defaultValue}`}
)
@ -76,13 +76,13 @@ function DynamicInput({
disabled={readonly}
value={inputValue ?? ''}
onChange={setInputValue}
placeholder={placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder}
placeholder={placeholderCode ? localize(placeholder) ?? placeholder : placeholder}
className={cn(defaultTextProps, 'flex h-10 max-h-10 w-full resize-none px-3 py-2')}
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={descriptionCode ? localize(description) ?? description : description}
side={ESide.Left}
/>
)}

View file

@ -9,19 +9,19 @@ import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
function DynamicInputNumber({
label,
label = '',
settingKey,
defaultValue,
description,
description = '',
columnSpan,
setOption,
optionType,
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
placeholderCode,
placeholder,
showDefault = false,
labelCode = false,
descriptionCode = false,
placeholderCode = false,
placeholder = '',
conversation,
range,
className = '',
@ -30,7 +30,7 @@ function DynamicInputNumber({
const localize = useLocalize();
const { preset } = useChatContext();
const [setInputValue, inputValue] = useDebouncedInput<ValueType | null>({
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<ValueType | null>({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
initialValue:
optionType !== OptionTypes.Custom
@ -46,14 +46,14 @@ function DynamicInputNumber({
defaultValue: typeof defaultValue === 'undefined' ? '' : defaultValue,
conversation,
inputValue,
setInputValue,
setInputValue: setLocalValue,
});
return (
<div
className={cn(
'flex flex-col items-center justify-start gap-6',
columnSpan ? `col-span-${columnSpan}` : 'col-span-full',
columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full',
className,
)}
>
@ -64,7 +64,7 @@ function DynamicInputNumber({
htmlFor={`${settingKey}-dynamic-setting`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue})
@ -79,9 +79,7 @@ function DynamicInputNumber({
min={range?.min}
max={range?.max}
step={range?.step}
placeholder={
placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder
}
placeholder={placeholderCode ? localize(placeholder) ?? placeholder : placeholder}
controls={false}
className={cn(
defaultTextProps,
@ -96,7 +94,7 @@ function DynamicInputNumber({
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={descriptionCode ? localize(description) ?? description : description}
side={ESide.Left}
/>
)}

View file

@ -9,27 +9,30 @@ import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
function DynamicSlider({
label,
label = '',
settingKey,
defaultValue,
range,
description,
description = '',
columnSpan,
setOption,
optionType,
options,
readonly = false,
showDefault = true,
showDefault = false,
includeInput = true,
labelCode,
descriptionCode,
labelCode = false,
descriptionCode = false,
conversation,
}: DynamicSettingProps) {
const localize = useLocalize();
const { preset } = useChatContext();
const isEnum = useMemo(() => !range && options && options.length > 0, [options, range]);
const isEnum = useMemo(
() => (!range && options && options.length > 0) ?? false,
[options, range],
);
const [setInputValue, inputValue] = useDebouncedInput<string | number>({
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<string | number>({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
initialValue: optionType !== OptionTypes.Custom ? conversation?.[settingKey] : defaultValue,
setter: () => ({}),
@ -43,7 +46,7 @@ function DynamicSlider({
defaultValue,
conversation,
inputValue,
setInputValue,
setInputValue: setLocalValue,
preventDelayedUpdate: isEnum,
});
@ -87,6 +90,16 @@ function DynamicSlider({
[isEnum, setInputValue, valueToEnumOption],
);
const max = useMemo(() => {
if (isEnum && options) {
return options.length - 1;
} else if (range) {
return range.max;
} else {
return 0;
}
}, [isEnum, options, range]);
if (!range && !isEnum) {
return null;
}
@ -94,18 +107,18 @@ function DynamicSlider({
return (
<div
className={cn(
'flex flex-col items-center justify-start gap-6',
columnSpan ? `col-span-${columnSpan}` : 'col-span-full',
'flex flex-col items-center justify-start gap-2',
columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full',
)}
>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<div className="flex w-full items-center justify-between">
<Label
htmlFor={`${settingKey}-dynamic-setting`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue})
@ -156,15 +169,16 @@ function DynamicSlider({
]}
onValueChange={(value) => handleValueChange(value[0])}
doubleClickHandler={() => setInputValue(defaultValue as string | number)}
max={isEnum && options ? options.length - 1 : range ? range.max : 0}
max={max}
min={range ? range.min : 0}
step={range ? range.step ?? 1 : 1}
className="flex h-4 w-full"
trackClassName="bg-surface-hover"
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={descriptionCode ? localize(description) ?? description : description}
side={ESide.Left}
/>
)}

View file

@ -8,17 +8,17 @@ import OptionHover from './OptionHover';
import { ESide } from '~/common';
function DynamicSwitch({
label,
label = '',
settingKey,
defaultValue,
description,
description = '',
columnSpan,
setOption,
optionType,
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
showDefault = false,
labelCode = false,
descriptionCode = false,
conversation,
}: DynamicSettingProps) {
const localize = useLocalize();
@ -55,7 +55,7 @@ function DynamicSwitch({
return (
<div
className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`}
>
<HoverCard openDelay={300}>
@ -65,10 +65,11 @@ function DynamicSwitch({
htmlFor={`${settingKey}-dynamic-switch`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue ? 'com_ui_on' : 'com_ui_off'})
({localize('com_endpoint_default')}:{' '}
{defaultValue != null ? 'com_ui_on' : 'com_ui_off'})
</small>
)}
</Label>
@ -83,7 +84,7 @@ function DynamicSwitch({
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={descriptionCode ? localize(description) ?? description : description}
side={ESide.Left}
/>
)}

View file

@ -10,19 +10,19 @@ import OptionHover from './OptionHover';
import { ESide } from '~/common';
function DynamicTags({
label,
label = '',
settingKey,
defaultValue = [],
description,
description = '',
columnSpan,
setOption,
optionType,
placeholder,
placeholder = '',
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
placeholderCode,
showDefault = false,
labelCode = false,
descriptionCode = false,
placeholderCode = false,
descriptionSide = ESide.Left,
conversation,
minTags,
@ -65,7 +65,7 @@ function DynamicTags({
return defaultValue ?? [];
}
return conversation?.[settingKey];
return conversation[settingKey];
}, [conversation, defaultValue, optionType, settingKey, tags]);
const onTagRemove = useCallback(
@ -74,7 +74,7 @@ function DynamicTags({
return;
}
if (minTags && currentTags.length <= minTags) {
if (minTags != null && currentTags.length <= minTags) {
showToast({
message: localize('com_ui_min_tags', minTags + ''),
status: 'warning',
@ -93,7 +93,7 @@ function DynamicTags({
}
let update = [...(currentTags ?? []), tagText];
if (maxTags && update.length > maxTags) {
if (maxTags != null && update.length > maxTags) {
showToast({
message: localize('com_ui_max_tags', maxTags + ''),
status: 'warning',
@ -117,7 +117,7 @@ function DynamicTags({
return (
<div
className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`}
>
<HoverCard openDelay={300}>
@ -127,11 +127,11 @@ function DynamicTags({
htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
(
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
{typeof defaultValue === 'undefined' || !(defaultValue as string).length
? localize('com_endpoint_default_blank')
: `${localize('com_endpoint_default')}: ${defaultValue}`}
)
@ -171,9 +171,7 @@ function DynamicTags({
}
}}
onChange={(e) => setTagText(e.target.value)}
placeholder={
placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder
}
placeholder={placeholderCode ? localize(placeholder) ?? placeholder : placeholder}
className={cn(defaultTextProps, 'flex h-10 max-h-10 px-3 py-2')}
/>
</div>
@ -181,7 +179,7 @@ function DynamicTags({
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={descriptionCode ? localize(description) ?? description : description}
side={descriptionSide as ESide}
/>
)}

View file

@ -9,25 +9,25 @@ import OptionHover from './OptionHover';
import { ESide } from '~/common';
function DynamicTextarea({
label,
label = '',
settingKey,
defaultValue,
description,
description = '',
columnSpan,
setOption,
optionType,
placeholder,
placeholder = '',
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
placeholderCode,
showDefault = false,
labelCode = false,
descriptionCode = false,
placeholderCode = false,
conversation,
}: DynamicSettingProps) {
const localize = useLocalize();
const { preset } = useChatContext();
const [setInputValue, inputValue] = useDebouncedInput<string | null>({
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<string | null>({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
initialValue:
optionType !== OptionTypes.Custom
@ -43,13 +43,13 @@ function DynamicTextarea({
defaultValue: typeof defaultValue === 'undefined' ? '' : defaultValue,
conversation,
inputValue,
setInputValue,
setInputValue: setLocalValue,
});
return (
<div
className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`}
>
<HoverCard openDelay={300}>
@ -59,11 +59,11 @@ function DynamicTextarea({
htmlFor={`${settingKey}-dynamic-textarea`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
(
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
{typeof defaultValue === 'undefined' || !(defaultValue as string).length
? localize('com_endpoint_default_blank')
: `${localize('com_endpoint_default')}: ${defaultValue}`}
)
@ -76,7 +76,7 @@ function DynamicTextarea({
disabled={readonly}
value={inputValue ?? ''}
onChange={setInputValue}
placeholder={placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder}
placeholder={placeholderCode ? localize(placeholder) ?? placeholder : placeholder}
className={cn(
defaultTextProps,
// TODO: configurable max height
@ -86,7 +86,7 @@ function DynamicTextarea({
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={descriptionCode ? localize(description) ?? description : description}
side={ESide.Left}
/>
)}

View file

@ -1,203 +1,46 @@
import { ComponentTypes } from 'librechat-data-provider';
import type {
DynamicSettingProps,
SettingDefinition,
SettingsConfiguration,
} from 'librechat-data-provider';
import { useSetIndexOptions } from '~/hooks';
import React, { useMemo, useState, useCallback } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { getSettingsKeys, tPresetUpdateSchema } from 'librechat-data-provider';
import type { TPreset } from 'librechat-data-provider';
import { SaveAsPresetDialog } from '~/components/Endpoints';
import { useSetIndexOptions, useLocalize } from '~/hooks';
import { componentMapping } from './components';
import { useChatContext } from '~/Providers';
import {
DynamicDropdown,
DynamicCheckbox,
DynamicTextarea,
DynamicSlider,
DynamicSwitch,
DynamicInput,
DynamicTags,
} from './';
const settingsConfiguration: SettingsConfiguration = [
{
key: 'temperature',
label: 'com_endpoint_temperature',
labelCode: true,
description: 'com_endpoint_openai_temp',
descriptionCode: true,
type: 'number',
default: 1,
range: {
min: 0,
max: 2,
step: 0.01,
},
component: 'slider',
optionType: 'model',
// columnSpan: 2,
// includeInput: false,
},
{
key: 'top_p',
label: 'com_endpoint_top_p',
labelCode: true,
description: 'com_endpoint_openai_topp',
descriptionCode: true,
type: 'number',
default: 1,
range: {
min: 0,
max: 1,
step: 0.01,
},
component: 'slider',
optionType: 'model',
},
{
key: 'presence_penalty',
label: 'com_endpoint_presence_penalty',
labelCode: true,
description: 'com_endpoint_openai_pres',
descriptionCode: true,
type: 'number',
default: 0,
range: {
min: -2,
max: 2,
step: 0.01,
},
component: 'slider',
optionType: 'model',
},
{
key: 'frequency_penalty',
label: 'com_endpoint_frequency_penalty',
labelCode: true,
description: 'com_endpoint_openai_freq',
descriptionCode: true,
type: 'number',
default: 0,
range: {
min: -2,
max: 2,
step: 0.01,
},
component: 'slider',
optionType: 'model',
},
{
key: 'chatGptLabel',
label: 'com_endpoint_custom_name',
labelCode: true,
type: 'string',
default: '',
component: 'input',
placeholder: 'com_endpoint_openai_custom_name_placeholder',
placeholderCode: true,
optionType: 'conversation',
},
{
key: 'promptPrefix',
label: 'com_endpoint_prompt_prefix',
labelCode: true,
type: 'string',
default: '',
component: 'textarea',
placeholder: 'com_endpoint_openai_prompt_prefix_placeholder',
placeholderCode: true,
optionType: 'conversation',
// columnSpan: 2,
},
{
key: 'resendFiles',
label: 'com_endpoint_plug_resend_files',
labelCode: true,
description: 'com_endpoint_openai_resend_files',
descriptionCode: true,
type: 'boolean',
default: true,
component: 'switch',
optionType: 'conversation',
showDefault: false,
columnSpan: 2,
},
{
key: 'imageDetail',
label: 'com_endpoint_plug_image_detail',
labelCode: true,
description: 'com_endpoint_openai_detail',
descriptionCode: true,
type: 'enum',
default: 'auto',
options: ['low', 'auto', 'high'],
optionType: 'conversation',
component: 'slider',
showDefault: false,
columnSpan: 2,
},
{
key: 'stop',
label: 'com_endpoint_stop',
labelCode: true,
description: 'com_endpoint_openai_stop',
descriptionCode: true,
placeholder: 'com_endpoint_stop_placeholder',
placeholderCode: true,
type: 'array',
default: [],
component: 'tags',
optionType: 'conversation',
columnSpan: 4,
minTags: 1,
maxTags: 4,
},
];
const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettingProps>> = {
[ComponentTypes.Slider]: DynamicSlider,
[ComponentTypes.Dropdown]: DynamicDropdown,
[ComponentTypes.Switch]: DynamicSwitch,
[ComponentTypes.Textarea]: DynamicTextarea,
[ComponentTypes.Input]: DynamicInput,
[ComponentTypes.Checkbox]: DynamicCheckbox,
[ComponentTypes.Tags]: DynamicTags,
};
import { settings } from './settings';
export default function Parameters() {
const localize = useLocalize();
const { conversation } = useChatContext();
const { setOption } = useSetIndexOptions();
const temperature = settingsConfiguration.find(
(setting) => setting.key === 'temperature',
) as SettingDefinition;
const TempComponent = componentMapping[temperature.component];
const { key: temp, default: tempDefault, ...tempSettings } = temperature;
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [preset, setPreset] = useState<TPreset | null>(null);
const imageDetail = settingsConfiguration.find(
(setting) => setting.key === 'imageDetail',
) as SettingDefinition;
const DetailComponent = componentMapping[imageDetail.component];
const { key: detail, default: detailDefault, ...detailSettings } = imageDetail;
const { data: endpointsConfig } = useGetEndpointsQuery();
const resendFiles = settingsConfiguration.find(
(setting) => setting.key === 'resendFiles',
) as SettingDefinition;
const Switch = componentMapping[resendFiles.component];
const { key: switchKey, default: switchDefault, ...switchSettings } = resendFiles;
const bedrockRegions = useMemo(() => {
return endpointsConfig?.[conversation?.endpoint ?? '']?.availableRegions ?? [];
}, [endpointsConfig, conversation?.endpoint]);
const promptPrefix = settingsConfiguration.find(
(setting) => setting.key === 'promptPrefix',
) as SettingDefinition;
const Textarea = componentMapping[promptPrefix.component];
const { key: textareaKey, default: textareaDefault, ...textareaSettings } = promptPrefix;
const parameters = useMemo(() => {
const [combinedKey, endpointKey] = getSettingsKeys(
conversation?.endpoint ?? '',
conversation?.model ?? '',
);
return settings[combinedKey] ?? settings[endpointKey];
}, [conversation]);
const chatGptLabel = settingsConfiguration.find(
(setting) => setting.key === 'chatGptLabel',
) as SettingDefinition;
const Input = componentMapping[chatGptLabel.component];
const { key: inputKey, default: inputDefault, ...inputSettings } = chatGptLabel;
const openDialog = useCallback(() => {
const newPreset = tPresetUpdateSchema.parse({
...conversation,
}) as TPreset;
setPreset(newPreset);
setIsDialogOpen(true);
}, [conversation]);
const stop = settingsConfiguration.find((setting) => setting.key === 'stop') as SettingDefinition;
const Tags = componentMapping[stop.component];
const { key: stopKey, default: stopDefault, ...stopSettings } = stop;
if (!parameters) {
return null;
}
return (
<div className="h-auto max-w-full overflow-x-hidden p-3">
@ -205,49 +48,38 @@ export default function Parameters() {
{' '}
{/* This is the parent element containing all settings */}
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
<Input
settingKey={inputKey}
defaultValue={inputDefault}
{...inputSettings}
setOption={setOption}
conversation={conversation}
/>
<Textarea
settingKey={textareaKey}
defaultValue={textareaDefault}
{...textareaSettings}
setOption={setOption}
conversation={conversation}
/>
<TempComponent
settingKey={temp}
defaultValue={tempDefault}
{...tempSettings}
setOption={setOption}
conversation={conversation}
/>
<Switch
settingKey={switchKey}
defaultValue={switchDefault}
{...switchSettings}
setOption={setOption}
conversation={conversation}
/>
<DetailComponent
settingKey={detail}
defaultValue={detailDefault}
{...detailSettings}
setOption={setOption}
conversation={conversation}
/>
<Tags
settingKey={stopKey}
defaultValue={stopDefault}
{...stopSettings}
setOption={setOption}
conversation={conversation}
/>
{parameters.map((setting) => {
const Component = componentMapping[setting.component];
const { key, default: defaultValue, ...rest } = setting;
if (key === 'region' && bedrockRegions.length) {
rest.options = bedrockRegions;
}
return (
<Component
key={key}
settingKey={key}
defaultValue={defaultValue}
{...rest}
setOption={setOption}
conversation={conversation}
/>
);
})}
</div>
<div className="mt-6 flex justify-center">
<button
onClick={openDialog}
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"
type="button"
>
{localize('com_endpoint_save_as_preset')}
</button>
</div>
{preset && (
<SaveAsPresetDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} preset={preset} />
)}
</div>
);
}

View file

@ -0,0 +1,23 @@
import { ComponentTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import {
DynamicCombobox,
DynamicDropdown,
DynamicCheckbox,
DynamicTextarea,
DynamicSlider,
DynamicSwitch,
DynamicInput,
DynamicTags,
} from './';
export const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettingProps>> = {
[ComponentTypes.Slider]: DynamicSlider,
[ComponentTypes.Dropdown]: DynamicDropdown,
[ComponentTypes.Switch]: DynamicSwitch,
[ComponentTypes.Textarea]: DynamicTextarea,
[ComponentTypes.Input]: DynamicInput,
[ComponentTypes.Checkbox]: DynamicCheckbox,
[ComponentTypes.Tags]: DynamicTags,
[ComponentTypes.Combobox]: DynamicCombobox,
};

View file

@ -1,4 +1,5 @@
export { default as DynamicInputNumber } from './DynamicInputNumber';
export { default as DynamicCombobox } from './DynamicCombobox';
export { default as DynamicDropdown } from './DynamicDropdown';
export { default as DynamicCheckbox } from './DynamicCheckbox';
export { default as DynamicTextarea } from './DynamicTextarea';
@ -6,3 +7,4 @@ export { default as DynamicSlider } from './DynamicSlider';
export { default as DynamicSwitch } from './DynamicSwitch';
export { default as DynamicInput } from './DynamicInput';
export { default as DynamicTags } from './DynamicTags';
export { default as OptionHoverAlt } from './OptionHover';

View file

@ -0,0 +1,354 @@
import { EModelEndpoint, BedrockProviders } from 'librechat-data-provider';
import type { SettingsConfiguration, SettingDefinition } from 'librechat-data-provider';
// Base definitions
const baseDefinitions: Record<string, Partial<SettingDefinition>> = {
model: {
key: 'model',
label: 'com_ui_model',
labelCode: true,
type: 'string',
component: 'dropdown',
optionType: 'model',
selectPlaceholder: 'com_ui_select_model',
searchPlaceholder: 'com_ui_select_search_model',
searchPlaceholderCode: true,
selectPlaceholderCode: true,
columnSpan: 4,
},
temperature: {
key: 'temperature',
label: 'com_endpoint_temperature',
labelCode: true,
description: 'com_endpoint_openai_temp',
descriptionCode: true,
type: 'number',
component: 'slider',
optionType: 'model',
columnSpan: 4,
},
topP: {
key: 'topP',
label: 'com_endpoint_top_p',
labelCode: true,
description: 'com_endpoint_anthropic_topp',
descriptionCode: true,
type: 'number',
component: 'slider',
optionType: 'model',
columnSpan: 4,
},
};
const bedrock: Record<string, SettingDefinition> = {
region: {
key: 'region',
type: 'string',
label: 'com_ui_region',
labelCode: true,
component: 'combobox',
optionType: 'conversation',
selectPlaceholder: 'com_ui_select_region',
searchPlaceholder: 'com_ui_select_search_region',
searchPlaceholderCode: true,
selectPlaceholderCode: true,
columnSpan: 2,
},
};
const createDefinition = (
base: Partial<SettingDefinition>,
overrides: Partial<SettingDefinition>,
): SettingDefinition => {
return { ...base, ...overrides } as SettingDefinition;
};
const librechat: Record<string, SettingDefinition> = {
modelLabel: {
key: 'modelLabel',
label: 'com_endpoint_custom_name',
labelCode: true,
type: 'string',
default: '',
component: 'input',
placeholder: 'com_endpoint_openai_custom_name_placeholder',
placeholderCode: true,
optionType: 'conversation',
},
maxContextTokens: {
key: 'maxContextTokens',
label: 'com_endpoint_context_tokens',
labelCode: true,
type: 'number',
component: 'input',
placeholder: 'com_endpoint_context_info',
placeholderCode: true,
optionType: 'model',
columnSpan: 2,
},
resendFiles: {
key: 'resendFiles',
label: 'com_endpoint_plug_resend_files',
labelCode: true,
description: 'com_endpoint_openai_resend_files',
descriptionCode: true,
type: 'boolean',
default: true,
component: 'switch',
optionType: 'conversation',
showDefault: false,
columnSpan: 2,
},
promptPrefix: {
key: 'promptPrefix',
label: 'com_endpoint_prompt_prefix',
labelCode: true,
type: 'string',
default: '',
component: 'textarea',
placeholder: 'com_endpoint_openai_prompt_prefix_placeholder',
placeholderCode: true,
optionType: 'model',
},
};
const anthropic: Record<string, SettingDefinition> = {
system: {
key: 'system',
label: 'com_endpoint_prompt_prefix',
labelCode: true,
type: 'string',
default: '',
component: 'textarea',
placeholder: 'com_endpoint_openai_prompt_prefix_placeholder',
placeholderCode: true,
optionType: 'model',
},
maxTokens: {
key: 'maxTokens',
label: 'com_endpoint_max_output_tokens',
labelCode: true,
type: 'number',
component: 'input',
placeholder: 'com_endpoint_anthropic_maxoutputtokens',
placeholderCode: true,
optionType: 'model',
columnSpan: 2,
},
temperature: createDefinition(baseDefinitions.temperature, {
default: 1,
range: { min: 0, max: 1, step: 0.01 },
}),
topP: createDefinition(baseDefinitions.topP, {
default: 0.999,
range: { min: 0, max: 1, step: 0.01 },
}),
topK: {
key: 'topK',
label: 'com_endpoint_top_k',
labelCode: true,
description: 'com_endpoint_anthropic_topk',
descriptionCode: true,
type: 'number',
range: { min: 0, max: 500, step: 1 },
component: 'slider',
optionType: 'model',
columnSpan: 4,
},
stop: {
key: 'stop',
label: 'com_endpoint_stop',
labelCode: true,
description: 'com_endpoint_openai_stop',
descriptionCode: true,
placeholder: 'com_endpoint_stop_placeholder',
placeholderCode: true,
type: 'array',
default: [],
component: 'tags',
optionType: 'conversation',
minTags: 0,
maxTags: 4,
},
};
const mistral: Record<string, SettingDefinition> = {
temperature: createDefinition(baseDefinitions.temperature, {
default: 0.7,
range: { min: 0, max: 1, step: 0.01 },
}),
topP: createDefinition(baseDefinitions.topP, {
range: { min: 0, max: 1, step: 0.01 },
}),
};
const cohere: Record<string, SettingDefinition> = {
temperature: createDefinition(baseDefinitions.temperature, {
default: 0.3,
range: { min: 0, max: 1, step: 0.01 },
}),
topP: createDefinition(baseDefinitions.topP, {
default: 0.75,
range: { min: 0.01, max: 0.99, step: 0.01 },
}),
};
const meta: Record<string, SettingDefinition> = {
temperature: createDefinition(baseDefinitions.temperature, {
default: 0.5,
range: { min: 0, max: 1, step: 0.01 },
}),
topP: createDefinition(baseDefinitions.topP, {
default: 0.9,
range: { min: 0, max: 1, step: 0.01 },
}),
};
const bedrockAnthropic: SettingsConfiguration = [
librechat.modelLabel,
anthropic.system,
librechat.maxContextTokens,
anthropic.maxTokens,
anthropic.temperature,
anthropic.topP,
anthropic.topK,
anthropic.stop,
bedrock.region,
librechat.resendFiles,
];
const bedrockMistral: SettingsConfiguration = [
librechat.modelLabel,
librechat.promptPrefix,
librechat.maxContextTokens,
anthropic.maxTokens,
mistral.temperature,
mistral.topP,
bedrock.region,
librechat.resendFiles,
];
const bedrockCohere: SettingsConfiguration = [
librechat.modelLabel,
librechat.promptPrefix,
librechat.maxContextTokens,
anthropic.maxTokens,
cohere.temperature,
cohere.topP,
bedrock.region,
librechat.resendFiles,
];
const bedrockGeneral: SettingsConfiguration = [
librechat.modelLabel,
librechat.promptPrefix,
librechat.maxContextTokens,
meta.temperature,
meta.topP,
bedrock.region,
librechat.resendFiles,
];
const bedrockAnthropicCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
anthropic.system,
anthropic.stop,
];
const bedrockAnthropicCol2: SettingsConfiguration = [
librechat.maxContextTokens,
anthropic.maxTokens,
anthropic.temperature,
anthropic.topP,
anthropic.topK,
bedrock.region,
librechat.resendFiles,
];
const bedrockMistralCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
librechat.promptPrefix,
];
const bedrockMistralCol2: SettingsConfiguration = [
librechat.maxContextTokens,
anthropic.maxTokens,
mistral.temperature,
mistral.topP,
bedrock.region,
librechat.resendFiles,
];
const bedrockCohereCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
librechat.promptPrefix,
];
const bedrockCohereCol2: SettingsConfiguration = [
librechat.maxContextTokens,
anthropic.maxTokens,
cohere.temperature,
cohere.topP,
bedrock.region,
librechat.resendFiles,
];
const bedrockGeneralCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
librechat.promptPrefix,
];
const bedrockGeneralCol2: SettingsConfiguration = [
librechat.maxContextTokens,
meta.temperature,
meta.topP,
bedrock.region,
librechat.resendFiles,
];
export const settings: Record<string, SettingsConfiguration | undefined> = {
[`${EModelEndpoint.bedrock}-${BedrockProviders.Anthropic}`]: bedrockAnthropic,
[`${EModelEndpoint.bedrock}-${BedrockProviders.MistralAI}`]: bedrockMistral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Cohere}`]: bedrockCohere,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneral,
};
export const presetSettings: Record<
string,
| {
col1: SettingsConfiguration;
col2: SettingsConfiguration;
}
| undefined
> = {
[`${EModelEndpoint.bedrock}-${BedrockProviders.Anthropic}`]: {
col1: bedrockAnthropicCol1,
col2: bedrockAnthropicCol2,
},
[`${EModelEndpoint.bedrock}-${BedrockProviders.MistralAI}`]: {
col1: bedrockMistralCol1,
col2: bedrockMistralCol2,
},
[`${EModelEndpoint.bedrock}-${BedrockProviders.Cohere}`]: {
col1: bedrockCohereCol1,
col2: bedrockCohereCol2,
},
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: {
col1: bedrockGeneralCol1,
col2: bedrockGeneralCol2,
},
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: {
col1: bedrockGeneralCol1,
col2: bedrockGeneralCol2,
},
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: {
col1: bedrockGeneralCol1,
col2: bedrockGeneralCol2,
},
};

View file

@ -0,0 +1,23 @@
import { cn } from '~/utils';
export default function BedrockIcon({
size = 25,
className = '',
}: {
size?: number;
className?: string;
}) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={cn('fill-current text-black', className)}
>
<g fill="currentColor">
<path d="M12,18.1397014 L9.574,18.9487014 L8.628,18.3177014 L9.658,17.9737014 L9.342,17.0257014 L7.574,17.6147014 L7,17.2327014 L7,14.4997014 C7,14.3107014 6.893,14.1377014 6.724,14.0527014 L5,13.1907014 L5,10.8087014 L6.5,10.0587014 L8,10.8087014 L8,12.4997014 C8,12.6897014 8.107,12.8627014 8.276,12.9477014 L10.276,13.9477014 L10.724,13.0527014 L9,12.1907014 L9,10.8087014 L10.724,9.94770136 C10.893,9.86270136 11,9.68970136 11,9.49970136 L11,7.99970136 L10,7.99970136 L10,9.19070136 L8.5,9.94070136 L7,9.19070136 L7,6.76770136 L8,6.10070136 L8,7.99970136 L9,7.99970136 L9,5.43470136 L9.574,5.05170136 L12,5.86070136 L12,18.1397014 Z M17.5,16.9997014 C17.775,16.9997014 18,17.2237014 18,17.4997014 C18,17.7757014 17.775,17.9997014 17.5,17.9997014 C17.225,17.9997014 17,17.7757014 17,17.4997014 C17,17.2237014 17.225,16.9997014 17.5,16.9997014 L17.5,16.9997014 Z M16.5,5.99970136 C16.775,5.99970136 17,6.22370136 17,6.49970136 C17,6.77570136 16.775,6.99970136 16.5,6.99970136 C16.225,6.99970136 16,6.77570136 16,6.49970136 C16,6.22370136 16.225,5.99970136 16.5,5.99970136 L16.5,5.99970136 Z M18.5,11.9997014 C18.775,11.9997014 19,12.2237014 19,12.4997014 C19,12.7757014 18.775,12.9997014 18.5,12.9997014 C18.225,12.9997014 18,12.7757014 18,12.4997014 C18,12.2237014 18.225,11.9997014 18.5,11.9997014 L18.5,11.9997014 Z M17.092,12.9997014 C17.299,13.5807014 17.849,13.9997014 18.5,13.9997014 C19.327,13.9997014 20,13.3277014 20,12.4997014 C20,11.6727014 19.327,10.9997014 18.5,10.9997014 C17.849,10.9997014 17.299,11.4197014 17.092,11.9997014 L13,11.9997014 L13,9.99970136 L16.5,9.99970136 C16.776,9.99970136 17,9.77670136 17,9.49970136 L17,7.90770136 C17.581,7.70070136 18,7.15070136 18,6.49970136 C18,5.67270136 17.327,4.99970136 16.5,4.99970136 C15.673,4.99970136 15,5.67270136 15,6.49970136 C15,7.15070136 15.419,7.70070136 16,7.90770136 L16,8.99970136 L13,8.99970136 L13,5.49970136 C13,5.28470136 12.862,5.09370136 12.658,5.02570136 L9.658,4.02570136 C9.511,3.97670136 9.351,3.99870136 9.223,4.08370136 L6.223,6.08370136 C6.084,6.17670136 6,6.33270136 6,6.49970136 L6,9.19070136 L4.276,10.0527014 C4.107,10.1377014 4,10.3107014 4,10.4997014 L4,13.4997014 C4,13.6897014 4.107,13.8627014 4.276,13.9477014 L6,14.8087014 L6,17.4997014 C6,17.6667014 6.084,17.8237014 6.223,17.9157014 L9.223,19.9157014 C9.306,19.9717014 9.402,19.9997014 9.5,19.9997014 C9.553,19.9997014 9.606,19.9917014 9.658,19.9737014 L12.658,18.9737014 C12.862,18.9067014 13,18.7157014 13,18.4997014 L13,15.9997014 L15.293,15.9997014 L16.146,16.8537014 L16.159,16.8407014 C16.061,17.0407014 16,17.2627014 16,17.4997014 C16,18.3267014 16.673,18.9997014 17.5,18.9997014 C18.327,18.9997014 19,18.3267014 19,17.4997014 C19,16.6727014 18.327,15.9997014 17.5,15.9997014 C17.262,15.9997014 17.04,16.0607014 16.841,16.1597014 L16.854,16.1467014 L15.854,15.1467014 C15.76,15.0527014 15.633,14.9997014 15.5,14.9997014 L13,14.9997014 L13,12.9997014 L17.092,12.9997014 Z" />
</g>
</svg>
);
}

View file

@ -1,37 +1,34 @@
import React from 'react';
import { cn } from '~/utils';
const EditIcon = React.forwardRef<SVGSVGElement>(
(
props: {
className?: string;
size?: string;
},
ref,
) => {
const { className = 'icon-md', size = '1.2em' } = props;
return (
<svg
ref={ref}
fill="none"
strokeWidth="2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
height={size}
width={size}
className={cn(className)}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.2929 4.29291C15.0641 2.52167 17.9359 2.52167 19.7071 4.2929C21.4783 6.06414 21.4783 8.93588 19.7071 10.7071L18.7073 11.7069L11.1603 19.2539C10.7182 19.696 10.1489 19.989 9.53219 20.0918L4.1644 20.9864C3.84584 21.0395 3.52125 20.9355 3.29289 20.7071C3.06453 20.4788 2.96051 20.1542 3.0136 19.8356L3.90824 14.4678C4.01103 13.8511 4.30396 13.2818 4.7461 12.8397L13.2929 4.29291ZM13 7.41422L6.16031 14.2539C6.01293 14.4013 5.91529 14.591 5.88102 14.7966L5.21655 18.7835L9.20339 18.119C9.40898 18.0847 9.59872 17.9871 9.7461 17.8397L16.5858 11L13 7.41422ZM18 9.5858L14.4142 6.00001L14.7071 5.70712C15.6973 4.71693 17.3027 4.71693 18.2929 5.70712C19.2831 6.69731 19.2831 8.30272 18.2929 9.29291L18 9.5858Z"
fill="currentColor"
></path>
</svg>
);
},
);
type IconProps = {
className?: string;
size?: string;
};
const EditIcon = React.forwardRef<SVGSVGElement, IconProps>((props: IconProps, ref) => {
const { className = 'icon-md', size = '1.2em' } = props;
return (
<svg
ref={ref}
fill="none"
strokeWidth="2"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
height={size}
width={size}
className={cn(className)}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.2929 4.29291C15.0641 2.52167 17.9359 2.52167 19.7071 4.2929C21.4783 6.06414 21.4783 8.93588 19.7071 10.7071L18.7073 11.7069L11.1603 19.2539C10.7182 19.696 10.1489 19.989 9.53219 20.0918L4.1644 20.9864C3.84584 21.0395 3.52125 20.9355 3.29289 20.7071C3.06453 20.4788 2.96051 20.1542 3.0136 19.8356L3.90824 14.4678C4.01103 13.8511 4.30396 13.2818 4.7461 12.8397L13.2929 4.29291ZM13 7.41422L6.16031 14.2539C6.01293 14.4013 5.91529 14.591 5.88102 14.7966L5.21655 18.7835L9.20339 18.119C9.40898 18.0847 9.59872 17.9871 9.7461 17.8397L16.5858 11L13 7.41422ZM18 9.5858L14.4142 6.00001L14.7071 5.70712C15.6973 4.71693 17.3027 4.71693 18.2929 5.70712C19.2831 6.69731 19.2831 8.30272 18.2929 9.29291L18 9.5858Z"
fill="currentColor"
></path>
</svg>
);
});
export default EditIcon;

View file

@ -55,3 +55,4 @@ export { default as Sparkles } from './Sparkles';
export { default as SpeechIcon } from './SpeechIcon';
export { default as SaveIcon } from './SaveIcon';
export { default as CircleHelpIcon } from './CircleHelpIcon';
export { default as BedrockIcon } from './BedrockIcon';

View file

@ -19,29 +19,29 @@ export default function MultiSearch({
const localize = useLocalize();
const onChangeHandler: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => onChange(e.target.value),
[],
[onChange],
);
return (
<div
className={cn(
'group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-gradient-to-b from-white from-65% to-transparent px-2 px-3 py-2 text-black transition-colors duration-300 focus:bg-gradient-to-b focus:from-white focus:to-white/50 dark:from-gray-700 dark:to-transparent dark:text-white dark:focus:from-white/10 dark:focus:to-white/20',
'group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-gradient-to-b from-white from-65% to-transparent px-3 py-2 text-black transition-colors duration-300 focus:bg-gradient-to-b focus:from-white focus:to-white/50 dark:from-gray-700 dark:to-transparent dark:text-white dark:focus:from-white/10 dark:focus:to-white/20',
className,
)}
>
<Search className="h-4 w-4 text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300" />
<input
type="text"
value={value || ''}
value={value ?? ''}
onChange={onChangeHandler}
placeholder={placeholder || localize('com_ui_select_search_model')}
placeholder={placeholder ?? localize('com_ui_select_search_model')}
className="flex-1 rounded-md border-none bg-transparent px-2.5 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-gray-700/10 dark:focus:ring-gray-200/10"
/>
<div className="relative flex h-5 w-5 items-center justify-end text-gray-500">
<X
className={cn(
'text-gray-500 dark:text-gray-300',
value?.length ? 'cursor-pointer opacity-100' : 'opacity-0',
value?.length ?? 0 ? 'cursor-pointer opacity-100' : 'opacity-0',
)}
onClick={() => onChange('')}
/>
@ -104,20 +104,24 @@ export function useMultiSearch<OptionsType extends unknown[]>({
// Iterate said options
const filteredOptions = useMemo(() => {
if (!shouldShowSearch || !filterValue || !availableOptions.length) {
const currentFilter = filterValue ?? '';
if (!shouldShowSearch || !currentFilter || !availableOptions.length) {
// Don't render if available options aren't present, there's no filter active
return availableOptions;
}
// Filter through the values, using a simple text-based search
// nothing too fancy, but we can add a better search algo later if we need
const upperFilterValue = filterValue.toUpperCase();
const upperFilterValue = currentFilter.toUpperCase();
return availableOptions.filter((value) =>
getTextKeyHelper(value).includes(upperFilterValue),
) as OptionsType;
}, [availableOptions, getTextKeyHelper, filterValue, shouldShowSearch]);
const onSearchChange = useCallback((nextFilterValue) => setFilterValue(nextFilterValue), []);
const onSearchChange = useCallback(
(nextFilterValue: string) => setFilterValue(nextFilterValue),
[],
);
const searchRender = shouldShowSearch ? (
<MultiSearch

View file

@ -24,6 +24,7 @@ type DialogTemplateProps = {
leftButtons?: ReactNode;
selection?: SelectionProps;
className?: string;
overlayClassName?: string;
headerClassName?: string;
mainClassName?: string;
footerClassName?: string;
@ -40,11 +41,12 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
selection,
className,
leftButtons,
description,
description = '',
mainClassName,
headerClassName,
footerClassName,
showCloseButton,
overlayClassName,
showCancelButton = true,
} = props;
const { selectHandler, selectClasses, selectText } = selection || {};
@ -54,11 +56,12 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
'bg-gray-800 text-white transition-colors hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-gray-200';
return (
<OGDialogContent
overlayClassName={overlayClassName}
showCloseButton={showCloseButton}
ref={ref}
className={cn(
'bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300',
className || '',
className ?? '',
)}
onClick={(e) => e.stopPropagation()}
>
@ -66,21 +69,21 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
<OGDialogTitle>{title}</OGDialogTitle>
{description && <OGDialogDescription className="">{description}</OGDialogDescription>}
</OGDialogHeader>
<div className={cn('px-0', mainClassName)}>{main ? main : null}</div>
<div className={cn('px-0', mainClassName)}>{main != null ? main : null}</div>
<OGDialogFooter className={footerClassName}>
<div>{leftButtons ? leftButtons : null}</div>
<div>{leftButtons != null ? leftButtons : null}</div>
<div className="flex h-auto gap-3">
{showCancelButton && (
<OGDialogClose className="btn btn-neutral border-token-border-light relative rounded-lg text-sm ring-offset-2 dark:ring-offset-0 focus:ring-2 focus:ring-black">
{Cancel}
</OGDialogClose>
)}
{buttons ? buttons : null}
{buttons != null ? buttons : null}
{selection ? (
<OGDialogClose
onClick={selectHandler}
className={`${
selectClasses || defaultSelect
selectClasses ?? defaultSelect
} inline-flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm`}
>
{selectText}

View file

@ -29,14 +29,15 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
disableScroll?: boolean;
overlayClassName?: string;
};
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(({ className, showCloseButton = true, children, ...props }, ref) => (
>(({ className, overlayClassName, showCloseButton = true, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogOverlay className={overlayClassName} />
<DialogPrimitive.Content
ref={ref}
className={cn(

View file

@ -25,6 +25,7 @@ type SelectDropDownProps = {
showAbove?: boolean;
showLabel?: boolean;
iconSide?: 'left' | 'right';
optionIconSide?: 'left' | 'right';
renderOption?: () => React.ReactNode;
containerClassName?: string;
currentValueClass?: string;
@ -38,17 +39,30 @@ type SelectDropDownProps = {
showOptionIcon?: boolean;
};
function getOptionText(option: string | Option | OptionWithIcon): string {
if (typeof option === 'string') {
return option;
}
if ('label' in option) {
return option.label ?? '';
}
if ('value' in option) {
return (option.value ?? '') + '';
}
return '';
}
function SelectDropDown({
title: _title,
value,
disabled,
setValue,
tabIndex,
availableValues,
showAbove = false,
showLabel = true,
emptyTitle = false,
iconSide = 'right',
optionIconSide = 'left',
placeholder,
containerClassName,
optionsListClass,
@ -59,7 +73,7 @@ function SelectDropDown({
renderOption,
searchClassName,
searchPlaceholder,
showOptionIcon,
showOptionIcon = false,
}: SelectDropDownProps) {
const localize = useLocalize();
const transitionProps = { className: 'top-full mt-3' };
@ -71,7 +85,7 @@ function SelectDropDown({
if (emptyTitle) {
title = '';
} else if (!title) {
} else if (!(title ?? '')) {
title = localize('com_ui_model');
}
@ -81,12 +95,16 @@ function SelectDropDown({
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
availableOptions: availableValues,
placeholder: searchPlaceholder,
getTextKeyOverride: (option) => ((option as Option)?.label || '').toUpperCase(),
getTextKeyOverride: (option) => getOptionText(option).toUpperCase(),
className: searchClassName,
disabled,
});
const hasSearchRender = Boolean(searchRender);
const hasSearchRender = searchRender != null;
const options = hasSearchRender ? filteredValues : availableValues;
console.log({ hasSearchRender, options });
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;
return (
<div className={cn('flex items-center justify-center gap-2 ', containerClassName ?? '')}>
<div className={cn('relative w-full', subContainerClassName ?? '')}>
@ -121,20 +139,27 @@ function SelectDropDown({
{!showLabel && !emptyTitle && (
<span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span>
)}
{showOptionIcon && value && (value as OptionWithIcon)?.icon && (
{renderIcon && optionIconSide !== 'right' && (
<span className="icon-md flex items-center">
{(value as OptionWithIcon).icon}
</span>
)}
{value ? (
typeof value !== 'string' ? (
value?.label ?? ''
) : (
value
)
) : (
<span className="text-gray-500 dark:text-gray-400">{placeholder}</span>
{renderIcon && (
<span className="icon-md absolute right-0 mr-8 flex items-center">
{(value as OptionWithIcon).icon}
</span>
)}
{(() => {
if (!value) {
return <span className="text-text-secondary">{placeholder}</span>;
}
if (typeof value !== 'string') {
return value.label ?? '';
}
return value;
})()}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
@ -188,10 +213,10 @@ function SelectDropDown({
}
const currentLabel =
typeof option === 'string' ? option : option?.label ?? option?.value ?? '';
const currentValue = typeof option === 'string' ? option : option?.value ?? '';
typeof option === 'string' ? option : option.label ?? option.value ?? '';
const currentValue = typeof option === 'string' ? option : option.value ?? '';
const currentIcon =
typeof option === 'string' ? null : (option?.icon as React.ReactNode) ?? null;
typeof option === 'string' ? null : (option.icon as React.ReactNode) ?? null;
let activeValue: string | number | null | Option = value;
if (typeof activeValue !== 'string') {
activeValue = activeValue?.value ?? '';
@ -204,7 +229,7 @@ function SelectDropDown({
className={({ active }) =>
cn(
'group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden pl-3 pr-9 text-gray-800 hover:bg-gray-20 dark:text-white dark:hover:bg-gray-600',
active ? 'bg-surface-tertiary' : '',
active ? 'bg-surface-active text-text-primary' : '',
optionsClass ?? '',
)
}
@ -217,7 +242,16 @@ function SelectDropDown({
iconSide === 'left' ? 'ml-4' : '',
)}
>
{currentIcon && <span className="mr-1">{currentIcon}</span>}
{currentIcon != null && (
<span
className={cn(
'mr-1',
optionIconSide === 'right' ? 'absolute right-0 pr-2' : '',
)}
>
{currentIcon}
</span>
)}
{currentLabel}
</span>
{currentValue === activeValue && (

View file

@ -2,20 +2,26 @@ import * as React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { useDoubleClick } from '@zattoo/use-double-click';
import type { clickEvent } from '@zattoo/use-double-click';
import { cn } from '../../utils';
import { cn } from '~/utils';
interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
doubleClickHandler?: clickEvent;
trackClassName?: string;
}
const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
({ className, doubleClickHandler, ...props }, ref) => (
(
{ className, trackClassName = 'bg-gray-200 dark:bg-gray-850', doubleClickHandler, ...props },
ref,
) => (
<SliderPrimitive.Root
ref={ref}
className={cn('relative flex w-full touch-none select-none items-center', className ?? '')}
{...props}
>
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-850">
<SliderPrimitive.Track
className={cn('relative h-1 w-full grow overflow-hidden rounded-full', trackClassName)}
>
<SliderPrimitive.Range className="absolute h-full bg-gray-850 dark:bg-white" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb

View file

@ -241,6 +241,7 @@ export default function useChatFunctions({
},
},
];
setShowStopButton(true);
} else {
setShowStopButton(true);
}

View file

@ -41,7 +41,7 @@ function useDebouncedInput<T = unknown>({
const newValue: T =
typeof e !== 'object'
? e
: ((e as React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>)?.target
: ((e as React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>).target
.value as unknown as T);
setValue(newValue);
setDebouncedOption(newValue);

View file

@ -38,29 +38,31 @@ function useParameterEffects<T = unknown>({
/** Resets the local state if conversationId changed */
useEffect(() => {
if (!conversation?.conversationId) {
const conversationId = conversation?.conversationId ?? '';
if (!conversationId) {
return;
}
if (idRef.current === conversation?.conversationId) {
if (idRef.current === conversationId) {
return;
}
idRef.current = conversation?.conversationId;
idRef.current = conversationId;
setInputValue(defaultValue as T);
}, [setInputValue, conversation?.conversationId, defaultValue]);
/** Resets the local state if presetId changed */
useEffect(() => {
if (!preset?.presetId) {
const presetId = preset?.presetId ?? '';
if (!presetId) {
return;
}
if (presetIdRef.current === preset?.presetId) {
if (presetIdRef.current === presetId) {
return;
}
presetIdRef.current = preset?.presetId;
presetIdRef.current = presetId;
setInputValue(defaultValue as T);
}, [setInputValue, preset?.presetId, defaultValue]);
}

View file

@ -1,8 +1,13 @@
import { useRecoilValue } from 'recoil';
import { useCallback, useMemo } from 'react';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import { isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import { useChatContext, useAddedChatContext, useAssistantsMapContext } from '~/Providers';
import {
useChatContext,
useAddedChatContext,
useAssistantsMapContext,
useAgentsMapContext,
} from '~/Providers';
import useCopyToClipboard from './useCopyToClipboard';
import { useAuthContext } from '~/hooks/AuthContext';
import useLocalize from '~/hooks/useLocalize';
@ -35,6 +40,8 @@ export default function useMessageActions(props: TMessageActions) {
() => (isMultiMessage === true ? addedConvo : rootConvo),
[isMultiMessage, addedConvo, rootConvo],
);
const agentMap = useAgentsMapContext();
const assistantMap = useAssistantsMapContext();
const { text, content, messageId = null, isCreatedByUser } = message ?? {};
@ -56,6 +63,26 @@ export default function useMessageActions(props: TMessageActions) {
return assistantMap?.[endpointKey] ? assistantMap[endpointKey][modelKey] : undefined;
}, [conversation?.endpoint, message?.model, assistantMap]);
const agent = useMemo(() => {
if (!isAgentsEndpoint(conversation?.endpoint)) {
return undefined;
}
if (!agentMap) {
return undefined;
}
const modelKey = message?.model ?? '';
if (modelKey) {
return agentMap[modelKey];
}
const agentId = conversation?.agent_id ?? '';
if (agentId) {
return agentMap[agentId];
}
}, [agentMap, conversation?.agent_id, conversation?.endpoint, message?.model]);
const isSubmitting = useMemo(
() => (isMultiMessage === true ? isSubmittingAdditional : isSubmittingRoot),
[isMultiMessage, isSubmittingAdditional, isSubmittingRoot],
@ -74,17 +101,20 @@ export default function useMessageActions(props: TMessageActions) {
const messageLabel = useMemo(() => {
if (message?.isCreatedByUser === true) {
return UsernameDisplay ? (user?.name ?? '') || user?.username : localize('com_user_message');
} else if (agent) {
return agent.name ?? 'Assistant';
} else if (assistant) {
return assistant.name ?? 'Assistant';
} else {
return message?.sender;
}
}, [message, assistant, UsernameDisplay, user, localize]);
}, [message, agent, assistant, UsernameDisplay, user, localize]);
return {
ask,
edit,
index,
agent,
assistant,
enterEdit,
conversation,

View file

@ -103,7 +103,7 @@ export default function useMessageHelpers(props: TMessageProps) {
const modelKey = message?.model ?? '';
return agentMap ? agentMap[modelKey] : undefined;
}, [agentMap, conversation?.endpoint]);
}, [agentMap, conversation?.endpoint, message?.model]);
const regenerateMessage = () => {
if ((isSubmitting && isCreatedByUser === true) || !message) {

View file

@ -1,15 +1,11 @@
import { useMemo } from 'react';
import { MessageSquareQuote, ArrowRightToLine, Settings2, Bookmark } from 'lucide-react';
import {
ArrowRightToLine,
MessageSquareQuote,
Bookmark,
// Settings2,
} from 'lucide-react';
import {
EModelEndpoint,
isAssistantsEndpoint,
isAgentsEndpoint,
PermissionTypes,
paramEndpoints,
EModelEndpoint,
Permissions,
} from 'librechat-data-provider';
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
@ -18,7 +14,7 @@ import BookmarkPanel from '~/components/SidePanel/Bookmarks/BookmarkPanel';
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
import AgentPanelSwitch from '~/components/SidePanel/Agents/AgentPanelSwitch';
import PromptsAccordion from '~/components/Prompts/PromptsAccordion';
// import Parameters from '~/components/SidePanel/Parameters/Panel';
import Parameters from '~/components/SidePanel/Parameters/Panel';
import FilesPanel from '~/components/SidePanel/Files/Panel';
import { Blocks, AttachmentIcon } from '~/components/svg';
import { useHasAccess } from '~/hooks';
@ -54,7 +50,7 @@ export default function useSideNavLinks({
assistants &&
assistants.disableBuilder !== true &&
keyProvided &&
interfaceConfig.parameters
interfaceConfig.parameters === true
) {
links.push({
title: 'com_sidepanel_assistant_builder',
@ -70,7 +66,7 @@ export default function useSideNavLinks({
agents &&
// agents.disableBuilder !== true &&
keyProvided &&
interfaceConfig.parameters
interfaceConfig.parameters === true
) {
links.push({
title: 'com_sidepanel_agent_builder',
@ -91,6 +87,16 @@ export default function useSideNavLinks({
});
}
if (interfaceConfig.parameters === true && paramEndpoints.has(endpoint ?? '') && keyProvided) {
links.push({
title: 'com_sidepanel_parameters',
label: '',
icon: Settings2,
id: 'parameters',
Component: Parameters,
});
}
links.push({
title: 'com_sidepanel_attach_files',
label: '',
@ -119,13 +125,14 @@ export default function useSideNavLinks({
return links;
}, [
assistants,
agents,
keyProvided,
hidePanel,
endpoint,
interfaceConfig.parameters,
keyProvided,
assistants,
endpoint,
agents,
hasAccessToPrompts,
hasAccessToBookmarks,
hidePanel,
]);
return Links;

View file

@ -117,8 +117,8 @@ export default function useSSE(
};
createdHandler(data, { ...submission, userMessage } as EventSubmission);
} else if (data.event) {
stepHandler(data);
} else if (data.event != null) {
stepHandler(data, { ...submission, userMessage } as EventSubmission);
} else if (data.sync != null) {
const runId = v4();
setActiveRunId(runId);

View file

@ -1,6 +1,12 @@
import { useCallback, useRef } from 'react';
import { StepTypes, ContentTypes, ToolCallTypes } from 'librechat-data-provider';
import type { Agents, PartMetadata, TMessage } from 'librechat-data-provider';
import type {
Agents,
PartMetadata,
TMessage,
TMessageContentParts,
EventSubmission,
} from 'librechat-data-provider';
import { getNonEmptyValue } from 'librechat-data-provider';
type TUseStepHandler = {
@ -13,8 +19,17 @@ type TStepEvent = {
data: Agents.MessageDeltaEvent | Agents.RunStep | Agents.ToolEndEvent;
};
type MessageDeltaUpdate = { type: ContentTypes.TEXT; text: string; tool_call_ids?: string[] };
type AllContentTypes =
| ContentTypes.TEXT
| ContentTypes.TOOL_CALL
| ContentTypes.IMAGE_FILE
| ContentTypes.IMAGE_URL
| ContentTypes.ERROR;
export default function useStepHandler({ setMessages, getMessages }: TUseStepHandler) {
const toolCallIdMap = useRef(new Map<string, string>());
const toolCallIdMap = useRef(new Map<string, string | undefined>());
const messageMap = useRef(new Map<string, TMessage>());
const stepMap = useRef(new Map<string, Agents.RunStep>());
@ -24,41 +39,53 @@ export default function useStepHandler({ setMessages, getMessages }: TUseStepHan
contentPart: Agents.MessageContentComplex,
finalUpdate = false,
) => {
if (!contentPart.type) {
const contentType = contentPart.type ?? '';
if (!contentType) {
console.warn('No content type found in content part');
return message;
}
const updatedContent = [...(message.content || [])];
const updatedContent = [...(message.content || [])] as Array<
Partial<TMessageContentParts> | undefined
>;
if (!updatedContent[index]) {
updatedContent[index] = { type: contentPart.type };
updatedContent[index] = { type: contentPart.type as AllContentTypes };
}
if (
contentPart.type.startsWith(ContentTypes.TEXT) &&
contentType.startsWith(ContentTypes.TEXT) &&
ContentTypes.TEXT in contentPart &&
typeof contentPart.text === 'string'
) {
const currentContent = updatedContent[index] as { type: ContentTypes.TEXT; text: string };
updatedContent[index] = {
const currentContent = updatedContent[index] as MessageDeltaUpdate;
const update: MessageDeltaUpdate = {
type: ContentTypes.TEXT,
text: (currentContent.text || '') + contentPart.text,
};
} else if (contentPart.type === 'image_url' && 'image_url' in contentPart) {
const currentContent = updatedContent[index] as { type: 'image_url'; image_url: string };
if (contentPart.tool_call_ids != null) {
update.tool_call_ids = contentPart.tool_call_ids;
}
updatedContent[index] = update;
} else if (contentType === ContentTypes.IMAGE_URL && 'image_url' in contentPart) {
const currentContent = updatedContent[index] as {
type: ContentTypes.IMAGE_URL;
image_url: string;
};
updatedContent[index] = {
...currentContent,
};
} else if (contentPart.type === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) {
const existingContent = updatedContent[index] as Agents.ToolCallContent;
} else if (contentType === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) {
const existingContent = updatedContent[index] as Agents.ToolCallContent | undefined;
const existingToolCall = existingContent?.tool_call;
const toolCallArgs = (contentPart.tool_call.args as unknown as string | undefined) ?? '';
const args = finalUpdate
? contentPart.tool_call.args
: (existingContent?.tool_call?.args || '') + (contentPart.tool_call.args || '');
: (existingToolCall?.args ?? '') + toolCallArgs;
const id = getNonEmptyValue([contentPart.tool_call.id, existingContent?.tool_call?.id]) ?? '';
const name =
getNonEmptyValue([contentPart.tool_call.name, existingContent?.tool_call?.name]) ?? '';
const id = getNonEmptyValue([contentPart.tool_call.id, existingToolCall?.id]) ?? '';
const name = getNonEmptyValue([contentPart.tool_call.name, existingToolCall?.name]) ?? '';
const newToolCall: Agents.ToolCall & PartMetadata = {
id,
@ -78,16 +105,17 @@ export default function useStepHandler({ setMessages, getMessages }: TUseStepHan
};
}
return { ...message, content: updatedContent };
return { ...message, content: updatedContent as TMessageContentParts[] };
};
return useCallback(
({ event, data }: TStepEvent) => {
({ event, data }: TStepEvent, submission: EventSubmission) => {
const messages = getMessages() || [];
const { userMessage } = submission;
if (event === 'on_run_step') {
const runStep = data as Agents.RunStep;
const responseMessageId = runStep.runId;
const responseMessageId = runStep.runId ?? '';
if (!responseMessageId) {
console.warn('No message id found in run step event');
return;
@ -98,12 +126,11 @@ export default function useStepHandler({ setMessages, getMessages }: TUseStepHan
if (!response) {
const responseMessage = messages[messages.length - 1] as TMessage;
const userMessage = messages[messages.length - 2];
response = {
...responseMessage,
parentMessageId: userMessage?.messageId,
conversationId: userMessage?.conversationId,
parentMessageId: userMessage.messageId,
conversationId: userMessage.conversationId,
messageId: responseMessageId,
content: [],
};
@ -115,20 +142,23 @@ export default function useStepHandler({ setMessages, getMessages }: TUseStepHan
// Store tool call IDs if present
if (runStep.stepDetails.type === StepTypes.TOOL_CALLS) {
runStep.stepDetails.tool_calls.forEach((toolCall) => {
if ('id' in toolCall && toolCall.id) {
toolCallIdMap.current.set(runStep.id, toolCall.id);
const toolCallId = toolCall.id ?? '';
if ('id' in toolCall && toolCallId) {
toolCallIdMap.current.set(runStep.id, toolCallId);
}
});
}
} else if (event === 'on_message_delta') {
const messageDelta = data as Agents.MessageDeltaEvent;
const runStep = stepMap.current.get(messageDelta.id);
if (!runStep || !runStep.runId) {
const responseMessageId = runStep?.runId ?? '';
if (!runStep || !responseMessageId) {
console.warn('No run step or runId found for message delta event');
return;
}
const response = messageMap.current.get(runStep.runId);
const response = messageMap.current.get(responseMessageId);
if (response && messageDelta.delta.content) {
const contentPart = Array.isArray(messageDelta.delta.content)
? messageDelta.delta.content[0]
@ -136,19 +166,21 @@ export default function useStepHandler({ setMessages, getMessages }: TUseStepHan
const updatedResponse = updateContent(response, runStep.index, contentPart);
messageMap.current.set(runStep.runId, updatedResponse);
messageMap.current.set(responseMessageId, updatedResponse);
const currentMessages = getMessages() || [];
setMessages([...currentMessages.slice(0, -1), updatedResponse]);
}
} else if (event === 'on_run_step_delta') {
const runStepDelta = data as Agents.RunStepDeltaEvent;
const runStep = stepMap.current.get(runStepDelta.id);
if (!runStep || !runStep.runId) {
const responseMessageId = runStep?.runId ?? '';
if (!runStep || !responseMessageId) {
console.warn('No run step or runId found for run step delta event');
return;
}
const response = messageMap.current.get(runStep.runId);
const response = messageMap.current.get(responseMessageId);
if (
response &&
runStepDelta.delta.type === StepTypes.TOOL_CALLS &&
@ -157,13 +189,13 @@ export default function useStepHandler({ setMessages, getMessages }: TUseStepHan
let updatedResponse = { ...response };
runStepDelta.delta.tool_calls.forEach((toolCallDelta) => {
const toolCallId = toolCallIdMap.current.get(runStepDelta.id) || '';
const toolCallId = toolCallIdMap.current.get(runStepDelta.id) ?? '';
const contentPart: Agents.MessageContentComplex = {
type: ContentTypes.TOOL_CALL,
tool_call: {
name: toolCallDelta.name ?? '',
args: toolCallDelta.args || '',
args: toolCallDelta.args ?? '',
id: toolCallId,
},
};
@ -171,7 +203,7 @@ export default function useStepHandler({ setMessages, getMessages }: TUseStepHan
updatedResponse = updateContent(updatedResponse, runStep.index, contentPart);
});
messageMap.current.set(runStep.runId, updatedResponse);
messageMap.current.set(responseMessageId, updatedResponse);
const updatedMessages = messages.map((msg) =>
msg.messageId === runStep.runId ? updatedResponse : msg,
);
@ -184,12 +216,14 @@ export default function useStepHandler({ setMessages, getMessages }: TUseStepHan
const { id: stepId } = result;
const runStep = stepMap.current.get(stepId);
if (!runStep || !runStep.runId) {
const responseMessageId = runStep?.runId ?? '';
if (!runStep || !responseMessageId) {
console.warn('No run step or runId found for completed tool call event');
return;
}
const response = messageMap.current.get(runStep.runId);
const response = messageMap.current.get(responseMessageId);
if (response) {
let updatedResponse = { ...response };
@ -200,7 +234,7 @@ export default function useStepHandler({ setMessages, getMessages }: TUseStepHan
updatedResponse = updateContent(updatedResponse, runStep.index, contentPart, true);
messageMap.current.set(runStep.runId, updatedResponse);
messageMap.current.set(responseMessageId, updatedResponse);
const updatedMessages = messages.map((msg) =>
msg.messageId === runStep.runId ? updatedResponse : msg,
);

View file

@ -3,7 +3,7 @@ import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
type TUseGenerations = {
endpoint?: string;
message: TMessage;
message?: TMessage;
isSubmitting: boolean;
isEditing?: boolean;
latestMessage: TMessage | null;
@ -16,15 +16,25 @@ export default function useGenerationsByLatest({
isEditing = false,
latestMessage,
}: TUseGenerations) {
const { error, messageId, searchResult, finish_reason, isCreatedByUser } = message ?? {};
const isEditableEndpoint = !![
EModelEndpoint.openAI,
EModelEndpoint.custom,
EModelEndpoint.google,
EModelEndpoint.anthropic,
EModelEndpoint.gptPlugins,
EModelEndpoint.azureOpenAI,
].find((e) => e === endpoint);
const {
messageId,
searchResult = false,
error = false,
finish_reason = '',
isCreatedByUser = false,
} = message ?? {};
const isEditableEndpoint = Boolean(
[
EModelEndpoint.openAI,
EModelEndpoint.custom,
EModelEndpoint.google,
EModelEndpoint.agents,
EModelEndpoint.bedrock,
EModelEndpoint.anthropic,
EModelEndpoint.gptPlugins,
EModelEndpoint.azureOpenAI,
].find((e) => e === endpoint),
);
const continueSupported =
latestMessage?.messageId === messageId &&
@ -34,18 +44,20 @@ export default function useGenerationsByLatest({
!searchResult &&
isEditableEndpoint;
const branchingSupported =
// 5/21/23: Bing is allowing editing and Message regenerating
!![
const branchingSupported = Boolean(
[
EModelEndpoint.azureOpenAI,
EModelEndpoint.openAI,
EModelEndpoint.custom,
EModelEndpoint.agents,
EModelEndpoint.bedrock,
EModelEndpoint.chatGPTBrowser,
EModelEndpoint.google,
EModelEndpoint.bingAI,
EModelEndpoint.gptPlugins,
EModelEndpoint.anthropic,
].find((e) => e === endpoint);
].find((e) => e === endpoint),
);
const regenerateEnabled =
!isCreatedByUser && !searchResult && !isEditing && !isSubmitting && branchingSupported;

View file

@ -5,7 +5,12 @@ import {
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import { useNavigate } from 'react-router-dom';
import { FileSources, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider';
import {
FileSources,
LocalStorageKeys,
isAssistantsEndpoint,
paramEndpoints,
} from 'librechat-data-provider';
import { useRecoilState, useRecoilValue, useSetRecoilState, useRecoilCallback } from 'recoil';
import type {
TPreset,
@ -67,7 +72,7 @@ const useNewConvo = (index = 0) => {
) => {
const modelsConfig = modelsData ?? modelsQuery.data;
const { endpoint = null } = conversation;
const buildDefaultConversation = endpoint === null || buildDefault;
const buildDefaultConversation = (endpoint === null || buildDefault) ?? false;
const activePreset =
// use default preset only when it's defined,
// preset is not provided,
@ -95,27 +100,24 @@ const useNewConvo = (index = 0) => {
const isAssistantEndpoint = isAssistantsEndpoint(defaultEndpoint);
const assistants: AssistantListItem[] = assistantsListMap[defaultEndpoint] ?? [];
const currentAssistantId = conversation.assistant_id ?? '';
const currentAssistant = assistantsListMap[defaultEndpoint]?.[currentAssistantId] as
| AssistantListItem
| undefined;
if (
conversation.assistant_id &&
!assistantsListMap[defaultEndpoint]?.[conversation.assistant_id]
) {
if (currentAssistantId && !currentAssistant) {
conversation.assistant_id = undefined;
}
if (!conversation.assistant_id && isAssistantEndpoint) {
if (!currentAssistantId && isAssistantEndpoint) {
conversation.assistant_id =
localStorage.getItem(
`${LocalStorageKeys.ASST_ID_PREFIX}${index}${defaultEndpoint}`,
) ?? assistants[0]?.id;
}
if (
conversation.assistant_id &&
isAssistantEndpoint &&
conversation.conversationId === 'new'
) {
const assistant = assistants.find((asst) => asst.id === conversation.assistant_id);
if (currentAssistantId && isAssistantEndpoint && conversation.conversationId === 'new') {
const assistant = assistants.find((asst) => asst.id === currentAssistantId);
conversation.model = assistant?.model;
updateLastSelectedModel({
endpoint: defaultEndpoint,
@ -123,7 +125,7 @@ const useNewConvo = (index = 0) => {
});
}
if (conversation.assistant_id && !isAssistantEndpoint) {
if (currentAssistantId && !isAssistantEndpoint) {
conversation.assistant_id = undefined;
}
@ -136,17 +138,17 @@ const useNewConvo = (index = 0) => {
});
}
if (!keepAddedConvos) {
if (!(keepAddedConvos ?? false)) {
clearAllConversations(true);
}
setConversation(conversation);
setSubmission({} as TSubmission);
if (!keepLatestMessage) {
if (!(keepLatestMessage ?? false)) {
clearAllLatestMessages();
}
if (conversation.conversationId === 'new' && !modelsData) {
const appTitle = localStorage.getItem(LocalStorageKeys.APP_TITLE);
const appTitle = localStorage.getItem(LocalStorageKeys.APP_TITLE) ?? '';
if (appTitle) {
document.title = appTitle;
}
@ -166,7 +168,7 @@ const useNewConvo = (index = 0) => {
const newConversation = useCallback(
({
template = {},
template: _template = {},
preset: _preset,
modelsData,
buildDefault = true,
@ -182,6 +184,16 @@ const useNewConvo = (index = 0) => {
} = {}) => {
pauseGlobalAudio();
const templateConvoId = _template.conversationId ?? '';
const isParamEndpoint =
paramEndpoints.has(_template.endpoint ?? '') ||
paramEndpoints.has(_preset?.endpoint ?? '') ||
paramEndpoints.has(_template.endpointType ?? '');
const template =
isParamEndpoint && templateConvoId && templateConvoId === 'new'
? { endpoint: _template.endpoint }
: _template;
const conversation = {
conversationId: 'new',
title: 'New Chat',
@ -193,7 +205,12 @@ const useNewConvo = (index = 0) => {
let preset = _preset;
const defaultModelSpec = getDefaultModelSpec(startupConfig?.modelSpecs?.list);
if (!preset && startupConfig && startupConfig.modelSpecs?.prioritize && defaultModelSpec) {
if (
!preset &&
startupConfig &&
startupConfig.modelSpecs?.prioritize === true &&
defaultModelSpec
) {
preset = {
...defaultModelSpec.preset,
iconURL: getModelSpecIconURL(defaultModelSpec),
@ -203,10 +220,17 @@ const useNewConvo = (index = 0) => {
if (conversation.conversationId === 'new' && !modelsData) {
const filesToDelete = Array.from(files.values())
.filter((file) => file.filepath && file.source && !file.embedded && file.temp_file_id)
.filter(
(file) =>
file.filepath != null &&
file.filepath !== '' &&
file.source &&
!(file.embedded ?? false) &&
file.temp_file_id,
)
.map((file) => ({
file_id: file.file_id,
embedded: !!file.embedded,
embedded: !!(file.embedded ?? false),
filepath: file.filepath as string,
source: file.source as FileSources, // Ensure that the source is of type FileSources
}));

View file

@ -140,12 +140,15 @@ export default {
com_ui_endpoint: 'Endpoint',
com_ui_provider: 'Provider',
com_ui_model: 'Model',
com_ui_region: 'Region',
com_ui_model_parameters: 'Model Parameters',
com_ui_model_save_success: 'Model parameters saved successfully',
com_ui_select_model: 'Select a model',
com_ui_select_region: 'Select a region',
com_ui_select_provider: 'Select a provider',
com_ui_select_provider_first: 'Select a provider first',
com_ui_select_search_model: 'Search model by name',
com_ui_select_search_region: 'Search region by name',
com_ui_select_search_plugin: 'Search plugin by name',
com_ui_use_prompt: 'Use prompt',
com_ui_prev: 'Prev',

View file

@ -74,6 +74,7 @@ const conversationByIndex = atomFamily<TConversation | null, string | number>({
({ onSet, node }) => {
onSet(async (newValue) => {
const index = Number(node.key.split('__')[1]);
logger.log('conversation', 'Setting conversation:', { index, newValue });
if (newValue?.assistant_id) {
localStorage.setItem(
`${LocalStorageKeys.ASST_ID_PREFIX}${index}${newValue.endpoint}`,

View file

@ -9,19 +9,17 @@ import getLocalStorageItems from './getLocalStorageItems';
const buildDefaultConvo = ({
conversation,
endpoint,
endpoint = null,
models,
lastConversationSetup,
}: {
conversation: TConversation;
endpoint: EModelEndpoint;
endpoint: EModelEndpoint | null;
models: string[];
// TODO: fix this type as we should allow undefined
lastConversationSetup: TConversation;
}) => {
const { lastSelectedModel, lastSelectedTools, lastBingSettings } = getLocalStorageItems();
const { jailbreak, toneStyle } = lastBingSettings;
const endpointType = lastConversationSetup?.endpointType ?? conversation?.endpointType;
lastConversationSetup: TConversation | null;
}): TConversation => {
const { lastSelectedModel, lastSelectedTools } = getLocalStorageItems();
const endpointType = lastConversationSetup?.endpointType ?? conversation.endpointType;
if (!endpoint) {
return {
@ -32,10 +30,10 @@ const buildDefaultConvo = ({
}
const availableModels = models;
const model = lastConversationSetup?.model ?? lastSelectedModel?.[endpoint];
const secondaryModel =
const model = lastConversationSetup?.model ?? lastSelectedModel?.[endpoint] ?? '';
const secondaryModel: string | null =
endpoint === EModelEndpoint.gptPlugins
? lastConversationSetup?.agentOptions?.model ?? lastSelectedModel?.secondaryModel
? lastConversationSetup?.agentOptions?.model ?? lastSelectedModel?.secondaryModel ?? null
: null;
let possibleModels: string[], secondaryModels: string[];
@ -46,7 +44,7 @@ const buildDefaultConvo = ({
possibleModels = [...availableModels];
}
if (secondaryModel && availableModels.includes(secondaryModel)) {
if (secondaryModel != null && secondaryModel !== '' && availableModels.includes(secondaryModel)) {
secondaryModels = [secondaryModel, ...availableModels];
} else {
secondaryModels = [...availableModels];
@ -70,18 +68,20 @@ const buildDefaultConvo = ({
};
// Ensures assistant_id is always defined
if (isAssistantsEndpoint(endpoint) && !defaultConvo.assistant_id && convo.assistant_id) {
defaultConvo.assistant_id = convo.assistant_id;
const assistantId = convo?.assistant_id ?? '';
const defaultAssistantId = lastConversationSetup?.assistant_id ?? '';
if (isAssistantsEndpoint(endpoint) && !defaultAssistantId && assistantId) {
defaultConvo.assistant_id = assistantId;
}
// Ensures agent_id is always defined
if (isAgentsEndpoint(endpoint) && !defaultConvo.agent_id && convo.agent_id) {
defaultConvo.agent_id = convo.agent_id;
const agentId = convo?.agent_id ?? '';
const defaultAgentId = lastConversationSetup?.agent_id ?? '';
if (isAgentsEndpoint(endpoint) && !defaultAgentId && agentId) {
defaultConvo.agent_id = agentId;
}
defaultConvo.tools = lastConversationSetup?.tools ?? lastSelectedTools ?? defaultConvo.tools;
defaultConvo.jailbreak = jailbreak ?? defaultConvo.jailbreak;
defaultConvo.toneStyle = toneStyle ?? defaultConvo.toneStyle;
return defaultConvo;
};

View file

@ -1,3 +1,4 @@
import { EarthIcon } from 'lucide-react';
import { alternateName } from 'librechat-data-provider';
import type { Agent, TFile } from 'librechat-data-provider';
import type { DropdownValueSetter, TAgentOption } from '~/common';
@ -39,14 +40,22 @@ export const createProviderOption = (provider: string) => ({
type FileTuple = [string, Partial<TFile>];
type FileList = Array<FileTuple>;
export const processAgentOption = (
_agent?: Agent,
fileMap?: Record<string, TFile>,
): TAgentOption => {
export const processAgentOption = ({
agent: _agent,
fileMap,
instanceProjectId,
}: {
agent?: Agent;
fileMap?: Record<string, TFile | undefined>;
instanceProjectId?: string;
}): TAgentOption => {
const isGlobal =
(instanceProjectId != null && _agent?.projectIds?.includes(instanceProjectId)) ?? false;
const agent: TAgentOption = {
...(_agent ?? ({} as Agent)),
label: _agent?.name ?? '',
value: _agent?.id ?? '',
icon: isGlobal ? <EarthIcon className="icon-md text-green-400" /> : null,
// files: _agent?.file_ids ? ([] as FileList) : undefined,
// code_files: _agent?.tool_resources?.code_interpreter?.file_ids
// ? ([] as FileList)
@ -58,7 +67,7 @@ export const processAgentOption = (
}
const handleFile = (file_id: string, list?: FileList) => {
const file = fileMap?.[file_id];
const file = fileMap[file_id];
if (file) {
list?.push([file_id, file]);
} else {
@ -82,7 +91,7 @@ export const processAgentOption = (
}
if (agent.code_files && _agent?.tool_resources?.code_interpreter?.file_ids) {
_agent.tool_resources?.code_interpreter?.file_ids?.forEach((file_id) =>
_agent.tool_resources.code_interpreter.file_ids.forEach((file_id) =>
handleFile(file_id, agent.code_files),
);
}

View file

@ -1,24 +1,25 @@
import { LocalStorageKeys } from 'librechat-data-provider';
import { LocalStorageKeys, TConversation } from 'librechat-data-provider';
export default function getLocalStorageItems() {
const items = {
lastSelectedModel: localStorage.getItem(LocalStorageKeys.LAST_MODEL),
lastSelectedTools: localStorage.getItem(LocalStorageKeys.LAST_TOOLS),
lastBingSettings: localStorage.getItem(LocalStorageKeys.LAST_BING),
lastConversationSetup: localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP + '_0'),
lastSelectedModel: localStorage.getItem(LocalStorageKeys.LAST_MODEL) ?? '',
lastSelectedTools: localStorage.getItem(LocalStorageKeys.LAST_TOOLS) ?? '',
lastConversationSetup: localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP + '_0') ?? '',
};
const lastSelectedModel = items.lastSelectedModel ? JSON.parse(items.lastSelectedModel) : {};
const lastSelectedTools = items.lastSelectedTools ? JSON.parse(items.lastSelectedTools) : [];
const lastBingSettings = items.lastBingSettings ? JSON.parse(items.lastBingSettings) : {};
const lastSelectedModel = items.lastSelectedModel
? (JSON.parse(items.lastSelectedModel) as Record<string, string | undefined> | null)
: {};
const lastSelectedTools = items.lastSelectedTools
? (JSON.parse(items.lastSelectedTools) as string[] | null)
: [];
const lastConversationSetup = items.lastConversationSetup
? JSON.parse(items.lastConversationSetup)
? (JSON.parse(items.lastConversationSetup) as Partial<TConversation> | null)
: {};
return {
lastSelectedModel,
lastSelectedTools,
lastBingSettings,
lastConversationSetup,
};
}