🤖 feat: OpenAI Assistants v2 (initial support) (#2781)

* 🤖 Assistants V2 Support: Part 1

- Separated Azure Assistants to its own endpoint
- File Search / Vector Store integration is incomplete, but can toggle and use storage from playground
- Code Interpreter resource files can be added but not deleted
- GPT-4o is supported
- Many improvements to the Assistants Endpoint overall

data-provider v2 changes

copy existing route as v1

chore: rename new endpoint to reduce comparison operations and add new azure filesource

api: add azureAssistants part 1

force use of version for assistants/assistantsAzure

chore: switch name back to azureAssistants

refactor type version: string | number

Ensure assistants endpoints have version set

fix: isArchived type issue in ConversationListParams

refactor: update assistants mutations/queries with endpoint/version definitions, update Assistants Map structure

chore:  FilePreview component ExtendedFile type assertion

feat: isAssistantsEndpoint helper

chore: remove unused useGenerations

chore(buildTree): type issue

chore(Advanced): type issue (unused component, maybe in future)

first pass for multi-assistant endpoint rewrite

fix(listAssistants): pass params correctly

feat: list separate assistants by endpoint

fix(useTextarea): access assistantMap correctly

fix: assistant endpoint switching, resetting ID

fix: broken during rewrite, selecting assistant mention

fix: set/invalidate assistants endpoint query data correctly

feat: Fix issue with assistant ID not being reset correctly

getOpenAIClient helper function

feat: add toast for assistant deletion

fix: assistants delete right after create issue for azure

fix: assistant patching

refactor: actions to use getOpenAIClient

refactor: consolidate logic into helpers file

fix: issue where conversation data was not initially available

v1 chat support

refactor(spendTokens): only early return if completionTokens isNaN

fix(OpenAIClient): ensure spendTokens has all necessary params

refactor: route/controller logic

fix(assistants/initializeClient): use defaultHeaders field

fix: sanitize default operation id

chore: bump openai package

first pass v2 action service

feat: retroactive domain parsing for actions added via v1

feat: delete db records of actions/assistants on openai assistant deletion

chore: remove vision tools from v2 assistants

feat: v2 upload and delete assistant vision images

WIP first pass, thread attachments

fix: show assistant vision files (save local/firebase copy)

v2 image continue

fix: annotations

fix: refine annotations

show analyze as error if is no longer submitting before progress reaches 1 and show file_search as retrieval tool

fix: abort run, undefined endpoint issue

refactor: consolidate capabilities logic and anticipate versioning

frontend version 2 changes

fix: query selection and filter

add endpoint to unknown filepath

add file ids to resource, deleting in progress

enable/disable file search

remove version log

* 🤖 Assistants V2 Support: Part 2

🎹 fix: Autocompletion Chrome Bug on Action API Key Input

chore: remove `useOriginNavigate`

chore: set correct OpenAI Storage Source

fix: azure file deletions, instantiate clients by source for deletion

update code interpret files info

feat: deleteResourceFileId

chore: increase poll interval as azure easily rate limits

fix: openai file deletions, TODO: evaluate rejected deletion settled promises to determine which to delete from db records

file source icons

update table file filters

chore: file search info and versioning

fix: retrieval update with necessary tool_resources if specified

fix(useMentions): add optional chaining in case listMap value is undefined

fix: force assistant avatar roundedness

fix: azure assistants, check correct flag

chore: bump data-provider

* fix: merge conflict

* ci: fix backend tests due to new updates

* chore: update .env.example

* meilisearch improvements

* localization updates

* chore: update comparisons

* feat: add additional metadata: endpoint, author ID

* chore: azureAssistants ENDPOINTS exclusion warning
This commit is contained in:
Danny Avila 2024-05-19 12:56:55 -04:00 committed by GitHub
parent af8bcb08d6
commit 1a452121fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
158 changed files with 4184 additions and 1204 deletions

View file

@ -4,7 +4,11 @@ import type { Option, ExtendedFile } from './types';
export type TAssistantOption =
| string
| (Option & Assistant & { files?: Array<[string, ExtendedFile]> });
| (Option &
Assistant & {
files?: Array<[string, ExtendedFile]>;
code_files?: Array<[string, ExtendedFile]>;
});
export type Actions = {
[Capabilities.code_interpreter]: boolean;

View file

@ -8,10 +8,12 @@ import type {
TPreset,
TPlugin,
TMessage,
Assistant,
TLoginUser,
AuthTypeEnum,
TConversation,
EModelEndpoint,
AssistantsEndpoint,
AuthorizationTypeEnum,
TSetOption as SetOption,
TokenExchangeMethodEnum,
@ -19,6 +21,13 @@ import type {
import type { UseMutationResult } from '@tanstack/react-query';
import type { LucideIcon } from 'lucide-react';
export type AssistantListItem = {
id: string;
name: string;
metadata: Assistant['metadata'];
model: string;
};
export type TPluginMap = Record<string, TPlugin>;
export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
@ -101,6 +110,8 @@ export type AssistantPanelProps = {
actions?: Action[];
assistant_id?: string;
activePanel?: string;
endpoint: AssistantsEndpoint;
version: number | string;
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
@ -315,6 +326,7 @@ export type IconProps = Pick<TMessage, 'isCreatedByUser' | 'model'> &
iconURL?: string;
message?: boolean;
className?: string;
iconClassName?: string;
endpoint?: EModelEndpoint | string | null;
endpointType?: EModelEndpoint | null;
assistantName?: string;
@ -327,7 +339,11 @@ export type Option = Record<string, unknown> & {
};
export type OptionWithIcon = Option & { icon?: React.ReactNode };
export type MentionOption = OptionWithIcon & { type: string; value: string; description?: string };
export type MentionOption = OptionWithIcon & {
type: string;
value: string;
description?: string;
};
export type TOptionSettings = {
showExamples?: boolean;

View file

@ -3,8 +3,8 @@ import { useForm } from 'react-hook-form';
import { memo, useCallback, useRef, useMemo } from 'react';
import {
supportsFiles,
EModelEndpoint,
mergeFileConfig,
isAssistantsEndpoint,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
@ -74,8 +74,9 @@ const ChatForm = ({ index = 0 }) => {
const endpointFileConfig = fileConfig.endpoints[endpoint ?? ''];
const invalidAssistant = useMemo(
() =>
conversation?.endpoint === EModelEndpoint.assistants &&
(!conversation?.assistant_id || !assistantMap?.[conversation?.assistant_id ?? '']),
isAssistantsEndpoint(conversation?.endpoint) &&
(!conversation?.assistant_id ||
!assistantMap?.[conversation?.endpoint ?? '']?.[conversation?.assistant_id ?? '']),
[conversation?.assistant_id, conversation?.endpoint, assistantMap],
);
const disableInputs = useMemo(

View file

@ -2,6 +2,7 @@ import type { TFile } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import FileIcon from '~/components/svg/Files/FileIcon';
import ProgressCircle from './ProgressCircle';
import SourceIcon from './SourceIcon';
import { useProgress } from '~/hooks';
import { cn } from '~/utils';
@ -20,8 +21,7 @@ const FilePreview = ({
}) => {
const radius = 55; // Radius of the SVG circle
const circumference = 2 * Math.PI * radius;
const progress = useProgress(file?.['progress'] ?? 1, 0.001, file?.size ?? 1);
console.log(progress);
const progress = useProgress(file?.['progress'] ?? 1, 0.001, (file as ExtendedFile)?.size ?? 1);
// Calculate the offset based on the loading progress
const offset = circumference - progress * circumference;
@ -32,6 +32,7 @@ const FilePreview = ({
return (
<div className={cn('h-10 w-10 shrink-0 overflow-hidden rounded-md', className)}>
<FileIcon file={file} fileType={fileType} />
<SourceIcon source={file?.source} />
{progress < 1 && (
<ProgressCircle
circumference={circumference}

View file

@ -1,4 +1,5 @@
import { useEffect } from 'react';
import { EToolResources } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation } from '~/data-provider';
import { useFileDeletion } from '~/hooks/Files';
@ -10,6 +11,7 @@ export default function FileRow({
setFiles,
setFilesLoading,
assistant_id,
tool_resource,
fileFilter,
Wrapper,
}: {
@ -18,6 +20,7 @@ export default function FileRow({
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
fileFilter?: (file: ExtendedFile) => boolean;
assistant_id?: string;
tool_resource?: EToolResources;
Wrapper?: React.FC<{ children: React.ReactNode }>;
}) {
const files = Array.from(_files.values()).filter((file) =>
@ -25,7 +28,8 @@ export default function FileRow({
);
const { mutateAsync } = useDeleteFilesMutation({
onMutate: async () => console.log('Deleting files: assistant_id', assistant_id),
onMutate: async () =>
console.log('Deleting files: assistant_id, tool_resource', assistant_id, tool_resource),
onSuccess: () => {
console.log('Files deleted');
},
@ -34,7 +38,7 @@ export default function FileRow({
},
});
const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id });
const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id, tool_resource });
useEffect(() => {
if (!files) {
@ -82,6 +86,7 @@ export default function FileRow({
url={file.preview}
onDelete={handleDelete}
progress={file.progress}
source={file.source}
/>
);
}

View file

@ -12,16 +12,9 @@ export default function Files({ open, onOpenChange }) {
const { data: files = [] } = useGetFiles<TFile[]>({
select: (files) =>
files.map((file) => {
if (file.source === FileSources.local || file.source === FileSources.openai) {
file.context = file.context ?? FileContext.unknown;
return file;
} else {
return {
...file,
context: file.context ?? FileContext.unknown,
source: FileSources.local,
};
}
file.context = file.context ?? FileContext.unknown;
file.filterSource = file.source === FileSources.firebase ? FileSources.local : file.source;
return file;
}),
});

View file

@ -1,3 +1,4 @@
import { FileSources } from 'librechat-data-provider';
import ImagePreview from './ImagePreview';
import RemoveFile from './RemoveFile';
@ -6,16 +7,18 @@ const Image = ({
url,
onDelete,
progress = 1,
source = FileSources.local,
}: {
imageBase64?: string;
url?: string;
onDelete: () => void;
progress: number; // between 0 and 1
source?: FileSources;
}) => {
return (
<div className="group relative inline-block text-sm text-black/70 dark:text-white/90">
<div className="relative overflow-hidden rounded-xl border border-gray-200 dark:border-gray-600">
<ImagePreview imageBase64={imageBase64} url={url} progress={progress} />
<ImagePreview source={source} imageBase64={imageBase64} url={url} progress={progress} />
</div>
<RemoveFile onRemove={onDelete} />
</div>

View file

@ -1,4 +1,6 @@
import { FileSources } from 'librechat-data-provider';
import ProgressCircle from './ProgressCircle';
import SourceIcon from './SourceIcon';
import { cn } from '~/utils';
type styleProps = {
@ -13,11 +15,13 @@ const ImagePreview = ({
url,
progress = 1,
className = '',
source,
}: {
imageBase64?: string;
url?: string;
progress?: number; // between 0 and 1
className?: string;
source?: FileSources;
}) => {
let style: styleProps = {
backgroundSize: 'cover',
@ -65,6 +69,7 @@ const ImagePreview = ({
circleCSSProperties={circleCSSProperties}
/>
)}
<SourceIcon source={source} />
</div>
);
};

View file

@ -0,0 +1,45 @@
import { EModelEndpoint, FileSources } from 'librechat-data-provider';
import { MinimalIcon } from '~/components/Endpoints';
import { cn } from '~/utils';
const sourceToEndpoint = {
[FileSources.openai]: EModelEndpoint.openAI,
[FileSources.azure]: EModelEndpoint.azureOpenAI,
};
const sourceToClassname = {
[FileSources.openai]: 'bg-black/65',
[FileSources.azure]: 'azure-bg-color opacity-85',
};
const defaultClassName =
'absolute right-0 bottom-0 rounded-full p-[0.15rem] text-gray-600 transition-colors';
export default function SourceIcon({
source,
className = defaultClassName,
}: {
source?: FileSources;
className?: string;
}) {
if (source === FileSources.local || source === FileSources.firebase) {
return null;
}
const endpoint = sourceToEndpoint[source ?? ''];
if (!endpoint) {
return null;
}
return (
<button type="button" className={cn(className, sourceToClassname[source ?? ''] ?? '')}>
<span className="flex items-center justify-center">
<MinimalIcon
endpoint={endpoint}
size={14}
isCreatedByUser={false}
iconClassName="h-3 w-3"
/>
</span>
</button>
);
}

View file

@ -7,6 +7,7 @@ import ImagePreview from '~/components/Chat/Input/Files/ImagePreview';
import FilePreview from '~/components/Chat/Input/Files/FilePreview';
import { SortFilterHeader } from './SortFilterHeader';
import { OpenAIMinimalIcon } from '~/components/svg';
import { AzureMinimalIcon } from '~/components/svg';
import { Button, Checkbox } from '~/components/ui';
import { formatDate, getFileType } from '~/utils';
import useLocalize from '~/hooks/useLocalize';
@ -71,10 +72,11 @@ export const columns: ColumnDef<TFile>[] = [
const file = row.original;
if (file.type?.startsWith('image')) {
return (
<div className="flex gap-2 ">
<div className="flex gap-2">
<ImagePreview
url={file.filepath}
className="h-10 w-10 shrink-0 overflow-hidden rounded-md"
className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
source={file?.source}
/>
<span className="self-center truncate ">{file.filename}</span>
</div>
@ -84,7 +86,7 @@ export const columns: ColumnDef<TFile>[] = [
const fileType = getFileType(file.type);
return (
<div className="flex gap-2">
{fileType && <FilePreview fileType={fileType} />}
{fileType && <FilePreview fileType={fileType} className="relative" file={file} />}
<span className="self-center truncate">{file.filename}</span>
</div>
);
@ -108,7 +110,7 @@ export const columns: ColumnDef<TFile>[] = [
cell: ({ row }) => formatDate(row.original.updatedAt),
},
{
accessorKey: 'source',
accessorKey: 'filterSource',
header: ({ column }) => {
const localize = useLocalize();
return (
@ -117,10 +119,14 @@ export const columns: ColumnDef<TFile>[] = [
title={localize('com_ui_storage')}
filters={{
Storage: Object.values(FileSources).filter(
(value) => value === FileSources.local || value === FileSources.openai,
(value) =>
value === FileSources.local ||
value === FileSources.openai ||
value === FileSources.azure,
),
}}
valueMap={{
[FileSources.azure]: 'Azure',
[FileSources.openai]: 'OpenAI',
[FileSources.local]: 'com_ui_host',
}}
@ -137,6 +143,13 @@ export const columns: ColumnDef<TFile>[] = [
{'OpenAI'}
</div>
);
} else if (source === FileSources.azure) {
return (
<div className="flex flex-wrap items-center gap-2">
<AzureMinimalIcon className="icon-sm text-cyan-700" />
{'Azure'}
</div>
);
}
return (
<div className="flex flex-wrap items-center gap-2">

View file

@ -48,7 +48,12 @@ const contextMap = {
[FileContext.bytes]: 'com_ui_size',
};
type Style = { width?: number | string; maxWidth?: number | string; minWidth?: number | string };
type Style = {
width?: number | string;
maxWidth?: number | string;
minWidth?: number | string;
zIndex?: number;
};
export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
const localize = useLocalize();
@ -142,7 +147,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header, index) => {
const style: Style = { maxWidth: '32px', minWidth: '125px' };
const style: Style = { maxWidth: '32px', minWidth: '125px', zIndex: 50 };
if (header.id === 'filename') {
style.maxWidth = '50%';
style.width = '50%';

View file

@ -17,7 +17,9 @@ export default function Mention({
}) {
const localize = useLocalize();
const assistantMap = useAssistantsMapContext();
const { options, modelsConfig, assistants, onSelectMention } = useMentions({ assistantMap });
const { options, modelsConfig, assistantListMap, onSelectMention } = useMentions({
assistantMap,
});
const [activeIndex, setActiveIndex] = useState(0);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
@ -47,7 +49,12 @@ export default function Mention({
if (mention.type === 'endpoint' && mention.value === EModelEndpoint.assistants) {
setSearchValue('');
setInputOptions(assistants);
setInputOptions(assistantListMap[EModelEndpoint.assistants]);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint' && mention.value === EModelEndpoint.azureAssistants) {
setSearchValue('');
setInputOptions(assistantListMap[EModelEndpoint.azureAssistants]);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint') {

View file

@ -1,4 +1,4 @@
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { ReactNode } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
@ -30,7 +30,8 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
const iconURL = conversation?.iconURL;
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const assistant = endpoint === EModelEndpoint.assistants && assistantMap?.[assistant_id ?? ''];
const isAssistant = isAssistantsEndpoint(endpoint);
const assistant = isAssistant && assistantMap?.[endpoint]?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || '';
const assistantDesc = (assistant && assistant?.description) || '';
const avatar = (assistant && (assistant?.metadata?.avatar as string)) || '';
@ -77,7 +78,7 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
</div>
) : (
<div className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
{endpoint === EModelEndpoint.assistants
{isAssistant
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
: conversation?.greeting ?? localize('com_nav_welcome_message')}
</div>

View file

@ -15,6 +15,24 @@ import {
import UnknownIcon from './UnknownIcon';
import { cn } from '~/utils';
const AssistantAvatar = ({ className = '', assistantName, avatar, size }: IconMapProps) => {
if (assistantName && avatar) {
return (
<img
src={avatar}
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
alt={assistantName}
width="80"
height="80"
/>
);
} else if (assistantName) {
return <AssistantIcon className={cn('text-token-secondary', className)} size={size} />;
}
return <Sparkles className={cn(assistantName === '' ? 'icon-2xl' : '', className)} />;
};
export const icons = {
[EModelEndpoint.azureOpenAI]: AzureMinimalIcon,
[EModelEndpoint.openAI]: GPTIcon,
@ -24,22 +42,7 @@ export const icons = {
[EModelEndpoint.google]: GoogleMinimalIcon,
[EModelEndpoint.bingAI]: BingAIMinimalIcon,
[EModelEndpoint.custom]: CustomMinimalIcon,
[EModelEndpoint.assistants]: ({ className = '', assistantName, avatar, size }: IconMapProps) => {
if (assistantName && avatar) {
return (
<img
src={avatar}
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
alt={assistantName}
width="80"
height="80"
/>
);
} else if (assistantName) {
return <AssistantIcon className={cn('text-token-secondary', className)} size={size} />;
}
return <Sparkles className={cn(assistantName === '' ? 'icon-2xl' : '', className)} />;
},
[EModelEndpoint.assistants]: AssistantAvatar,
[EModelEndpoint.azureAssistants]: AssistantAvatar,
unknown: UnknownIcon,
};

View file

@ -1,5 +1,5 @@
import { Content, Portal, Root } from '@radix-ui/react-popover';
import { alternateName, EModelEndpoint } from 'librechat-data-provider';
import { alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { FC } from 'react';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
@ -16,7 +16,8 @@ const EndpointsMenu: FC = () => {
const { endpoint = '', assistant_id = null } = conversation ?? {};
const assistantMap = useAssistantsMapContext();
const assistant = endpoint === EModelEndpoint.assistants && assistantMap?.[assistant_id ?? ''];
const assistant =
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint ?? '']?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || 'Assistant';
if (!endpoint) {

View file

@ -1,6 +1,7 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import ProgressCircle from './ProgressCircle';
import CancelledIcon from './CancelledIcon';
import ProgressText from './ProgressText';
import FinishedIcon from './FinishedIcon';
import MarkdownLite from './MarkdownLite';
@ -11,10 +12,12 @@ export default function CodeAnalyze({
initialProgress = 0.1,
code,
outputs = [],
isSubmitting,
}: {
initialProgress: number;
code: string;
outputs: Record<string, unknown>[];
isSubmitting: boolean;
}) {
const showCodeDefault = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showCodeDefault);
@ -35,7 +38,13 @@ export default function CodeAnalyze({
<div className="my-2.5 flex items-center gap-2.5">
<div className="relative h-5 w-5 shrink-0">
{progress < 1 ? (
<CodeInProgress offset={offset} circumference={circumference} radius={radius} />
<CodeInProgress
offset={offset}
radius={radius}
progress={progress}
isSubmitting={isSubmitting}
circumference={circumference}
/>
) : (
<FinishedIcon />
)}
@ -74,18 +83,25 @@ const CodeInProgress = ({
offset,
circumference,
radius,
isSubmitting,
progress,
}: {
progress: number;
offset: number;
circumference: number;
radius: number;
isSubmitting: boolean;
}) => {
if (progress < 1 && !isSubmitting) {
return <CancelledIcon />;
}
return (
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="77"
>
<div className='absolute right-[1.5px] bottom-[1.5px]'>
<div className="absolute bottom-[1.5px] right-[1.5px]">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"

View file

@ -79,11 +79,13 @@ export default function Part({
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
part.type === ContentTypes.TOOL_CALL &&
part[ContentTypes.TOOL_CALL].type === ToolCallTypes.RETRIEVAL
(part[ContentTypes.TOOL_CALL].type === ToolCallTypes.RETRIEVAL ||
part[ContentTypes.TOOL_CALL].type === ToolCallTypes.FILE_SEARCH)
) {
const toolCall = part[ContentTypes.TOOL_CALL];
return <RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />;

View file

@ -1,5 +1,4 @@
import { useState } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import type { TConversation, TMessage } from 'librechat-data-provider';
import { Clipboard, CheckMark, EditIcon, RegenerateIcon, ContinueIcon } from '~/components/svg';
import { useGenerationsByLatest, useLocalize } from '~/hooks';
@ -35,14 +34,19 @@ export default function HoverButtons({
const { endpoint: _endpoint, endpointType } = conversation ?? {};
const endpoint = endpointType ?? _endpoint;
const [isCopied, setIsCopied] = useState(false);
const { hideEditButton, regenerateEnabled, continueSupported, forkingSupported } =
useGenerationsByLatest({
isEditing,
isSubmitting,
message,
endpoint: endpoint ?? '',
latestMessage,
});
const {
hideEditButton,
regenerateEnabled,
continueSupported,
forkingSupported,
isEditableEndpoint,
} = useGenerationsByLatest({
isEditing,
isSubmitting,
message,
endpoint: endpoint ?? '',
latestMessage,
});
if (!conversation) {
return null;
}
@ -58,7 +62,7 @@ export default function HoverButtons({
return (
<div className="visible mt-0 flex justify-center gap-1 self-end text-gray-400 lg:justify-start">
{endpoint !== EModelEndpoint.assistants && (
{isEditableEndpoint && (
<button
className={cn(
'hover-button rounded-md p-1 text-gray-400 hover:text-gray-900 dark:text-gray-400/70 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',

View file

@ -1,8 +1,13 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { Assistant, TConversation, TEndpointsConfig, TPreset } from 'librechat-data-provider';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TAssistantsMap,
TConversation,
TEndpointsConfig,
TPreset,
} from 'librechat-data-provider';
import { getEndpointField, getIconKey, getIconEndpoint } from '~/utils';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { getEndpointField, getIconKey, getIconEndpoint } from '~/utils';
export default function ConvoIcon({
conversation,
@ -15,7 +20,7 @@ export default function ConvoIcon({
}: {
conversation: TConversation | TPreset | null;
endpointsConfig: TEndpointsConfig;
assistantMap: Record<string, Assistant>;
assistantMap: TAssistantsMap;
containerClassName?: string;
context?: 'message' | 'nav' | 'landing' | 'menu-item';
className?: string;
@ -25,7 +30,7 @@ export default function ConvoIcon({
let endpoint = conversation?.endpoint;
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const assistant =
endpoint === EModelEndpoint.assistants && assistantMap?.[conversation?.assistant_id ?? ''];
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint]?.[conversation?.assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || '';
const avatar = (assistant && (assistant?.metadata?.avatar as string)) || '';

View file

@ -1,5 +1,10 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { Assistant, TConversation, TEndpointsConfig, TPreset } from 'librechat-data-provider';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TConversation,
TEndpointsConfig,
TPreset,
TAssistantsMap,
} from 'librechat-data-provider';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import MinimalIcon from '~/components/Endpoints/MinimalIcon';
import { getEndpointField, getIconEndpoint } from '~/utils';
@ -15,7 +20,7 @@ export default function EndpointIcon({
endpointsConfig: TEndpointsConfig;
containerClassName?: string;
context?: 'message' | 'nav' | 'landing' | 'menu-item';
assistantMap?: Record<string, Assistant>;
assistantMap?: TAssistantsMap;
className?: string;
size?: number;
}) {
@ -27,7 +32,7 @@ export default function EndpointIcon({
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
const assistant =
endpoint === EModelEndpoint.assistants && assistantMap?.[conversation?.assistant_id ?? ''];
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint]?.[conversation?.assistant_id ?? ''];
const assistantAvatar = (assistant && (assistant?.metadata?.avatar as string)) || '';
const assistantName = (assistant && assistant?.name) || '';

View file

@ -1,4 +1,4 @@
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon';
import {
Plugin,
@ -27,35 +27,38 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
assistantName,
} = props;
const assistantsIcon = {
icon: props.iconURL ? (
<div className="relative flex h-6 w-6 items-center justify-center">
<div
title={assistantName}
style={{
width: size,
height: size,
}}
className={cn('overflow-hidden rounded-full', props.className ?? '')}
>
<img
className="shadow-stroke h-full w-full object-cover"
src={props.iconURL}
alt={assistantName}
style={{ height: '80', width: '80' }}
/>
</div>
</div>
) : (
<div className="h-6 w-6">
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<AssistantIcon className="h-2/3 w-2/3 text-gray-400" />
</div>
</div>
),
name: endpoint,
};
const endpointIcons = {
[EModelEndpoint.assistants]: {
icon: props.iconURL ? (
<div className="relative flex h-6 w-6 items-center justify-center">
<div
title={assistantName}
style={{
width: size,
height: size,
}}
className={cn('overflow-hidden rounded-full', props.className ?? '')}
>
<img
className="shadow-stroke h-full w-full object-cover"
src={props.iconURL}
alt={assistantName}
style={{ height: '80', width: '80' }}
/>
</div>
</div>
) : (
<div className="h-6 w-6">
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<AssistantIcon className="h-2/3 w-2/3 text-gray-400" />
</div>
</div>
),
name: endpoint,
},
[EModelEndpoint.assistants]: assistantsIcon,
[EModelEndpoint.azureAssistants]: assistantsIcon,
[EModelEndpoint.azureOpenAI]: {
icon: <AzureMinimalIcon size={size * 0.5555555555555556} />,
bg: 'linear-gradient(0.375turn, #61bde2, #4389d0)',
@ -136,7 +139,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
({ icon, bg, name } = endpointIcons[iconURL]);
}
if (endpoint === EModelEndpoint.assistants) {
if (isAssistantsEndpoint(endpoint)) {
return icon;
}

View file

@ -15,7 +15,7 @@ import { cn } from '~/utils';
import { IconProps } from '~/common';
const MinimalIcon: React.FC<IconProps> = (props) => {
const { size = 30, error } = props;
const { size = 30, iconClassName, error } = props;
let endpoint = 'default'; // Default value for endpoint
@ -25,10 +25,13 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
const endpointIcons = {
[EModelEndpoint.azureOpenAI]: {
icon: <AzureMinimalIcon />,
icon: <AzureMinimalIcon className={iconClassName} />,
name: props.chatGptLabel || 'ChatGPT',
},
[EModelEndpoint.openAI]: {
icon: <OpenAIMinimalIcon className={iconClassName} />,
name: props.chatGptLabel || 'ChatGPT',
},
[EModelEndpoint.openAI]: { icon: <OpenAIMinimalIcon />, name: props.chatGptLabel || 'ChatGPT' },
[EModelEndpoint.gptPlugins]: { icon: <MinimalPlugin />, name: 'Plugins' },
[EModelEndpoint.google]: { icon: <GoogleMinimalIcon />, name: props.modelLabel || 'Google' },
[EModelEndpoint.anthropic]: {
@ -42,6 +45,7 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
[EModelEndpoint.bingAI]: { icon: <BingAIMinimalIcon />, name: 'BingAI' },
[EModelEndpoint.chatGPTBrowser]: { icon: <LightningIcon />, name: 'ChatGPT' },
[EModelEndpoint.assistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.azureAssistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
default: {
icon: (
<UnknownIcon

View file

@ -1,5 +1,7 @@
import TextareaAutosize from 'react-textarea-autosize';
import { ImageDetail, imageDetailNumeric, imageDetailValue } from 'librechat-data-provider';
import type { ValueType } from '@rc-component/mini-decimal';
import type { TModelSelectProps } from '~/common';
import {
Input,
Label,
@ -11,7 +13,6 @@ import {
} from '~/components/ui';
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/';
import { useLocalize, useDebouncedInput } from '~/hooks';
import type { TModelSelectProps } from '~/common';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@ -127,7 +128,7 @@ export default function Settings({
id="temp-int"
disabled={readonly}
value={temperatureValue as number}
onChange={setTemperature}
onChange={setTemperature as (value: ValueType | null) => void}
max={2}
min={0}
step={0.01}

View file

@ -1,12 +1,10 @@
import { useState, useMemo, useEffect } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { defaultOrderQuery } from 'librechat-data-provider';
import type { TPreset } from 'librechat-data-provider';
import type { TModelSelectProps, Option } from '~/common';
import { Label, HoverCard, SelectDropDown, HoverCardTrigger } from '~/components/ui';
import { cn, defaultTextProps, removeFocusOutlines, mapAssistants } from '~/utils';
import { useLocalize, useDebouncedInput } from '~/hooks';
import { useListAssistantsQuery } from '~/data-provider';
import { useLocalize, useDebouncedInput, useAssistantListMap } from '~/hooks';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@ -17,23 +15,21 @@ export default function Settings({ conversation, setOption, models, readonly }:
[localize],
);
const { data: assistants = [] } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) =>
[
defaultOption,
...res.data.map(({ id, name }) => ({
label: name,
value: id,
})),
].filter(Boolean),
});
const { data: assistantMap = {} } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) => mapAssistants(res.data),
});
const assistantListMap = useAssistantListMap((res) => mapAssistants(res.data));
const { model, endpoint, assistant_id, endpointType, promptPrefix, instructions } =
conversation ?? {};
const assistants = useMemo(() => {
return [
defaultOption,
...(assistantListMap[endpoint ?? ''] ?? []).map(({ id, name }) => ({
label: name,
value: id,
})),
].filter(Boolean);
}, [assistantListMap, endpoint, defaultOption]);
const [onPromptPrefixChange, promptPrefixValue] = useDebouncedInput({
setOption,
optionKey: 'promptPrefix',
@ -47,11 +43,11 @@ export default function Settings({ conversation, setOption, models, readonly }:
const activeAssistant = useMemo(() => {
if (assistant_id) {
return assistantMap[assistant_id];
return assistantListMap[endpoint ?? '']?.[assistant_id];
}
return null;
}, [assistant_id, assistantMap]);
}, [assistant_id, assistantListMap, endpoint]);
const modelOptions = useMemo(() => {
return models.map((model) => ({
@ -89,7 +85,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
return;
}
const assistant = assistantMap[value];
const assistant = assistantListMap[endpoint ?? '']?.[value];
if (!assistant) {
setAssistantValue(defaultOption);
return;

View file

@ -9,6 +9,7 @@ import OpenAISettings from './OpenAI';
const settings: { [key: string]: FC<TModelSelectProps> } = {
[EModelEndpoint.assistants]: AssistantsSettings,
[EModelEndpoint.azureAssistants]: AssistantsSettings,
[EModelEndpoint.openAI]: OpenAISettings,
[EModelEndpoint.custom]: OpenAISettings,
[EModelEndpoint.azureOpenAI]: OpenAISettings,

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TDialogProps } from '~/common';
import DialogTemplate from '~/components/ui/DialogTemplate';
@ -21,6 +21,7 @@ const endpointComponents = {
[EModelEndpoint.azureOpenAI]: OpenAIConfig,
[EModelEndpoint.gptPlugins]: OpenAIConfig,
[EModelEndpoint.assistants]: OpenAIConfig,
[EModelEndpoint.azureAssistants]: OpenAIConfig,
default: OtherConfig,
};
@ -30,6 +31,7 @@ const formSet: Set<string> = new Set([
EModelEndpoint.azureOpenAI,
EModelEndpoint.gptPlugins,
EModelEndpoint.assistants,
EModelEndpoint.azureAssistants,
]);
const EXPIRY = {
@ -97,7 +99,7 @@ const SetKeyDialog = ({
isAzure ||
endpoint === EModelEndpoint.openAI ||
endpoint === EModelEndpoint.gptPlugins ||
endpoint === EModelEndpoint.assistants;
isAssistantsEndpoint(endpoint);
if (isAzure) {
data.apiKey = 'n/a';
}

View file

@ -71,6 +71,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
}}
placeholder={localize('com_nav_search_placeholder')}
onKeyUp={handleKeyUp}
autoComplete="off"
/>
<X
className={cn(

View file

@ -1,10 +1,10 @@
import { useEffect, useMemo } from 'react';
import { Combobox } from '~/components/ui';
import { EModelEndpoint, defaultOrderQuery, LocalStorageKeys } from 'librechat-data-provider';
import type { SwitcherProps } from '~/common';
import { useSetIndexOptions, useSelectAssistant, useLocalize } from '~/hooks';
import { isAssistantsEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { SwitcherProps, AssistantListItem } from '~/common';
import { useSetIndexOptions, useSelectAssistant, useLocalize, useAssistantListMap } from '~/hooks';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useListAssistantsQuery } from '~/data-provider';
import Icon from '~/components/Endpoints/Icon';
export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
@ -15,26 +15,29 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
/* `selectedAssistant` must be defined with `null` to cause re-render on update */
const { assistant_id: selectedAssistant = null, endpoint } = conversation ?? {};
const { data: assistants = [] } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) => res.data.map(({ id, name, metadata }) => ({ id, name, metadata })),
});
const assistantListMap = useAssistantListMap((res) =>
res.data.map(({ id, name, metadata }) => ({ id, name, metadata })),
);
const assistants: Omit<AssistantListItem, 'model'>[] = useMemo(
() => assistantListMap[endpoint ?? ''] ?? [],
[endpoint, assistantListMap],
);
const assistantMap = useAssistantsMapContext();
const { onSelect } = useSelectAssistant();
const { onSelect } = useSelectAssistant(endpoint as AssistantsEndpoint);
useEffect(() => {
if (!selectedAssistant && assistants && assistants.length && assistantMap) {
const assistant_id =
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}`) ??
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}${endpoint}`) ??
assistants[0]?.id ??
'';
const assistant = assistantMap?.[assistant_id];
const assistant = assistantMap?.[endpoint ?? '']?.[assistant_id];
if (!assistant) {
return;
}
if (endpoint !== EModelEndpoint.assistants) {
if (!isAssistantsEndpoint(endpoint)) {
return;
}
@ -43,7 +46,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
}
}, [index, assistants, selectedAssistant, assistantMap, endpoint, setOption]);
const currentAssistant = assistantMap?.[selectedAssistant ?? ''];
const currentAssistant = assistantMap?.[endpoint ?? '']?.[selectedAssistant ?? ''];
const assistantOptions = useMemo(() => {
return assistants.map((assistant) => {
@ -53,14 +56,14 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
icon: (
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
endpoint={endpoint}
assistantName={assistant.name ?? ''}
iconURL={(assistant.metadata?.avatar as string) ?? ''}
/>
),
};
});
}, [assistants]);
}, [assistants, endpoint]);
return (
<Combobox
@ -78,7 +81,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
SelectIcon={
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
endpoint={endpoint}
assistantName={currentAssistant?.name ?? ''}
iconURL={(currentAssistant?.metadata?.avatar as string) ?? ''}
/>

View file

@ -134,7 +134,7 @@ const ApiKey = () => {
<input
placeholder="<HIDDEN>"
type="password"
autoComplete="off"
autoComplete="new-password"
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
{...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })}
/>

View file

@ -7,10 +7,11 @@ import {
AuthTypeEnum,
} from 'librechat-data-provider';
import type {
ValidationResult,
Action,
FunctionTool,
ActionMetadata,
ValidationResult,
AssistantsEndpoint,
} from 'librechat-data-provider';
import type { ActionAuthForm } from '~/common';
import type { Spec } from './ActionsTable';
@ -32,10 +33,14 @@ const debouncedValidation = debounce(
export default function ActionsInput({
action,
assistant_id,
endpoint,
version,
setAction,
}: {
action?: Action;
assistant_id?: string;
endpoint: AssistantsEndpoint;
version: number | string;
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
}) {
const handleResult = (result: ValidationResult) => {
@ -173,7 +178,9 @@ export default function ActionsInput({
metadata,
functions,
assistant_id,
model: assistantMap[assistant_id].model,
endpoint,
version,
model: assistantMap[endpoint][assistant_id].model,
});
});

View file

@ -18,9 +18,11 @@ import { Panel } from '~/common';
export default function ActionsPanel({
// activePanel,
action,
endpoint,
version,
setAction,
setActivePanel,
assistant_id,
setActivePanel,
}: AssistantPanelProps) {
const localize = useLocalize();
const { showToast } = useToastContext();
@ -130,9 +132,10 @@ export default function ActionsPanel({
const confirmed = confirm('Are you sure you want to delete this action?');
if (confirmed) {
deleteAction.mutate({
model: assistantMap[assistant_id].model,
model: assistantMap[endpoint][assistant_id].model,
action_id: action.action_id,
assistant_id,
endpoint,
});
}
}}
@ -185,7 +188,13 @@ export default function ActionsPanel({
</DialogTrigger>
<ActionsAuth setOpenAuthDialog={setOpenAuthDialog} />
</Dialog>
<ActionsInput action={action} assistant_id={assistant_id} setAction={setAction} />
<ActionsInput
action={action}
assistant_id={assistant_id}
setAction={setAction}
endpoint={endpoint}
version={version}
/>
</div>
</form>
</FormProvider>

View file

@ -10,9 +10,10 @@ import {
import type { UseMutationResult } from '@tanstack/react-query';
import type {
Metadata,
AssistantListResponse,
Assistant,
AssistantsEndpoint,
AssistantCreateParams,
AssistantListResponse,
} from 'librechat-data-provider';
import { useUploadAssistantAvatarMutation, useGetFileConfig } from '~/data-provider';
import { AssistantAvatar, NoImage, AvatarMenu } from './Images';
@ -22,10 +23,14 @@ import { useLocalize } from '~/hooks';
// import { cn } from '~/utils/';
function Avatar({
endpoint,
version,
assistant_id,
metadata,
createMutation,
}: {
endpoint: AssistantsEndpoint;
version: number | string;
assistant_id: string | null;
metadata: null | Metadata;
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
@ -46,8 +51,8 @@ function Avatar({
const { showToast } = useToastContext();
const activeModel = useMemo(() => {
return assistantsMap[assistant_id ?? '']?.model ?? '';
}, [assistant_id, assistantsMap]);
return assistantsMap[endpoint][assistant_id ?? '']?.model ?? '';
}, [assistantsMap, endpoint, assistant_id]);
const { mutate: uploadAvatar } = useUploadAssistantAvatarMutation({
onMutate: () => {
@ -65,6 +70,7 @@ function Avatar({
const res = queryClient.getQueryData<AssistantListResponse>([
QueryKeys.assistants,
endpoint,
defaultOrderQuery,
]);
@ -83,10 +89,13 @@ function Avatar({
return assistant;
}) ?? [];
queryClient.setQueryData<AssistantListResponse>([QueryKeys.assistants, defaultOrderQuery], {
...res,
data: assistants,
});
queryClient.setQueryData<AssistantListResponse>(
[QueryKeys.assistants, endpoint, defaultOrderQuery],
{
...res,
data: assistants,
},
);
setProgress(1);
},
@ -149,9 +158,20 @@ function Avatar({
model: activeModel,
postCreation: true,
formData,
endpoint,
version,
});
}
}, [createMutation.data, createMutation.isSuccess, input, previewUrl, uploadAvatar, activeModel]);
}, [
createMutation.data,
createMutation.isSuccess,
input,
previewUrl,
uploadAvatar,
activeModel,
endpoint,
version,
]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const file = event.target.files?.[0];
@ -183,6 +203,8 @@ function Avatar({
assistant_id,
model: activeModel,
formData,
endpoint,
version,
});
} else {
showToast({

View file

@ -1,23 +1,23 @@
import { useState, useMemo, useEffect } from 'react';
import { useState, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useForm, FormProvider, Controller, useWatch } from 'react-hook-form';
import { useGetModelsQuery, useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import {
Tools,
QueryKeys,
Capabilities,
EModelEndpoint,
actionDelimiter,
ImageVisionTool,
defaultAssistantFormValues,
} from 'librechat-data-provider';
import type { FunctionTool, TConfig, TPlugin } from 'librechat-data-provider';
import type { AssistantForm, AssistantPanelProps } from '~/common';
import type { FunctionTool, TPlugin, TEndpointsConfig } from 'librechat-data-provider';
import { useCreateAssistantMutation, useUpdateAssistantMutation } from '~/data-provider';
import { SelectDropDown, Checkbox, QuestionMark } from '~/components/ui';
import { useAssistantsMapContext, useToastContext } from '~/Providers';
import { useSelectAssistant, useLocalize } from '~/hooks';
import { ToolSelectDialog } from '~/components/Tools';
import CapabilitiesForm from './CapabilitiesForm';
import { SelectDropDown } from '~/components/ui';
import AssistantAvatar from './AssistantAvatar';
import AssistantSelect from './AssistantSelect';
import AssistantAction from './AssistantAction';
@ -35,17 +35,20 @@ const inputClass =
export default function AssistantPanel({
// index = 0,
setAction,
endpoint,
actions = [],
setActivePanel,
assistant_id: current_assistant_id,
setCurrentAssistantId,
}: AssistantPanelProps) {
assistantsConfig,
version,
}: AssistantPanelProps & { assistantsConfig?: TConfig | null }) {
const queryClient = useQueryClient();
const modelsQuery = useGetModelsQuery();
const assistantMap = useAssistantsMapContext();
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const allTools = queryClient.getQueryData<TPlugin[]>([QueryKeys.tools]) ?? [];
const { onSelect: onSelectAssistant } = useSelectAssistant();
const { onSelect: onSelectAssistant } = useSelectAssistant(endpoint);
const { showToast } = useToastContext();
const localize = useLocalize();
@ -55,44 +58,31 @@ export default function AssistantPanel({
const [showToolDialog, setShowToolDialog] = useState(false);
const { control, handleSubmit, reset, setValue, getValues } = methods;
const { control, handleSubmit, reset } = methods;
const assistant = useWatch({ control, name: 'assistant' });
const functions = useWatch({ control, name: 'functions' });
const assistant_id = useWatch({ control, name: 'id' });
const model = useWatch({ control, name: 'model' });
const activeModel = useMemo(() => {
return assistantMap?.[assistant_id]?.model;
}, [assistantMap, assistant_id]);
return assistantMap?.[endpoint]?.[assistant_id]?.model;
}, [assistantMap, endpoint, assistant_id]);
const assistants = useMemo(() => endpointsConfig?.[EModelEndpoint.assistants], [endpointsConfig]);
const retrievalModels = useMemo(() => new Set(assistants?.retrievalModels ?? []), [assistants]);
const toolsEnabled = useMemo(
() => assistants?.capabilities?.includes(Capabilities.tools),
[assistants],
() => assistantsConfig?.capabilities?.includes(Capabilities.tools),
[assistantsConfig],
);
const actionsEnabled = useMemo(
() => assistants?.capabilities?.includes(Capabilities.actions),
[assistants],
() => assistantsConfig?.capabilities?.includes(Capabilities.actions),
[assistantsConfig],
);
const retrievalEnabled = useMemo(
() => assistants?.capabilities?.includes(Capabilities.retrieval),
[assistants],
() => assistantsConfig?.capabilities?.includes(Capabilities.retrieval),
[assistantsConfig],
);
const codeEnabled = useMemo(
() => assistants?.capabilities?.includes(Capabilities.code_interpreter),
[assistants],
() => assistantsConfig?.capabilities?.includes(Capabilities.code_interpreter),
[assistantsConfig],
);
const imageVisionEnabled = useMemo(
() => assistants?.capabilities?.includes(Capabilities.image_vision),
[assistants],
);
useEffect(() => {
if (model && !retrievalModels.has(model)) {
setValue(Capabilities.retrieval, false);
}
}, [model, setValue, retrievalModels]);
/* Mutations */
const update = useUpdateAssistantMutation({
@ -145,7 +135,7 @@ export default function AssistantPanel({
if (!functionName.includes(actionDelimiter)) {
return functionName;
} else {
const assistant = assistantMap?.[assistant_id];
const assistant = assistantMap?.[endpoint]?.[assistant_id];
const tool = assistant?.tools?.find((tool) => tool.function?.name === functionName);
if (assistant && tool) {
return tool;
@ -160,7 +150,7 @@ export default function AssistantPanel({
tools.push({ type: Tools.code_interpreter });
}
if (data.retrieval) {
tools.push({ type: Tools.retrieval });
tools.push({ type: version == 2 ? Tools.file_search : Tools.retrieval });
}
if (data.image_vision) {
tools.push(ImageVisionTool);
@ -183,6 +173,7 @@ export default function AssistantPanel({
instructions,
model,
tools,
endpoint,
},
});
return;
@ -194,6 +185,8 @@ export default function AssistantPanel({
instructions,
model,
tools,
endpoint,
version,
});
};
@ -211,6 +204,7 @@ export default function AssistantPanel({
<AssistantSelect
reset={reset}
value={field.value}
endpoint={endpoint}
setCurrentAssistantId={setCurrentAssistantId}
selectedAssistant={current_assistant_id ?? null}
createMutation={create}
@ -239,6 +233,8 @@ export default function AssistantPanel({
createMutation={create}
assistant_id={assistant_id ?? null}
metadata={assistant?.['metadata'] ?? null}
endpoint={endpoint}
version={version}
/>
<label className={labelClass} htmlFor="name">
{localize('com_ui_name')}
@ -324,7 +320,7 @@ export default function AssistantPanel({
emptyTitle={true}
value={field.value}
setValue={field.onChange}
availableValues={modelsQuery.data?.[EModelEndpoint.assistants] ?? []}
availableValues={modelsQuery.data?.[endpoint] ?? []}
showAbove={false}
showLabel={false}
className={cn(
@ -343,120 +339,17 @@ export default function AssistantPanel({
/>
</div>
{/* Knowledge */}
{(codeEnabled || retrievalEnabled) && (
<Knowledge assistant_id={assistant_id} files={files} />
{(codeEnabled || retrievalEnabled) && version == 1 && (
<Knowledge assistant_id={assistant_id} files={files} endpoint={endpoint} />
)}
{/* Capabilities */}
<div className="mb-6">
<div className="mb-1.5 flex items-center">
<span>
<label className="text-token-text-primary block font-medium">
{localize('com_assistants_capabilities')}
</label>
</span>
</div>
<div className="flex flex-col items-start gap-2">
{codeEnabled && (
<div className="flex items-center">
<Controller
name={Capabilities.code_interpreter}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
/>
)}
/>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={Capabilities.code_interpreter}
onClick={() =>
setValue(
Capabilities.code_interpreter,
!getValues(Capabilities.code_interpreter),
{
shouldDirty: true,
},
)
}
>
<div className="flex items-center">
{localize('com_assistants_code_interpreter')}
<QuestionMark />
</div>
</label>
</div>
)}
{imageVisionEnabled && (
<div className="flex items-center">
<Controller
name={Capabilities.image_vision}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
/>
)}
/>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={Capabilities.image_vision}
onClick={() =>
setValue(Capabilities.image_vision, !getValues(Capabilities.image_vision), {
shouldDirty: true,
})
}
>
<div className="flex items-center">
{localize('com_assistants_image_vision')}
<QuestionMark />
</div>
</label>
</div>
)}
{retrievalEnabled && (
<div className="flex items-center">
<Controller
name={Capabilities.retrieval}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
disabled={!retrievalModels.has(model)}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
/>
)}
/>
<label
className={cn(
'form-check-label text-token-text-primary w-full',
!retrievalModels.has(model) ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
)}
htmlFor={Capabilities.retrieval}
onClick={() =>
retrievalModels.has(model) &&
setValue(Capabilities.retrieval, !getValues(Capabilities.retrieval), {
shouldDirty: true,
})
}
>
{localize('com_assistants_retrieval')}
</label>
</div>
)}
</div>
</div>
<CapabilitiesForm
version={version}
endpoint={endpoint}
codeEnabled={codeEnabled}
assistantsConfig={assistantsConfig}
retrievalEnabled={retrievalEnabled}
/>
{/* Tools */}
<div className="mb-6">
<label className={labelClass}>
@ -520,6 +413,7 @@ export default function AssistantPanel({
activeModel={activeModel}
setCurrentAssistantId={setCurrentAssistantId}
createMutation={create}
endpoint={endpoint}
/>
{/* Secondary Select Button */}
{assistant_id && (
@ -554,6 +448,7 @@ export default function AssistantPanel({
isOpen={showToolDialog}
setIsOpen={setShowToolDialog}
assistant_id={assistant_id}
endpoint={endpoint}
/>
</form>
</FormProvider>

View file

@ -1,21 +1,22 @@
import { Plus } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import {
defaultAssistantFormValues,
defaultOrderQuery,
isImageVisionTool,
EModelEndpoint,
Capabilities,
Tools,
FileSources,
Capabilities,
EModelEndpoint,
LocalStorageKeys,
isImageVisionTool,
defaultAssistantFormValues,
} from 'librechat-data-provider';
import type { UseFormReset } from 'react-hook-form';
import type { UseMutationResult } from '@tanstack/react-query';
import type { Assistant, AssistantCreateParams } from 'librechat-data-provider';
import type { Assistant, AssistantCreateParams, AssistantsEndpoint } from 'librechat-data-provider';
import type {
AssistantForm,
Actions,
TAssistantOption,
ExtendedFile,
AssistantForm,
TAssistantOption,
LastSelectedModels,
} from '~/common';
import SelectDropDown from '~/components/ui/SelectDropDown';
@ -29,12 +30,14 @@ const keys = new Set(['name', 'id', 'description', 'instructions', 'model']);
export default function AssistantSelect({
reset,
value,
endpoint,
selectedAssistant,
setCurrentAssistantId,
createMutation,
}: {
reset: UseFormReset<AssistantForm>;
value: TAssistantOption;
endpoint: AssistantsEndpoint;
selectedAssistant: string | null;
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
@ -43,42 +46,69 @@ export default function AssistantSelect({
const fileMap = useFileMapContext();
const lastSelectedAssistant = useRef<string | null>(null);
const [lastSelectedModels] = useLocalStorage<LastSelectedModels>(
'lastSelectedModel',
LocalStorageKeys.LAST_MODEL,
{} as LastSelectedModels,
);
const assistants = useListAssistantsQuery(defaultOrderQuery, {
const assistants = useListAssistantsQuery(endpoint, undefined, {
select: (res) =>
res.data.map((_assistant) => {
const source =
endpoint === EModelEndpoint.assistants ? FileSources.openai : FileSources.azure;
const assistant = {
..._assistant,
label: _assistant?.name ?? '',
value: _assistant.id,
files: _assistant?.file_ids ? ([] as Array<[string, ExtendedFile]>) : undefined,
code_files: _assistant?.tool_resources?.code_interpreter?.file_ids
? ([] as Array<[string, ExtendedFile]>)
: undefined,
};
const handleFile = (file_id: string, list?: Array<[string, ExtendedFile]>) => {
const file = fileMap?.[file_id];
if (file) {
list?.push([
file_id,
{
file_id: file.file_id,
type: file.type,
filepath: file.filepath,
filename: file.filename,
width: file.width,
height: file.height,
size: file.bytes,
preview: file.filepath,
progress: 1,
source,
},
]);
} else {
list?.push([
file_id,
{
file_id,
type: '',
filename: '',
size: 1,
progress: 1,
filepath: endpoint,
source,
},
]);
}
};
if (assistant.files && _assistant.file_ids) {
_assistant.file_ids.forEach((file_id) => {
const file = fileMap?.[file_id];
if (file) {
assistant.files?.push([
file_id,
{
file_id: file.file_id,
type: file.type,
filepath: file.filepath,
filename: file.filename,
width: file.width,
height: file.height,
size: file.bytes,
preview: file.filepath,
progress: 1,
source: FileSources.openai,
},
]);
}
});
_assistant.file_ids.forEach((file_id) => handleFile(file_id, assistant.files));
}
if (assistant.code_files && _assistant.tool_resources?.code_interpreter?.file_ids) {
_assistant.tool_resources?.code_interpreter?.file_ids?.forEach((file_id) =>
handleFile(file_id, assistant.code_files),
);
}
return assistant;
}),
});
@ -92,7 +122,7 @@ export default function AssistantSelect({
setCurrentAssistantId(undefined);
return reset({
...defaultAssistantFormValues,
model: lastSelectedModels?.[EModelEndpoint.assistants] ?? '',
model: lastSelectedModels?.[endpoint] ?? '',
});
}
@ -112,6 +142,9 @@ export default function AssistantSelect({
?.filter((tool) => tool.type !== 'function' || isImageVisionTool(tool))
?.map((tool) => tool?.function?.name || tool.type)
.forEach((tool) => {
if (tool === Tools.file_search) {
actions[Capabilities.retrieval] = true;
}
actions[tool] = true;
});
@ -141,7 +174,7 @@ export default function AssistantSelect({
reset(formValues);
setCurrentAssistantId(assistant?.id);
},
[assistants.data, reset, setCurrentAssistantId, createMutation, lastSelectedModels],
[assistants.data, reset, setCurrentAssistantId, createMutation, endpoint, lastSelectedModels],
);
useEffect(() => {

View file

@ -0,0 +1,51 @@
import { useMemo } from 'react';
import { Capabilities } from 'librechat-data-provider';
import type { TConfig, AssistantsEndpoint } from 'librechat-data-provider';
import ImageVision from './ImageVision';
import { useLocalize } from '~/hooks';
import Retrieval from './Retrieval';
import Code from './Code';
export default function CapabilitiesForm({
version,
endpoint,
codeEnabled,
retrievalEnabled,
assistantsConfig,
}: {
version: number | string;
codeEnabled?: boolean;
retrievalEnabled?: boolean;
endpoint: AssistantsEndpoint;
assistantsConfig?: TConfig | null;
}) {
const localize = useLocalize();
const retrievalModels = useMemo(
() => new Set(assistantsConfig?.retrievalModels ?? []),
[assistantsConfig],
);
const imageVisionEnabled = useMemo(
() => assistantsConfig?.capabilities?.includes(Capabilities.image_vision),
[assistantsConfig],
);
return (
<div className="mb-6">
<div className="mb-1.5 flex items-center">
<span>
<label className="text-token-text-primary block font-medium">
{localize('com_assistants_capabilities')}
</label>
</span>
</div>
<div className="flex flex-col items-start gap-2">
{codeEnabled && <Code endpoint={endpoint} version={version} />}
{imageVisionEnabled && version == 1 && <ImageVision />}
{retrievalEnabled && (
<Retrieval endpoint={endpoint} version={version} retrievalModels={retrievalModels} />
)}
</div>
</div>
);
}

View file

@ -0,0 +1,70 @@
import { useMemo } from 'react';
import { Capabilities } from 'librechat-data-provider';
import { useFormContext, Controller, useWatch } from 'react-hook-form';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { AssistantForm } from '~/common';
import { Checkbox, QuestionMark } from '~/components/ui';
import { useLocalize } from '~/hooks';
import CodeFiles from './CodeFiles';
export default function Code({
version,
endpoint,
}: {
version: number | string;
endpoint: AssistantsEndpoint;
}) {
const localize = useLocalize();
const methods = useFormContext<AssistantForm>();
const { control, setValue, getValues } = methods;
const assistant = useWatch({ control, name: 'assistant' });
const assistant_id = useWatch({ control, name: 'id' });
const files = useMemo(() => {
if (typeof assistant === 'string') {
return [];
}
return assistant.code_files;
}, [assistant]);
return (
<>
<div className="flex items-center">
<Controller
name={Capabilities.code_interpreter}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
/>
)}
/>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={Capabilities.code_interpreter}
onClick={() =>
setValue(Capabilities.code_interpreter, !getValues(Capabilities.code_interpreter), {
shouldDirty: true,
})
}
>
<div className="flex select-none items-center">
{localize('com_assistants_code_interpreter')}
<QuestionMark />
</div>
</label>
</div>
{version == 2 && (
<CodeFiles
assistant_id={assistant_id}
version={version}
endpoint={endpoint}
files={files}
/>
)}
</>
);
}

View file

@ -0,0 +1,98 @@
import { useState, useRef, useEffect } from 'react';
import {
EToolResources,
mergeFileConfig,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { useGetFileConfig } from '~/data-provider';
import { useFileHandling } from '~/hooks/Files';
import useLocalize from '~/hooks/useLocalize';
import { useChatContext } from '~/Providers';
const tool_resource = EToolResources.code_interpreter;
export default function CodeFiles({
endpoint,
assistant_id,
files: _files,
}: {
version: number | string;
endpoint: AssistantsEndpoint;
assistant_id: string;
files?: [string, ExtendedFile][];
}) {
const localize = useLocalize();
const { setFilesLoading } = useChatContext();
const fileInputRef = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
});
const { handleFileChange } = useFileHandling({
overrideEndpoint: endpoint,
additionalMetadata: { assistant_id, tool_resource },
fileSetter: setFiles,
});
useEffect(() => {
if (_files) {
setFiles(new Map(_files));
}
}, [_files]);
const endpointFileConfig = fileConfig.endpoints[endpoint];
if (endpointFileConfig?.disabled) {
return null;
}
const handleButtonClick = () => {
// necessary to reset the input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
fileInputRef.current?.click();
};
return (
<div className={'mb-2'}>
<div className="flex flex-col gap-4">
<div className="text-token-text-tertiary rounded-lg text-xs">
{localize('com_assistants_code_interpreter_files')}
</div>
<FileRow
files={files}
setFiles={setFiles}
assistant_id={assistant_id}
tool_resource={tool_resource}
setFilesLoading={setFilesLoading}
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
/>
<div>
<button
type="button"
disabled={!assistant_id}
className="btn btn-neutral border-token-border-light relative h-8 rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-2">
<input
multiple={true}
type="file"
style={{ display: 'none' }}
tabIndex={-1}
ref={fileInputRef}
disabled={!assistant_id}
onChange={handleFileChange}
/>
{localize('com_ui_upload_files')}
</div>
</button>
</div>
</div>
</div>
);
}

View file

@ -1,26 +1,29 @@
import * as Popover from '@radix-ui/react-popover';
import type { Assistant, AssistantCreateParams } from 'librechat-data-provider';
import type { Assistant, AssistantCreateParams, AssistantsEndpoint } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import { Dialog, DialogTrigger, Label } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { useChatContext, useToastContext } from '~/Providers';
import { useDeleteAssistantMutation } from '~/data-provider';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { useLocalize, useSetIndexOptions } from '~/hooks';
import { cn, removeFocusOutlines } from '~/utils/';
import { NewTrashIcon } from '~/components/svg';
import { useChatContext } from '~/Providers';
export default function ContextButton({
activeModel,
assistant_id,
setCurrentAssistantId,
createMutation,
endpoint,
}: {
activeModel: string;
assistant_id: string;
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
endpoint: AssistantsEndpoint;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
const { conversation } = useChatContext();
const { setOption } = useSetIndexOptions();
@ -31,6 +34,11 @@ export default function ContextButton({
return;
}
showToast({
message: localize('com_ui_assistant_deleted'),
status: 'success',
});
if (createMutation.data?.id) {
console.log('[deleteAssistant] resetting createMutation');
createMutation.reset();
@ -55,6 +63,13 @@ export default function ContextButton({
setCurrentAssistantId(firstAssistant.id);
},
onError: (error) => {
console.error(error);
showToast({
message: localize('com_ui_assistant_delete_error'),
status: 'error',
});
},
});
if (!assistant_id) {
@ -138,7 +153,8 @@ export default function ContextButton({
</>
}
selection={{
selectHandler: () => deleteAssistant.mutate({ assistant_id, model: activeModel }),
selectHandler: () =>
deleteAssistant.mutate({ assistant_id, model: activeModel, endpoint }),
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}

View file

@ -0,0 +1,43 @@
import { useFormContext, Controller } from 'react-hook-form';
import { Capabilities } from 'librechat-data-provider';
import type { AssistantForm } from '~/common';
import { Checkbox, QuestionMark } from '~/components/ui';
import { useLocalize } from '~/hooks';
export default function ImageVision() {
const localize = useLocalize();
const methods = useFormContext<AssistantForm>();
const { control, setValue, getValues } = methods;
return (
<div className="flex items-center">
<Controller
name={Capabilities.image_vision}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
/>
)}
/>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={Capabilities.image_vision}
onClick={() =>
setValue(Capabilities.image_vision, !getValues(Capabilities.image_vision), {
shouldDirty: true,
})
}
>
<div className="flex items-center">
{localize('com_assistants_image_vision')}
<QuestionMark />
</div>
</label>
</div>
);
}

View file

@ -41,10 +41,10 @@ export const AssistantAvatar = ({
return (
<div>
<div className="relative overflow-hidden rounded-full">
<div className="relative h-20 w-20 overflow-hidden rounded-full">
<img
src={url}
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full"
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
alt="GPT"
width="80"
height="80"

View file

@ -1,10 +1,10 @@
import { useState, useRef, useEffect } from 'react';
import {
EModelEndpoint,
mergeFileConfig,
retrievalMimeTypes,
fileConfig as defaultFileConfig,
mergeFileConfig,
} from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { useGetFileConfig } from '~/data-provider';
@ -26,9 +26,11 @@ const CodeInterpreterFiles = ({ children }: { children: React.ReactNode }) => {
};
export default function Knowledge({
endpoint,
assistant_id,
files: _files,
}: {
endpoint: AssistantsEndpoint;
assistant_id: string;
files?: [string, ExtendedFile][];
}) {
@ -40,7 +42,7 @@ export default function Knowledge({
select: (data) => mergeFileConfig(data),
});
const { handleFileChange } = useFileHandling({
overrideEndpoint: EModelEndpoint.assistants,
overrideEndpoint: endpoint,
additionalMetadata: { assistant_id },
fileSetter: setFiles,
});
@ -51,7 +53,7 @@ export default function Knowledge({
}
}, [_files]);
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.assistants];
const endpointFileConfig = fileConfig.endpoints[endpoint];
if (endpointFileConfig?.disabled) {
return null;

View file

@ -1,5 +1,7 @@
import { useState, useEffect } from 'react';
import type { Action } from 'librechat-data-provider';
import { useState, useEffect, useMemo } from 'react';
import { defaultAssistantsVersion } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { Action, AssistantsEndpoint, TEndpointsConfig } from 'librechat-data-provider';
import { useGetActionsQuery } from '~/data-provider';
import AssistantPanel from './AssistantPanel';
import { useChatContext } from '~/Providers';
@ -9,11 +11,18 @@ import { Panel } from '~/common';
export default function PanelSwitch() {
const { conversation, index } = useChatContext();
const [activePanel, setActivePanel] = useState(Panel.builder);
const [action, setAction] = useState<Action | undefined>(undefined);
const [currentAssistantId, setCurrentAssistantId] = useState<string | undefined>(
conversation?.assistant_id,
);
const [action, setAction] = useState<Action | undefined>(undefined);
const { data: actions = [] } = useGetActionsQuery();
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const { data: actions = [] } = useGetActionsQuery(conversation?.endpoint as AssistantsEndpoint);
const assistantsConfig = useMemo(
() => endpointsConfig?.[conversation?.endpoint ?? ''],
[conversation?.endpoint, endpointsConfig],
);
useEffect(() => {
if (conversation?.assistant_id) {
@ -21,6 +30,12 @@ export default function PanelSwitch() {
}
}, [conversation?.assistant_id]);
if (!conversation?.endpoint) {
return null;
}
const version = assistantsConfig?.version ?? defaultAssistantsVersion[conversation.endpoint];
if (activePanel === Panel.actions || action) {
return (
<ActionsPanel
@ -32,6 +47,8 @@ export default function PanelSwitch() {
setActivePanel={setActivePanel}
assistant_id={currentAssistantId}
setCurrentAssistantId={setCurrentAssistantId}
endpoint={conversation.endpoint as AssistantsEndpoint}
version={version}
/>
);
} else if (activePanel === Panel.builder) {
@ -45,6 +62,9 @@ export default function PanelSwitch() {
setActivePanel={setActivePanel}
assistant_id={currentAssistantId}
setCurrentAssistantId={setCurrentAssistantId}
endpoint={conversation.endpoint as AssistantsEndpoint}
assistantsConfig={assistantsConfig}
version={version}
/>
);
}

View file

@ -0,0 +1,94 @@
import { useEffect, useMemo } from 'react';
import { useFormContext, Controller, useWatch } from 'react-hook-form';
import { Capabilities } from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { AssistantForm } from '~/common';
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
import { Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
import { cn } from '~/utils/';
export default function Retrieval({
version,
retrievalModels,
}: {
version: number | string;
retrievalModels: Set<string>;
endpoint: AssistantsEndpoint;
}) {
const localize = useLocalize();
const methods = useFormContext<AssistantForm>();
const { control, setValue, getValues } = methods;
const model = useWatch({ control, name: 'model' });
const assistant = useWatch({ control, name: 'assistant' });
const vectorStores = useMemo(() => {
if (typeof assistant === 'string') {
return [];
}
return assistant.tool_resources?.file_search;
}, [assistant]);
const isDisabled = useMemo(() => !retrievalModels.has(model), [model, retrievalModels]);
useEffect(() => {
if (model && isDisabled) {
setValue(Capabilities.retrieval, false);
}
}, [model, setValue, isDisabled]);
return (
<>
<div className="flex items-center">
<Controller
name={Capabilities.retrieval}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
disabled={isDisabled}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
/>
)}
/>
<HoverCard openDelay={50}>
<HoverCardTrigger asChild>
<label
className={cn(
'form-check-label text-token-text-primary w-full select-none',
isDisabled ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
)}
htmlFor={Capabilities.retrieval}
onClick={() =>
retrievalModels.has(model) &&
setValue(Capabilities.retrieval, !getValues(Capabilities.retrieval), {
shouldDirty: true,
})
}
>
{version == 1
? localize('com_assistants_retrieval')
: localize('com_assistants_file_search')}
</label>
</HoverCardTrigger>
<OptionHover
side={ESide.Top}
disabled={!isDisabled}
description="com_assistants_non_retrieval_model"
langCode={true}
sideOffset={20}
/>
</HoverCard>
</div>
{version == 2 && (
<div className="text-token-text-tertiary rounded-lg text-xs">
{localize('com_assistants_file_search_info')}
</div>
)}
</>
);
}

View file

@ -1,8 +1,10 @@
import { useCallback } from 'react';
import {
fileConfig as defaultFileConfig,
checkOpenAIStorage,
mergeFileConfig,
megabyte,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type { Row } from '@tanstack/react-table';
import type { TFile } from 'librechat-data-provider';
@ -36,6 +38,18 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
return showToast({ message: localize('com_ui_attach_error'), status: 'error' });
}
if (checkOpenAIStorage(fileData?.source ?? '') && !isAssistantsEndpoint(endpoint)) {
return showToast({
message: localize('com_ui_attach_error_openai'),
status: 'error',
});
} else if (!checkOpenAIStorage(fileData?.source ?? '') && isAssistantsEndpoint(endpoint)) {
showToast({
message: localize('com_ui_attach_warn_endpoint'),
status: 'warning',
});
}
const { fileSizeLimit, supportedMimeTypes } =
fileConfig.endpoints[endpoint] ?? fileConfig.endpoints.default;
@ -81,7 +95,8 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
>
<ImagePreview
url={file.filepath}
className="h-10 w-10 shrink-0 overflow-hidden rounded-md"
className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
source={file.source}
/>
<span className="self-center truncate text-xs">{file.filename}</span>
</div>
@ -94,7 +109,7 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
onClick={handleFileClick}
className="flex cursor-pointer gap-2 rounded-md dark:hover:bg-gray-700"
>
{fileType && <FilePreview fileType={fileType} />}
{fileType && <FilePreview fileType={fileType} className="relative" file={file} />}
<span className="self-center truncate">{file.filename}</span>
</div>
);

View file

@ -7,11 +7,21 @@ type TOptionHoverProps = {
description: string;
langCode?: boolean;
sideOffset?: number;
disabled?: boolean;
side: ESide;
};
function OptionHover({ side, description, langCode, sideOffset = 30 }: TOptionHoverProps) {
function OptionHover({
side,
description,
disabled,
langCode,
sideOffset = 30,
}: TOptionHoverProps) {
const localize = useLocalize();
if (disabled) {
return null;
}
const text = langCode ? localize(description) : description;
return (
<HoverCardPortal>

View file

@ -1,5 +1,5 @@
import throttle from 'lodash/throttle';
import { EModelEndpoint, getConfigDefaults } from 'librechat-data-provider';
import { getConfigDefaults } from 'librechat-data-provider';
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
import {
useGetEndpointsQuery,
@ -61,7 +61,7 @@ const SidePanel = ({
return activePanel ? activePanel : undefined;
}, []);
const assistants = useMemo(() => endpointsConfig?.[EModelEndpoint.assistants], [endpointsConfig]);
const assistants = useMemo(() => endpointsConfig?.[endpoint ?? ''], [endpoint, endpointsConfig]);
const userProvidesKey = useMemo(
() => !!endpointsConfig?.[endpoint ?? '']?.userProvide,
[endpointsConfig, endpoint],

View file

@ -1,18 +1,18 @@
import { EModelEndpoint } from 'librechat-data-provider';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { SwitcherProps } from '~/common';
import { Separator } from '~/components/ui/Separator';
import AssistantSwitcher from './AssistantSwitcher';
import ModelSwitcher from './ModelSwitcher';
export default function Switcher(props: SwitcherProps) {
if (props.endpoint === EModelEndpoint.assistants && props.endpointKeyProvided) {
if (isAssistantsEndpoint(props.endpoint) && props.endpointKeyProvided) {
return (
<>
<AssistantSwitcher {...props} />
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
</>
);
} else if (props.endpoint === EModelEndpoint.assistants) {
} else if (isAssistantsEndpoint(props.endpoint)) {
return null;
}

View file

@ -3,7 +3,7 @@ import { Search, X } from 'lucide-react';
import { Dialog } from '@headlessui/react';
import { useFormContext } from 'react-hook-form';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import type { TError, TPluginAction } from 'librechat-data-provider';
import type { AssistantsEndpoint, TError, TPluginAction } from 'librechat-data-provider';
import type { TPluginStoreDialogProps } from '~/common/types';
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
import { useLocalize, usePluginDialogHelpers } from '~/hooks';
@ -13,10 +13,11 @@ import ToolItem from './ToolItem';
function ToolSelectDialog({
isOpen,
setIsOpen,
}: TPluginStoreDialogProps & { assistant_id?: string }) {
endpoint,
}: TPluginStoreDialogProps & { assistant_id?: string; endpoint: AssistantsEndpoint }) {
const localize = useLocalize();
const { getValues, setValue } = useFormContext();
const { data: tools = [] } = useAvailableToolsQuery();
const { data: tools = [] } = useAvailableToolsQuery(endpoint);
const {
maxPage,

View file

@ -1,4 +1,8 @@
import { LocalStorageKeys } from 'librechat-data-provider';
import {
EToolResources,
LocalStorageKeys,
defaultAssistantsVersion,
} from 'librechat-data-provider';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { UseMutationResult } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
@ -376,9 +380,10 @@ export const useUploadFileMutation = (
const { onSuccess, ...options } = _options || {};
return useMutation([MutationKeys.fileUpload], {
mutationFn: (body: FormData) => {
const height = body.get('height');
const width = body.get('width');
if (height && width) {
const height = body.get('height');
const version = body.get('version') as number | string;
if (height && width && (!version || version != 2)) {
return dataService.uploadImage(body);
}
@ -391,8 +396,10 @@ export const useUploadFileMutation = (
...(_files ?? []),
]);
const endpoint = formData.get('endpoint');
const assistant_id = formData.get('assistant_id');
const message_file = formData.get('message_file');
const tool_resource = formData.get('tool_resource');
if (!assistant_id || message_file === 'true') {
onSuccess?.(data, formData, context);
@ -400,7 +407,7 @@ export const useUploadFileMutation = (
}
queryClient.setQueryData<t.AssistantListResponse>(
[QueryKeys.assistants, defaultOrderQuery],
[QueryKeys.assistants, endpoint, defaultOrderQuery],
(prev) => {
if (!prev) {
return prev;
@ -409,13 +416,29 @@ export const useUploadFileMutation = (
return {
...prev,
data: prev?.data.map((assistant) => {
if (assistant.id === assistant_id) {
return {
...assistant,
file_ids: [...assistant.file_ids, data.file_id],
if (assistant.id !== assistant_id) {
return assistant;
}
const update = {};
if (!tool_resource) {
update['file_ids'] = [...assistant.file_ids, data.file_id];
}
if (tool_resource === EToolResources.code_interpreter) {
const prevResources = assistant.tool_resources ?? {};
const prevResource = assistant.tool_resources?.[tool_resource as string] ?? {
file_ids: [],
};
prevResource.file_ids.push(data.file_id);
update['tool_resources'] = {
...prevResources,
[tool_resource as string]: prevResource,
};
}
return assistant;
return {
...assistant,
...update,
};
}),
};
},
@ -436,7 +459,8 @@ export const useDeleteFilesMutation = (
const queryClient = useQueryClient();
const { onSuccess, ...options } = _options || {};
return useMutation([MutationKeys.fileDelete], {
mutationFn: (body: t.DeleteFilesBody) => dataService.deleteFiles(body.files, body.assistant_id),
mutationFn: (body: t.DeleteFilesBody) =>
dataService.deleteFiles(body.files, body.assistant_id, body.tool_resource),
...(options || {}),
onSuccess: (data, ...args) => {
queryClient.setQueryData<t.TFile[] | undefined>([QueryKeys.files], (cachefiles) => {
@ -542,6 +566,7 @@ export const useCreateAssistantMutation = (
onSuccess: (newAssistant, variables, context) => {
const listRes = queryClient.getQueryData<t.AssistantListResponse>([
QueryKeys.assistants,
variables.endpoint,
defaultOrderQuery,
]);
@ -552,7 +577,7 @@ export const useCreateAssistantMutation = (
const currentAssistants = [newAssistant, ...JSON.parse(JSON.stringify(listRes.data))];
queryClient.setQueryData<t.AssistantListResponse>(
[QueryKeys.assistants, defaultOrderQuery],
[QueryKeys.assistants, variables.endpoint, defaultOrderQuery],
{
...listRes,
data: currentAssistants,
@ -576,14 +601,23 @@ export const useUpdateAssistantMutation = (
> => {
const queryClient = useQueryClient();
return useMutation(
({ assistant_id, data }: { assistant_id: string; data: t.AssistantUpdateParams }) =>
dataService.updateAssistant(assistant_id, data),
({ assistant_id, data }: { assistant_id: string; data: t.AssistantUpdateParams }) => {
const { endpoint } = data;
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return dataService.updateAssistant({
data,
version,
assistant_id,
});
},
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (updatedAssistant, variables, context) => {
const listRes = queryClient.getQueryData<t.AssistantListResponse>([
QueryKeys.assistants,
variables.data.endpoint,
defaultOrderQuery,
]);
@ -592,7 +626,7 @@ export const useUpdateAssistantMutation = (
}
queryClient.setQueryData<t.AssistantListResponse>(
[QueryKeys.assistants, defaultOrderQuery],
[QueryKeys.assistants, variables.data.endpoint, defaultOrderQuery],
{
...listRes,
data: listRes.data.map((assistant) => {
@ -617,14 +651,18 @@ export const useDeleteAssistantMutation = (
): UseMutationResult<void, Error, t.DeleteAssistantBody> => {
const queryClient = useQueryClient();
return useMutation(
({ assistant_id, model }: t.DeleteAssistantBody) =>
dataService.deleteAssistant(assistant_id, model),
({ assistant_id, model, endpoint }: t.DeleteAssistantBody) => {
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return dataService.deleteAssistant({ assistant_id, model, version, endpoint });
},
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (_data, variables, context) => {
const listRes = queryClient.getQueryData<t.AssistantListResponse>([
QueryKeys.assistants,
variables.endpoint,
defaultOrderQuery,
]);
@ -635,7 +673,7 @@ export const useDeleteAssistantMutation = (
const data = listRes.data.filter((assistant) => assistant.id !== variables.assistant_id);
queryClient.setQueryData<t.AssistantListResponse>(
[QueryKeys.assistants, defaultOrderQuery],
[QueryKeys.assistants, variables.endpoint, defaultOrderQuery],
{
...listRes,
data,
@ -687,6 +725,7 @@ export const useUpdateAction = (
onSuccess: (updateActionResponse, variables, context) => {
const listRes = queryClient.getQueryData<t.AssistantListResponse>([
QueryKeys.assistants,
variables.endpoint,
defaultOrderQuery,
]);
@ -696,15 +735,18 @@ export const useUpdateAction = (
const updatedAssistant = updateActionResponse[1];
queryClient.setQueryData<t.AssistantListResponse>([QueryKeys.assistants, defaultOrderQuery], {
...listRes,
data: listRes.data.map((assistant) => {
if (assistant.id === variables.assistant_id) {
return updatedAssistant;
}
return assistant;
}),
});
queryClient.setQueryData<t.AssistantListResponse>(
[QueryKeys.assistants, variables.endpoint, defaultOrderQuery],
{
...listRes,
data: listRes.data.map((assistant) => {
if (assistant.id === variables.assistant_id) {
return updatedAssistant;
}
return assistant;
}),
},
);
queryClient.setQueryData<t.Action[]>([QueryKeys.actions], (prev) => {
return prev
@ -735,8 +777,15 @@ export const useDeleteAction = (
> => {
const queryClient = useQueryClient();
return useMutation([MutationKeys.deleteAction], {
mutationFn: (variables: t.DeleteActionVariables) =>
dataService.deleteAction(variables.assistant_id, variables.action_id, variables.model),
mutationFn: (variables: t.DeleteActionVariables) => {
const { endpoint } = variables;
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return dataService.deleteAction({
...variables,
version,
});
},
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
@ -750,7 +799,7 @@ export const useDeleteAction = (
});
queryClient.setQueryData<t.AssistantListResponse>(
[QueryKeys.assistants, defaultOrderQuery],
[QueryKeys.assistants, variables.endpoint, defaultOrderQuery],
(prev) => {
if (!prev) {
return prev;

View file

@ -1,4 +1,9 @@
import { EModelEndpoint, QueryKeys, dataService, defaultOrderQuery } from 'librechat-data-provider';
import {
QueryKeys,
dataService,
defaultOrderQuery,
defaultAssistantsVersion,
} from 'librechat-data-provider';
import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import type {
UseInfiniteQueryOptions,
@ -194,43 +199,46 @@ export const useSharedLinksInfiniteQuery = (
/**
* Hook for getting all available tools for Assistants
*/
export const useAvailableToolsQuery = (): QueryObserverResult<TPlugin[]> => {
export const useAvailableToolsQuery = (
endpoint: t.AssistantsEndpoint,
): QueryObserverResult<TPlugin[]> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([
QueryKeys.name,
EModelEndpoint.assistants,
]);
const userProvidesKey = !!endpointsConfig?.[EModelEndpoint.assistants]?.userProvide;
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[EModelEndpoint.assistants] && keyProvided;
return useQuery<TPlugin[]>([QueryKeys.tools], () => dataService.getAvailableTools(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled,
});
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<TPlugin[]>(
[QueryKeys.tools],
() => dataService.getAvailableTools(version, endpoint),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled,
},
);
};
/**
* Hook for listing all assistants, with optional parameters provided for pagination and sorting
*/
export const useListAssistantsQuery = <TData = AssistantListResponse>(
params: AssistantListParams = defaultOrderQuery,
endpoint: t.AssistantsEndpoint,
params: Omit<AssistantListParams, 'endpoint'> = defaultOrderQuery,
config?: UseQueryOptions<AssistantListResponse, unknown, TData>,
): QueryObserverResult<TData> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([
QueryKeys.name,
EModelEndpoint.assistants,
]);
const userProvidesKey = !!endpointsConfig?.[EModelEndpoint.assistants]?.userProvide;
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[EModelEndpoint.assistants] && keyProvided;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<AssistantListResponse, unknown, TData>(
[QueryKeys.assistants, params],
() => dataService.listAssistants(params),
[QueryKeys.assistants, endpoint, params],
() => dataService.listAssistants({ ...params, endpoint }, version),
{
// Example selector to sort them by created_at
// select: (res) => {
@ -246,6 +254,7 @@ export const useListAssistantsQuery = <TData = AssistantListResponse>(
);
};
/*
export const useListAssistantsInfiniteQuery = (
params?: AssistantListParams,
config?: UseInfiniteQueryOptions<AssistantListResponse, Error>,
@ -275,26 +284,31 @@ export const useListAssistantsInfiniteQuery = (
},
);
};
*/
/**
* Hook for retrieving details about a single assistant
*/
export const useGetAssistantByIdQuery = (
endpoint: t.AssistantsEndpoint,
assistant_id: string,
config?: UseQueryOptions<Assistant>,
): QueryObserverResult<Assistant> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([
QueryKeys.name,
EModelEndpoint.assistants,
]);
const userProvidesKey = !!endpointsConfig?.[EModelEndpoint.assistants]?.userProvide;
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[EModelEndpoint.assistants] && keyProvided;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<Assistant>(
[QueryKeys.assistant, assistant_id],
() => dataService.getAssistantById(assistant_id),
() =>
dataService.getAssistantById({
endpoint,
assistant_id,
version,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
@ -311,43 +325,53 @@ export const useGetAssistantByIdQuery = (
* Hook for retrieving user's saved Assistant Actions
*/
export const useGetActionsQuery = <TData = Action[]>(
endpoint: t.AssistantsEndpoint,
config?: UseQueryOptions<Action[], unknown, TData>,
): QueryObserverResult<TData> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([
QueryKeys.name,
EModelEndpoint.assistants,
]);
const userProvidesKey = !!endpointsConfig?.[EModelEndpoint.assistants]?.userProvide;
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[EModelEndpoint.assistants] && keyProvided;
return useQuery<Action[], unknown, TData>([QueryKeys.actions], () => dataService.getActions(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled,
});
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<Action[], unknown, TData>(
[QueryKeys.actions],
() =>
dataService.getActions({
endpoint,
version,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: config?.enabled !== undefined ? config?.enabled && enabled : enabled,
},
);
};
/**
* Hook for retrieving user's saved Assistant Documents (metadata saved to Database)
*/
export const useGetAssistantDocsQuery = (
endpoint: t.AssistantsEndpoint,
config?: UseQueryOptions<AssistantDocument[]>,
): QueryObserverResult<AssistantDocument[], unknown> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([
QueryKeys.name,
EModelEndpoint.assistants,
]);
const userProvidesKey = !!endpointsConfig?.[EModelEndpoint.assistants]?.userProvide;
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const enabled = !!endpointsConfig?.[EModelEndpoint.assistants] && keyProvided;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<AssistantDocument[]>(
[QueryKeys.assistantDocs],
() => dataService.getAssistantDocs(),
() =>
dataService.getAssistantDocs({
endpoint,
version,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,

View file

@ -1,2 +1,3 @@
export { default as useAssistantsMap } from './useAssistantsMap';
export { default as useSelectAssistant } from './useSelectAssistant';
export { default as useAssistantListMap } from './useAssistantListMap';

View file

@ -0,0 +1,44 @@
import { useMemo } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import type { AssistantListResponse, AssistantsEndpoint } from 'librechat-data-provider';
import type { AssistantListItem } from '~/common';
import { useListAssistantsQuery } from '~/data-provider';
const selectAssistantsResponse = (res: AssistantListResponse): AssistantListItem[] =>
res.data.map(({ id, name, metadata, model }) => ({
id,
name: name ?? '',
metadata,
model,
}));
export default function useAssistantListMap<T = AssistantListItem[] | null>(
selector: (res: AssistantListResponse) => T = selectAssistantsResponse as (
res: AssistantListResponse,
) => T,
): Record<AssistantsEndpoint, T> {
const { data: assistantsList = null } = useListAssistantsQuery(
EModelEndpoint.assistants,
undefined,
{
select: selector,
},
);
const { data: azureAssistants = null } = useListAssistantsQuery(
EModelEndpoint.azureAssistants,
undefined,
{
select: selector,
},
);
const assistantListMap = useMemo(() => {
return {
[EModelEndpoint.assistants]: assistantsList as T,
[EModelEndpoint.azureAssistants]: azureAssistants as T,
};
}, [assistantsList, azureAssistants]);
return assistantListMap;
}

View file

@ -1,12 +1,28 @@
import { defaultOrderQuery } from 'librechat-data-provider';
import { EModelEndpoint } from 'librechat-data-provider';
import type { TAssistantsMap } from 'librechat-data-provider';
import { useListAssistantsQuery } from '~/data-provider';
import { mapAssistants } from '~/utils';
export default function useAssistantsMap({ isAuthenticated }: { isAuthenticated: boolean }) {
const { data: assistantMap = {} } = useListAssistantsQuery(defaultOrderQuery, {
export default function useAssistantsMap({
isAuthenticated,
}: {
isAuthenticated: boolean;
}): TAssistantsMap {
const { data: assistants = {} } = useListAssistantsQuery(EModelEndpoint.assistants, undefined, {
select: (res) => mapAssistants(res.data),
enabled: isAuthenticated,
});
const { data: azureAssistants = {} } = useListAssistantsQuery(
EModelEndpoint.azureAssistants,
undefined,
{
select: (res) => mapAssistants(res.data),
enabled: isAuthenticated,
},
);
return assistantMap;
return {
[EModelEndpoint.assistants]: assistants,
[EModelEndpoint.azureAssistants]: azureAssistants,
};
}

View file

@ -1,32 +1,30 @@
import { useCallback } from 'react';
import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
import type { TConversation, TPreset } from 'librechat-data-provider';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { AssistantsEndpoint, TConversation, TPreset } from 'librechat-data-provider';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useListAssistantsQuery } from '~/data-provider';
import { useChatContext } from '~/Providers/ChatContext';
import useAssistantListMap from './useAssistantListMap';
import { mapAssistants } from '~/utils';
export default function useSelectAssistant() {
export default function useSelectAssistant(endpoint: AssistantsEndpoint) {
const getDefaultConversation = useDefaultConvo();
const { conversation, newConversation } = useChatContext();
const { data: assistantMap = {} } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) => mapAssistants(res.data),
});
const assistantMap = useAssistantListMap((res) => mapAssistants(res.data));
const onSelect = useCallback(
(value: string) => {
const assistant = assistantMap?.[value];
const assistant = assistantMap?.[endpoint]?.[value];
if (!assistant) {
return;
}
const template: Partial<TPreset | TConversation> = {
endpoint: EModelEndpoint.assistants,
endpoint,
assistant_id: assistant.id,
model: assistant.model,
conversationId: 'new',
};
if (conversation?.endpoint === EModelEndpoint.assistants) {
if (isAssistantsEndpoint(conversation?.endpoint)) {
const currentConvo = getDefaultConversation({
conversation: { ...(conversation ?? {}) },
preset: template,
@ -44,7 +42,7 @@ export default function useSelectAssistant() {
preset: template as Partial<TPreset>,
});
},
[assistantMap, conversation, getDefaultConversation, newConversation],
[endpoint, assistantMap, conversation, getDefaultConversation, newConversation],
);
return { onSelect };

View file

@ -1,4 +1,5 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil';
import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query';
import type {
@ -10,11 +11,10 @@ import type {
TEndpointsConfig,
} from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import useOriginNavigate from '../useOriginNavigate';
import store from '~/store';
const useConversation = () => {
const navigate = useOriginNavigate();
const navigate = useNavigate();
const setConversation = useSetRecoilState(store.conversation);
const resetLatestMessage = useResetRecoilState(store.latestMessage);
const setMessages = useSetRecoilState<TMessagesAtom>(store.messages);
@ -59,7 +59,7 @@ const useConversation = () => {
resetLatestMessage();
if (conversation.conversationId === 'new' && !modelsData) {
navigate('new');
navigate('/c/new');
}
},
[endpointsConfig, modelsQuery.data],

View file

@ -1,14 +1,14 @@
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useSetRecoilState, useResetRecoilState } from 'recoil';
import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import useOriginNavigate from '../useOriginNavigate';
import store from '~/store';
const useNavigateToConvo = (index = 0) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const navigate = useOriginNavigate();
const { setConversation } = store.useCreateConversationAtom(index);
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
@ -48,7 +48,7 @@ const useNavigateToConvo = (index = 0) => {
});
}
setConversation(convo);
navigate(convo?.conversationId);
navigate(`/c/${convo.conversationId ?? 'new'}`);
};
const navigateWithLastTools = (conversation: TConversation) => {

View file

@ -3,7 +3,7 @@ import exportFromJSON from 'export-from-json';
import { useCallback, useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
import { QueryKeys, modularEndpoints, EModelEndpoint } from 'librechat-data-provider';
import { QueryKeys, modularEndpoints, isAssistantsEndpoint } from 'librechat-data-provider';
import { useCreatePresetMutation, useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider';
import {
@ -174,8 +174,8 @@ export default function usePresets() {
const currentEndpointType = getEndpointField(endpointsConfig, endpoint, 'type');
const endpointType = getEndpointField(endpointsConfig, newPreset.endpoint, 'type');
const isAssistantSwitch =
newPreset.endpoint === EModelEndpoint.assistants &&
conversation?.endpoint === EModelEndpoint.assistants &&
isAssistantsEndpoint(newPreset.endpoint) &&
isAssistantsEndpoint(conversation?.endpoint) &&
conversation?.endpoint === newPreset.endpoint;
if (

View file

@ -1,5 +1,5 @@
import debounce from 'lodash/debounce';
import { FileSources } from 'librechat-data-provider';
import { FileSources, EToolResources } from 'librechat-data-provider';
import { useCallback, useState, useEffect } from 'react';
import type {
BatchFile,
@ -16,18 +16,20 @@ type FileMapSetter = GenericSetter<Map<string, ExtendedFile>>;
const useFileDeletion = ({
mutateAsync,
assistant_id,
tool_resource,
}: {
mutateAsync: UseMutateAsyncFunction<DeleteFilesResponse, unknown, DeleteFilesBody, unknown>;
assistant_id?: string;
tool_resource?: EToolResources;
}) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_batch, setFileDeleteBatch] = useState<BatchFile[]>([]);
const setFilesToDelete = useSetFilesToDelete();
const executeBatchDelete = useCallback(
(filesToDelete: BatchFile[], assistant_id?: string) => {
console.log('Deleting files:', filesToDelete, assistant_id);
mutateAsync({ files: filesToDelete, assistant_id });
(filesToDelete: BatchFile[], assistant_id?: string, tool_resource?: EToolResources) => {
console.log('Deleting files:', filesToDelete, assistant_id, tool_resource);
mutateAsync({ files: filesToDelete, assistant_id, tool_resource });
setFileDeleteBatch([]);
},
[mutateAsync],
@ -81,11 +83,11 @@ const useFileDeletion = ({
setFileDeleteBatch((prevBatch) => {
const newBatch = [...prevBatch, file];
debouncedDelete(newBatch, assistant_id);
debouncedDelete(newBatch, assistant_id, tool_resource);
return newBatch;
});
},
[debouncedDelete, setFilesToDelete, assistant_id],
[debouncedDelete, setFilesToDelete, assistant_id, tool_resource],
);
const deleteFiles = useCallback(

View file

@ -1,13 +1,18 @@
import { v4 } from 'uuid';
import debounce from 'lodash/debounce';
import { useQueryClient } from '@tanstack/react-query';
import { useState, useEffect, useCallback } from 'react';
import {
megabyte,
QueryKeys,
EModelEndpoint,
codeTypeMapping,
mergeFileConfig,
isAssistantsEndpoint,
defaultAssistantsVersion,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import type { TEndpointsConfig } from 'librechat-data-provider';
import type { ExtendedFile, FileSetter } from '~/common';
import { useUploadFileMutation, useGetFileConfig } from '~/data-provider';
import { useDelayedUploadToast } from './useDelayedUploadToast';
@ -20,10 +25,12 @@ const { checkType } = defaultFileConfig;
type UseFileHandling = {
overrideEndpoint?: EModelEndpoint;
fileSetter?: FileSetter;
additionalMetadata?: Record<string, string>;
fileFilter?: (file: File) => boolean;
additionalMetadata?: Record<string, string | undefined>;
};
const useFileHandling = (params?: UseFileHandling) => {
const queryClient = useQueryClient();
const { showToast } = useToastContext();
const [errors, setErrors] = useState<string[]>([]);
const { startUploadTimer, clearUploadTimer } = useDelayedUploadToast();
@ -141,15 +148,20 @@ const useFileHandling = (params?: UseFileHandling) => {
if (params?.additionalMetadata) {
for (const [key, value] of Object.entries(params.additionalMetadata)) {
formData.append(key, value);
if (value) {
formData.append(key, value);
}
}
}
if (
endpoint === EModelEndpoint.assistants &&
isAssistantsEndpoint(endpoint) &&
!formData.get('assistant_id') &&
conversation?.assistant_id
) {
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
formData.append('version', version);
formData.append('assistant_id', conversation.assistant_id);
formData.append('model', conversation?.model ?? '');
formData.append('message_file', 'true');

View file

@ -5,15 +5,42 @@ import {
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import { getConfigDefaults, EModelEndpoint, alternateName } from 'librechat-data-provider';
import type { Assistant } from 'librechat-data-provider';
import { useGetPresetsQuery, useListAssistantsQuery } from '~/data-provider';
import type { AssistantsEndpoint, TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
import type { MentionOption } from '~/common';
import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
import { mapEndpoints, getPresetTitle } from '~/utils';
import { EndpointIcon } from '~/components/Endpoints';
import { useGetPresetsQuery } from '~/data-provider';
import useSelectMention from './useSelectMention';
const defaultInterface = getConfigDefaults().interface;
export default function useMentions({ assistantMap }: { assistantMap: Record<string, Assistant> }) {
const assistantMapFn =
({
endpoint,
assistantMap,
endpointsConfig,
}: {
endpoint: AssistantsEndpoint;
assistantMap: TAssistantsMap;
endpointsConfig: TEndpointsConfig;
}) =>
({ id, name, description }) => ({
type: endpoint,
label: name ?? '',
value: id,
description: description ?? '',
icon: EndpointIcon({
conversation: { assistant_id: id, endpoint },
containerClassName: 'shadow-stroke overflow-hidden rounded-full',
endpointsConfig: endpointsConfig,
context: 'menu-item',
assistantMap,
size: 20,
}),
});
export default function useMentions({ assistantMap }: { assistantMap: TAssistantsMap }) {
const { data: presets } = useGetPresetsQuery();
const { data: modelsConfig } = useGetModelsQuery();
const { data: startupConfig } = useGetStartupConfig();
@ -21,30 +48,43 @@ export default function useMentions({ assistantMap }: { assistantMap: Record<str
const { data: endpoints = [] } = useGetEndpointsQuery({
select: mapEndpoints,
});
const { data: assistants = [] } = useListAssistantsQuery(undefined, {
select: (res) =>
res.data
.map(({ id, name, description }) => ({
type: 'assistant',
label: name ?? '',
value: id,
description: description ?? '',
icon: EndpointIcon({
conversation: { assistant_id: id, endpoint: EModelEndpoint.assistants },
containerClassName: 'shadow-stroke overflow-hidden rounded-full',
endpointsConfig: endpointsConfig,
context: 'menu-item',
const listMap = useAssistantListMap((res) =>
res.data.map(({ id, name, description }) => ({
id,
name,
description,
})),
);
const assistantListMap = useMemo(
() => ({
[EModelEndpoint.assistants]: listMap[EModelEndpoint.assistants]
?.map(
assistantMapFn({
endpoint: EModelEndpoint.assistants,
assistantMap,
size: 20,
endpointsConfig,
}),
}))
.filter(Boolean),
});
)
?.filter(Boolean),
[EModelEndpoint.azureAssistants]: listMap[EModelEndpoint.azureAssistants]
?.map(
assistantMapFn({
endpoint: EModelEndpoint.azureAssistants,
assistantMap,
endpointsConfig,
}),
)
?.filter(Boolean),
}),
[listMap, assistantMap, endpointsConfig],
);
const modelSpecs = useMemo(() => startupConfig?.modelSpecs?.list ?? [], [startupConfig]);
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
);
const { onSelectMention } = useSelectMention({
modelSpecs,
endpointsConfig,
@ -52,7 +92,7 @@ export default function useMentions({ assistantMap }: { assistantMap: Record<str
assistantMap,
});
const options = useMemo(() => {
const options: MentionOption[] = useMemo(() => {
const mentions = [
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
value: modelSpec.name,
@ -67,12 +107,12 @@ export default function useMentions({ assistantMap }: { assistantMap: Record<str
context: 'menu-item',
size: 20,
}),
type: 'modelSpec',
type: 'modelSpec' as const,
})),
...(interfaceConfig.endpointsMenu ? endpoints : []).map((endpoint) => ({
value: endpoint,
label: alternateName[endpoint] ?? endpoint ?? '',
type: 'endpoint',
type: 'endpoint' as const,
icon: EndpointIcon({
conversation: { endpoint },
endpointsConfig,
@ -80,7 +120,12 @@ export default function useMentions({ assistantMap }: { assistantMap: Record<str
size: 20,
}),
})),
...(endpointsConfig?.[EModelEndpoint.assistants] ? assistants : []),
...(endpointsConfig?.[EModelEndpoint.assistants]
? assistantListMap[EModelEndpoint.assistants]
: []),
...(endpointsConfig?.[EModelEndpoint.azureAssistants]
? assistantListMap[EModelEndpoint.azureAssistants]
: []),
...((interfaceConfig.presets ? presets : [])?.map((preset, index) => ({
value: preset.presetId ?? `preset-${index}`,
label: preset.title ?? preset.modelLabel ?? preset.chatGptLabel ?? '',
@ -93,7 +138,7 @@ export default function useMentions({ assistantMap }: { assistantMap: Record<str
assistantMap,
size: 20,
}),
type: 'preset',
type: 'preset' as const,
})) ?? []),
];
@ -102,17 +147,17 @@ export default function useMentions({ assistantMap }: { assistantMap: Record<str
presets,
endpoints,
modelSpecs,
assistants,
assistantMap,
endpointsConfig,
assistantListMap,
interfaceConfig.presets,
interfaceConfig.endpointsMenu,
]);
return {
options,
assistants,
modelsConfig,
onSelectMention,
assistantListMap,
};
}

View file

@ -1,12 +1,12 @@
import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TPreset,
TModelSpec,
TConversation,
TAssistantsMap,
TEndpointsConfig,
TPreset,
Assistant,
} from 'librechat-data-provider';
import type { MentionOption } from '~/common';
import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools } from '~/utils';
@ -23,7 +23,7 @@ export default function useSelectMention({
presets?: TPreset[];
modelSpecs: TModelSpec[];
endpointsConfig: TEndpointsConfig;
assistantMap: Record<string, Assistant>;
assistantMap: TAssistantsMap;
}) {
const { conversation } = useChatContext();
const { newConversation } = useNewConvo();
@ -194,10 +194,10 @@ export default function useSelectMention({
onSelectEndpoint(key, { model: option.label });
} else if (option.type === 'endpoint') {
onSelectEndpoint(key);
} else if (option.type === 'assistant') {
onSelectEndpoint(EModelEndpoint.assistants, {
} else if (isAssistantsEndpoint(option.type)) {
onSelectEndpoint(option.type, {
assistant_id: key,
model: assistantMap?.[key]?.model ?? '',
model: assistantMap?.[option.type]?.[key]?.model ?? '',
});
}
},

View file

@ -1,6 +1,6 @@
import debounce from 'lodash/debounce';
import { useEffect, useRef, useCallback } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import type { TEndpointOption } from 'librechat-data-provider';
import type { KeyboardEvent } from 'react';
@ -45,10 +45,11 @@ export default function useTextarea({
const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {};
const isNotAppendable =
((latestMessage?.unfinished && !isSubmitting) || latestMessage?.error) &&
endpoint !== EModelEndpoint.assistants;
!isAssistantsEndpoint(endpoint);
// && (conversationId?.length ?? 0) > 6; // also ensures that we don't show the wrong placeholder
const assistant = endpoint === EModelEndpoint.assistants && assistantMap?.[assistant_id ?? ''];
const assistant =
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint ?? '']?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || '';
// auto focus to input, when enter a conversation.
@ -86,9 +87,11 @@ export default function useTextarea({
if (disabled) {
return localize('com_endpoint_config_placeholder');
}
const currentEndpoint = conversation?.endpoint ?? '';
const currentAssistantId = conversation?.assistant_id ?? '';
if (
conversation?.endpoint === EModelEndpoint.assistants &&
(!conversation?.assistant_id || !assistantMap?.[conversation?.assistant_id ?? ''])
isAssistantsEndpoint(currentEndpoint) &&
(!currentAssistantId || !assistantMap?.[currentEndpoint]?.[currentAssistantId ?? ''])
) {
return localize('com_endpoint_assistant_placeholder');
}
@ -97,10 +100,9 @@ export default function useTextarea({
return localize('com_endpoint_message_not_appendable');
}
const sender =
conversation?.endpoint === EModelEndpoint.assistants
? getAssistantName({ name: assistantName, localize })
: getSender(conversation as TEndpointOption);
const sender = isAssistantsEndpoint(currentEndpoint)
? getAssistantName({ name: assistantName, localize })
: getSender(conversation as TEndpointOption);
return `${localize('com_endpoint_message')} ${sender ? sender : 'ChatGPT'}`;
};

View file

@ -1,5 +1,5 @@
import { useEffect, useRef, useCallback } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import useCopyToClipboard from './useCopyToClipboard';
@ -55,7 +55,8 @@ export default function useMessageHelpers(props: TMessageProps) {
}, [isSubmitting, setAbortScroll]);
const assistant =
conversation?.endpoint === EModelEndpoint.assistants && assistantMap?.[message?.model ?? ''];
isAssistantsEndpoint(conversation?.endpoint) &&
assistantMap?.[conversation?.endpoint ?? '']?.[message?.model ?? ''];
const regenerateMessage = () => {
if ((isSubmitting && isCreatedByUser) || !message) {

View file

@ -1,35 +1,44 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo, useCallback } from 'react';
export default function useProgress(initialProgress = 0.01, increment = 0.007, fileSize?: number) {
const calculateIncrement = (size?: number) => {
const baseRate = 0.05;
const minRate = 0.002;
const sizeMB = size ? size / (1024 * 1024) : 0;
const calculateIncrement = useCallback(
(size?: number) => {
const baseRate = 0.05;
const minRate = 0.002;
const sizeMB = size ? size / (1024 * 1024) : 0;
if (!size) {
return increment;
}
if (!size) {
return increment;
}
if (sizeMB <= 1) {
return baseRate * 2;
} else {
return Math.max(baseRate / Math.sqrt(sizeMB), minRate);
}
};
if (sizeMB <= 1) {
return baseRate * 2;
} else {
return Math.max(baseRate / Math.sqrt(sizeMB), minRate);
}
},
[increment],
);
const incrementValue = calculateIncrement(fileSize);
const incrementValue = useMemo(
() => calculateIncrement(fileSize),
[fileSize, calculateIncrement],
);
const [progress, setProgress] = useState(initialProgress);
const getDynamicIncrement = (currentProgress: number) => {
if (!fileSize) {
return incrementValue;
}
if (currentProgress < 0.7) {
return incrementValue;
} else {
return Math.max(0.0005, incrementValue * (1 - currentProgress));
}
};
const getDynamicIncrement = useCallback(
(currentProgress: number) => {
if (!fileSize) {
return incrementValue;
}
if (currentProgress < 0.7) {
return incrementValue;
} else {
return Math.max(0.0005, incrementValue * (1 - currentProgress));
}
},
[incrementValue, fileSize],
);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
@ -58,7 +67,7 @@ export default function useProgress(initialProgress = 0.01, increment = 0.007, f
clearInterval(timer);
clearTimeout(timeout);
};
}, [progress, initialProgress, incrementValue, fileSize]);
}, [progress, initialProgress, incrementValue, fileSize, getDynamicIncrement]);
return progress;
}

View file

@ -3,7 +3,7 @@ import {
ArrowRightToLine,
// Settings2,
} from 'lucide-react';
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { NavLink } from '~/common';
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
@ -26,7 +26,7 @@ export default function useSideNavLinks({
}) {
const Links = useMemo(() => {
const links: NavLink[] = [];
// if (endpoint !== EModelEndpoint.assistants) {
// if (!isAssistantsEndpoint(endpoint)) {
// links.push({
// title: 'com_sidepanel_parameters',
// label: '',
@ -36,7 +36,7 @@ export default function useSideNavLinks({
// });
// }
if (
endpoint === EModelEndpoint.assistants &&
isAssistantsEndpoint(endpoint) &&
assistants &&
assistants.disableBuilder !== true &&
keyProvided &&

View file

@ -12,10 +12,10 @@ import {
createPayload,
tPresetSchema,
tMessageSchema,
EModelEndpoint,
LocalStorageKeys,
tConvoUpdateSchema,
removeNullishValues,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type {
@ -441,7 +441,7 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
abortKey: _endpoint === EModelEndpoint.assistants ? runAbortKey : conversationId,
abortKey: isAssistantsEndpoint(_endpoint) ? runAbortKey : conversationId,
endpoint,
}),
});
@ -513,7 +513,7 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
const payloadData = createPayload(submission);
let { payload } = payloadData;
if (payload.endpoint === EModelEndpoint.assistants) {
if (isAssistantsEndpoint(payload.endpoint)) {
payload = removeNullishValues(payload);
}

View file

@ -18,10 +18,8 @@ export { default as useNewConvo } from './useNewConvo';
export { default as useLocalize } from './useLocalize';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useChatHelpers } from './useChatHelpers';
export { default as useGenerations } from './useGenerations';
export { default as useScrollToRef } from './useScrollToRef';
export { default as useLocalStorage } from './useLocalStorage';
export { default as useDelayedRender } from './useDelayedRender';
export { default as useOnClickOutside } from './useOnClickOutside';
export { default as useOriginNavigate } from './useOriginNavigate';
export { default as useGenerationsByLatest } from './useGenerationsByLatest';

View file

@ -3,10 +3,10 @@ import { useCallback, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import {
Constants,
EModelEndpoint,
QueryKeys,
parseCompactConvo,
ContentTypes,
parseCompactConvo,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
@ -215,7 +215,7 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) {
error: false,
};
if (endpoint === EModelEndpoint.assistants) {
if (isAssistantsEndpoint(endpoint)) {
initialResponse.model = conversation?.assistant_id ?? '';
initialResponse.text = '';
initialResponse.content = [

View file

@ -1,68 +0,0 @@
import type { TMessage } from 'librechat-data-provider';
import { EModelEndpoint } from 'librechat-data-provider';
import { useRecoilValue } from 'recoil';
import store from '~/store';
type TUseGenerations = {
endpoint?: string;
message: TMessage;
isSubmitting: boolean;
isEditing?: boolean;
latestMessage?: TMessage | null;
};
export default function useGenerations({
endpoint,
message,
isSubmitting,
isEditing = false,
latestMessage: _latestMessage,
}: TUseGenerations) {
const latestMessage = useRecoilValue(store.latestMessage) ?? _latestMessage;
const { error, messageId, searchResult, finish_reason, isCreatedByUser } = message ?? {};
const isEditableEndpoint = !![
EModelEndpoint.openAI,
EModelEndpoint.google,
EModelEndpoint.assistants,
EModelEndpoint.anthropic,
EModelEndpoint.gptPlugins,
EModelEndpoint.azureOpenAI,
].find((e) => e === endpoint);
const continueSupported =
latestMessage?.messageId === messageId &&
finish_reason &&
finish_reason !== 'stop' &&
!isEditing &&
!searchResult &&
isEditableEndpoint;
const branchingSupported =
// 5/21/23: Bing is allowing editing and Message regenerating
!![
EModelEndpoint.azureOpenAI,
EModelEndpoint.openAI,
EModelEndpoint.chatGPTBrowser,
EModelEndpoint.google,
EModelEndpoint.bingAI,
EModelEndpoint.gptPlugins,
EModelEndpoint.anthropic,
].find((e) => e === endpoint);
const regenerateEnabled =
!isCreatedByUser && !searchResult && !isEditing && !isSubmitting && branchingSupported;
const hideEditButton =
isSubmitting ||
error ||
searchResult ||
!branchingSupported ||
(!isEditableEndpoint && !isCreatedByUser);
return {
continueSupported,
regenerateEnabled,
hideEditButton,
};
}

View file

@ -1,5 +1,5 @@
import type { TMessage } from 'librechat-data-provider';
import { EModelEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
type TUseGenerations = {
endpoint?: string;
@ -21,7 +21,6 @@ export default function useGenerationsByLatest({
EModelEndpoint.openAI,
EModelEndpoint.custom,
EModelEndpoint.google,
EModelEndpoint.assistants,
EModelEndpoint.anthropic,
EModelEndpoint.gptPlugins,
EModelEndpoint.azureOpenAI,
@ -58,12 +57,13 @@ export default function useGenerationsByLatest({
!branchingSupported ||
(!isEditableEndpoint && !isCreatedByUser);
const forkingSupported = endpoint !== EModelEndpoint.assistants && !searchResult;
const forkingSupported = !isAssistantsEndpoint(endpoint) && !searchResult;
return {
forkingSupported,
continueSupported,
regenerateEnabled,
isEditableEndpoint,
hideEditButton,
};
}

View file

@ -4,12 +4,8 @@ import {
useGetStartupConfig,
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import {
FileSources,
EModelEndpoint,
LocalStorageKeys,
defaultOrderQuery,
} from 'librechat-data-provider';
import { useNavigate } from 'react-router-dom';
import { FileSources, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider';
import {
useRecoilState,
useRecoilValue,
@ -24,6 +20,7 @@ import type {
TConversation,
TEndpointsConfig,
} from 'librechat-data-provider';
import type { AssistantListItem } from '~/common';
import {
getEndpointField,
buildDefaultConvo,
@ -32,13 +29,14 @@ import {
getModelSpecIconURL,
updateLastSelectedModel,
} from '~/utils';
import { useDeleteFilesMutation, useListAssistantsQuery } from '~/data-provider';
import useOriginNavigate from './useOriginNavigate';
import useAssistantListMap from './Assistants/useAssistantListMap';
import { useDeleteFilesMutation } from '~/data-provider';
import { mainTextareaId } from '~/common';
import store from '~/store';
const useNewConvo = (index = 0) => {
const navigate = useOriginNavigate();
const navigate = useNavigate();
const { data: startupConfig } = useGetStartupConfig();
const defaultPreset = useRecoilValue(store.defaultPreset);
const { setConversation } = store.useCreateConversationAtom(index);
@ -48,11 +46,7 @@ const useNewConvo = (index = 0) => {
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const modelsQuery = useGetModelsQuery();
const timeoutIdRef = useRef<NodeJS.Timeout>();
const { data: assistants = [] } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) =>
res.data.map(({ id, name, metadata, model }) => ({ id, name, metadata, model })),
});
const assistantsListMap = useAssistantListMap();
const { mutateAsync } = useDeleteFilesMutation({
onSuccess: () => {
@ -100,12 +94,21 @@ const useNewConvo = (index = 0) => {
conversation.endpointType = undefined;
}
const isAssistantEndpoint = defaultEndpoint === EModelEndpoint.assistants;
const isAssistantEndpoint = isAssistantsEndpoint(defaultEndpoint);
const assistants: AssistantListItem[] = assistantsListMap[defaultEndpoint] ?? [];
if (
conversation.assistant_id &&
!assistantsListMap[defaultEndpoint]?.[conversation.assistant_id]
) {
conversation.assistant_id = undefined;
}
if (!conversation.assistant_id && isAssistantEndpoint) {
conversation.assistant_id =
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}`) ??
assistants[0]?.id;
localStorage.getItem(
`${LocalStorageKeys.ASST_ID_PREFIX}${index}${defaultEndpoint}`,
) ?? assistants[0]?.id;
}
if (
@ -116,7 +119,7 @@ const useNewConvo = (index = 0) => {
const assistant = assistants.find((asst) => asst.id === conversation.assistant_id);
conversation.model = assistant?.model;
updateLastSelectedModel({
endpoint: EModelEndpoint.assistants,
endpoint: defaultEndpoint,
model: conversation.model,
});
}
@ -145,7 +148,7 @@ const useNewConvo = (index = 0) => {
if (appTitle) {
document.title = appTitle;
}
navigate('new');
navigate('/c/new');
}
clearTimeout(timeoutIdRef.current);
@ -156,7 +159,7 @@ const useNewConvo = (index = 0) => {
}
}, 150);
},
[endpointsConfig, defaultPreset, assistants, modelsQuery.data],
[endpointsConfig, defaultPreset, assistantsListMap, modelsQuery.data],
);
const newConversation = useCallback(

View file

@ -1,18 +0,0 @@
import { useNavigate, useLocation } from 'react-router-dom';
const useOriginNavigate = () => {
const _navigate = useNavigate();
const location = useLocation();
const navigate = (url?: string | null, opts = {}) => {
if (!url) {
return;
}
const path = location.pathname.match(/^\/[^/]+\//);
_navigate(`${path ? path[0] : '/c/'}${url}`, opts);
};
return navigate;
};
export default useOriginNavigate;

View file

@ -297,6 +297,15 @@ export default {
com_nav_setting_general: 'عام',
com_nav_setting_data: 'تحكم في البيانات',
/* The following are AI translated */
com_assistants_file_search: 'بحث الملفات',
com_assistants_file_search_info:
'لا يتم دعم إرفاق مخازن الكتل الرقمية لميزة البحث في الملفات بعد. يمكنك إرفاقها من ملعب المزود أو إرفاق ملفات إلى الرسائل للبحث في الملفات على أساس المحادثة.',
com_assistants_non_retrieval_model:
'البحث في الملفات غير مُمكّن على هذا النموذج. يرجى تحديد نموذج آخر.',
com_ui_attach_error_openai: 'لا يمكن إرفاق ملفات المساعد إلى نقاط نهائية أخرى',
com_ui_attach_warn_endpoint: 'قد يتم تجاهل الملفات غير المساعدة دون وجود أداة متوافقة',
com_ui_assistant_deleted: 'تم حذف المساعد بنجاح',
com_ui_assistant_delete_error: 'حدث خطأ أثناء حذف المساعد',
com_ui_copied: 'تم النسخ',
com_ui_copy_code: 'نسخ الكود',
com_ui_copy_link: 'نسخ الرابط',
@ -1636,6 +1645,36 @@ export const comparisons = {
english: 'Data controls',
translated: 'تحكم في البيانات',
},
com_assistants_file_search: {
english: 'File Search',
translated: 'بحث الملفات',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'لا يتم دعم إرفاق مخازن الكتل الرقمية لميزة البحث في الملفات بعد. يمكنك إرفاقها من ملعب المزود أو إرفاق ملفات إلى الرسائل للبحث في الملفات على أساس المحادثة.',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated: 'البحث في الملفات غير مُمكّن على هذا النموذج. يرجى تحديد نموذج آخر.',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: 'لا يمكن إرفاق ملفات المساعد إلى نقاط نهائية أخرى',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated: 'قد يتم تجاهل الملفات غير المساعدة دون وجود أداة متوافقة',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: 'تم حذف المساعد بنجاح',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: 'حدث خطأ أثناء حذف المساعد',
},
com_ui_copied: {
english: 'Copied!',
translated: 'تم النسخ',

View file

@ -481,6 +481,16 @@ export default {
com_nav_setting_account: 'Konto',
com_nav_language: 'Sprache',
/* The following are AI Translated */
com_assistants_file_search: 'Dateisuche',
com_assistants_file_search_info:
'Das Anhängen von Vektorspeichern für die Dateisuche wird derzeit noch nicht unterstützt. Du kannst sie im Provider Playground anhängen oder Dateien für die Dateisuche pro Thread anhängen.',
com_assistants_non_retrieval_model:
'Die Dateisuche ist für dieses Modell nicht aktiviert. Bitte wähle ein anderes Modell aus.',
com_ui_attach_error_openai: 'Assistent-Dateien können nicht an andere Endpunkte angehängt werden',
com_ui_attach_warn_endpoint:
'Nicht-Assistent-Dateien könnten ohne ein kompatibles Werkzeug ignoriert werden',
com_ui_assistant_deleted: 'Assistent erfolgreich gelöscht',
com_ui_assistant_delete_error: 'Beim Löschen des Assistenten ist ein Fehler aufgetreten.',
com_ui_copied: 'Kopiert',
com_ui_copy_code: 'Code kopieren',
com_ui_copy_link: 'Link kopieren',
@ -2305,6 +2315,37 @@ export const comparisons = {
english: 'Language',
translated: 'Sprache',
},
com_assistants_file_search: {
english: 'File Search',
translated: 'Dateisuche',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'Das Anhängen von Vektorspeichern für die Dateisuche wird derzeit noch nicht unterstützt. Du kannst sie im Provider Playground anhängen oder Dateien für die Dateisuche pro Thread anhängen.',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated:
'Die Dateisuche ist für dieses Modell nicht aktiviert. Bitte wähle ein anderes Modell aus.',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: 'Assistent-Dateien können nicht an andere Endpunkte angehängt werden',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated: 'Nicht-Assistent-Dateien könnten ohne ein kompatibles Werkzeug ignoriert werden',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: 'Assistent erfolgreich gelöscht',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: 'Beim Löschen des Assistenten ist ein Fehler aufgetreten.',
},
com_ui_copied: {
english: 'Copied!',
translated: 'Kopiert',

View file

@ -20,6 +20,9 @@ export default {
com_sidepanel_attach_files: 'Attach Files',
com_sidepanel_manage_files: 'Manage Files',
com_assistants_capabilities: 'Capabilities',
com_assistants_file_search: 'File Search',
com_assistants_file_search_info:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
com_assistants_knowledge: 'Knowledge',
com_assistants_knowledge_info:
'If you upload files under Knowledge, conversations with your Assistant may include file contents.',
@ -35,6 +38,8 @@ export default {
com_assistants_actions: 'Actions',
com_assistants_add_tools: 'Add Tools',
com_assistants_add_actions: 'Add Actions',
com_assistants_non_retrieval_model:
'File search is not enabled on this model. Please select another model.',
com_assistants_available_actions: 'Available Actions',
com_assistants_running_action: 'Running action',
com_assistants_completed_action: 'Talked to {0}',
@ -73,6 +78,8 @@ export default {
com_ui_field_required: 'This field is required',
com_ui_download_error: 'Error downloading file. The file may have been deleted.',
com_ui_attach_error_type: 'Unsupported file type for endpoint:',
com_ui_attach_error_openai: 'Cannot attach Assistant files to other endpoints',
com_ui_attach_warn_endpoint: 'Non-Assistant files may be ignored without a compatible tool',
com_ui_attach_error_size: 'File size limit exceeded for endpoint:',
com_ui_attach_error:
'Cannot attach file. Create or select a conversation, or try refreshing the page.',
@ -196,6 +203,8 @@ export default {
com_ui_result: 'Result',
com_ui_image_gen: 'Image Gen',
com_ui_assistant: 'Assistant',
com_ui_assistant_deleted: 'Successfully deleted assistant',
com_ui_assistant_delete_error: 'There was an error deleting the assistant',
com_ui_assistants: 'Assistants',
com_ui_attachment: 'Attachment',
com_ui_assistants_output: 'Assistants Output',

View file

@ -475,6 +475,17 @@ export default {
com_nav_lang_auto: 'Detección automática',
com_nav_lang_spanish: 'Español',
/* The following are AI Translated */
com_assistants_file_search: 'Búsqueda de Archivos',
com_assistants_file_search_info:
'Adjuntar almacenes vectoriales para la Búsqueda de Archivos aún no está soportado. Puede adjuntarlos desde el Área de Pruebas del Proveedor o adjuntar archivos a los mensajes para la búsqueda de archivos en una conversación específica.',
com_assistants_non_retrieval_model:
'La búsqueda de archivos no está habilitada en este modelo. Por favor, seleccione otro modelo.',
com_ui_attach_error_openai:
'No se pueden adjuntar archivos del Asistente a otros puntos de conexión',
com_ui_attach_warn_endpoint:
'Es posible que los archivos no compatibles con la herramienta sean ignorados',
com_ui_assistant_deleted: 'Asistente eliminado con éxito',
com_ui_assistant_delete_error: 'Hubo un error al eliminar el asistente',
com_ui_copied: '¡Copiado!',
com_ui_copy_code: 'Copiar código',
com_ui_copy_link: 'Copiar enlace',
@ -2286,6 +2297,37 @@ export const comparisons = {
english: 'Español',
translated: 'Español',
},
com_assistants_file_search: {
english: 'File Search',
translated: 'Búsqueda de Archivos',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'Adjuntar almacenes vectoriales para la Búsqueda de Archivos aún no está soportado. Puede adjuntarlos desde el Área de Pruebas del Proveedor o adjuntar archivos a los mensajes para la búsqueda de archivos en una conversación específica.',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated:
'La búsqueda de archivos no está habilitada en este modelo. Por favor, seleccione otro modelo.',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: 'No se pueden adjuntar archivos del Asistente a otros puntos de conexión',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated: 'Es posible que los archivos no compatibles con la herramienta sean ignorados',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: 'Asistente eliminado con éxito',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: 'Hubo un error al eliminar el asistente',
},
com_ui_copied: {
english: 'Copied!',
translated: '¡Copiado!',

View file

@ -364,6 +364,16 @@ export default {
com_nav_setting_data: 'Contrôles des données',
com_nav_setting_account: 'Compte',
/* The following are AI Translated */
com_assistants_file_search: 'Recherche de fichiers',
com_assistants_file_search_info:
'L\'ajout de vecteurs de stockage pour la recherche de fichiers n\'est pas encore pris en charge. Vous pouvez les ajouter depuis le terrain de jeu du fournisseur ou joindre des fichiers aux messages pour une recherche de fichiers au niveau du fil de discussion.',
com_assistants_non_retrieval_model:
'La recherche de fichiers n\'est pas activée pour ce modèle. Veuillez sélectionner un autre modèle.',
com_ui_attach_error_openai:
'Impossible de joindre les fichiers de l\'Assistant à d\'autres points d\'accès',
com_ui_attach_warn_endpoint: 'Les fichiers non compatibles avec l\'outil peuvent être ignorés',
com_ui_assistant_deleted: 'Assistant supprimé avec succès',
com_ui_assistant_delete_error: 'Une erreur s\'est produite lors de la suppression de l\'assistant.',
com_ui_copied: 'Copié !',
com_ui_copy_code: 'Copier le code',
com_ui_copy_link: 'Copier le lien',
@ -1863,6 +1873,37 @@ export const comparisons = {
english: 'Account',
translated: 'Compte',
},
com_assistants_file_search: {
english: 'File Search',
translated: 'Recherche de fichiers',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'L\'ajout de vecteurs de stockage pour la recherche de fichiers n\'est pas encore pris en charge. Vous pouvez les ajouter depuis le terrain de jeu du fournisseur ou joindre des fichiers aux messages pour une recherche de fichiers au niveau du fil de discussion.',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated:
'La recherche de fichiers n\'est pas activée pour ce modèle. Veuillez sélectionner un autre modèle.',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: 'Impossible de joindre les fichiers de l\'Assistant à d\'autres points d\'accès',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated: 'Les fichiers non compatibles avec l\'outil peuvent être ignorés',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: 'Assistant supprimé avec succès',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: 'Une erreur s\'est produite lors de la suppression de l\'assistant.',
},
com_ui_copied: {
english: 'Copied!',
translated: 'Copié !',

View file

@ -525,6 +525,16 @@ export default {
com_nav_setting_data: 'Controlli dati',
com_nav_setting_account: 'Account',
/* The following are AI Translated */
com_assistants_file_search: 'Ricerca File',
com_assistants_file_search_info:
'L\'aggiunta di archivi vettoriali per la Ricerca File non è ancora supportata. Puoi aggiungerli dal Provider Playground o allegare file ai messaggi per la ricerca file su base di thread.',
com_assistants_non_retrieval_model:
'La ricerca di file non è abilitata su questo modello. Seleziona un altro modello.',
com_ui_attach_error_openai: 'Non è possibile allegare file dell\'Assistente ad altri endpoint',
com_ui_attach_warn_endpoint:
'Attenzione: i file non compatibili con lo strumento potrebbero essere ignorati',
com_ui_assistant_deleted: 'Assistente eliminato con successo',
com_ui_assistant_delete_error: 'Si è verificato un errore durante l\'eliminazione dell\'assistente',
com_ui_copied: 'Copiato!',
com_ui_copy_code: 'Copia codice',
com_ui_copy_link: 'Copia link',
@ -2443,6 +2453,36 @@ export const comparisons = {
english: 'Account',
translated: 'Account',
},
com_assistants_file_search: {
english: 'File Search',
translated: 'Ricerca File',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'L\'aggiunta di archivi vettoriali per la Ricerca File non è ancora supportata. Puoi aggiungerli dal Provider Playground o allegare file ai messaggi per la ricerca file su base di thread.',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated: 'La ricerca di file non è abilitata su questo modello. Seleziona un altro modello.',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: 'Non è possibile allegare file dell\'Assistente ad altri endpoint',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated: 'Attenzione: i file non compatibili con lo strumento potrebbero essere ignorati',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: 'Assistente eliminato con successo',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: 'Si è verificato un errore durante l\'eliminazione dell\'assistente',
},
com_ui_copied: {
english: 'Copied!',
translated: 'Copiato!',

View file

@ -473,6 +473,16 @@ export default {
com_nav_setting_data: 'データ管理',
com_nav_setting_account: 'アカウント',
/* The following are AI translated */
com_assistants_file_search: 'ファイル検索',
com_assistants_file_search_info:
'ファイル検索用のベクトル ストアを添付することはまだサポートされていません。Provider Playgroundからそれらを添付するか、スレッド単位でメッセージにファイルを添付してファイル検索を行うことができます。',
com_assistants_non_retrieval_model:
'このモデルではファイル検索機能は有効になっていません。別のモデルを選択してください。',
com_ui_attach_error_openai: '他のエンドポイントにAssistantファイルを添付することはできません',
com_ui_attach_warn_endpoint:
'互換性のあるツールがない場合、非アシスタントのファイルは無視される可能性があります',
com_ui_assistant_deleted: 'アシスタントが正常に削除されました',
com_ui_assistant_delete_error: 'アシスタントの削除中にエラーが発生しました。',
com_ui_copied: 'コピーしました',
com_ui_copy_code: 'コードをコピーする',
com_ui_copy_link: 'リンクをコピー',
@ -2296,6 +2306,38 @@ export const comparisons = {
english: 'Account',
translated: 'アカウント',
},
com_assistants_file_search: {
english: 'File Search',
translated: 'ファイル検索',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'ファイル検索用のベクトル ストアを添付することはまだサポートされていません。Provider Playgroundからそれらを添付するか、スレッド単位でメッセージにファイルを添付してファイル検索を行うことができます。',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated:
'このモデルではファイル検索機能は有効になっていません。別のモデルを選択してください。',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: '他のエンドポイントにAssistantファイルを添付することはできません',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated:
'互換性のあるツールがない場合、非アシスタントのファイルは無視される可能性があります',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: 'アシスタントが正常に削除されました',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: 'アシスタントの削除中にエラーが発生しました。',
},
com_ui_copied: {
english: 'Copied!',
translated: 'コピーしました',

View file

@ -278,6 +278,15 @@ export default {
com_nav_setting_general: '일반',
com_nav_setting_data: '데이터 제어',
/* The following are AI Translated */
com_assistants_file_search: '파일 검색',
com_assistants_file_search_info:
'파일 검색을 위한 벡터 저장소 연결은 아직 지원되지 않습니다. Provider Playground에서 연결하거나 스레드 기반으로 메시지에 파일을 첨부하여 파일 검색을 할 수 있습니다.',
com_assistants_non_retrieval_model:
'이 모델에서는 파일 검색 기능을 사용할 수 없습니다. 다른 모델을 선택하세요.',
com_ui_attach_error_openai: '어시스턴트 파일을 다른 엔드포인트에 첨부할 수 없습니다.',
com_ui_attach_warn_endpoint: '호환되는 도구가 없으면 비어시스턴트 파일이 무시될 수 있습니다.',
com_ui_assistant_deleted: '어시스턴트가 성공적으로 삭제되었습니다',
com_ui_assistant_delete_error: '어시스턴트 삭제 중 오류가 발생했습니다.',
com_ui_copied: '복사됨',
com_ui_copy_code: '코드 복사',
com_ui_copy_link: '링크 복사',
@ -1581,6 +1590,36 @@ export const comparisons = {
english: 'Data controls',
translated: '데이터 제어',
},
com_assistants_file_search: {
english: 'File Search',
translated: '파일 검색',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'파일 검색을 위한 벡터 저장소 연결은 아직 지원되지 않습니다. Provider Playground에서 연결하거나 스레드 기반으로 메시지에 파일을 첨부하여 파일 검색을 할 수 있습니다.',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated: '이 모델에서는 파일 검색 기능을 사용할 수 없습니다. 다른 모델을 선택하세요.',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: '어시스턴트 파일을 다른 엔드포인트에 첨부할 수 없습니다.',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated: '호환되는 도구가 없으면 비어시스턴트 파일이 무시될 수 있습니다.',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: '어시스턴트가 성공적으로 삭제되었습니다',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: '어시스턴트 삭제 중 오류가 발생했습니다.',
},
com_ui_copied: {
english: 'Copied!',
translated: '복사됨',

View file

@ -381,6 +381,16 @@ export default {
com_ui_upload_error: 'Произошла ошибка при загрузке вашего файла',
com_user_message: 'Вы',
/* The following are AI Translated */
com_assistants_file_search: 'Поиск файлов',
com_assistants_file_search_info:
'Прикрепление векторных хранилищ для Поиска по файлам пока не поддерживается. Вы можете прикрепить их из Песочницы провайдера или прикрепить файлы к сообщениям для поиска по файлам в отдельных диалогах.',
com_assistants_non_retrieval_model:
'Поиск по файлам недоступен для этой модели. Пожалуйста, выберите другую модель.',
com_ui_attach_error_openai: 'Невозможно прикрепить файлы ассистента к другим режимам',
com_ui_attach_warn_endpoint:
'Файлы сторонних приложений могут быть проигнорированы без совместимого плагина',
com_ui_assistant_deleted: 'Ассистент успешно удален',
com_ui_assistant_delete_error: 'Произошла ошибка при удалении ассистента',
com_ui_copied: 'Скопировано',
com_ui_copy_code: 'Копировать код',
com_ui_copy_link: 'Копировать ссылку',
@ -1948,6 +1958,36 @@ export const comparisons = {
english: 'You',
translated: 'Вы',
},
com_assistants_file_search: {
english: 'File Search',
translated: 'Поиск файлов',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'Прикрепление векторных хранилищ для Поиска по файлам пока не поддерживается. Вы можете прикрепить их из Песочницы провайдера или прикрепить файлы к сообщениям для поиска по файлам в отдельных диалогах.',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated: 'Поиск по файлам недоступен для этой модели. Пожалуйста, выберите другую модель.',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: 'Невозможно прикрепить файлы ассистента к другим режимам',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated: 'Файлы сторонних приложений могут быть проигнорированы без совместимого плагина',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: 'Ассистент успешно удален',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: 'Произошла ошибка при удалении ассистента',
},
com_ui_copied: {
english: 'Copied!',
translated: 'Скопировано',

View file

@ -434,6 +434,14 @@ export default {
com_nav_setting_data: '数据管理',
com_nav_setting_account: '账户',
/* The following are AI Translated */
com_assistants_file_search: '文件搜索',
com_assistants_file_search_info:
'暂不支持为文件搜索附加向量存储。您可以从提供程序游乐场附加它们,或者在线程基础上为文件搜索附加文件。',
com_assistants_non_retrieval_model: '此模型未启用文件搜索功能。请选择其他模型。',
com_ui_attach_error_openai: '无法将助手文件附加到其他渠道',
com_ui_attach_warn_endpoint: '不兼容的工具可能会忽略非助手文件',
com_ui_assistant_deleted: '助手已成功删除',
com_ui_assistant_delete_error: '删除助手时出错。',
com_ui_date_october: '十月',
com_ui_date_november: '十一月',
com_ui_date_december: '十二月',
@ -2198,6 +2206,36 @@ export const comparisons = {
english: 'Account',
translated: '账户',
},
com_assistants_file_search: {
english: 'File Search',
translated: '文件搜索',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'暂不支持为文件搜索附加向量存储。您可以从提供程序游乐场附加它们,或者在线程基础上为文件搜索附加文件。',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated: '此模型未启用文件搜索功能。请选择其他模型。',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: '无法将助手文件附加到其他渠道',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated: '不兼容的工具可能会忽略非助手文件',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: '助手已成功删除',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: '删除助手时出错。',
},
com_ui_date_october: {
english: 'October',
translated: '十月',

View file

@ -283,6 +283,14 @@ export default {
com_nav_setting_general: '一般',
com_nav_setting_data: '資料控制',
/* The following are AI translated */
com_assistants_file_search: '檔案搜尋',
com_assistants_file_search_info:
'目前尚不支援為檔案搜尋附加向量存儲。您可以從提供者遊樂場附加它們,或在每個主題的基礎上為檔案搜尋附加檔案。',
com_assistants_non_retrieval_model: '此模型未啟用檔案搜尋功能。請選擇其他模型。',
com_ui_attach_error_openai: '無法將助理檔案附加至其他端點',
com_ui_attach_warn_endpoint: '非相容工具的非助理檔案可能會被忽略',
com_ui_assistant_deleted: '已成功刪除助理',
com_ui_assistant_delete_error: '刪除助理時發生錯誤',
com_ui_copied: '已複製!',
com_ui_copy_code: '複製程式碼',
com_ui_copy_link: '複製連結',
@ -1611,6 +1619,36 @@ export const comparisons = {
english: 'Data controls',
translated: '資料控制',
},
com_assistants_file_search: {
english: 'File Search',
translated: '檔案搜尋',
},
com_assistants_file_search_info: {
english:
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
translated:
'目前尚不支援為檔案搜尋附加向量存儲。您可以從提供者遊樂場附加它們,或在每個主題的基礎上為檔案搜尋附加檔案。',
},
com_assistants_non_retrieval_model: {
english: 'File search is not enabled on this model. Please select another model.',
translated: '此模型未啟用檔案搜尋功能。請選擇其他模型。',
},
com_ui_attach_error_openai: {
english: 'Cannot attach Assistant files to other endpoints',
translated: '無法將助理檔案附加至其他端點',
},
com_ui_attach_warn_endpoint: {
english: 'Non-Assistant files may be ignored without a compatible tool',
translated: '非相容工具的非助理檔案可能會被忽略',
},
com_ui_assistant_deleted: {
english: 'Successfully deleted assistant',
translated: '已成功刪除助理',
},
com_ui_assistant_delete_error: {
english: 'There was an error deleting the assistant',
translated: '刪除助理時發生錯誤',
},
com_ui_copied: {
english: 'Copied!',
translated: '已複製!',

View file

@ -270,4 +270,8 @@
.radix-side-top\:animate-slideDownAndFade[data-side=top] {
-webkit-animation:slideDownAndFade .4s cubic-bezier(.16,1,.3,1);
animation:slideDownAndFade .4s cubic-bezier(.16,1,.3,1)
}
.azure-bg-color {
background: linear-gradient(0.375turn, #61bde2, #4389d0);
}

View file

@ -1,15 +1,15 @@
import { useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { defaultOrderQuery } from 'librechat-data-provider';
import { EModelEndpoint } from 'librechat-data-provider';
import {
useGetModelsQuery,
useGetStartupConfig,
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import type { TPreset } from 'librechat-data-provider';
import { useGetConvoIdQuery, useListAssistantsQuery } from '~/data-provider';
import { useNewConvo, useAppStartup, useAssistantListMap } from '~/hooks';
import { getDefaultModelSpec, getModelSpecIconURL } from '~/utils';
import { useNewConvo, useAppStartup } from '~/hooks';
import { useGetConvoIdQuery } from '~/data-provider';
import ChatView from '~/components/Chat/ChatView';
import useAuthRedirect from './useAuthRedirect';
import { Spinner } from '~/components/svg';
@ -35,10 +35,7 @@ export default function ChatRoute() {
enabled: isAuthenticated && conversationId !== 'new',
});
const endpointsQuery = useGetEndpointsQuery({ enabled: isAuthenticated });
const { data: assistants = null } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) =>
res.data.map(({ id, name, metadata, model }) => ({ id, name, metadata, model })),
});
const assistantListMap = useAssistantListMap();
useEffect(() => {
if (
@ -87,7 +84,8 @@ export default function ChatRoute() {
!hasSetConversation.current &&
!modelsQuery.data?.initial &&
conversationId === 'new' &&
assistants
assistantListMap[EModelEndpoint.assistants] &&
assistantListMap[EModelEndpoint.azureAssistants]
) {
const spec = getDefaultModelSpec(startupConfig.modelSpecs?.list);
newConversation({
@ -108,7 +106,8 @@ export default function ChatRoute() {
startupConfig &&
!hasSetConversation.current &&
!modelsQuery.data?.initial &&
assistants
assistantListMap[EModelEndpoint.assistants] &&
assistantListMap[EModelEndpoint.azureAssistants]
) {
newConversation({
template: initialConvoQuery.data,
@ -120,7 +119,13 @@ export default function ChatRoute() {
}
/* Creates infinite render if all dependencies included due to newConversation invocations exceeding call stack before hasSetConversation.current becomes truthy */
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startupConfig, initialConvoQuery.data, endpointsQuery.data, modelsQuery.data, assistants]);
}, [
startupConfig,
initialConvoQuery.data,
endpointsQuery.data,
modelsQuery.data,
assistantListMap,
]);
if (endpointsQuery.isLoading || modelsQuery.isLoading) {
return <Spinner className="m-auto text-black dark:text-white" />;

View file

@ -4,6 +4,7 @@ import type { TEndpointsConfig } from 'librechat-data-provider';
const defaultConfig: TEndpointsConfig = {
[EModelEndpoint.azureOpenAI]: null,
[EModelEndpoint.azureAssistants]: null,
[EModelEndpoint.assistants]: null,
[EModelEndpoint.openAI]: null,
[EModelEndpoint.bingAI]: null,

View file

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

View file

@ -1,4 +1,4 @@
import { parseConvo, EModelEndpoint } from 'librechat-data-provider';
import { parseConvo, EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TConversation } from 'librechat-data-provider';
import getLocalStorageItems from './getLocalStorageItems';
@ -65,7 +65,7 @@ const buildDefaultConvo = ({
};
// Ensures assistant_id is always defined
if (endpoint === EModelEndpoint.assistants && !defaultConvo.assistant_id && convo.assistant_id) {
if (isAssistantsEndpoint(endpoint) && !defaultConvo.assistant_id && convo.assistant_id) {
defaultConvo.assistant_id = convo.assistant_id;
}

View file

@ -31,7 +31,7 @@ export default function buildTree({
if (message.files && fileMap) {
messageMap[message.messageId].files = message.files.map(
(file) => fileMap[file.file_id] ?? file,
(file) => fileMap[file.file_id ?? ''] ?? file,
);
}

View file

@ -3,6 +3,7 @@ import {
defaultEndpoints,
modularEndpoints,
LocalStorageKeys,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type {
TConfig,
@ -139,8 +140,8 @@ export function getConvoSwitchLogic(params: ConversationInitParams): InitiatedTe
};
const isAssistantSwitch =
newEndpoint === EModelEndpoint.assistants &&
currentEndpoint === EModelEndpoint.assistants &&
isAssistantsEndpoint(newEndpoint) &&
isAssistantsEndpoint(currentEndpoint) &&
currentEndpoint === newEndpoint;
const conversationId = conversation?.conversationId;