Merge branch 'main' into re-add-download-audio

This commit is contained in:
Marco Beretta 2024-07-30 05:36:43 -04:00 committed by GitHub
commit c27e8566fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
152 changed files with 5856 additions and 1334 deletions

View file

@ -0,0 +1,9 @@
import { createContext, useContext } from 'react';
import type { TConversationTag } from 'librechat-data-provider';
type TBookmarkContext = { bookmarks: TConversationTag[] };
export const BookmarkContext = createContext<TBookmarkContext>({
bookmarks: [],
} as TBookmarkContext);
export const useBookmarkContext = () => useContext(BookmarkContext);

View file

@ -7,6 +7,7 @@ export * from './SearchContext';
export * from './FileMapContext';
export * from './AddedChatContext';
export * from './ChatFormContext';
export * from './BookmarkContext';
export * from './DashboardContext';
export * from './AssistantsContext';
export * from './AssistantsMapContext';

View file

@ -83,7 +83,7 @@ export type NavLink = {
label?: string;
icon: LucideIcon | React.FC;
Component?: React.ComponentType;
onClick?: () => void;
onClick?: (e?: React.MouseEvent) => void;
variant?: 'default' | 'ghost';
id: string;
};

View file

@ -1,5 +1,6 @@
import { useForm } from 'react-hook-form';
import React, { useState, useEffect } from 'react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { TLoginUser, TStartupConfig } from 'librechat-data-provider';
import type { TAuthContext } from '~/common';
import { useResendVerificationEmail } from '~/data-provider';
@ -22,6 +23,9 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
} = useForm<TLoginUser>();
const [showResendLink, setShowResendLink] = useState<boolean>(false);
const { data: config } = useGetStartupConfig();
const useUsernameLogin = config?.ldap?.username;
useEffect(() => {
if (error && error.includes('422') && !showResendLink) {
setShowResendLink(true);
@ -82,12 +86,15 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
<input
type="text"
id="email"
autoComplete="email"
autoComplete={useUsernameLogin ? 'username' : 'email'}
aria-label={localize('com_auth_email')}
{...register('email', {
required: localize('com_auth_email_required'),
maxLength: { value: 120, message: localize('com_auth_email_max_length') },
pattern: { value: /\S+@\S+\.\S+/, message: localize('com_auth_email_pattern') },
pattern: {
value: useUsernameLogin ? /\S+/ : /\S+@\S+\.\S+/,
message: localize('com_auth_email_pattern'),
},
})}
aria-invalid={!!errors.email}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
@ -97,7 +104,9 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
htmlFor="email"
className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
>
{localize('com_auth_email_address')}
{useUsernameLogin
? localize('com_auth_username').replace(/ \(.*$/, '')
: localize('com_auth_email_address')}
</label>
</div>
{renderError('email')}

View file

@ -21,7 +21,9 @@ const mockStartupConfig = {
openidLoginEnabled: true,
openidLabel: 'Test OpenID',
openidImageUrl: 'http://test-server.com',
ldapLoginEnabled: false,
ldap: {
enabled: false,
},
registrationEnabled: true,
emailLoginEnabled: true,
socialLoginEnabled: true,

View file

@ -23,7 +23,9 @@ const mockStartupConfig: TStartupConfig = {
passwordResetEnabled: true,
serverDomain: 'mock-server',
appTitle: '',
ldapLoginEnabled: false,
ldap: {
enabled: false,
},
emailEnabled: false,
checkBalance: false,
showBirthdayIcon: false,

View file

@ -0,0 +1,69 @@
import React, { useRef, useState } from 'react';
import { TConversationTag, TConversation } from 'librechat-data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { OGDialog, OGDialogTrigger, OGDialogClose } from '~/components/ui/';
import BookmarkForm from './BookmarkForm';
import { useLocalize } from '~/hooks';
import { Spinner } from '../svg';
type BookmarkEditDialogProps = {
bookmark?: TConversationTag;
conversation?: TConversation;
tags?: string[];
setTags?: (tags: string[]) => void;
trigger: React.ReactNode;
};
const BookmarkEditDialog = ({
bookmark,
conversation,
tags,
setTags,
trigger,
}: BookmarkEditDialogProps) => {
const localize = useLocalize();
const [isLoading, setIsLoading] = useState(false);
const [open, setOpen] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const handleSubmitForm = () => {
if (formRef.current) {
formRef.current.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
};
return (
<OGDialog open={open} onOpenChange={setOpen}>
<OGDialogTrigger asChild>{trigger}</OGDialogTrigger>
<OGDialogTemplate
title="Bookmark"
className="w-11/12 sm:w-1/4"
showCloseButton={false}
main={
<BookmarkForm
conversation={conversation}
onOpenChange={setOpen}
setIsLoading={setIsLoading}
bookmark={bookmark}
formRef={formRef}
setTags={setTags}
tags={tags}
/>
}
buttons={
<OGDialogClose asChild>
<button
type="submit"
disabled={isLoading}
onClick={handleSubmitForm}
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
>
{isLoading ? <Spinner /> : localize('com_ui_save')}
</button>
</OGDialogClose>
}
/>
</OGDialog>
);
};
export default BookmarkEditDialog;

View file

@ -0,0 +1,195 @@
import React, { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import type {
TConversationTag,
TConversation,
TConversationTagRequest,
} from 'librechat-data-provider';
import { cn, removeFocusOutlines, defaultTextProps } from '~/utils/';
import { useBookmarkContext } from '~/Providers/BookmarkContext';
import { useConversationTagMutation } from '~/data-provider';
import { Checkbox, Label, TextareaAutosize } from '~/components/ui/';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
type TBookmarkFormProps = {
bookmark?: TConversationTag;
conversation?: TConversation;
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>;
formRef: React.RefObject<HTMLFormElement>;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
tags?: string[];
setTags?: (tags: string[]) => void;
};
const BookmarkForm = ({
bookmark,
conversation,
onOpenChange,
formRef,
setIsLoading,
tags,
setTags,
}: TBookmarkFormProps) => {
const { showToast } = useToastContext();
const localize = useLocalize();
const mutation = useConversationTagMutation(bookmark?.tag);
const { bookmarks } = useBookmarkContext();
const {
register,
handleSubmit,
setValue,
getValues,
control,
formState: { errors },
} = useForm<TConversationTagRequest>({
defaultValues: {
tag: bookmark?.tag || '',
description: bookmark?.description || '',
conversationId: conversation?.conversationId || '',
addToConversation: conversation ? true : false,
},
});
useEffect(() => {
if (bookmark) {
setValue('tag', bookmark.tag || '');
setValue('description', bookmark.description || '');
}
}, [bookmark, setValue]);
const onSubmit = (data: TConversationTagRequest) => {
if (mutation.isLoading) {
return;
}
if (data.tag === bookmark?.tag && data.description === bookmark?.description) {
return;
}
setIsLoading(true);
mutation.mutate(data, {
onSuccess: () => {
showToast({
message: bookmark
? localize('com_ui_bookmarks_update_success')
: localize('com_ui_bookmarks_create_success'),
});
setIsLoading(false);
onOpenChange(false);
if (setTags && data.addToConversation) {
const newTags = [...(tags || []), data.tag].filter(
(tag) => tag !== undefined,
) as string[];
setTags(newTags);
}
},
onError: () => {
showToast({
message: bookmark
? localize('com_ui_bookmarks_update_error')
: localize('com_ui_bookmarks_create_error'),
severity: NotificationSeverity.ERROR,
});
setIsLoading(false);
},
});
};
return (
<form
ref={formRef}
className="mt-6"
aria-label="Bookmark form"
method="POST"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="bookmark-tag" className="text-left text-sm font-medium">
{localize('com_ui_bookmarks_title')}
</Label>
<input
type="text"
id="bookmark-tag"
aria-label="Bookmark"
{...register('tag', {
required: 'tag is required',
maxLength: {
value: 128,
message: localize('com_auth_password_max_length'),
},
validate: (value) => {
return (
value === bookmark?.tag ||
bookmarks.every((bookmark) => bookmark.tag !== value) ||
'tag must be unique'
);
},
})}
aria-invalid={!!errors.tag}
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
removeFocusOutlines,
)}
placeholder=" "
/>
{errors.tag && <span className="text-sm text-red-500">{errors.tag.message}</span>}
</div>
<div className="grid w-full items-center gap-2">
<Label htmlFor="bookmark-description" className="text-left text-sm font-medium">
{localize('com_ui_bookmarks_description')}
</Label>
<TextareaAutosize
{...register('description', {
maxLength: {
value: 1048,
message: 'Maximum 1048 characters',
},
})}
id="bookmark-description"
disabled={false}
className={cn(
defaultTextProps,
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2',
)}
/>
</div>
{conversation && (
<div className="mt-2 flex w-full items-center">
<Controller
name="addToConversation"
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="addToConversation"
onClick={() =>
setValue('addToConversation', !getValues('addToConversation'), {
shouldDirty: true,
})
}
>
<div className="flex select-none items-center">
{localize('com_ui_bookmarks_add_to_conversation')}
</div>
</label>
</div>
)}
</div>
</form>
);
};
export default BookmarkForm;

View file

@ -0,0 +1,74 @@
import { useState } from 'react';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import type { FC } from 'react';
import { Spinner } from '~/components/svg';
import { cn } from '~/utils';
type MenuItemProps = {
tag: string;
selected: boolean;
count?: number;
handleSubmit: (tag: string) => Promise<void>;
icon?: React.ReactNode;
highlightSelected?: boolean;
};
const BookmarkItem: FC<MenuItemProps> = ({
tag,
selected,
count,
handleSubmit,
icon,
highlightSelected,
...rest
}) => {
const [isLoading, setIsLoading] = useState(false);
const clickHandler = async () => {
setIsLoading(true);
await handleSubmit(tag);
setIsLoading(false);
};
return (
<div
role="menuitem"
className={cn(
'group m-1.5 flex cursor-pointer gap-2 rounded px-2 py-2.5 !pr-3 text-sm !opacity-100 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50',
'hover:bg-black/5 dark:hover:bg-white/5',
highlightSelected && selected && 'bg-black/5 dark:bg-white/5',
)}
tabIndex={-1}
{...rest}
onClick={clickHandler}
>
<div className="flex grow items-center justify-between gap-2">
<div className="flex items-center gap-2">
{icon ? (
icon
) : isLoading ? (
<Spinner className="size-4" />
) : selected ? (
<BookmarkFilledIcon className="size-4" />
) : (
<BookmarkIcon className="size-4" />
)}
<div className="break-all">{tag}</div>
</div>
{count !== undefined && (
<div className="flex items-center justify-end">
<span
className={cn(
'ml-auto w-9 min-w-max whitespace-nowrap rounded-md bg-white px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-gray-600',
'dark:bg-gray-800 dark:text-white',
)}
aria-hidden="true"
>
{count}
</span>
</div>
)}
</div>
</div>
);
};
export default BookmarkItem;

View file

@ -0,0 +1,30 @@
import type { FC } from 'react';
import { useBookmarkContext } from '~/Providers/BookmarkContext';
import BookmarkItem from './BookmarkItem';
const BookmarkItems: FC<{
tags: string[];
handleSubmit: (tag: string) => Promise<void>;
header: React.ReactNode;
highlightSelected?: boolean;
}> = ({ tags, handleSubmit, header, highlightSelected }) => {
const { bookmarks } = useBookmarkContext();
return (
<>
{header}
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
{bookmarks.length > 0 &&
bookmarks.map((bookmark) => (
<BookmarkItem
key={bookmark.tag}
tag={bookmark.tag}
selected={tags.includes(bookmark.tag)}
count={bookmark.count}
handleSubmit={handleSubmit}
highlightSelected={highlightSelected}
/>
))}
</>
);
};
export default BookmarkItems;

View file

@ -0,0 +1,58 @@
import { useCallback } from 'react';
import type { FC } from 'react';
import { useDeleteConversationTagMutation } from '~/data-provider';
import TooltipIcon from '~/components/ui/TooltipIcon';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { TrashIcon } from '~/components/svg';
import { Label } from '~/components/ui';
import { useLocalize } from '~/hooks';
const DeleteBookmarkButton: FC<{
bookmark: string;
tabIndex?: number;
onFocus?: () => void;
onBlur?: () => void;
}> = ({ bookmark, tabIndex = 0, onFocus, onBlur }) => {
const localize = useLocalize();
const { showToast } = useToastContext();
const deleteBookmarkMutation = useDeleteConversationTagMutation({
onSuccess: () => {
showToast({
message: localize('com_ui_bookmarks_delete_success'),
});
},
onError: () => {
showToast({
message: localize('com_ui_bookmarks_delete_error'),
severity: NotificationSeverity.ERROR,
});
},
});
const confirmDelete = useCallback(async () => {
await deleteBookmarkMutation.mutateAsync(bookmark);
}, [bookmark, deleteBookmarkMutation]);
return (
<TooltipIcon
disabled={false}
appendLabel={false}
title="Delete Bookmark"
confirmMessage={
<Label htmlFor="bookmark" className="text-left text-sm font-medium">
{localize('com_ui_bookmark_delete_confirm')} {bookmark}
</Label>
}
confirm={confirmDelete}
className="transition-color flex h-7 w-7 min-w-7 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
icon={<TrashIcon className="size-4" />}
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}
/>
);
};
export default DeleteBookmarkButton;

View file

@ -0,0 +1,42 @@
import type { FC } from 'react';
import type { TConversationTag } from 'librechat-data-provider';
import BookmarkEditDialog from './BookmarkEditDialog';
import { EditIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui';
const EditBookmarkButton: FC<{
bookmark: TConversationTag;
tabIndex?: number;
onFocus?: () => void;
onBlur?: () => void;
}> = ({ bookmark, tabIndex = 0, onFocus, onBlur }) => {
const localize = useLocalize();
return (
<BookmarkEditDialog
bookmark={bookmark}
trigger={
<button
type="button"
className="transition-color flex h-7 w-7 min-w-7 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}
>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<EditIcon />
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_edit')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</button>
}
/>
);
};
export default EditBookmarkButton;

View file

@ -0,0 +1,6 @@
export { default as DeleteBookmarkButton } from './DeleteBookmarkButton';
export { default as EditBookmarkButton } from './EditBookmarkButton';
export { default as BookmarkEditDialog } from './BookmarkEditDialog';
export { default as BookmarkItems } from './BookmarkItems';
export { default as BookmarkItem } from './BookmarkItem';
export { default as BookmarkForm } from './BookmarkForm';

View file

@ -6,6 +6,7 @@ import type { ContextType } from '~/common';
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
import ExportAndShareMenu from './ExportAndShareMenu';
import HeaderOptions from './Input/HeaderOptions';
import BookmarkMenu from './Menus/BookmarkMenu';
import AddMultiConvo from './AddMultiConvo';
import { useMediaQuery } from '~/hooks';
@ -37,6 +38,7 @@ export default function Header() {
className="pl-0"
/>
)}
<BookmarkMenu />
<AddMultiConvo />
</div>
{!isSmallScreen && (

View file

@ -2,7 +2,7 @@ export default function RemoveFile({ onRemove }: { onRemove: () => void }) {
return (
<button
type="button"
className="absolute right-1 top-1 -translate-y-1/2 translate-x-1/2 rounded-full border border-white bg-gray-500 p-0.5 text-white transition-colors hover:bg-black hover:opacity-100 group-hover:opacity-100 md:opacity-0"
className="absolute right-1 top-1 -translate-y-1/2 translate-x-1/2 rounded-full border border-gray-500 bg-gray-500 p-0.5 text-white transition-colors hover:bg-gray-700 hover:opacity-100 group-hover:opacity-100 md:opacity-0"
onClick={onRemove}
>
<span>
@ -15,6 +15,8 @@ export default function RemoveFile({ onRemove }: { onRemove: () => void }) {
strokeLinejoin="round"
className="icon-sm"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />

View file

@ -32,7 +32,7 @@ import {
DropdownMenuTrigger,
} from '~/components/ui';
import { useDeleteFilesFromTable } from '~/hooks/Files';
import { NewTrashIcon, Spinner } from '~/components/svg';
import { TrashIcon, Spinner } from '~/components/svg';
import useLocalize from '~/hooks/useLocalize';
interface DataTableProps<TData, TValue> {
@ -102,7 +102,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
{isDeleting ? (
<Spinner className="h-4 w-4" />
) : (
<NewTrashIcon className="h-4 w-4 text-red-400" />
<TrashIcon className="h-4 w-4 text-red-400" />
)}
{localize('com_ui_delete')}
</Button>

View file

@ -0,0 +1,134 @@
import { useEffect, useState, type FC } from 'react';
import { useRecoilValue } from 'recoil';
import { useLocation } from 'react-router-dom';
import { TConversation } from 'librechat-data-provider';
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import { useConversationTagsQuery, useTagConversationMutation } from '~/data-provider';
import { BookmarkMenuItems } from './Bookmarks/BookmarkMenuItems';
import { BookmarkContext } from '~/Providers/BookmarkContext';
import { Spinner } from '~/components';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import store from '~/store';
const SAVED_TAG = 'Saved';
const BookmarkMenu: FC = () => {
const localize = useLocalize();
const location = useLocation();
const activeConvo = useRecoilValue(store.conversationByIndex(0));
const globalConvo = useRecoilValue(store.conversation) ?? ({} as TConversation);
const [tags, setTags] = useState<string[]>();
const [open, setIsOpen] = useState(false);
const [conversation, setConversation] = useState<TConversation>();
let thisConversation: TConversation | null | undefined;
if (location.state?.from?.pathname.includes('/chat')) {
thisConversation = globalConvo;
} else {
thisConversation = activeConvo;
}
const { mutateAsync, isLoading } = useTagConversationMutation(
thisConversation?.conversationId ?? '',
);
const { data } = useConversationTagsQuery();
useEffect(() => {
if (
(!conversation && thisConversation) ||
(conversation &&
thisConversation &&
conversation.conversationId !== thisConversation.conversationId)
) {
setConversation(thisConversation);
setTags(thisConversation.tags ?? []);
}
if (tags === undefined && conversation) {
setTags(conversation.tags ?? []);
}
}, [thisConversation, conversation, tags]);
const isActiveConvo =
thisConversation &&
thisConversation.conversationId &&
thisConversation.conversationId !== 'new' &&
thisConversation.conversationId !== 'search';
if (!isActiveConvo) {
return <></>;
}
const onOpenChange = async (open: boolean) => {
if (!open) {
setIsOpen(open);
return;
}
if (open && tags && tags.length > 0) {
setIsOpen(open);
} else {
if (thisConversation && thisConversation.conversationId) {
await mutateAsync({
conversationId: thisConversation.conversationId,
tags: [SAVED_TAG],
});
setTags([SAVED_TAG]);
setConversation({ ...thisConversation, tags: [SAVED_TAG] });
}
}
};
return (
<Root open={open} onOpenChange={onOpenChange}>
<Trigger asChild>
<button
className={cn(
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:ring-0 focus:ring-offset-0',
)}
title={localize('com_ui_bookmarks')}
>
{isLoading ? (
<Spinner />
) : tags && tags.length > 0 ? (
<BookmarkFilledIcon className="icon-sm" />
) : (
<BookmarkIcon className="icon-sm" />
)}
</button>
</Trigger>
<Portal>
<Content
className={cn(
'grid w-full',
'mt-2 min-w-[240px] overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white',
'max-h-[500px]',
)}
side="bottom"
align="start"
>
{data && conversation && (
// Display all bookmarks registered by the user and highlight the tags of the currently selected conversation
<BookmarkContext.Provider value={{ bookmarks: data }}>
<BookmarkMenuItems
// Currently selected conversation
conversation={conversation}
setConversation={setConversation}
// Tags in the conversation
tags={tags ?? []}
// Update tags in the conversation
setTags={setTags}
/>
</BookmarkContext.Provider>
)}
</Content>
</Portal>
</Root>
);
};
export default BookmarkMenu;

View file

@ -0,0 +1,77 @@
import { useCallback } from 'react';
import { BookmarkPlusIcon } from 'lucide-react';
import type { FC } from 'react';
import type { TConversation } from 'librechat-data-provider';
import { BookmarkItems, BookmarkEditDialog } from '~/components/Bookmarks';
import { useTagConversationMutation } from '~/data-provider';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
export const BookmarkMenuItems: FC<{
conversation: TConversation;
tags: string[];
setTags: (tags: string[]) => void;
setConversation: (conversation: TConversation) => void;
}> = ({ conversation, tags, setTags, setConversation }) => {
const { showToast } = useToastContext();
const localize = useLocalize();
const { mutateAsync } = useTagConversationMutation(conversation?.conversationId ?? '');
const handleSubmit = useCallback(
async (tag: string): Promise<void> => {
if (tags !== undefined && conversation?.conversationId) {
const newTags = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag];
await mutateAsync(
{
conversationId: conversation.conversationId,
tags: newTags,
},
{
onSuccess: (newTags: string[]) => {
setTags(newTags);
setConversation({ ...conversation, tags: newTags });
},
onError: () => {
showToast({
message: 'Error adding bookmark',
severity: NotificationSeverity.ERROR,
});
},
},
);
}
},
[tags, conversation],
);
return (
<BookmarkItems
tags={tags}
handleSubmit={handleSubmit}
header={
<div>
<BookmarkEditDialog
conversation={conversation}
tags={tags}
setTags={setTags}
trigger={
<div
role="menuitem"
className="group m-1.5 flex cursor-pointer gap-2 rounded px-2 !pr-3.5 pb-2.5 pt-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
tabIndex={-1}
>
<div className="flex grow items-center justify-between gap-2">
<div className="flex items-center gap-2">
<BookmarkPlusIcon className="size-4" />
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
</div>
</div>
</div>
}
/>
</div>
}
/>
);
};

View file

@ -33,6 +33,7 @@ export default function MessagesView({
<div
onScroll={debouncedHandleScroll}
ref={scrollableRef}
tabIndex={0}
style={{
height: '100%',
overflowY: 'auto',

View file

@ -1,6 +1,7 @@
import { useRecoilValue } from 'recoil';
import { useParams } from 'react-router-dom';
import { useState, useRef, useMemo } from 'react';
import { Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
import { useUpdateConversationMutation } from '~/data-provider';
@ -9,14 +10,14 @@ import { useConversations, useNavigateToConvo } from '~/hooks';
import { NotificationSeverity } from '~/common';
import { ArchiveIcon } from '~/components/svg';
import { useToastContext } from '~/Providers';
import DropDownMenu from './DropDownMenu';
import ArchiveButton from './ArchiveButton';
import DropDownMenu from './DropDownMenu';
import DeleteButton from './DeleteButton';
import RenameButton from './RenameButton';
import HoverToggle from './HoverToggle';
import ShareButton from './ShareButton';
import { cn } from '~/utils';
import store from '~/store';
import ShareButton from './ShareButton';
type KeyEvent = KeyboardEvent<HTMLInputElement>;
@ -51,7 +52,8 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
// set document title
document.title = title;
navigateWithLastTools(conversation);
/* Note: Latest Message should not be reset if existing convo */
navigateWithLastTools(conversation, !conversationId || conversationId === Constants.NEW_CONVO);
};
const renameHandler = (e: MouseEvent<HTMLButtonElement>) => {

View file

@ -1,5 +1,5 @@
import React from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { anthropicSettings } from 'librechat-data-provider';
import type { TModelSelectProps, OnInputNumberChange } from '~/common';
import {
Input,
@ -41,15 +41,31 @@ export default function Settings({ conversation, setOption, models, readonly }:
return null;
}
const setModel = setOption('model');
const setModelLabel = setOption('modelLabel');
const setPromptPrefix = setOption('promptPrefix');
const setTemperature = setOption('temperature');
const setTopP = setOption('topP');
const setTopK = setOption('topK');
const setMaxOutputTokens = setOption('maxOutputTokens');
const setResendFiles = setOption('resendFiles');
const setModel = (newModel: string) => {
const modelSetter = setOption('model');
const maxOutputSetter = setOption('maxOutputTokens');
if (maxOutputTokens) {
maxOutputSetter(anthropicSettings.maxOutputTokens.set(maxOutputTokens, newModel));
}
modelSetter(newModel);
};
const setMaxOutputTokens = (value: number) => {
const setter = setOption('maxOutputTokens');
if (model) {
setter(anthropicSettings.maxOutputTokens.set(value, model));
} else {
setter(value);
}
};
return (
<div className="grid grid-cols-5 gap-6">
<div className="col-span-5 flex flex-col items-center justify-start gap-6 sm:col-span-3">
@ -139,14 +155,16 @@ export default function Settings({ conversation, setOption, models, readonly }:
<div className="flex justify-between">
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
{localize('com_endpoint_temperature')}{' '}
<small className="opacity-40">({localize('com_endpoint_default')}: 1)</small>
<small className="opacity-40">
({localize('com_endpoint_default')}: {anthropicSettings.temperature.default})
</small>
</Label>
<InputNumber
id="temp-int"
disabled={readonly}
value={temperature}
onChange={(value) => setTemperature(Number(value))}
max={1}
max={anthropicSettings.temperature.max}
min={0}
step={0.01}
controls={false}
@ -161,10 +179,10 @@ export default function Settings({ conversation, setOption, models, readonly }:
</div>
<Slider
disabled={readonly}
value={[temperature ?? 1]}
value={[temperature ?? anthropicSettings.temperature.default]}
onValueChange={(value) => setTemperature(value[0])}
doubleClickHandler={() => setTemperature(1)}
max={1}
doubleClickHandler={() => setTemperature(anthropicSettings.temperature.default)}
max={anthropicSettings.temperature.max}
min={0}
step={0.01}
className="flex h-4 w-full"
@ -178,7 +196,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_p')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', '0.7')})
({localize('com_endpoint_default_with_num', anthropicSettings.topP.default + '')})
</small>
</Label>
<InputNumber
@ -186,7 +204,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
disabled={readonly}
value={topP}
onChange={(value) => setTopP(Number(value))}
max={1}
max={anthropicSettings.topP.max}
min={0}
step={0.01}
controls={false}
@ -203,8 +221,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
disabled={readonly}
value={[topP ?? 0.7]}
onValueChange={(value) => setTopP(value[0])}
doubleClickHandler={() => setTopP(1)}
max={1}
doubleClickHandler={() => setTopP(anthropicSettings.topP.default)}
max={anthropicSettings.topP.max}
min={0}
step={0.01}
className="flex h-4 w-full"
@ -219,7 +237,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_k')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', '5')})
({localize('com_endpoint_default_with_num', anthropicSettings.topK.default + '')})
</small>
</Label>
<InputNumber
@ -227,7 +245,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
disabled={readonly}
value={topK}
onChange={(value) => setTopK(Number(value))}
max={40}
max={anthropicSettings.topK.max}
min={1}
step={0.01}
controls={false}
@ -244,8 +262,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
disabled={readonly}
value={[topK ?? 5]}
onValueChange={(value) => setTopK(value[0])}
doubleClickHandler={() => setTopK(0)}
max={40}
doubleClickHandler={() => setTopK(anthropicSettings.topK.default)}
max={anthropicSettings.topK.max}
min={1}
step={0.01}
className="flex h-4 w-full"
@ -258,16 +276,14 @@ export default function Settings({ conversation, setOption, models, readonly }:
<div className="flex justify-between">
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
{localize('com_endpoint_max_output_tokens')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', '4000')})
</small>
<small className="opacity-40">({anthropicSettings.maxOutputTokens.default})</small>
</Label>
<InputNumber
id="max-tokens-int"
disabled={readonly}
value={maxOutputTokens}
onChange={(value) => setMaxOutputTokens(Number(value))}
max={4000}
max={anthropicSettings.maxOutputTokens.max}
min={1}
step={1}
controls={false}
@ -282,10 +298,12 @@ export default function Settings({ conversation, setOption, models, readonly }:
</div>
<Slider
disabled={readonly}
value={[maxOutputTokens ?? 4000]}
value={[maxOutputTokens ?? anthropicSettings.maxOutputTokens.default]}
onValueChange={(value) => setMaxOutputTokens(value[0])}
doubleClickHandler={() => setMaxOutputTokens(0)}
max={4000}
doubleClickHandler={() =>
setMaxOutputTokens(anthropicSettings.maxOutputTokens.default)
}
max={anthropicSettings.maxOutputTokens.max}
min={1}
step={1}
className="flex h-4 w-full"

View file

@ -1,5 +1,5 @@
import React from 'react';
import { CrossIcon, NewTrashIcon } from '~/components/svg';
import { CrossIcon, TrashIcon } from '~/components/svg';
import { Button } from '~/components/ui';
type DeleteIconButtonProps = {
@ -10,7 +10,7 @@ export default function DeleteIconButton({ onClick }: DeleteIconButtonProps) {
return (
<div className="w-fit">
<Button className="bg-red-400 p-3" onClick={onClick}>
<NewTrashIcon />
<TrashIcon />
</Button>
</div>
);

View file

@ -32,7 +32,7 @@ import {
DropdownMenuTrigger,
} from '~/components/ui';
import { useDeleteFilesFromTable } from '~/hooks/Files';
import { NewTrashIcon, Spinner } from '~/components/svg';
import { TrashIcon, Spinner } from '~/components/svg';
import useLocalize from '~/hooks/useLocalize';
import ActionButton from '../ActionButton';
import UploadFileButton from './UploadFileButton';
@ -112,7 +112,7 @@ export default function DataTableFile<TData, TValue>({
{isDeleting ? (
<Spinner className="h-4 w-4" />
) : (
<NewTrashIcon className="h-4 w-4 text-red-400" />
<TrashIcon className="h-4 w-4 text-red-400" />
)}
{localize('com_ui_delete')}
</Button>

View file

@ -1,6 +1,6 @@
import type { TFile } from 'librechat-data-provider';
import React from 'react';
import { NewTrashIcon } from '~/components/svg';
import { TrashIcon } from '~/components/svg';
import { Button } from '~/components/ui';
type FileListItemProps = {
@ -25,7 +25,7 @@ export default function FileListItem({ file, deleteFile, width = '400px' }: File
className="my-0 ml-3 bg-transparent p-0 text-[#666666] hover:bg-slate-200"
onClick={() => deleteFile(file._id)}
>
<NewTrashIcon className="m-0 p-0" />
<TrashIcon className="m-0 p-0" />
</Button>
</div>
</div>

View file

@ -2,7 +2,7 @@ import type { TFile } from 'librechat-data-provider';
import { FileIcon, PlusIcon } from 'lucide-react';
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { DotsIcon, NewTrashIcon } from '~/components/svg';
import { DotsIcon, TrashIcon } from '~/components/svg';
import { Button } from '~/components/ui';
type FileListItemProps = {
@ -68,7 +68,7 @@ export default function FileListItem2({
className="w-min bg-transparent text-[#666666] hover:bg-slate-200"
onClick={() => deleteFile(file._id)}
>
<NewTrashIcon className="" />
<TrashIcon className="" />
</Button>
</div>
</div>

View file

@ -1,7 +1,7 @@
import { TFile } from 'librechat-data-provider/dist/types';
import React, { useState } from 'react';
import { TThread, TVectorStore } from '~/common';
import { CheckMark, NewTrashIcon } from '~/components/svg';
import { CheckMark, TrashIcon } from '~/components/svg';
import { Button } from '~/components/ui';
import DeleteIconButton from '../DeleteIconButton';
import VectorStoreButton from '../VectorStore/VectorStoreButton';
@ -140,7 +140,7 @@ export default function FilePreview() {
}}
variant={'ghost'}
>
<NewTrashIcon className="m-0 p-0" />
<TrashIcon className="m-0 p-0" />
</Button>
</div>
</div>
@ -167,7 +167,7 @@ export default function FilePreview() {
console.log('Remove from thread');
}}
>
<NewTrashIcon className="m-0 p-0" />
<TrashIcon className="m-0 p-0" />
</Button>
</div>
</div>

View file

@ -1,7 +1,7 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { TVectorStore } from '~/common';
import { DotsIcon, NewTrashIcon, TrashIcon } from '~/components/svg';
import { DotsIcon, TrashIcon, TrashIcon } from '~/components/svg';
import { Button } from '~/components/ui';
type VectorStoreListItemProps = {
@ -39,7 +39,7 @@ export default function VectorStoreListItem({
className="m-0 w-full bg-transparent p-0 text-[#666666] hover:bg-slate-200 sm:w-fit"
onClick={() => deleteVectorStore(vectorStore._id)}
>
<NewTrashIcon className="m-0 p-0" />
<TrashIcon className="m-0 p-0" />
</Button>
</div>
</div>

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import DeleteIconButton from '../DeleteIconButton';
import { Button } from '~/components/ui';
import { NewTrashIcon } from '~/components/svg';
import { TrashIcon } from '~/components/svg';
import { TFile } from 'librechat-data-provider/dist/types';
import UploadFileButton from '../FileList/UploadFileButton';
import UploadFileModal from '../FileList/UploadFileModal';
@ -204,7 +204,7 @@ export default function VectorStorePreview() {
className="my-0 ml-3 h-min bg-transparent p-0 text-[#666666] hover:bg-slate-200"
onClick={() => console.log('click')}
>
<NewTrashIcon className="m-0 p-0" />
<TrashIcon className="m-0 p-0" />
</Button>
</div>
</div>

View file

@ -0,0 +1,99 @@
import { useState, type FC } from 'react';
import { useRecoilValue } from 'recoil';
import { useLocation } from 'react-router-dom';
import { TConversation } from 'librechat-data-provider';
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import { useGetConversationTags } from 'librechat-data-provider/react-query';
import { BookmarkContext } from '~/Providers/BookmarkContext';
import BookmarkNavItems from './BookmarkNavItems';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import store from '~/store';
type BookmarkNavProps = {
tags: string[];
setTags: (tags: string[]) => void;
};
const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags }: BookmarkNavProps) => {
const localize = useLocalize();
const location = useLocation();
const { data } = useGetConversationTags();
const activeConvo = useRecoilValue(store.conversationByIndex(0));
const globalConvo = useRecoilValue(store.conversation) ?? ({} as TConversation);
const [open, setIsOpen] = useState(false);
let conversation: TConversation | null | undefined;
if (location.state?.from?.pathname.includes('/chat')) {
conversation = globalConvo;
} else {
conversation = activeConvo;
}
// Hide the button if there are no tags
if (!data || !data.some((tag) => tag.count > 0)) {
return null;
}
return (
<Root open={open} onOpenChange={setIsOpen}>
<Trigger asChild>
<button
className={cn(
'group-ui-open:bg-gray-100 dark:group-ui-open:bg-gray-700 duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800',
open ? 'bg-gray-100 dark:bg-gray-800' : '',
)}
id="presets-button"
data-testid="presets-button"
title={localize('com_endpoint_examples')}
>
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
<div className="relative flex">
<div className="relative flex h-8 w-8 items-center justify-center rounded-full p-1 dark:text-white">
{tags.length > 0 ? (
<BookmarkFilledIcon className="h-6 w-6" />
) : (
<BookmarkIcon className="h-6 w-6" />
)}
</div>
</div>
</div>
<div
className="mt-2 grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-black dark:text-gray-100"
style={{ marginTop: '0', marginLeft: '0' }}
>
{tags.length > 0 ? tags.join(',') : localize('com_ui_bookmarks')}
</div>
</button>
</Trigger>
<Portal>
<div className="fixed left-0 top-0 z-auto translate-x-[268px] translate-y-[50px]">
<Content
side="bottom"
align="start"
className="mt-2 max-h-96 min-w-[240px] overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-96"
>
{data && conversation && data.some((tag) => tag.count > 0) && (
// Display bookmarks and highlight the selected tag
<BookmarkContext.Provider value={{ bookmarks: data.filter((tag) => tag.count > 0) }}>
<BookmarkNavItems
// Currently selected conversation
conversation={conversation}
// List of selected tags(string)
tags={tags}
// When a user selects a tag, this `setTags` function is called to refetch the list of conversations for the selected tag
setTags={setTags}
/>
</BookmarkContext.Provider>
)}
</Content>
</div>
</Portal>
</Root>
);
};
export default BookmarkNav;

View file

@ -0,0 +1,58 @@
import { useEffect, useState, type FC } from 'react';
import { CrossCircledIcon } from '@radix-ui/react-icons';
import type { TConversation } from 'librechat-data-provider';
import { BookmarkItems, BookmarkItem } from '~/components/Bookmarks';
const BookmarkNavItems: FC<{
conversation: TConversation;
tags: string[];
setTags: (tags: string[]) => void;
}> = ({ conversation, tags, setTags }) => {
const [currentConversation, setCurrentConversation] = useState<TConversation>();
useEffect(() => {
if (!currentConversation) {
setCurrentConversation(conversation);
}
}, [conversation, currentConversation]);
const getUpdatedSelected = (tag: string) => {
if (tags.some((selectedTag) => selectedTag === tag)) {
return tags.filter((selectedTag) => selectedTag !== tag);
} else {
return [...(tags || []), tag];
}
};
const handleSubmit = (tag: string) => {
const updatedSelected = getUpdatedSelected(tag);
setTags(updatedSelected);
return Promise.resolve();
};
const clear = () => {
setTags([]);
return Promise.resolve();
};
return (
<>
<BookmarkItems
tags={tags}
handleSubmit={handleSubmit}
highlightSelected={true}
header={
<BookmarkItem
tag="Clear all"
data-testid="bookmark-item-clear"
handleSubmit={clear}
selected={false}
icon={<CrossCircledIcon className="h-4 w-4" />}
/>
}
/>
</>
);
};
export default BookmarkNavItems;

View file

@ -1,6 +1,7 @@
import { useCallback, useEffect, useState, useMemo, memo } from 'react';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useCallback, useEffect, useState, useMemo, memo } from 'react';
import type { ConversationListResponse } from 'librechat-data-provider';
import {
useMediaQuery,
useAuthContext,
@ -12,6 +13,7 @@ import {
import { useConversationsInfiniteQuery } from '~/data-provider';
import { TooltipProvider, Tooltip } from '~/components/ui';
import { Conversations } from '~/components/Conversations';
import BookmarkNav from './Bookmarks/BookmarkNav';
import { useSearchContext } from '~/Providers';
import { Spinner } from '~/components/svg';
import SearchBar from './SearchBar';
@ -19,7 +21,6 @@ import NavToggle from './NavToggle';
import NavLinks from './NavLinks';
import NewChat from './NewChat';
import { cn } from '~/utils';
import { ConversationListResponse } from 'librechat-data-provider';
import store from '~/store';
const Nav = ({ navVisible, setNavVisible }) => {
@ -58,12 +59,21 @@ const Nav = ({ navVisible, setNavVisible }) => {
const { refreshConversations } = useConversations();
const { pageNumber, searchQuery, setPageNumber, searchQueryRes } = useSearchContext();
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useConversationsInfiniteQuery(
{ pageNumber: pageNumber.toString(), isArchived: false },
{ enabled: isAuthenticated },
);
const [tags, setTags] = useState<string[]>([]);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } =
useConversationsInfiniteQuery(
{
pageNumber: pageNumber.toString(),
isArchived: false,
tags: tags.length === 0 ? undefined : tags,
},
{ enabled: isAuthenticated },
);
useEffect(() => {
// When a tag is selected, refetch the list of conversations related to that tag
refetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tags]);
const { containerRef, moveToTop } = useNavScrolling<ConversationListResponse>({
setShowLoading,
hasNextPage: searchQuery ? searchQueryRes.hasNextPage : hasNextPage,
@ -154,6 +164,7 @@ const Nav = ({ navVisible, setNavVisible }) => {
/>
)}
</div>
<BookmarkNav tags={tags} setTags={setTags} />
<NavLinks />
</nav>
</div>
@ -168,7 +179,7 @@ const Nav = ({ navVisible, setNavVisible }) => {
navVisible={navVisible}
className="fixed left-0 top-1/2 z-40 hidden md:flex"
/>
<div className={`nav-mask${navVisible ? ' active' : ''}`} onClick={toggleNavVisible} />
<div className={`nav-mask${navVisible ? 'active' : ''}`} onClick={toggleNavVisible} />
</Tooltip>
</TooltipProvider>
);

View file

@ -1,12 +1,9 @@
import { useRecoilState } from 'recoil';
import * as Tabs from '@radix-ui/react-tabs';
import { Lightbulb, Cog } from 'lucide-react';
import { SettingsTabValues } from 'librechat-data-provider';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { Lightbulb, Cog } from 'lucide-react';
import { useOnClickOutside, useMediaQuery } from '~/hooks';
import store from '~/store';
import { cn } from '~/utils';
import ConversationModeSwitch from './ConversationModeSwitch';
import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query';
import {
CloudBrowserVoicesSwitch,
AutomaticPlaybackSwitch,
@ -24,7 +21,10 @@ import {
EngineSTTDropdown,
DecibelSelector,
} from './STT';
import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query';
import ConversationModeSwitch from './ConversationModeSwitch';
import { useOnClickOutside, useMediaQuery } from '~/hooks';
import { cn, logger } from '~/utils';
import store from '~/store';
function Speech() {
const [confirmClear, setConfirmClear] = useState(false);
@ -75,7 +75,11 @@ function Speech() {
playbackRate: { value: playbackRate, setFunc: setPlaybackRate },
};
if (settings[key].value !== newValue || settings[key].value === newValue || !settings[key]) {
if (
(settings[key].value !== newValue || settings[key].value === newValue || !settings[key]) &&
settings[key].value === 'sttExternal' &&
settings[key].value === 'ttsExternal'
) {
return;
}
@ -131,8 +135,7 @@ function Speech() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
console.log(sttExternal);
console.log(ttsExternal);
logger.log({ sttExternal, ttsExternal });
const contentRef = useRef(null);
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);

View file

@ -18,7 +18,7 @@ import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { RenameButton } from '~/components/Conversations';
import { useLocalize, useAuthContext } from '~/hooks';
import { NewTrashIcon } from '~/components/svg';
import { TrashIcon } from '~/components/svg';
import { cn } from '~/utils/';
export default function DashGroupItem({
@ -169,7 +169,7 @@ export default function DashGroupItem({
e.stopPropagation();
}}
>
<NewTrashIcon className="icon-md text-gray-600 dark:text-gray-300" />
<TrashIcon className="icon-md text-gray-600 dark:text-gray-300" />
</Button>
</DialogTrigger>
<DialogTemplate

View file

@ -3,7 +3,7 @@ import { useForm, useFieldArray, Controller, useWatch } from 'react-hook-form';
import type { TPromptGroup } from 'librechat-data-provider';
import { extractVariableInfo, wrapVariable, replaceSpecialVars } from '~/utils';
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
import { Input } from '~/components/ui';
import { Textarea } from '~/components/ui';
type FormValues = {
fields: { variable: string; value: string }[];
@ -103,11 +103,18 @@ export default function VariableForm({
name={`fields.${index}.value`}
control={control}
render={({ field }) => (
<Input
<Textarea
{...field}
id={`fields.${index}.value`}
className="input text-grey-darker rounded border px-3 py-2 focus:bg-white dark:border-gray-500 dark:focus:bg-gray-700"
className="input text-grey-darker h-10 rounded border px-3 py-2 focus:bg-white dark:border-gray-500 dark:focus:bg-gray-700"
placeholder={uniqueVariables[index]}
onKeyDown={(e) => {
// Submit the form on enter like you would with an Input component
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit((data) => onSubmit(data))();
}
}}
/>
)}
/>

View file

@ -70,7 +70,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
onBlur={() => setIsEditing(false)}
/>
) : (
<span className="block break-words px-2 py-1 dark:text-gray-200">{field.value}</span>
<pre className="block break-words px-2 py-1 dark:text-gray-200">{field.value}</pre>
)
}
/>

View file

@ -1,20 +1,31 @@
import { memo } from 'react';
import { useParams } from 'react-router-dom';
import { useGetSharedMessages } from 'librechat-data-provider/react-query';
import { useGetSharedMessages, useGetStartupConfig } from 'librechat-data-provider/react-query';
import { useLocalize, useDocumentTitle } from '~/hooks';
import { ShareContext } from '~/Providers';
import { Spinner } from '~/components/svg';
import MessagesView from './MessagesView';
import { useLocalize } from '~/hooks';
import { buildTree } from '~/utils';
import Footer from '../Chat/Footer';
function SharedView() {
const localize = useLocalize();
const { data: config } = useGetStartupConfig();
const { shareId } = useParams();
const { data, isLoading } = useGetSharedMessages(shareId ?? '');
const dataTree = data && buildTree({ messages: data.messages });
const messagesTree = dataTree?.length === 0 ? null : dataTree ?? null;
// configure document title
let docTitle = '';
if (config?.appTitle && data?.title) {
docTitle = `${data?.title} | ${config.appTitle}`;
} else {
docTitle = data?.title || config?.appTitle || document.title;
}
useDocumentTitle(docTitle);
return (
<ShareContext.Provider value={{ isSharedConvo: true }}>
<div

View file

@ -0,0 +1,46 @@
import { BookmarkPlusIcon } from 'lucide-react';
import { useConversationTagsQuery, useRebuildConversationTagsMutation } from '~/data-provider';
import { Button } from '~/components/ui';
import { BookmarkContext } from '~/Providers/BookmarkContext';
import { BookmarkEditDialog } from '~/components/Bookmarks';
import BookmarkTable from './BookmarkTable';
import { Spinner } from '~/components/svg';
import { useLocalize } from '~/hooks';
import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings';
const BookmarkPanel = () => {
const localize = useLocalize();
const { mutate, isLoading } = useRebuildConversationTagsMutation();
const { data } = useConversationTagsQuery();
const rebuildTags = () => {
mutate({});
};
return (
<div className="h-auto max-w-full overflow-x-hidden">
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
<BookmarkTable />
<div className="flex justify-between gap-2">
<Button variant="outline" onClick={rebuildTags} className="w-50 text-sm">
{isLoading ? (
<Spinner />
) : (
<div className="flex gap-2">
{localize('com_ui_bookmarks_rebuild')}
<HoverCardSettings side="top" text="com_nav_info_bookmarks_rebuild" />
</div>
)}
</Button>
<BookmarkEditDialog
trigger={
<Button variant="outline" onClick={rebuildTags} className="w-full text-sm">
<BookmarkPlusIcon className="mr-1 size-4" />
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
</Button>
}
/>
</div>
</BookmarkContext.Provider>
</div>
);
};
export default BookmarkPanel;

View file

@ -0,0 +1,97 @@
import React, { useCallback, useEffect, useState } from 'react';
import type { ConversationTagsResponse, TConversationTag } from 'librechat-data-provider';
import { Table, TableHeader, TableBody, TableRow, TableCell, Input, Button } from '~/components/ui';
import { BookmarkContext, useBookmarkContext } from '~/Providers/BookmarkContext';
import BookmarkTableRow from './BookmarkTableRow';
import { useLocalize } from '~/hooks';
const BookmarkTable = () => {
const localize = useLocalize();
const [rows, setRows] = useState<ConversationTagsResponse>([]);
const [pageIndex, setPageIndex] = useState(0);
const [searchQuery, setSearchQuery] = useState('');
const pageSize = 10;
const { bookmarks } = useBookmarkContext();
useEffect(() => {
setRows(bookmarks?.map((item) => ({ id: item.tag, ...item })) || []);
}, [bookmarks]);
const moveRow = useCallback((dragIndex: number, hoverIndex: number) => {
setRows((prevTags: TConversationTag[]) => {
const updatedRows = [...prevTags];
const [movedRow] = updatedRows.splice(dragIndex, 1);
updatedRows.splice(hoverIndex, 0, movedRow);
return updatedRows;
});
}, []);
const renderRow = useCallback((row: TConversationTag, position: number) => {
return <BookmarkTableRow key={row.tag} moveRow={moveRow} row={row} position={position} />;
}, []);
const filteredRows = rows.filter((row) =>
row.tag.toLowerCase().includes(searchQuery.toLowerCase()),
);
const currentRows = filteredRows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
return (
<BookmarkContext.Provider value={{ bookmarks }}>
<div className="flex items-center gap-4 py-4">
<Input
placeholder={localize('com_ui_bookmarks_filter')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full dark:border-gray-700"
/>
</div>
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">
<Table className="table-fixed border-separate border-spacing-0">
<TableHeader>
<TableRow>
<TableCell className="w-full px-3 py-3.5 pl-6 dark:bg-gray-700">
<div>{localize('com_ui_bookmarks_title')}</div>
</TableCell>
<TableCell className="w-full px-3 py-3.5 dark:bg-gray-700 sm:pl-6">
<div>{localize('com_ui_bookmarks_count')}</div>
</TableCell>
</TableRow>
</TableHeader>
<TableBody>{currentRows.map((row, i) => renderRow(row, i))}</TableBody>
</Table>
</div>
<div className="flex items-center justify-between py-4">
<div className="pl-1 text-gray-400">
{localize('com_ui_showing')} {pageIndex * pageSize + 1} -{' '}
{Math.min((pageIndex + 1) * pageSize, filteredRows.length)} {localize('com_ui_of')}{' '}
{filteredRows.length}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setPageIndex((prev) => Math.max(prev - 1, 0))}
disabled={pageIndex === 0}
>
{localize('com_ui_prev')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
setPageIndex((prev) =>
(prev + 1) * pageSize < filteredRows.length ? prev + 1 : prev,
)
}
disabled={(pageIndex + 1) * pageSize >= filteredRows.length}
>
{localize('com_ui_next')}
</Button>
</div>
</div>
</BookmarkContext.Provider>
);
};
export default BookmarkTable;

View file

@ -0,0 +1,85 @@
import React, { useState } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import type { TConversationTag } from 'librechat-data-provider';
import { DeleteBookmarkButton, EditBookmarkButton } from '~/components/Bookmarks';
import { TableRow, TableCell } from '~/components/ui';
interface BookmarkTableRowProps {
row: TConversationTag;
moveRow: (dragIndex: number, hoverIndex: number) => void;
position: number;
}
const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, position }) => {
const [isHovered, setIsHovered] = useState(false);
const ref = React.useRef<HTMLTableRowElement>(null);
const [, drop] = useDrop({
accept: 'bookmark',
hover(item: { index: number }) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = position;
if (dragIndex === hoverIndex) {
return;
}
moveRow(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
type: 'bookmark',
item: { index: position },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drag(drop(ref));
return (
<TableRow
ref={ref}
className="cursor-move hover:bg-gray-100 dark:hover:bg-gray-800"
style={{ opacity: isDragging ? 0.5 : 1 }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<TableCell className="w-full px-3 py-3.5 pl-6">
<div className="truncate">{row.tag}</div>
</TableCell>
<TableCell className="w-full px-3 py-3.5 sm:pl-6">
<div className="flex items-center justify-between py-1">
<div>{row.count}</div>
<div
className="flex items-center gap-2"
style={{
opacity: isHovered ? 1 : 0,
transition: 'opacity 0.1s ease-in-out',
}}
onFocus={() => setIsHovered(true)}
onBlur={() => setIsHovered(false)}
>
<EditBookmarkButton
bookmark={row}
tabIndex={0}
onFocus={() => setIsHovered(true)}
onBlur={() => setIsHovered(false)}
/>
<DeleteBookmarkButton
bookmark={row.tag}
tabIndex={0}
onFocus={() => setIsHovered(true)}
onBlur={() => setIsHovered(false)}
/>
</div>
</div>
</TableCell>
</TableRow>
);
};
export default BookmarkTableRow;

View file

@ -7,9 +7,10 @@ import {
} from 'librechat-data-provider';
import type { AssistantPanelProps, ActionAuthForm } from '~/common';
import { useAssistantsMapContext, useToastContext } from '~/Providers';
import { Dialog, DialogTrigger } from '~/components/ui';
import { Dialog, DialogTrigger, OGDialog, OGDialogTrigger, Label } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useDeleteAction } from '~/data-provider';
import { NewTrashIcon } from '~/components/svg';
import { TrashIcon } from '~/components/svg';
import useLocalize from '~/hooks/useLocalize';
import ActionsInput from './ActionsInput';
import ActionsAuth from './ActionsAuth';
@ -119,33 +120,52 @@ export default function ActionsPanel({
</div>
</button>
</div>
{!!action && (
<div className="absolute right-0 top-6">
<button
type="button"
disabled={!assistant_id || !action.action_id}
className="btn relative bg-transparent text-red-500 hover:bg-gray-100 dark:hover:bg-gray-800"
onClick={() => {
if (!assistant_id) {
return prompt('No assistant_id found, is the assistant created?');
}
const confirmed = confirm('Are you sure you want to delete this action?');
if (confirmed) {
<OGDialog>
<OGDialogTrigger asChild>
<div className="absolute right-0 top-6">
<button
type="button"
disabled={!assistant_id || !action.action_id}
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
>
<TrashIcon className="text-red-500" />
</button>
</div>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_action')}
className="max-w-[450px]"
main={
<Label className="text-left text-sm font-medium">
{localize('com_ui_delete_action_confirm')}
</Label>
}
selection={{
selectHandler: () => {
if (!assistant_id) {
return showToast({
message: 'No assistant_id found, is the assistant created?',
status: 'error',
});
}
deleteAction.mutate({
model: assistantMap[endpoint][assistant_id].model,
action_id: action.action_id,
assistant_id,
endpoint,
});
}
},
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
selectText: localize('com_ui_delete'),
}}
>
<div className="flex w-full items-center justify-center gap-2">
<NewTrashIcon className="icon-md text-red-500" />
</div>
</button>
</div>
/>
</OGDialog>
)}
<div className="text-xl font-medium">{(action ? 'Edit' : 'Add') + ' ' + 'actions'}</div>
<div className="text-token-text-tertiary text-sm">
{localize('com_assistants_actions_info')}

View file

@ -1,3 +1,4 @@
import { useState } from 'react';
import type { Action } from 'librechat-data-provider';
import GearIcon from '~/components/svg/GearIcon';
@ -8,11 +9,15 @@ export default function AssistantAction({
action: Action;
onClick: () => void;
}) {
const [isHovering, setIsHovering] = useState(false);
return (
<div>
<div
onClick={onClick}
className="border-token-border-medium flex w-full rounded-lg border text-sm hover:cursor-pointer"
className="flex w-full rounded-lg text-sm hover:cursor-pointer"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<div
className="h-9 grow whitespace-nowrap px-3 py-2"
@ -20,13 +25,14 @@ export default function AssistantAction({
>
{action.metadata.domain}
</div>
<div className="w-px bg-gray-300 dark:bg-gray-600" />
<button
type="button"
className="flex h-9 w-9 min-w-9 items-center justify-center rounded-lg rounded-l-none"
>
<GearIcon className="icon-sm" />
</button>
{isHovering && (
<button
type="button"
className="transition-color flex h-9 w-9 min-w-9 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
>
<GearIcon className="icon-sm" />
</button>
)}
</div>
</div>
);

View file

@ -13,6 +13,7 @@ import {
import type { FunctionTool, TConfig, TPlugin } from 'librechat-data-provider';
import type { AssistantForm, AssistantPanelProps } from '~/common';
import { useCreateAssistantMutation, useUpdateAssistantMutation } from '~/data-provider';
import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils';
import { useAssistantsMapContext, useToastContext } from '~/Providers';
import { useSelectAssistant, useLocalize } from '~/hooks';
import { ToolSelectDialog } from '~/components/Tools';
@ -24,13 +25,15 @@ import AssistantAction from './AssistantAction';
import ContextButton from './ContextButton';
import AssistantTool from './AssistantTool';
import { Spinner } from '~/components/svg';
import { cn, cardStyle } from '~/utils/';
import Knowledge from './Knowledge';
import { Panel } from '~/common';
const labelClass = 'mb-2 block text-xs font-bold text-gray-700 dark:text-gray-400';
const inputClass =
'focus:shadow-outline w-full appearance-none rounded-md border px-3 py-2 text-sm leading-tight text-gray-700 dark:text-white shadow focus:border-green-500 focus:outline-none focus:ring-0 dark:bg-gray-800 dark:border-gray-700/80';
const labelClass = 'mb-2 text-token-text-primary block font-medium';
const inputClass = cn(
defaultTextProps,
'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800',
removeFocusOutlines,
);
export default function AssistantPanel({
// index = 0,
@ -297,7 +300,7 @@ export default function AssistantPanel({
{...field}
value={field.value ?? ''}
{...{ max: 32768 }}
className="focus:shadow-outline min-h-[150px] w-full resize-none resize-y appearance-none rounded-md border px-3 py-2 text-sm leading-tight text-gray-700 shadow focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-700/80 dark:bg-gray-800 dark:text-white"
className={cn(inputClass, 'min-h-[100px] resize-none resize-y')}
id="instructions"
placeholder={localize('com_assistants_instructions_placeholder')}
rows={3}
@ -357,7 +360,7 @@ export default function AssistantPanel({
${toolsEnabled && actionsEnabled ? ' + ' : ''}
${actionsEnabled ? localize('com_assistants_actions') : ''}`}
</label>
<div className="space-y-1">
<div className="space-y-2">
{functions.map((func, i) => (
<AssistantTool
key={`${func}-${i}-${assistant_id}`}
@ -373,37 +376,39 @@ export default function AssistantPanel({
<AssistantAction key={i} action={action} onClick={() => setAction(action)} />
);
})}
{toolsEnabled && (
<button
type="button"
onClick={() => setShowToolDialog(true)}
className="btn border-token-border-light relative mx-1 mt-2 h-8 rounded-lg bg-transparent font-medium hover:bg-gray-100 dark:hover:bg-gray-800"
>
<div className="flex w-full items-center justify-center gap-2">
{localize('com_assistants_add_tools')}
</div>
</button>
)}
{actionsEnabled && (
<button
type="button"
disabled={!assistant_id}
onClick={() => {
if (!assistant_id) {
return showToast({
message: localize('com_assistants_actions_disabled'),
status: 'warning',
});
}
setActivePanel(Panel.actions);
}}
className="btn border-token-border-light relative mt-2 h-8 rounded-lg bg-transparent font-medium hover:bg-gray-100 dark:hover:bg-gray-800"
>
<div className="flex w-full items-center justify-center gap-2">
{localize('com_assistants_add_actions')}
</div>
</button>
)}
<div className="flex space-x-2">
{toolsEnabled && (
<button
type="button"
onClick={() => setShowToolDialog(true)}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
>
<div className="flex w-full items-center justify-center gap-2">
{localize('com_assistants_add_tools')}
</div>
</button>
)}
{actionsEnabled && (
<button
type="button"
disabled={!assistant_id}
onClick={() => {
if (!assistant_id) {
return showToast({
message: localize('com_assistants_actions_disabled'),
status: 'warning',
});
}
setActivePanel(Panel.actions);
}}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
>
<div className="flex w-full items-center justify-center gap-2">
{localize('com_assistants_add_actions')}
</div>
</button>
)}
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2">
@ -415,23 +420,9 @@ export default function AssistantPanel({
createMutation={create}
endpoint={endpoint}
/>
{/* Secondary Select Button */}
{assistant_id && (
<button
className="btn btn-secondary"
type="button"
disabled={!assistant_id}
onClick={(e) => {
e.preventDefault();
onSelectAssistant(assistant_id);
}}
>
{localize('com_ui_select')}
</button>
)}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex w-[90px] items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
type="submit"
>
{create.isLoading || update.isLoading ? (

View file

@ -1,5 +1,12 @@
import React, { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import type { TPlugin } from 'librechat-data-provider';
import GearIcon from '~/components/svg/GearIcon';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useToastContext } from '~/Providers';
import { TrashIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
export default function AssistantTool({
@ -11,42 +18,89 @@ export default function AssistantTool({
allTools: TPlugin[];
assistant_id?: string;
}) {
const [isHovering, setIsHovering] = useState(false);
const localize = useLocalize();
const { showToast } = useToastContext();
const updateUserPlugins = useUpdateUserPluginsMutation();
const { getValues, setValue } = useFormContext();
const currentTool = allTools.find((t) => t.pluginKey === tool);
const removeTool = (tool: string) => {
if (tool) {
updateUserPlugins.mutate(
{ pluginKey: tool, action: 'uninstall', auth: null, isAssistantTool: true },
{
onError: (error: unknown) => {
showToast({ message: `Error while deleting the tool: ${error}`, status: 'error' });
},
onSuccess: () => {
const fns = getValues('functions').filter((fn) => fn !== tool);
setValue('functions', fns);
showToast({ message: 'Tool deleted successfully', status: 'success' });
},
},
);
}
};
if (!currentTool) {
return null;
}
return (
<div>
<OGDialog>
<div
className={cn(
'border-token-border-medium flex w-full rounded-lg border text-sm hover:cursor-pointer',
'flex w-full items-center rounded-lg text-sm',
!assistant_id ? 'opacity-40' : '',
)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
{currentTool.icon && (
<div className="flex h-9 w-9 items-center justify-center overflow-hidden rounded-full">
<div
className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full bg-center bg-no-repeat dark:bg-white/20"
style={{ backgroundImage: `url(${currentTool.icon})`, backgroundSize: 'cover' }}
/>
<div className="flex grow items-center">
{currentTool.icon && (
<div className="flex h-9 w-9 items-center justify-center overflow-hidden rounded-full">
<div
className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full bg-center bg-no-repeat dark:bg-white/20"
style={{ backgroundImage: `url(${currentTool.icon})`, backgroundSize: 'cover' }}
/>
</div>
)}
<div
className="h-9 grow px-3 py-2"
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
>
{currentTool.name}
</div>
)}
<div
className="h-9 grow px-3 py-2"
style={{ textOverflow: 'ellipsis', wordBreak: 'break-all', overflow: 'hidden' }}
>
{currentTool.name}
</div>
<div className="w-px bg-gray-300 dark:bg-gray-600" />
<button
type="button"
className="flex h-9 w-9 min-w-9 items-center justify-center rounded-lg rounded-l-none"
>
<GearIcon className="icon-sm" />
</button>
{isHovering && (
<OGDialogTrigger asChild>
<button
type="button"
className="transition-color flex h-9 w-9 min-w-9 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
>
<TrashIcon />
</button>
</OGDialogTrigger>
)}
</div>
</div>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_tool')}
className="max-w-[450px]"
main={
<Label className="text-left text-sm font-medium">
{localize('com_ui_delete_tool_confirm')}
</Label>
}
selection={{
selectHandler: () => removeTool(currentTool.pluginKey),
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
selectText: localize('com_ui_delete'),
}}
/>
</OGDialog>
);
}

View file

@ -1,9 +1,12 @@
import { useMemo } from 'react';
import { Capabilities } from 'librechat-data-provider';
import { useFormContext, useWatch } from 'react-hook-form';
import type { TConfig, AssistantsEndpoint } from 'librechat-data-provider';
import type { AssistantForm } from '~/common';
import ImageVision from './ImageVision';
import { useLocalize } from '~/hooks';
import Retrieval from './Retrieval';
import CodeFiles from './CodeFiles';
import Code from './Code';
export default function CapabilitiesForm({
@ -21,6 +24,17 @@ export default function CapabilitiesForm({
}) {
const localize = useLocalize();
const methods = useFormContext<AssistantForm>();
const { control } = 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]);
const retrievalModels = useMemo(
() => new Set(assistantsConfig?.retrievalModels ?? []),
[assistantsConfig],
@ -31,7 +45,7 @@ export default function CapabilitiesForm({
);
return (
<div className="mb-6">
<div className="mb-4">
<div className="mb-1.5 flex items-center">
<span>
<label className="text-token-text-primary block font-medium">
@ -40,11 +54,19 @@ export default function CapabilitiesForm({
</span>
</div>
<div className="flex flex-col items-start gap-2">
{codeEnabled && <Code endpoint={endpoint} version={version} />}
{imageVisionEnabled && version == 1 && <ImageVision />}
{codeEnabled && <Code version={version} />}
{retrievalEnabled && (
<Retrieval endpoint={endpoint} version={version} retrievalModels={retrievalModels} />
)}
{imageVisionEnabled && version == 1 && <ImageVision />}
{codeEnabled && version && (
<CodeFiles
assistant_id={assistant_id}
version={version}
endpoint={endpoint}
files={files}
/>
)}
</div>
</div>
);

View file

@ -1,70 +1,66 @@
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 { useFormContext, Controller } from 'react-hook-form';
import type { AssistantForm } from '~/common';
import { Checkbox, QuestionMark } from '~/components/ui';
import {
Checkbox,
HoverCard,
HoverCardContent,
HoverCardPortal,
HoverCardTrigger,
} from '~/components/ui';
import { CircleHelpIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import CodeFiles from './CodeFiles';
import { ESide } from '~/common';
export default function Code({
version,
endpoint,
}: {
version: number | string;
endpoint: AssistantsEndpoint;
}) {
export default function Code({ version }: { version: number | string }) {
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 />
<HoverCard openDelay={50}>
<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()}
/>
)}
/>
<div className="flex items-center space-x-2">
<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,
})
}
>
{localize('com_assistants_code_interpreter')}
</label>
<HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
</HoverCardTrigger>
</div>
</label>
</div>
{version == 2 && (
<CodeFiles
assistant_id={assistant_id}
version={version}
endpoint={endpoint}
files={files}
/>
)}
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">
{version == 2 && localize('com_assistants_code_interpreter_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</div>
</HoverCard>
</>
);
}

View file

@ -58,7 +58,7 @@ export default function CodeFiles({
};
return (
<div className={'mb-2'}>
<div className="mb-2 w-full">
<div className="flex flex-col gap-4">
<div className="text-token-text-tertiary rounded-lg text-xs">
{localize('com_assistants_code_interpreter_files')}
@ -75,7 +75,7 @@ export default function CodeFiles({
<button
type="button"
disabled={!assistant_id}
className="btn btn-neutral border-token-border-light relative h-8 rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-2">

View file

@ -1,4 +1,3 @@
import * as Popover from '@radix-ui/react-popover';
import type { Assistant, AssistantCreateParams, AssistantsEndpoint } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import { Dialog, DialogTrigger, Label } from '~/components/ui';
@ -7,7 +6,7 @@ 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 { TrashIcon } from '~/components/svg';
export default function ContextButton({
activeModel,
@ -78,88 +77,40 @@ export default function ContextButton({
return (
<Dialog>
<Popover.Root>
<Popover.Trigger asChild>
<button
className={cn(
'btn border-token-border-light relative h-9 rounded-lg bg-transparent font-medium hover:bg-gray-100 dark:hover:bg-gray-800',
removeFocusOutlines,
)}
type="button"
>
<div className="flex w-full items-center justify-center gap-2">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-md"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3 12C3 10.8954 3.89543 10 5 10C6.10457 10 7 10.8954 7 12C7 13.1046 6.10457 14 5 14C3.89543 14 3 13.1046 3 12ZM10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12ZM17 12C17 10.8954 17.8954 10 19 10C20.1046 10 21 10.8954 21 12C21 13.1046 20.1046 14 19 14C17.8954 14 17 13.1046 17 12Z"
fill="currentColor"
/>
</svg>
</div>
</button>
</Popover.Trigger>
<div
style={{
position: 'fixed',
left: ' 0px',
top: ' 0px',
transform: 'translate(1772.8px, 49.6px)',
minWidth: 'max-content',
zIndex: 'auto',
}}
dir="ltr"
<DialogTrigger asChild>
<button
className={cn(
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
removeFocusOutlines,
)}
type="button"
>
<Popover.Content
side="top"
role="menu"
className="bg-token-surface-primary min-w-[180px] max-w-xs rounded-lg border border-gray-100 bg-white shadow-lg dark:border-gray-900 dark:bg-gray-850"
style={{ outline: 'none', pointerEvents: 'auto' }}
sideOffset={8}
tabIndex={-1}
align="end"
>
<DialogTrigger asChild>
<Popover.Close
role="menuitem"
className="group m-1.5 flex w-full cursor-pointer gap-2 rounded p-2.5 text-sm text-red-500 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"
tabIndex={-1}
>
<NewTrashIcon />
{localize('com_ui_delete') + ' ' + localize('com_ui_assistant')}
</Popover.Close>
</DialogTrigger>
</Popover.Content>
</div>
<DialogTemplate
title={localize('com_ui_delete') + ' ' + localize('com_ui_assistant')}
className="max-w-[450px]"
main={
<>
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="delete-assistant" className="text-left text-sm font-medium">
{localize('com_ui_delete_assistant_confirm')}
</Label>
</div>
<div className="flex w-full items-center justify-center gap-2 text-red-500">
<TrashIcon />
</div>
</button>
</DialogTrigger>
<DialogTemplate
title={localize('com_ui_delete') + ' ' + localize('com_ui_assistant')}
className="max-w-[450px]"
main={
<>
<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full items-center gap-2">
<Label htmlFor="delete-assistant" className="text-left text-sm font-medium">
{localize('com_ui_delete_assistant_confirm')}
</Label>
</div>
</>
}
selection={{
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'),
}}
/>
</Popover.Root>
</div>
</>
}
selection={{
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'),
}}
/>
</Dialog>
);
}

View file

@ -33,10 +33,7 @@ export default function ImageVision() {
})
}
>
<div className="flex items-center">
{localize('com_assistants_image_vision')}
<QuestionMark />
</div>
<div className="flex items-center">{localize('com_assistants_image_vision')}</div>
</label>
</div>
);

View file

@ -1,10 +1,17 @@
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 { useFormContext, Controller, useWatch } from 'react-hook-form';
import {
Checkbox,
HoverCard,
HoverCardContent,
HoverCardPortal,
HoverCardTrigger,
} from '~/components/ui';
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
import { Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui';
import { CircleHelpIcon } from '~/components/svg';
import type { AssistantForm } from '~/common';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
import { cn } from '~/utils/';
@ -40,23 +47,23 @@ export default function Retrieval({
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>
<HoverCard openDelay={50}>
<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()}
/>
)}
/>
<div className="flex items-center space-x-2">
<label
className={cn(
'form-check-label text-token-text-primary w-full select-none',
@ -74,21 +81,29 @@ export default function Retrieval({
? localize('com_assistants_retrieval')
: localize('com_assistants_file_search')}
</label>
</HoverCardTrigger>
<HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
</HoverCardTrigger>
</div>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} disabled={isDisabled} className="ml-16 w-80">
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">
{version == 2 && localize('com_assistants_file_search_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
<OptionHover
side={ESide.Top}
disabled={!isDisabled}
description="com_assistants_non_retrieval_model"
langCode={true}
sideOffset={20}
className="ml-16"
/>
</HoverCard>
</div>
{version == 2 && (
<div className="text-token-text-tertiary rounded-lg text-xs">
{localize('com_assistants_file_search_info')}
</div>
)}
</HoverCard>
</>
);
}

View file

@ -72,12 +72,12 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
return (
<>
<div className="flex items-center gap-4 px-2 py-4">
<div className="flex items-center gap-4 py-4">
<Input
placeholder={localize('com_files_filter')}
value={(table.getColumn('filename')?.getFilterValue() as string) ?? ''}
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
className="max-w-xs dark:border-gray-700"
className="w-full dark:border-gray-700"
/>
</div>
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">
@ -90,7 +90,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
<TableHead
key={header.id}
style={{ width: index === 0 ? '75%' : '25%' }}
className="sticky top-0 h-auto border-b border-black/10 bg-white py-1 text-left font-medium text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-100"
className="sticky top-0 h-auto bg-white py-1 text-left font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-100"
>
{header.isPlaceholder
? null
@ -133,17 +133,19 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
</TableBody>
</Table>
</div>
<div className="flex items-center justify-around space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => setShowFiles(true)}
className="flex gap-2"
>
<LucideArrowUpLeft className="icon-sm" />
{localize('com_sidepanel_manage_files')}
</Button>
<div className="flex gap-2">
<div className="flex items-center justify-between py-4">
<div className="flex items-center">
<Button
variant="outline"
size="sm"
onClick={() => setShowFiles(true)}
className="flex gap-2"
>
<LucideArrowUpLeft className="icon-sm" />
{localize('com_sidepanel_manage_files')}
</Button>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"

View file

@ -41,9 +41,9 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
? 'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'
: '',
)}
onClick={() => {
onClick={(e) => {
if (link.onClick) {
link.onClick();
link.onClick(e);
setActive('');
return;
}
@ -87,9 +87,9 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
'hover:bg-gray-50 data-[state=open]:bg-gray-50 data-[state=open]:text-black dark:hover:bg-gray-700 dark:data-[state=open]:bg-gray-700 dark:data-[state=open]:text-white',
'w-full justify-start rounded-md border dark:border-gray-700',
)}
onClick={() => {
onClick={(e) => {
if (link.onClick) {
link.onClick();
link.onClick(e);
setActive('');
}
}}

View file

@ -9,6 +9,7 @@ type TOptionHoverProps = {
sideOffset?: number;
disabled?: boolean;
side: ESide;
className?: string;
};
function OptionHover({
@ -17,6 +18,7 @@ function OptionHover({
disabled,
langCode,
sideOffset = 30,
className,
}: TOptionHoverProps) {
const localize = useLocalize();
if (disabled) {
@ -25,11 +27,7 @@ function OptionHover({
const text = langCode ? localize(description) : description;
return (
<HoverCardPortal>
<HoverCardContent
side={side}
className="z-[999] w-80 dark:bg-gray-700"
sideOffset={sideOffset}
>
<HoverCardContent side={side} className={`z-[999] w-80 ${className}`} sideOffset={sideOffset}>
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">{text}</p>
</div>

View file

@ -12,6 +12,7 @@ import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/compo
import { TooltipProvider, Tooltip } from '~/components/ui/Tooltip';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import { useMediaQuery, useLocalStorage } from '~/hooks';
import BookmarkPanel from './Bookmarks/BookmarkPanel';
import NavToggle from '~/components/Nav/NavToggle';
import { useChatContext } from '~/Providers';
import Switcher from './Switcher';
@ -79,8 +80,20 @@ const SidePanel = ({
localStorage.setItem('fullPanelCollapse', 'true');
panelRef.current?.collapse();
}, []);
const [showBookmarks, setShowBookmarks] = useState(false);
const manageBookmarks = useCallback((e) => {
e.preventDefault();
setShowBookmarks((prev) => !prev);
}, []);
const Links = useSideNavLinks({ hidePanel, assistants, keyProvided, endpoint, interfaceConfig });
const Links = useSideNavLinks({
hidePanel,
assistants,
keyProvided,
endpoint,
interfaceConfig,
manageBookmarks,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
const throttledSaveLayout = useCallback(
@ -128,6 +141,7 @@ const SidePanel = ({
return (
<>
{showBookmarks && <BookmarkPanel open={showBookmarks} onOpenChange={setShowBookmarks} />}
<TooltipProvider delayDuration={0}>
<ResizablePanelGroup
direction="horizontal"
@ -216,7 +230,7 @@ const SidePanel = ({
</ResizablePanelGroup>
</TooltipProvider>
<div
className={`nav-mask${!isCollapsed ? ' active' : ''}`}
className={`nav-mask${!isCollapsed ? 'active' : ''}`}
onClick={() => {
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');

View file

@ -1,19 +0,0 @@
export default function NewTrashIcon({ className = 'icon-md' }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.5555 4C10.099 4 9.70052 4.30906 9.58693 4.75114L9.29382 5.8919H14.715L14.4219 4.75114C14.3083 4.30906 13.9098 4 13.4533 4H10.5555ZM16.7799 5.8919L16.3589 4.25342C16.0182 2.92719 14.8226 2 13.4533 2H10.5555C9.18616 2 7.99062 2.92719 7.64985 4.25342L7.22886 5.8919H4C3.44772 5.8919 3 6.33961 3 6.8919C3 7.44418 3.44772 7.8919 4 7.8919H4.10069L5.31544 19.3172C5.47763 20.8427 6.76455 22 8.29863 22H15.7014C17.2354 22 18.5224 20.8427 18.6846 19.3172L19.8993 7.8919H20C20.5523 7.8919 21 7.44418 21 6.8919C21 6.33961 20.5523 5.8919 20 5.8919H16.7799ZM17.888 7.8919H6.11196L7.30423 19.1057C7.3583 19.6142 7.78727 20 8.29863 20H15.7014C16.2127 20 16.6417 19.6142 16.6958 19.1057L17.888 7.8919ZM10 10C10.5523 10 11 10.4477 11 11V16C11 16.5523 10.5523 17 10 17C9.44772 17 9 16.5523 9 16V11C9 10.4477 9.44772 10 10 10ZM14 10C14.5523 10 15 10.4477 15 11V16C15 16.5523 14.5523 17 14 17C13.4477 17 13 16.5523 13 16V11C13 10.4477 13.4477 10 14 10Z"
fill="currentColor"
/>
</svg>
);
}

View file

@ -1,6 +1,10 @@
import { cn } from '~/utils';
export default function TrashIcon({ className = '' }) {
type TrashIconProps = {
className?: string;
};
export default function TrashIcon({ className = '' }: TrashIconProps) {
return (
<svg
fill="none"
@ -8,7 +12,7 @@ export default function TrashIcon({ className = '' }) {
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className={cn('icon-md', className)}
className={cn('icon-md h-4 w-4', className)}
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"

View file

@ -29,7 +29,6 @@ export { default as DotsIcon } from './DotsIcon';
export { default as GearIcon } from './GearIcon';
export { default as PinIcon } from './PinIcon';
export { default as TrashIcon } from './TrashIcon';
export { default as NewTrashIcon } from './NewTrashIcon';
export { default as MinimalPlugin } from './MinimalPlugin';
export { default as AzureMinimalIcon } from './AzureMinimalIcon';
export { default as OpenAIMinimalIcon } from './OpenAIMinimalIcon';

View file

@ -11,19 +11,25 @@ const HoverCardPortal = HoverCardPrimitive.Portal;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className = '', align = 'center', sideOffset = 6, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-64 rounded-md border border-gray-200 bg-white p-4 shadow-md outline-none animate-in fade-in-0 dark:border-gray-800 dark:bg-gray-800',
className,
)}
{...props}
/>
));
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> & { disabled?: boolean }
>(({ className = '', align = 'center', sideOffset = 6, disabled = false, ...props }, ref) => {
if (disabled) {
return null;
}
return (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-64 rounded-md border border-gray-200 bg-white p-4 shadow-md outline-none animate-in fade-in-0 dark:border-gray-800 dark:bg-gray-800',
className,
)}
{...props}
/>
);
});
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent, HoverCardPortal };

View file

@ -0,0 +1,95 @@
import { forwardRef, ReactNode, Ref } from 'react';
import {
OGDialogTitle,
OGDialogClose,
OGDialogFooter,
OGDialogHeader,
OGDialogContent,
OGDialogDescription,
} from './';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
type SelectionProps = {
selectHandler?: () => void;
selectClasses?: string;
selectText?: string;
};
type DialogTemplateProps = {
title: string;
description?: string;
main?: ReactNode;
buttons?: ReactNode;
leftButtons?: ReactNode;
selection?: SelectionProps;
className?: string;
headerClassName?: string;
mainClassName?: string;
footerClassName?: string;
showCloseButton?: boolean;
showCancelButton?: boolean;
};
const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDivElement>) => {
const localize = useLocalize();
const {
title,
main,
buttons,
selection,
className,
leftButtons,
description,
mainClassName,
headerClassName,
footerClassName,
showCloseButton,
showCancelButton = true,
} = props;
const { selectHandler, selectClasses, selectText } = selection || {};
const Cancel = localize('com_ui_cancel');
const defaultSelect =
'bg-gray-800 text-white transition-colors hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-gray-200';
return (
<OGDialogContent
showCloseButton={showCloseButton}
ref={ref}
className={cn(
'bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300',
className || '',
)}
onClick={(e) => e.stopPropagation()}
>
<OGDialogHeader className={cn(headerClassName ?? '')}>
<OGDialogTitle>{title}</OGDialogTitle>
{description && <OGDialogDescription className="">{description}</OGDialogDescription>}
</OGDialogHeader>
<div className={cn('px-0', mainClassName)}>{main ? main : null}</div>
<OGDialogFooter className={footerClassName}>
<div>{leftButtons ? leftButtons : null}</div>
<div className="flex h-auto gap-3">
{showCancelButton && (
<OGDialogClose className="btn btn-neutral border-token-border-light relative rounded-lg text-sm">
{Cancel}
</OGDialogClose>
)}
{buttons ? buttons : null}
{selection ? (
<OGDialogClose
onClick={selectHandler}
className={`${
selectClasses || defaultSelect
} inline-flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm`}
>
{selectText}
</OGDialogClose>
) : null}
</div>
</OGDialogFooter>
</OGDialogContent>
);
});
export default OGDialogTemplate;

View file

@ -26,10 +26,15 @@ const DialogOverlay = React.forwardRef<
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
disableScroll?: boolean;
};
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
DialogContentProps
>(({ className, showCloseButton = true, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
@ -41,10 +46,12 @@ const DialogContent = React.forwardRef<
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
{showCloseButton && (
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));

View file

@ -0,0 +1,96 @@
import { ReactElement } from 'react';
import {
OGDialog,
OGDialogTrigger,
Label,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { CrossIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
export default function TooltipIcon({
disabled,
appendLabel = false,
title,
className = '',
confirm,
confirmMessage,
icon,
tabIndex,
onFocus,
onBlur,
}: {
disabled: boolean;
title: string;
appendLabel?: boolean;
className?: string;
confirm?: () => void;
confirmMessage?: ReactElement;
icon?: ReactElement;
tabIndex?: number;
onFocus?: () => void;
onBlur?: () => void;
}) {
const localize = useLocalize();
const renderDeleteButton = () => {
if (appendLabel) {
return (
<>
{icon} {localize('com_ui_delete')}
</>
);
}
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<span>{icon}</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_delete')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
if (!confirmMessage) {
return (
<button
className={className}
onClick={confirm}
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}
>
{disabled ? <CrossIcon /> : renderDeleteButton()}
</button>
);
}
return (
<OGDialog>
<OGDialogTrigger asChild>
<button className={className} tabIndex={tabIndex} onFocus={onFocus} onBlur={onBlur}>
{disabled ? <CrossIcon /> : renderDeleteButton()}
</button>
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={title}
className="max-w-[450px]"
main={<Label className="text-left text-sm font-medium">{confirmMessage}</Label>}
selection={{
selectHandler: confirm,
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_delete'),
}}
/>
</OGDialog>
);
}

View file

@ -3,12 +3,21 @@ import {
LocalStorageKeys,
InfiniteCollections,
defaultAssistantsVersion,
ConversationListResponse,
} from 'librechat-data-provider';
import { useSetRecoilState } from 'recoil';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import type { InfiniteData, UseMutationResult } from '@tanstack/react-query';
import { updateConversationTag } from '~/utils/conversationTags';
import { normalizeData } from '~/utils/collection';
import store from '~/store';
import {
useConversationTagsQuery,
useConversationsInfiniteQuery,
useSharedLinksInfiniteQuery,
} from './queries';
import {
/* Shared Links */
addSharedLink,
@ -19,9 +28,6 @@ import {
updateConversation,
deleteConversation,
} from '~/utils';
import { useConversationsInfiniteQuery, useSharedLinksInfiniteQuery } from './queries';
import { normalizeData } from '~/utils/collection';
import store from '~/store';
export type TGenTitleMutation = UseMutationResult<
t.TGenTitleResponse,
@ -83,6 +89,112 @@ export const useUpdateConversationMutation = (
);
};
const useUpdateTagsInConversation = () => {
const queryClient = useQueryClient();
// Update the queryClient cache with the new tag when a new tag is added/removed to a conversation
const updateTagsInConversation = (conversationId: string, tags: string[]) => {
// Update the tags for the current conversation
const currentConvo = queryClient.getQueryData<t.TConversation>([
QueryKeys.conversation,
conversationId,
]);
if (!currentConvo) {
return;
}
const updatedConvo = {
...currentConvo,
tags,
} as t.TConversation;
queryClient.setQueryData([QueryKeys.conversation, conversationId], updatedConvo);
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
if (!convoData) {
return convoData;
}
return updateConvoFields(
convoData,
{
conversationId: currentConvo.conversationId,
tags: updatedConvo.tags,
} as t.TConversation,
true,
);
});
};
// update the tag to newTag in all conversations when a tag is updated to a newTag
// The difference with updateTagsInConversation is that it adds or removes tags for a specific conversation,
// whereas this function is for changing the title of a specific tag.
const replaceTagsInAllConversations = (tag: string, newTag: string) => {
const data = queryClient.getQueryData<InfiniteData<ConversationListResponse>>([
QueryKeys.allConversations,
]);
const conversationIdsWithTag = [] as string[];
// update tag to newTag in all conversations
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
const page = newData.pages[pageIndex];
page.conversations = page.conversations.map((conversation) => {
if (conversation.conversationId && conversation.tags?.includes(tag)) {
conversationIdsWithTag.push(conversation.conversationId);
conversation.tags = conversation.tags.map((t) => (t === tag ? newTag : t));
}
return conversation;
});
}
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
[QueryKeys.allConversations],
newData,
);
// update the tag to newTag from the cache of each conversation
for (let i = 0; i < conversationIdsWithTag.length; i++) {
const conversationId = conversationIdsWithTag[i];
const conversation = queryClient.getQueryData<t.TConversation>([
QueryKeys.conversation,
conversationId,
]);
if (conversation && conversation.tags) {
const updatedConvo = {
...conversation,
tags: conversation.tags.map((t) => (t === tag ? newTag : t)),
} as t.TConversation;
queryClient.setQueryData<t.TConversation>(
[QueryKeys.conversation, conversationId],
updatedConvo,
);
}
}
};
return { updateTagsInConversation, replaceTagsInAllConversations };
};
/**
* Add or remove tags for a conversation
*/
export const useTagConversationMutation = (
conversationId: string,
): UseMutationResult<t.TTagConversationResponse, unknown, t.TTagConversationRequest, unknown> => {
const query = useConversationTagsQuery();
const { updateTagsInConversation } = useUpdateTagsInConversation();
return useMutation(
(payload: t.TTagConversationRequest) =>
dataService.addTagToConversation(conversationId, payload),
{
onSuccess: (updatedTags) => {
// Because the logic for calculating the bookmark count is complex,
// the client does not perform the calculation,
// but instead refetch the data from the API.
query.refetch();
updateTagsInConversation(conversationId, updatedTags);
},
},
);
};
export const useArchiveConversationMutation = (
id: string,
): UseMutationResult<
@ -273,6 +385,138 @@ export const useDeleteSharedLinkMutation = (
});
};
// If the number of conversations tagged is incorrect, recalculate the tag information.
export const useRebuildConversationTagsMutation = (): UseMutationResult<
t.TConversationTagsResponse,
unknown,
unknown,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.rebuildConversationTags(), {
onSuccess: (_data) => {
queryClient.setQueryData<t.TConversationTag[]>([QueryKeys.conversationTags], _data);
},
});
};
// Add a tag or update tag information (tag, description, position, etc.)
export const useConversationTagMutation = (
tag?: string,
options?: t.UpdateConversationTagOptions,
): UseMutationResult<t.TConversationTagResponse, unknown, t.TConversationTagRequest, unknown> => {
const queryClient = useQueryClient();
const { ..._options } = options || {};
const { updateTagsInConversation, replaceTagsInAllConversations } = useUpdateTagsInConversation();
return useMutation(
(payload: t.TConversationTagRequest) =>
tag
? dataService.updateConversationTag(tag, payload)
: dataService.createConversationTag(payload),
{
onSuccess: (_data, vars) => {
queryClient.setQueryData<t.TConversationTag[]>([QueryKeys.conversationTags], (data) => {
if (!data) {
return [
{
tag: 'Saved',
count: 1,
position: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
] as t.TConversationTag[];
}
return updateConversationTag(data, vars, _data, tag);
});
if (vars.addToConversation && vars.conversationId && _data.tag) {
const currentConvo = queryClient.getQueryData<t.TConversation>([
QueryKeys.conversation,
vars.conversationId,
]);
if (!currentConvo) {
return;
}
updateTagsInConversation(vars.conversationId, [...(currentConvo.tags || []), _data.tag]);
}
// Change the tag title to the new title
if (tag) {
replaceTagsInAllConversations(tag, _data.tag);
}
},
...(_options || {}),
},
);
};
// When a bookmark is deleted, remove that bookmark(tag) from all conversations associated with it
export const useDeleteTagInConversations = () => {
const queryClient = useQueryClient();
const deleteTagInAllConversation = (deletedTag: string) => {
const data = queryClient.getQueryData<InfiniteData<ConversationListResponse>>([
QueryKeys.allConversations,
]);
const conversationIdsWithTag = [] as string[];
// remove deleted tag from conversations
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
const page = newData.pages[pageIndex];
page.conversations = page.conversations.map((conversation) => {
if (conversation.conversationId && conversation.tags?.includes(deletedTag)) {
conversationIdsWithTag.push(conversation.conversationId);
conversation.tags = conversation.tags.filter((t) => t !== deletedTag);
}
return conversation;
});
}
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
[QueryKeys.allConversations],
newData,
);
// Remove the deleted tag from the cache of each conversation
for (let i = 0; i < conversationIdsWithTag.length; i++) {
const conversationId = conversationIdsWithTag[i];
const conversationData = queryClient.getQueryData<t.TConversation>([
QueryKeys.conversation,
conversationId,
]);
if (conversationData && conversationData.tags) {
conversationData.tags = conversationData.tags.filter((t) => t !== deletedTag);
queryClient.setQueryData<t.TConversation>(
[QueryKeys.conversation, conversationId],
conversationData,
);
}
}
};
return deleteTagInAllConversation;
};
// Delete a tag
export const useDeleteConversationTagMutation = (
options?: t.DeleteConversationTagOptions,
): UseMutationResult<t.TConversationTagResponse, unknown, string, void> => {
const queryClient = useQueryClient();
const deleteTagInAllConversations = useDeleteTagInConversations();
const { onSuccess, ..._options } = options || {};
return useMutation((tag: string) => dataService.deleteConversationTag(tag), {
onSuccess: (_data, vars, context) => {
queryClient.setQueryData<t.TConversationTag[]>([QueryKeys.conversationTags], (data) => {
if (!data) {
return data;
}
return data.filter((t) => t.tag !== vars);
});
deleteTagInAllConversations(vars);
onSuccess?.(_data, vars, context);
},
...(_options || {}),
});
};
export const useDeleteConversationMutation = (
options?: t.DeleteConversationOptions,
): UseMutationResult<

View file

@ -150,6 +150,7 @@ export const useConversationsInfiniteQuery = (
...params,
pageNumber: pageParam?.toString(),
isArchived: params?.isArchived || false,
tags: params?.tags || [],
}),
{
getNextPageParam: (lastPage) => {
@ -193,6 +194,21 @@ export const useSharedLinksInfiniteQuery = (
);
};
export const useConversationTagsQuery = (
config?: UseQueryOptions<t.TConversationTagsResponse>,
): QueryObserverResult<t.TConversationTagsResponse> => {
return useQuery<t.TConversationTag[]>(
[QueryKeys.conversationTags],
() => dataService.getConversationTags(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
/**
* ASSISTANTS
*/

View file

@ -107,6 +107,16 @@ export default function useChatFunctions({
const intermediateId = overrideUserMessageId ?? v4();
parentMessageId = parentMessageId || latestMessage?.messageId || Constants.NO_PARENT;
logger.dir('Ask function called with:', {
index,
latestMessage,
conversationId,
intermediateId,
parentMessageId,
currentMessages,
});
logger.log('=====================================');
if (conversationId == Constants.NEW_CONVO) {
parentMessageId = Constants.NO_PARENT;
currentMessages = [];

View file

@ -12,7 +12,7 @@ import type {
TModelsConfig,
TEndpointsConfig,
} from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils';
import store from '~/store';
const useConversation = () => {
@ -60,6 +60,10 @@ const useConversation = () => {
setMessages(messages);
setSubmission({} as TSubmission);
resetLatestMessage();
logger.log(
'[useConversation] Switched to conversation and reset Latest Message',
conversation,
);
if (conversation.conversationId === 'new' && !modelsData) {
queryClient.invalidateQueries([QueryKeys.messages, 'new']);

View file

@ -1,7 +1,7 @@
import { useSetRecoilState } from 'recoil';
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import { QueryKeys, EModelEndpoint, LocalStorageKeys, Constants } from 'librechat-data-provider';
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import store from '~/store';
@ -10,7 +10,7 @@ const useNavigateToConvo = (index = 0) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const clearAllConversations = store.useClearConvoState();
const clearAllLatestMessages = store.useClearLatestMessages();
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
const { setConversation } = store.useCreateConversationAtom(index);
@ -50,10 +50,10 @@ const useNavigateToConvo = (index = 0) => {
}
clearAllConversations(true);
setConversation(convo);
navigate(`/c/${convo.conversationId ?? 'new'}`);
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`);
};
const navigateWithLastTools = (conversation: TConversation) => {
const navigateWithLastTools = (conversation: TConversation, _resetLatestMessage?: boolean) => {
// set conversation to the new conversation
if (conversation?.endpoint === EModelEndpoint.gptPlugins) {
let lastSelectedTools = [];
@ -63,12 +63,15 @@ const useNavigateToConvo = (index = 0) => {
} catch (e) {
// console.error(e);
}
navigateToConvo({
...conversation,
tools: conversation?.tools?.length ? conversation?.tools : lastSelectedTools,
});
navigateToConvo(
{
...conversation,
tools: conversation?.tools?.length ? conversation?.tools : lastSelectedTools,
},
_resetLatestMessage,
);
} else {
navigateToConvo(conversation);
navigateToConvo(conversation, _resetLatestMessage);
}
};

View file

@ -1,9 +1,9 @@
import { useEffect, useRef, useCallback } from 'react';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import { Constants, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { getLatestText, getLengthAndFirstFiveChars } from '~/utils';
import useCopyToClipboard from './useCopyToClipboard';
import { getTextKey, logger } from '~/utils';
export default function useMessageHelpers(props: TMessageProps) {
const latestText = useRef<string | number>('');
@ -27,7 +27,8 @@ export default function useMessageHelpers(props: TMessageProps) {
const isLast = !children?.length;
useEffect(() => {
if (conversation?.conversationId === 'new') {
const convoId = conversation?.conversationId;
if (convoId === Constants.NEW_CONVO) {
return;
}
if (!message) {
@ -37,15 +38,25 @@ export default function useMessageHelpers(props: TMessageProps) {
return;
}
const text = getLatestText(message);
const textKey = `${message?.messageId ?? ''}${getLengthAndFirstFiveChars(text)}`;
const textKey = getTextKey(message, convoId);
if (textKey === latestText.current) {
return;
// Check for text/conversation change
const logInfo = {
textKey,
'latestText.current': latestText.current,
messageId: message?.messageId,
convoId,
};
if (
textKey !== latestText.current ||
(latestText.current && convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2])
) {
logger.log('[useMessageHelpers] Setting latest message: ', logInfo);
latestText.current = textKey;
setLatestMessage({ ...message });
} else {
logger.log('No change in latest message', logInfo);
}
latestText.current = textKey;
setLatestMessage({ ...message });
}, [isLast, message, setLatestMessage, conversation?.conversationId]);
const enterEdit = useCallback(

View file

@ -1,8 +1,9 @@
import { useRecoilValue } from 'recoil';
import { Constants } from 'librechat-data-provider';
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
import type { TMessage } from 'librechat-data-provider';
import { useChatContext, useAddedChatContext } from '~/Providers';
import { getLatestText, getLengthAndFirstFiveChars } from '~/utils';
import { getTextKey, logger } from '~/utils';
import store from '~/store';
export default function useMessageProcess({ message }: { message?: TMessage | null }) {
@ -26,7 +27,8 @@ export default function useMessageProcess({ message }: { message?: TMessage | nu
);
useEffect(() => {
if (conversation?.conversationId === 'new') {
const convoId = conversation?.conversationId;
if (convoId === Constants.NEW_CONVO) {
return;
}
if (!message) {
@ -36,15 +38,27 @@ export default function useMessageProcess({ message }: { message?: TMessage | nu
return;
}
const text = getLatestText(message);
const textKey = `${message?.messageId ?? ''}${getLengthAndFirstFiveChars(text)}`;
const textKey = getTextKey(message, convoId);
if (textKey === latestText.current) {
return;
// Check for text/conversation change
const logInfo = {
textKey,
'latestText.current': latestText.current,
messageId: message?.messageId,
convoId,
};
if (
textKey !== latestText.current ||
(convoId &&
latestText.current &&
convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2])
) {
logger.log('[useMessageProcess] Setting latest message: ', logInfo);
latestText.current = textKey;
setLatestMessage({ ...message });
} else {
logger.log('No change in latest message', logInfo);
}
latestText.current = textKey;
setLatestMessage({ ...message });
}, [hasNoChildren, message, setLatestMessage, conversation?.conversationId]);
const handleScroll = useCallback(() => {

View file

@ -2,6 +2,7 @@ import { useMemo } from 'react';
import {
ArrowRightToLine,
MessageSquareQuote,
Bookmark,
// Settings2,
} from 'lucide-react';
import {
@ -12,6 +13,7 @@ import {
} from 'librechat-data-provider';
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { NavLink } from '~/common';
import BookmarkPanel from '~/components/SidePanel/Bookmarks/BookmarkPanel';
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
import PromptsAccordion from '~/components/Prompts/PromptsAccordion';
// import Parameters from '~/components/SidePanel/Parameters/Panel';
@ -73,6 +75,14 @@ export default function useSideNavLinks({
Component: FilesPanel,
});
links.push({
title: 'com_sidepanel_conversation_tags',
label: '',
icon: Bookmark,
id: 'bookmarks',
Component: BookmarkPanel,
});
links.push({
title: 'com_sidepanel_hide_panel',
label: '',

View file

@ -27,3 +27,4 @@ export { default as useOnClickOutside } from './useOnClickOutside';
export { default as useSpeechToText } from './Input/useSpeechToText';
export { default as useTextToSpeech } from './Input/useTextToSpeech';
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
export { default as useDocumentTitle } from './useDocumentTitle';

View file

@ -34,9 +34,9 @@ const useNewConvo = (index = 0) => {
const { data: startupConfig } = useGetStartupConfig();
const clearAllConversations = store.useClearConvoState();
const defaultPreset = useRecoilValue(store.defaultPreset);
const clearAllLatestMessages = store.useClearLatestMessages();
const { setConversation } = store.useCreateConversationAtom(index);
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`);
const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index));
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();

View file

@ -74,6 +74,20 @@ export default {
com_ui_unarchive: 'إلغاء الأرشفة',
com_ui_unarchive_error: 'فشل في إلغاء الأرشفة',
com_ui_more_options: 'المزيد',
com_ui_bookmarks: 'الإشارات المرجعية',
com_ui_bookmarks_rebuild: 'إعادة بناء',
com_ui_bookmarks_new: 'إشارة مرجعية جديدة',
com_ui_bookmark_delete_confirm: 'هل أنت متأكد أنك تريد حذف هذه الإشارة المرجعية؟',
com_ui_bookmarks_title: 'عنوان',
com_ui_bookmarks_count: 'العدد',
com_ui_bookmarks_description: 'وصف',
com_ui_bookmarks_create_success: 'تم إنشاء الإشارة المرجعية بنجاح',
com_ui_bookmarks_update_success: 'تم تحديث الإشارة المرجعية بنجاح',
com_ui_bookmarks_delete_success: 'تم حذف الإشارة المرجعية بنجاح',
com_ui_bookmarks_create_error: 'حدث خطأ أثناء إنشاء الإشارة المرجعية',
com_ui_bookmarks_update_error: 'حدث خطأ أثناء تحديث الإشارة المرجعية',
com_ui_bookmarks_delete_error: 'حدث خطأ أثناء حذف الإشارة المرجعية',
com_ui_bookmarks_add_to_conversation: 'أضف إلى المحادثة الحالية',
com_auth_error_login:
'تعذر تسجيل الدخول باستخدام المعلومات المقدمة. يرجى التحقق من بيانات الاعتماد الخاصة بك والمحاولة مرة أخرى.',
com_auth_error_login_rl:
@ -297,6 +311,8 @@ export default {
com_nav_help_faq: 'مساعدة & الأسئلة الشائعة',
com_nav_settings: 'الإعدادات',
com_nav_search_placeholder: 'بحث في الرسائل',
com_nav_info_bookmarks_rebuild:
'إذا كان عدد الإشارات المرجعية غير صحيح، يرجى إعادة بناء معلومات الإشارة المرجعية. سيتم إعادة حساب عدد الإشارات المرجعية وستتم استعادة البيانات إلى حالتها الصحيحة.',
com_nav_setting_general: 'عام',
com_nav_setting_data: 'تحكم في البيانات',
/* The following are AI translated */
@ -346,6 +362,7 @@ export default {
com_sidepanel_hide_panel: 'إخفاء اللوحة',
com_sidepanel_attach_files: 'إرفاق الملفات',
com_sidepanel_manage_files: 'إدارة الملفات',
com_sidepanel_conversation_tags: 'الإشارات المرجعية',
com_assistants_capabilities: 'قدرات',
com_assistants_knowledge: 'المعرفة',
com_assistants_completed_function: 'تم تشغيل {0}',
@ -856,6 +873,62 @@ export const comparisons = {
english: 'More',
translated: 'المزيد',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'الإشارات المرجعية',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'إعادة بناء',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'إشارة مرجعية جديدة',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'هل أنت متأكد أنك تريد حذف هذه الإشارة المرجعية؟',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'عنوان',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'العدد',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'وصف',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'تم إنشاء الإشارة المرجعية بنجاح',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'تم تحديث الإشارة المرجعية بنجاح',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'تم حذف الإشارة المرجعية بنجاح',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'حدث خطأ أثناء إنشاء الإشارة المرجعية',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'حدث خطأ أثناء تحديث الإشارة المرجعية',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'حدث خطأ أثناء حذف الإشارة المرجعية',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'أضف إلى المحادثة الحالية',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1652,6 +1725,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'بحث في الرسائل',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'إذا كان عدد الإشارات المرجعية غير صحيح، يرجى إعادة بناء معلومات الإشارة المرجعية. سيتم إعادة حساب عدد الإشارات المرجعية وستتم استعادة البيانات إلى حالتها الصحيحة.',
},
com_nav_setting_general: {
english: 'General',
translated: 'عام',
@ -1832,6 +1911,10 @@ export const comparisons = {
english: 'Manage Files',
translated: 'إدارة الملفات',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: 'الإشارات المرجعية',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: 'قدرات',

View file

@ -11,6 +11,7 @@ export default {
com_sidepanel_hide_panel: 'Ocultar Painel',
com_sidepanel_attach_files: 'Anexar Arquivos',
com_sidepanel_manage_files: 'Gerenciar Arquivos',
com_sidepanel_conversation_tags: 'Favoritos',
com_assistants_capabilities: 'Capacidades',
com_assistants_knowledge: 'Conhecimento',
com_assistants_knowledge_info:
@ -168,6 +169,20 @@ export default {
'O envio de "{0}" está levando mais tempo do que o esperado. Aguarde enquanto o arquivo é indexado para recuperação.',
com_ui_privacy_policy: 'Política de privacidade',
com_ui_terms_of_service: 'Termos de serviço',
com_ui_bookmarks: 'Favoritos',
com_ui_bookmarks_rebuild: 'Reconstruir',
com_ui_bookmarks_new: 'Novo Favorito',
com_ui_bookmark_delete_confirm: 'Tem certeza de que deseja excluir este favorito?',
com_ui_bookmarks_title: 'Título',
com_ui_bookmarks_count: 'Contagem',
com_ui_bookmarks_description: 'Descrição',
com_ui_bookmarks_create_success: 'Favorito criado com sucesso',
com_ui_bookmarks_update_success: 'Favorito atualizado com sucesso',
com_ui_bookmarks_delete_success: 'Favorito excluído com sucesso',
com_ui_bookmarks_create_error: 'Houve um erro ao criar o favorito',
com_ui_bookmarks_update_error: 'Houve um erro ao atualizar o favorito',
com_ui_bookmarks_delete_error: 'Houve um erro ao excluir o favorito',
com_ui_bookmarks_add_to_conversation: 'Adicionar à conversa atual',
com_auth_error_login:
'Não foi possível fazer login com as informações fornecidas. Por favor, verifique suas credenciais e tente novamente.',
com_auth_error_login_rl:
@ -467,6 +482,8 @@ export default {
com_nav_help_faq: 'Ajuda & FAQ',
com_nav_settings: 'Configurações',
com_nav_search_placeholder: 'Pesquisar mensagens',
com_nav_info_bookmarks_rebuild:
'Se a contagem de favoritos estiver incorreta, por favor, reconstrua as informações de favoritos. A contagem de favoritos será recalculada e os dados serão restaurados ao estado correto.',
com_nav_setting_general: 'Geral',
com_nav_setting_beta: 'Recursos beta',
com_nav_setting_data: 'Controles de dados',
@ -509,6 +526,10 @@ export const comparisons = {
english: 'Manage Files',
translated: 'Gerenciar Arquivos',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: 'Favoritos',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: 'Capacidades',
@ -1103,6 +1124,62 @@ export const comparisons = {
english: 'Terms of service',
translated: 'Termos de serviço',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Favoritos',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Reconstruir',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Novo Favorito',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'Tem certeza de que deseja excluir este favorito?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Título',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Contagem',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Descrição',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Favorito criado com sucesso',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Favorito atualizado com sucesso',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Favorito excluído com sucesso',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Houve um erro ao criar o favorito',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Houve um erro ao atualizar o favorito',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Houve um erro ao excluir o favorito',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Adicionar à conversa atual',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -2160,6 +2237,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Pesquisar mensagens',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Se a contagem de favoritos estiver incorreta, por favor, reconstrua as informações de favoritos. A contagem de favoritos será recalculada e os dados serão restaurados ao estado correto.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Geral',

View file

@ -12,6 +12,7 @@ export default {
com_sidepanel_hide_panel: 'Seitenleiste ausblenden',
com_sidepanel_attach_files: 'Dateien anhängen',
com_sidepanel_manage_files: 'Dateien verwalten',
com_sidepanel_conversation_tags: 'Lesezeichen',
com_assistants_capabilities: 'Fähigkeiten',
com_assistants_knowledge: 'Wissen',
com_assistants_knowledge_info:
@ -180,6 +181,20 @@ export default {
'Das Hochladen von "{0}" dauert länger als erwartet. Bitte warte, während die Datei zum Abruf indiziert wird.',
com_ui_privacy_policy: 'Datenschutzrichtlinie',
com_ui_terms_of_service: 'Nutzungsbedingungen',
com_ui_bookmarks: 'Lesezeichen',
com_ui_bookmarks_rebuild: 'Neu aufbauen',
com_ui_bookmarks_new: 'Neues Lesezeichen',
com_ui_bookmark_delete_confirm: 'Möchten Sie dieses Lesezeichen wirklich löschen?',
com_ui_bookmarks_title: 'Titel',
com_ui_bookmarks_count: 'Anzahl',
com_ui_bookmarks_description: 'Beschreibung',
com_ui_bookmarks_create_success: 'Lesezeichen erfolgreich erstellt',
com_ui_bookmarks_update_success: 'Lesezeichen erfolgreich aktualisiert',
com_ui_bookmarks_delete_success: 'Lesezeichen erfolgreich gelöscht',
com_ui_bookmarks_create_error: 'Fehler beim Erstellen des Lesezeichens',
com_ui_bookmarks_update_error: 'Fehler beim Aktualisieren des Lesezeichens',
com_ui_bookmarks_delete_error: 'Fehler beim Löschen des Lesezeichens',
com_ui_bookmarks_add_to_conversation: 'Zur aktuellen Konversation hinzufügen',
com_auth_error_login:
'Die Anmeldung mit den angegebenen Daten ist fehlgeschlagen. Bitte überprüfe deine Anmeldedaten und versuche es erneut.',
com_auth_error_login_rl:
@ -480,6 +495,8 @@ export default {
com_nav_help_faq: 'Hilfe & FAQ',
com_nav_settings: 'Einstellungen',
com_nav_search_placeholder: 'Nachrichten suchen',
com_nav_info_bookmarks_rebuild:
'Wenn die Lesezeichenanzahl falsch ist, bauen Sie bitte die Lesezeicheninformationen neu auf. Die Lesezeichenanzahl wird neu berechnet und die Daten werden in ihren korrekten Zustand wiederhergestellt.',
com_nav_setting_general: 'Allgemein',
com_nav_setting_beta: 'Beta-Funktionen',
com_nav_setting_data: 'Datenkontrollen',
@ -634,6 +651,10 @@ export const comparisons = {
english: 'Manage Files',
translated: 'Dateien verwalten',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: 'Lesezeichen',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: 'Fähigkeiten',
@ -1253,6 +1274,90 @@ export const comparisons = {
english: 'Terms of service',
translated: 'Nutzungsbedingungen',
},
com_ui_bookmarks: {
english:
'Bookmarks',
translated:
'Lesezeichen',
},
com_ui_bookmarks_rebuild: {
english:
'Rebuild',
translated:
'Neu aufbauen',
},
com_ui_bookmarks_new: {
english:
'New Bookmark',
translated:
'Neues Lesezeichen',
},
com_ui_bookmark_delete_confirm: {
english:
'Are you sure you want to delete this bookmark?',
translated:
'Möchten Sie dieses Lesezeichen wirklich löschen?',
},
com_ui_bookmarks_title: {
english:
'Title',
translated:
'Titel',
},
com_ui_bookmarks_count: {
english:
'Count',
translated:
'Anzahl',
},
com_ui_bookmarks_description: {
english:
'Description',
translated:
'Beschreibung',
},
com_ui_bookmarks_create_success: {
english:
'Bookmark created successfully',
translated:
'Lesezeichen erfolgreich erstellt',
},
com_ui_bookmarks_update_success: {
english:
'Bookmark updated successfully',
translated:
'Lesezeichen erfolgreich aktualisiert',
},
com_ui_bookmarks_delete_success: {
english:
'Bookmark deleted successfully',
translated:
'Lesezeichen erfolgreich gelöscht',
},
com_ui_bookmarks_create_error: {
english:
'There was an error creating the bookmark',
translated:
'Fehler beim Erstellen des Lesezeichens',
},
com_ui_bookmarks_update_error: {
english:
'There was an error updating the bookmark',
translated:
'Fehler beim Aktualisieren des Lesezeichens',
},
com_ui_bookmarks_delete_error: {
english:
'There was an error deleting the bookmark',
translated:
'Fehler beim Löschen des Lesezeichens',
},
com_ui_bookmarks_add_to_conversation: {
english:
'Add to current conversation',
translated:
'Zur aktuellen Konversation hinzufügen',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -2317,6 +2422,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Nachrichten suchen',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Wenn die Lesezeichenanzahl falsch ist, bauen Sie bitte die Lesezeicheninformationen neu auf. Die Lesezeichenanzahl wird neu berechnet und die Daten werden in ihren korrekten Zustand wiederhergestellt.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Allgemein',

View file

@ -19,10 +19,13 @@ export default {
com_sidepanel_hide_panel: 'Hide Panel',
com_sidepanel_attach_files: 'Attach Files',
com_sidepanel_manage_files: 'Manage Files',
com_sidepanel_conversation_tags: 'Bookmarks',
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.',
'File search enables the assistant with knowledge from files that you or your users upload. Once a file is uploaded, the assistant automatically decides when to retrieve content based on user requests. 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_code_interpreter_info:
'Code Interpreter enables the assistant to write and run code. This tool can process files with diverse data and formatting, and generate files such as graphs.',
com_assistants_knowledge: 'Knowledge',
com_assistants_knowledge_info:
'If you upload files under Knowledge, conversations with your Assistant may include file contents.',
@ -30,8 +33,7 @@ export default {
'Assistant must be created, and Code Interpreter or Retrieval must be enabled and saved before uploading files as Knowledge.',
com_assistants_image_vision: 'Image Vision',
com_assistants_code_interpreter: 'Code Interpreter',
com_assistants_code_interpreter_files:
'The following files are only available for Code Interpreter:',
com_assistants_code_interpreter_files: 'Files below are for Code Interpreter only:',
com_assistants_retrieval: 'Retrieval',
com_assistants_search_name: 'Search assistants by name',
com_assistants_tools: 'Tools',
@ -266,6 +268,10 @@ export default {
com_ui_shared_link_not_found: 'Shared link not found',
com_ui_delete_conversation: 'Delete chat?',
com_ui_delete_confirm: 'This will delete',
com_ui_delete_tool: 'Delete Tool',
com_ui_delete_tool_confirm: 'Are you sure you want to delete this tool?',
com_ui_delete_action: 'Delete Action',
com_ui_delete_action_confirm: 'Are you sure you want to delete this action?',
com_ui_delete_confirm_prompt_version_var:
'This will delete the selected version for "{0}." If no other versions exist, the prompt will be deleted.',
com_ui_delete_assistant_confirm:
@ -290,6 +296,21 @@ export default {
com_ui_hold_mouse_download: 'Hold for {0} more seconds to download the audio',
com_ui_info_read_aloud: 'Hold click 3 seconds to download',
com_ui_downloading: 'Downloading...',
com_ui_bookmarks: 'Bookmarks',
com_ui_bookmarks_rebuild: 'Rebuild',
com_ui_bookmarks_new: 'New Bookmark',
com_ui_bookmark_delete_confirm: 'Are you sure you want to delete this bookmark?',
com_ui_bookmarks_title: 'Title',
com_ui_bookmarks_count: 'Count',
com_ui_bookmarks_description: 'Description',
com_ui_bookmarks_create_success: 'Bookmark created successfully',
com_ui_bookmarks_update_success: 'Bookmark updated successfully',
com_ui_bookmarks_delete_success: 'Bookmark deleted successfully',
com_ui_bookmarks_create_error: 'There was an error creating the bookmark',
com_ui_bookmarks_update_error: 'There was an error updating the bookmark',
com_ui_bookmarks_delete_error: 'There was an error deleting the bookmark',
com_ui_bookmarks_add_to_conversation: 'Add to current conversation',
com_ui_bookmarks_filter: 'Filter bookmarks...',
com_auth_error_login:
'Unable to login with the information provided. Please check your credentials and try again.',
com_auth_error_login_rl:
@ -394,7 +415,7 @@ export default {
com_endpoint_google_topk:
'Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model\'s vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).',
com_endpoint_google_maxoutputtokens:
' Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses.',
'Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.',
com_endpoint_google_custom_name_placeholder: 'Set a custom name for Google',
com_endpoint_prompt_prefix_placeholder: 'Set custom instructions or context. Ignored if empty.',
com_endpoint_instructions_assistants_placeholder:
@ -442,7 +463,7 @@ export default {
com_endpoint_anthropic_topk:
'Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model\'s vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).',
com_endpoint_anthropic_maxoutputtokens:
'Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses.',
'Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.',
com_endpoint_anthropic_custom_name_placeholder: 'Set a custom name for Anthropic',
com_endpoint_frequency_penalty: 'Frequency Penalty',
com_endpoint_presence_penalty: 'Presence Penalty',
@ -662,6 +683,8 @@ export default {
'This action will revoke and remove all the API keys that you have provided. You will need to re-enter these credentials to continue using those endpoints.',
com_nav_info_delete_cache_storage:
'This action will delete all cached TTS (Text-to-Speech) audio files stored on your device. Cached audio files are used to speed up playback of previously generated TTS audio, but they can consume storage space on your device.',
com_nav_info_bookmarks_rebuild:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
com_nav_setting_general: 'General',
com_nav_setting_chat: 'Chat',
com_nav_setting_beta: 'Beta features',

View file

@ -11,6 +11,7 @@ export default {
com_sidepanel_hide_panel: 'Ocultar Panel',
com_sidepanel_attach_files: 'Adjuntar Archivos',
com_sidepanel_manage_files: 'Administrar Archivos',
com_sidepanel_conversation_tags: 'Marcadores',
com_assistants_capabilities: 'Capacidades',
com_assistants_knowledge: 'Conocimiento',
com_assistants_knowledge_info:
@ -170,6 +171,20 @@ export default {
'La carga de "{0}" está tomando más tiempo del esperado. Espere mientras el archivo termina de indexarse para su recuperación.',
com_ui_privacy_policy: 'Política de privacidad',
com_ui_terms_of_service: 'Términos de servicio',
com_ui_bookmarks: 'Marcadores',
com_ui_bookmarks_rebuild: 'Reconstruir',
com_ui_bookmarks_new: 'Nuevo marcador',
com_ui_bookmark_delete_confirm: '¿Está seguro de que desea eliminar este marcador?',
com_ui_bookmarks_title: 'Título',
com_ui_bookmarks_count: 'Conteo',
com_ui_bookmarks_description: 'Descripción',
com_ui_bookmarks_create_success: 'Marcador creado con éxito',
com_ui_bookmarks_update_success: 'Marcador actualizado con éxito',
com_ui_bookmarks_delete_success: 'Marcador eliminado con éxito',
com_ui_bookmarks_create_error: 'Hubo un error al crear el marcador',
com_ui_bookmarks_update_error: 'Hubo un error al actualizar el marcador',
com_ui_bookmarks_delete_error: 'Hubo un error al eliminar el marcador',
com_ui_bookmarks_add_to_conversation: 'Agregar a la conversación actual',
com_auth_error_login:
'No se puede iniciar sesión con la información proporcionada. Verifique sus credenciales y vuelva a intentarlo.',
com_auth_error_login_rl:
@ -473,6 +488,8 @@ export default {
com_nav_help_faq: 'Ayuda y preguntas frecuentes',
com_nav_settings: 'Configuración',
com_nav_search_placeholder: 'Buscar mensajes',
com_nav_info_bookmarks_rebuild:
'Si el conteo de marcadores es incorrecto, por favor reconstruya la información de los marcadores. El conteo de los marcadores se recalculará y los datos se restaurarán a su estado correcto.',
com_nav_setting_general: 'General',
com_nav_setting_beta: 'Funciones beta',
com_nav_setting_data: 'Controles de datos',
@ -631,6 +648,10 @@ export const comparisons = {
english: 'Manage Files',
translated: 'Administrar Archivos',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: 'Marcadores',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: 'Capacidades',
@ -1226,6 +1247,62 @@ export const comparisons = {
english: 'Terms of service',
translated: 'Términos de servicio',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Marcadores',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Reconstruir',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Nuevo marcador',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: '¿Está seguro de que desea eliminar este marcador?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Título',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Conteo',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Descripción',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Marcador creado con éxito',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Marcador actualizado con éxito',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Marcador eliminado con éxito',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Hubo un error al crear el marcador',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Hubo un error al actualizar el marcador',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Hubo un error al eliminar el marcador',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Agregar a la conversación actual',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -2292,6 +2369,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Buscar mensajes',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Si el conteo de marcadores es incorrecto, por favor reconstruya la información de los marcadores. El conteo de los marcadores se recalculará y los datos se restaurarán a su estado correcto.',
},
com_nav_setting_general: {
english: 'General',
translated: 'General',

View file

@ -292,6 +292,21 @@ export default {
com_ui_use_micrphone: 'Käytä mikrofonia',
com_ui_min_tags: 'Enempää arvoja ei voida poistaa. Niiden minimimäärä on {0}.',
com_ui_max_tags: 'Maksimimäärä on {0}. käytetään viimeisimpiä arvoja.',
com_ui_bookmarks: 'Kirjanmerkit',
com_ui_bookmarks_rebuild: 'Uudelleenkokoa',
com_ui_bookmarks_new: 'Uusi kirjanmerkki',
com_ui_bookmark_delete_confirm:
'Oletko varma, että haluat poistaa tämän kirjanmerkin?',
com_ui_bookmarks_title: 'Otsikko',
com_ui_bookmarks_count: 'Määrä',
com_ui_bookmarks_description: 'Kuvaus',
com_ui_bookmarks_create_success: 'Kirjanmerkki luotu onnistuneesti',
com_ui_bookmarks_update_success: 'Kirjanmerkki päivitetty onnistuneesti',
com_ui_bookmarks_delete_success: 'Kirjanmerkki poistettu onnistuneesti',
com_ui_bookmarks_create_error: 'Virhe kirjanmerkin luomisessa',
com_ui_bookmarks_update_error: 'Virhe kirjanmerkin päivittämisessä',
com_ui_bookmarks_delete_error: 'Virhe kirjanmerkin poistamisessa',
com_ui_bookmarks_add_to_conversation: 'Lisää nykyiseen keskusteluun',
com_auth_error_login:
'Kirjautuminen annetuilla tiedoilla ei onnistunut. Tarkista kirjautumistiedot, ja yritä uudestaan.',
com_auth_error_login_rl:
@ -415,7 +430,7 @@ export default {
com_endpoint_stop: 'Pysäytyssekvenssit',
com_endpoint_stop_placeholder: 'Erota arvot toisistaan rivinvaihdoilla',
com_endpoint_openai_max_tokens: `Valinnainen \`max_tokens\` -kenttä, joka kuvaa keskustelun vastauksessa generoitujen tokeneiden maksimimäärää.
Syötteen ja vastauksen kokonaispituutta rajoittaa mallin konteksti-ikkuna. Konteksti -ikkunan koon ylittämisestä voi seurata virheitä.`,
com_endpoint_openai_temp:
'Korkeampi arvo = satunnaisempi; matalampi arvo = keskittyneempi ja deterministisempi. Suosittelemme, että muokkaat tätä tai Top P:tä, mutta ei molempia.',

View file

@ -90,6 +90,20 @@ export default {
com_ui_preview: 'Aperçu',
com_ui_upload: 'Téléverser',
com_ui_connect: 'Connecter',
com_ui_bookmarks: 'Signets',
com_ui_bookmarks_rebuild: 'Reconstruire',
com_ui_bookmarks_new: 'Nouveau signet',
com_ui_bookmark_delete_confirm: 'Êtes-vous sûr de vouloir supprimer ce signet?',
com_ui_bookmarks_title: 'Titre',
com_ui_bookmarks_count: 'Nombre',
com_ui_bookmarks_description: 'Description',
com_ui_bookmarks_create_success: 'Signet créé avec succès',
com_ui_bookmarks_update_success: 'Signet mis à jour avec succès',
com_ui_bookmarks_delete_success: 'Signet supprimé avec succès',
com_ui_bookmarks_create_error: 'Une erreur est survenue lors de la création du signet',
com_ui_bookmarks_update_error: 'Une erreur est survenue lors de la mise à jour du signet',
com_ui_bookmarks_delete_error: 'Une erreur est survenue lors de la suppression du signet',
com_ui_bookmarks_add_to_conversation: 'Ajouter à la conversation en cours',
com_auth_error_login:
'Impossible de se connecter avec les informations fournies. Veuillez vérifier vos identifiants et réessayer.',
com_auth_error_login_rl:
@ -363,6 +377,8 @@ export default {
com_nav_help_faq: 'Aide & FAQ',
com_nav_settings: 'Paramètres',
com_nav_search_placeholder: 'Rechercher des messages',
com_nav_info_bookmarks_rebuild:
'Si le nombre de signets est incorrect, veuillez reconstruire les informations des signets. Le nombre de signets sera recalculé et les données seront restaurées à leur état correct.',
com_nav_setting_general: 'Général',
com_nav_setting_beta: 'Fonctionnalités bêta',
com_nav_setting_data: 'Contrôles des données',
@ -449,6 +465,7 @@ export default {
com_sidepanel_hide_panel: 'Masquer le panneau',
com_sidepanel_attach_files: 'Joindre des fichiers',
com_sidepanel_manage_files: 'Gérer les fichiers',
com_sidepanel_conversation_tags: 'Signets',
com_assistants_capabilities: 'Capacités des assistants',
com_assistants_knowledge: 'Connaissances',
com_assistants_knowledge_info:
@ -1030,6 +1047,62 @@ export const comparisons = {
english: 'Connect',
translated: 'Connecter',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Signets',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Reconstruire',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Nouveau signet',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'Êtes-vous sûr de vouloir supprimer ce signet?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Titre',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Nombre',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Description',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Signet créé avec succès',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Signet mis à jour avec succès',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Signet supprimé avec succès',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Une erreur est survenue lors de la création du signet',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Une erreur est survenue lors de la mise à jour du signet',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Une erreur est survenue lors de la suppression du signet',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Ajouter à la conversation en cours',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1985,6 +2058,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Rechercher des messages',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Si le nombre de signets est incorrect, veuillez reconstruire les informations des signets. Le nombre de signets sera recalculé et les données seront restaurées à leur état correct.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Général',
@ -2306,6 +2385,10 @@ export const comparisons = {
english: 'Manage Files',
translated: 'Gérer les fichiers',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: 'Signets',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: 'Capacités des assistants',

View file

@ -5,6 +5,7 @@ export default {
com_sidepanel_select_assistant: 'בחר סייען',
com_sidepanel_assistant_builder: 'בניית סייען',
com_sidepanel_attach_files: 'צרף קבצים',
com_sidepanel_conversation_tags: 'סימניות',
com_assistants_knowledge: 'ידע',
com_assistants_knowledge_info:
'אם אתה מעלה קבצים תחת ידע, שיחות עם ה-סייען שלך עשויות לכלול תוכן מהקובץ.',
@ -119,6 +120,20 @@ export default {
_ui_preview: 'תצוגה מקדימה',
com_ui_upload: 'העלה',
com_ui_connect: 'התחבר',
com_ui_bookmarks: 'סימניות',
com_ui_bookmarks_rebuild: 'בנה מחדש',
com_ui_bookmarks_new: 'סימניה חדשה',
com_ui_bookmark_delete_confirm: 'האם אתה בטוח שברצונך למחוק את הסימניה הזו?',
com_ui_bookmarks_title: 'כותרת',
com_ui_bookmarks_count: 'ספירה',
com_ui_bookmarks_description: 'תיאור',
com_ui_bookmarks_create_success: 'הסימניה נוצרה בהצלחה',
com_ui_bookmarks_update_success: 'הסימניה עודכנה בהצלחה',
com_ui_bookmarks_delete_success: 'הסימניה נמחקה בהצלחה',
com_ui_bookmarks_create_error: 'אירעה שגיאה בעת יצירת הסימניה',
com_ui_bookmarks_update_error: 'אירעה שגיאה בעת עדכון הסימניה',
com_ui_bookmarks_delete_error: 'אירעה שגיאה בעת מחיקת הסימניה',
com_ui_bookmarks_add_to_conversation: 'הוסף לשיחה הנוכחית',
com_auth_error_login: 'לא ניתן להתחבר עם המידע שסופק. אנא בדוק את האישורים שלך ונסה שוב.',
com_auth_error_login_rl: 'יותר מדי ניסיונות כניסה בזמן קצר. בבקשה נסה שוב מאוחר יותר.',
com_auth_error_login_ban: 'החשבון שלך נחסם באופן זמני עקב הפרות של השירות שלנו.',
@ -392,6 +407,8 @@ export default {
com_nav_help_faq: 'עזרה ושאלות נפוצות',
com_nav_settings: 'הגדרות',
com_nav_search_placeholder: 'חפש הודעות',
com_nav_info_bookmarks_rebuild:
'אם ספירת הסימניות שגויה, נא לבנות מחדש את המידע של הסמניות. הספירה תחושב מחדש והנתונים ישוחזרו למצב הנכון.',
com_nav_setting_general: 'כללי',
com_nav_setting_beta: 'תכונות ביטא',
com_nav_setting_data: 'בקרות נתונים',
@ -411,6 +428,10 @@ export const comparisons = {
english: 'Attach Files',
translated: 'צרף קבצים',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: 'סימניות',
},
com_assistants_knowledge: {
english: 'Knowledge',
translated: 'ידע',
@ -842,6 +863,62 @@ export const comparisons = {
english: 'Connect',
translated: 'התחבר',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'סימניות',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'בנה מחדש',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'סימניה חדשה',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'האם אתה בטוח שברצונך למחו את הסימניה הזו?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'כותרת',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'ספירה',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'תיאור',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'הסימניה נוצרה בהצלחה',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'הסימניה עודכנה בהצלחה',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'הסימניה נמחקה בהצלחה',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'אירעה שגיאה בעת יצירת הסימניה',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'אירעה שגיאה בעת עדכון הסימניה',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'אירעה שגיאה בעת מחיקת הסימניה',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'הוסף לשיחה הנוכחית',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1858,6 +1935,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'חפש הודעות',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'אם ספירת הסימניות שגויה, נא לבנות מחדש את המידע של הסמניות. הספירה תחושב מחדש והנתונים ישוחזרו למצב הנכון.',
},
com_nav_setting_general: {
english: 'General',
translated: 'כללי',

View file

@ -88,6 +88,20 @@ export default {
com_ui_preview: 'Pratinjau',
com_ui_upload: 'Unggah',
com_ui_connect: 'Hubungkan',
com_ui_bookmarks: 'Penanda',
com_ui_bookmarks_rebuild: 'Bangun Kembali',
com_ui_bookmarks_new: 'Penanda Baru',
com_ui_bookmark_delete_confirm: 'Apakah Anda yakin ingin menghapus penanda ini?',
com_ui_bookmarks_title: 'Judul',
com_ui_bookmarks_count: 'Jumlah',
com_ui_bookmarks_description: 'Deskripsi',
com_ui_bookmarks_create_success: 'Penanda berhasil dibuat',
com_ui_bookmarks_update_success: 'Penanda berhasil diperbarui',
com_ui_bookmarks_delete_success: 'Penanda berhasil dihapus',
com_ui_bookmarks_create_error: 'Terjadi kesalahan saat membuat penanda',
com_ui_bookmarks_update_error: 'Terjadi kesalahan saat memperbarui penanda',
com_ui_bookmarks_delete_error: 'Terjadi kesalahan saat menghapus penanda',
com_ui_bookmarks_add_to_conversation: 'Tambahkan ke percakapan saat ini',
com_auth_error_login:
'Tidak dapat masuk dengan informasi yang diberikan. Silakan periksa kredensial Anda dan coba lagi.',
com_auth_error_login_rl:
@ -351,6 +365,8 @@ export default {
com_nav_help_faq: 'Bantuan & FAQ',
com_nav_settings: 'Pengaturan',
com_nav_search_placeholder: 'Cari pesan',
com_nav_info_bookmarks_rebuild:
'Jika jumlah penanda tidak benar, silakan bangun kembali informasi penanda. Jumlah penanda akan dihitung ulang dan data akan dipulihkan ke keadaan yang benar.',
com_nav_setting_general: 'Umum',
com_nav_setting_beta: 'Fitur beta',
com_nav_setting_data: 'Kontrol data',
@ -703,6 +719,62 @@ export const comparisons = {
english: 'Connect',
translated: 'Hubungkan',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Penanda',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Bangun Kembali',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Penanda Baru',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'Apakah Anda yakin ingin menghapus penanda ini?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Judul',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Jumlah',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Deskripsi',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Penanda berhasil dibuat',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Penanda berhasil diperbarui',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Penanda berhasil dihapus',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Terjadi kesalahan saat membuat penanda',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Terjadi kesalahan saat memperbarui penanda',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Terjadi kesalahan saat menghapus penanda',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Tambahkan ke percakapan saat ini',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1660,6 +1732,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Cari pesan',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Jika jumlah penanda tidak benar, silakan bangun kembali informasi penanda. Jumlah penanda akan dihitung ulang dan data akan dipulihkan ke keadaan yang benar.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Umum',

View file

@ -19,6 +19,7 @@ export default {
com_sidepanel_hide_panel: 'Nascondi Pannello',
com_sidepanel_attach_files: 'Allega File',
com_sidepanel_manage_files: 'Gestisci File',
com_sidepanel_conversation_tags: 'Segnalibri',
com_assistants_capabilities: 'Capacità',
com_assistants_knowledge: 'Conoscenza',
com_assistants_knowledge_info:
@ -224,6 +225,20 @@ export default {
com_ui_terms_of_service: 'Termini di servizio',
com_ui_min_tags: 'Impossibile rimuovere altri valori, è richiesto un minimo di {0}.',
com_ui_max_tags: 'Il numero massimo consentito è {0}, verranno utilizzati gli ultimi valori.',
com_ui_bookmarks: 'Segnalibri',
com_ui_bookmarks_rebuild: 'Ricostruisci',
com_ui_bookmarks_new: 'Nuovo Segnalibro',
com_ui_bookmark_delete_confirm: 'Sei sicuro di voler eliminare questo segnalibro?',
com_ui_bookmarks_title: 'Titolo',
com_ui_bookmarks_count: 'Conteggio',
com_ui_bookmarks_description: 'Descrizione',
com_ui_bookmarks_create_success: 'Segnalibro creato con successo',
com_ui_bookmarks_update_success: 'Segnalibro aggiornato con successo',
com_ui_bookmarks_delete_success: 'Segnalibro eliminato con successo',
com_ui_bookmarks_create_error: 'Si è verificato un errore durante la creazione del segnalibro',
com_ui_bookmarks_update_error: 'Si è verificato un errore durante l\'aggiornamento del segnalibro',
com_ui_bookmarks_delete_error: 'Si è verificato un errore durante l\'eliminazione del segnalibro',
com_ui_bookmarks_add_to_conversation: 'Aggiungi alla conversazione attuale',
com_auth_error_login:
'Impossibile eseguire l\'accesso con le informazioni fornite. Controlla le tue credenziali e riprova.',
com_auth_error_login_rl:
@ -535,6 +550,8 @@ export default {
com_dialog_delete_warning: 'ATTENZIONE: Questo cancellerà permanentemente il tuo account.',
com_dialog_delete_data_info: 'Tutti i tuoi dati verranno eliminati.',
com_dialog_delete_help_center: 'Per più informazioni, visita il nostro centro assistenza.',
com_nav_info_bookmarks_rebuild:
'Se il conteggio dei segnalibri è errato, ricostruisci le informazioni sui segnalibri. Il conteggio dei segnalibri verrà ricalcolato e i dati verranno ripristinati al loro stato corretto.',
com_nav_setting_general: 'Generale',
com_nav_setting_beta: 'Funzioni Beta',
com_nav_setting_data: 'Controlli dati',
@ -662,6 +679,10 @@ export const comparisons = {
english: 'Manage Files',
translated: 'Gestisci File',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: 'Segnalibri',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: 'Capacità',
@ -1397,6 +1418,62 @@ export const comparisons = {
english: 'Maximum number allowed is {0}, using latest values.',
translated: 'Il numero massimo consentito è {0}, verranno utilizzati gli ultimi valori.',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Segnalibri',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Ricostruisci',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Nuovo Segnalibro',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'Sei sicuro di voler eliminare questo segnalibro?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Titolo',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Conteggio',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Descrizione',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Segnalibro creato con successo',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Segnalibro aggiornato con successo',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Segnalibro eliminato con successo',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Si è verificato un errore durante la creazione del segnalibro',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Si è verificato un errore durante l\'aggiornamento del segnalibro',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Si è verificato un errore durante l\'eliminazione del segnalibro',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Aggiungi alla conversazione attuale',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -2471,6 +2548,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Cerca messaggi',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Se il conteggio dei segnalibri è errato, ricostruisci le informazioni sui segnalibri. Il conteggio dei segnalibri verrà ricalcolato e i dati verranno ripristinati al loro stato corretto.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Generali',

View file

@ -18,6 +18,7 @@ export default {
com_sidepanel_hide_panel: 'パネルを隠す',
com_sidepanel_attach_files: 'ファイルを添付する',
com_sidepanel_manage_files: 'ファイルを管理',
com_sidepanel_conversation_tags: 'ブックマーク',
com_assistants_capabilities: 'Capabilities',
com_assistants_knowledge: 'ナレッジ',
com_assistants_knowledge_info:
@ -181,6 +182,20 @@ export default {
com_ui_terms_of_service: '利用規約',
com_ui_min_tags: 'これ以上の値を削除できません。少なくとも {0} が必要です。',
com_ui_max_tags: '最新の値を使用した場合、許可される最大数は {0} です。',
com_ui_bookmarks: 'ブックマーク',
com_ui_bookmarks_rebuild: '再構築',
com_ui_bookmarks_new: '新しいブックマーク',
com_ui_bookmark_delete_confirm: 'このブックマークを削除してもよろしいですか?',
com_ui_bookmarks_title: 'タイトル',
com_ui_bookmarks_count: 'カウント',
com_ui_bookmarks_description: '説明',
com_ui_bookmarks_create_success: 'ブックマークが正常に作成されました',
com_ui_bookmarks_update_success: 'ブックマークが正常に更新されました',
com_ui_bookmarks_delete_success: 'ブックマークが正常に削除されました',
com_ui_bookmarks_create_error: 'ブックマークの作成中にエラーが発生しました',
com_ui_bookmarks_update_error: 'ブックマークの更新中にエラーが発生しました',
com_ui_bookmarks_delete_error: 'ブックマークの削除中にエラーが発生しました',
com_ui_bookmarks_add_to_conversation: '現在の会話に追加',
com_auth_error_login:
'入力された情報ではログインできませんでした。認証情報を確認した上で再度お試しください。',
com_auth_error_login_rl:
@ -473,6 +488,8 @@ export default {
com_nav_help_faq: 'ヘルプ & FAQ',
com_nav_settings: '設定',
com_nav_search_placeholder: 'メッセージ検索',
com_nav_info_bookmarks_rebuild:
'ブックマークのカウントが正しくない場合は、ブックマークの情報を再構築してください。ブックマークのカウントが再計算され、データが正しい状態に復元されます。',
com_nav_setting_general: '一般',
com_nav_setting_beta: 'ベータ版の機能',
com_nav_setting_data: 'データ管理',
@ -630,6 +647,10 @@ export const comparisons = {
english: 'Manage Files',
translated: 'ファイルを管理',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: 'ブックマーク',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: 'Capabilities',
@ -1248,6 +1269,62 @@ export const comparisons = {
english: 'Maximum number allowed is {0}, using latest values.',
translated: '最新の値を使用した場合、許可される最大数は {0} です。',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'ブックマーク',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: '再構築',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: '新しいブックマーク',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'このブックマークを削除してもよろしいですか?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'タイトル',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'カウント',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: '説明',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'ブックマークが正常に作成されました',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'ブックマークが正常に更新されました',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'ブックマークが正常に削除されました',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'ブックマークの作成中にエラーが発生しました',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'ブックマークの更新中にエラーが発生しました',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'ブックマークの削除中にエラーが発生しました',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: '現在の会話に追加',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -2303,6 +2380,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'メッセージ検索',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'ブックマークのカウントが正しくない場合は、ブックマークの情報を再構築してください。ブックマークのカウントが再計算され、データが正しい状態に復元されます。',
},
com_nav_setting_general: {
english: 'General',
translated: '一般',

View file

@ -74,6 +74,20 @@ export default {
com_ui_unarchive: '아카이브 해제',
com_ui_unarchive_error: '대화 아카이브 해제 실패',
com_ui_more_options: '더 보기',
com_ui_bookmarks: '북마크',
com_ui_bookmarks_rebuild: '재구축',
com_ui_bookmarks_new: '새 북마크',
com_ui_bookmark_delete_confirm: '이 북마크를 삭제하시겠습니까?',
com_ui_bookmarks_title: '제목',
com_ui_bookmarks_count: '개수',
com_ui_bookmarks_description: '설명',
com_ui_bookmarks_create_success: '북마크가 성공적으로 생성되었습니다',
com_ui_bookmarks_update_success: '북마크가 성공적으로 업데이트되었습니다',
com_ui_bookmarks_delete_success: '북마크가 성공적으로 삭제되었습니다',
com_ui_bookmarks_create_error: '북마크 생성 중 오류가 발생했습니다',
com_ui_bookmarks_update_error: '북마크 업데이트 중 오류가 발생했습니다',
com_ui_bookmarks_delete_error: '북마크 삭제 중 오류가 발생했습니다',
com_ui_bookmarks_add_to_conversation: '현재 대화에 추가',
com_auth_error_login: '제공된 정보로 로그인할 수 없습니다. 자격 증명을 확인하고 다시 시도하세요.',
com_auth_no_account: '계정이 없으신가요?',
com_auth_sign_up: '가입하기',
@ -279,6 +293,8 @@ export default {
com_nav_help_faq: '도움말 및 FAQ',
com_nav_settings: '설정',
com_nav_search_placeholder: '메시지 검색',
com_nav_info_bookmarks_rebuild:
'북마크 수가 정확하지 않은 경우 북마크 정보를 재구축하십시오. 북마크 수가 다시 계산되고 데이터가 올바른 상태로 복원됩니다.',
com_nav_setting_general: '일반',
com_nav_setting_data: '데이터 제어',
/* The following are AI Translated */
@ -328,6 +344,7 @@ export default {
com_sidepanel_hide_panel: '패널 숨기기',
com_sidepanel_attach_files: '파일 첨부',
com_sidepanel_manage_files: '파일 관리',
com_sidepanel_conversation_tags: '북마크',
com_assistants_capabilities: '기능',
com_assistants_knowledge: '지식',
com_assistants_knowledge_info:
@ -854,6 +871,62 @@ export const comparisons = {
english: 'More',
translated: '더 보기',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: '북마크',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: '재구축',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: '새 북마크',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: '이 북마크를 삭제하시겠습니까?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: '제목',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: '개수',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: '설명',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: '북마크가 성공적으로 생성되었습니다',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: '북마크가 성공적으로 업데이트되었습니다',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: '북마크가 성공적으로 삭제되었습니다',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: '북마크 생성 중 오류가 발생했습니다',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: '북마 업데이트 중 오류가 발생했습니다',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: '북마크 삭제 중 오류가 발생했습니다',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: '현재 대화에 추가',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1602,6 +1675,12 @@ export const comparisons = {
english: 'Search messages',
translated: '메시지 검색',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'북마크 수가 정확하지 않은 경우 북마크 정보를 재구축하십시오. 북마크 수가 다시 계산되고 데이터가 올바른 상태로 복원됩니다.',
},
com_nav_setting_general: {
english: 'General',
translated: '일반',
@ -1782,6 +1861,10 @@ export const comparisons = {
english: 'Manage Files',
translated: '파일 관리',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: '북마크',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: '기능',

View file

@ -80,6 +80,24 @@ export default {
com_ui_unarchive: 'Uit archiveren',
com_ui_unarchive_error: 'Kan conversatie niet uit archiveren',
com_ui_more_options: 'Meer',
com_ui_bookmarks: 'Bladwijzers',
com_ui_bookmarks_rebuild: 'Herbouwen',
com_ui_bookmarks_new: 'Nieuwe bladwijzer',
com_ui_bookmark_delete_confirm:
'Weet je zeker dat je deze bladwijzer wilt verwijderen?',
com_ui_bookmarks_title: 'Titel',
com_ui_bookmarks_count: 'Aantal',
com_ui_bookmarks_description: 'Beschrijving',
com_ui_bookmarks_create_success: 'Bladwijzer succesvol aangemaakt',
com_ui_bookmarks_update_success: 'Bladwijzer succesvol bijgewerkt',
com_ui_bookmarks_delete_success: 'Bladwijzer succesvol verwijderd',
com_ui_bookmarks_create_error:
'Er is een fout opgetreden bij het maken van de bladwijzer',
com_ui_bookmarks_update_error:
'Er is een fout opgetreden bij het bijwerken van de bladwijzer',
com_ui_bookmarks_delete_error:
'Er is een fout opgetreden bij het verwijderen van de bladwijzer',
com_ui_bookmarks_add_to_conversation: 'Toevoegen aan huidig gesprek',
com_auth_error_login:
'Kan niet inloggen met de verstrekte informatie. Controleer uw referenties en probeer het opnieuw.',
com_auth_error_login_rl: 'Te veel inlogpogingen in een korte tijd. Probeer het later nog eens.',
@ -308,6 +326,8 @@ export default {
com_nav_help_faq: 'Help & FAQ',
com_nav_settings: 'Instellingen',
com_nav_search_placeholder: 'Berichten doorzoeken',
com_nav_info_bookmarks_rebuild:
'Als het aantal bladwijzers onjuist is, bouw dan de bladwijzerinformatie opnieuw op. Het aantal bladwijzers zal worden herberekend en de gegevens zullen worden hersteld naar hun juiste staat.',
com_nav_setting_general: 'Algemeen',
com_nav_setting_data: 'Gegevensbesturing',
};
@ -610,6 +630,62 @@ export const comparisons = {
english: 'More',
translated: 'Meer',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Bladwijzers',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Herbouwen',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Nieuwe bladwijzer',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'Weet je zeker dat je deze bladwijzer wilt verwijderen?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Titel',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Aantal',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Beschrijving',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Bladwijzer succesvol aangemaakt',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Bladwijzer succesvol bijgewerkt',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Bladwijzer succesvol verwijderd',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Er is een fout opgetreden bij het maken van de bladwijzer',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Er is een fout opgetreden bij het bijwerken van de bladwijzer',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Er is een fout opgetreden bij het verwijderen van de bladwijzer',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Toevoegen aan huidig gesprek',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1416,6 +1492,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Berichten doorzoeken',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Als het aantal bladwijzers onjuist is, bouw dan de bladwijzerinformatie opnieuw op. Het aantal bladwijzers zal worden herberekend en de gegevens zullen worden hersteld naar hun juiste staat.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Algemeen',

View file

@ -53,6 +53,20 @@ export default {
com_ui_unarchive: 'Przywróć z archiwum',
com_ui_unarchive_error: 'Nie udało się odtworzyć rozmowy z archiwum',
com_ui_more_options: 'Więcej',
com_ui_bookmarks: 'Zakładki',
com_ui_bookmarks_rebuild: 'Przebuduj',
com_ui_bookmarks_new: 'Nowa zakładka',
com_ui_bookmark_delete_confirm: 'Czy na pewno chcesz usunąć tę zakładkę?',
com_ui_bookmarks_title: 'Tytuł',
com_ui_bookmarks_count: 'Licznik',
com_ui_bookmarks_description: 'Opis',
com_ui_bookmarks_create_success: 'Zakładka została pomyślnie utworzona',
com_ui_bookmarks_update_success: 'Zakładka została pomyślnie zaktualizowana',
com_ui_bookmarks_delete_success: 'Zakładka została pomyślnie usunięta',
com_ui_bookmarks_create_error: 'Wystąpił błąd podczas tworzenia zakładki',
com_ui_bookmarks_update_error: 'Wystąpił błąd podczas aktualizacji zakładki',
com_ui_bookmarks_delete_error: 'Wystąpił błąd podczas usuwania zakładki',
com_ui_bookmarks_add_to_conversation: 'Dodaj do bieżącej rozmowy',
com_auth_error_login:
'Nie udało się zalogować przy użyciu podanych danych. Sprawdź swoje dane logowania i spróbuj ponownie.',
com_auth_no_account: 'Nie masz konta?',
@ -239,6 +253,8 @@ export default {
com_nav_help_faq: 'Pomoc i często zadawane pytania',
com_nav_settings: 'Ustawienia',
com_nav_search_placeholder: 'Szukaj wiadomości',
com_nav_info_bookmarks_rebuild:
'Jeśli liczba zakładek jest nieprawidłowa, przebuduj informacje o zakładkach. Liczba zakładek zostanie ponownie obliczona, a dane przywrócone do prawidłowego stanu.',
com_nav_setting_general: 'Ogólne',
com_ui_import_conversation: 'Importuj',
com_ui_import_conversation_info: 'Importuj konwersacje z pliku JSON',
@ -424,6 +440,62 @@ export const comparisons = {
english: 'More',
translated: 'Więcej',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Zakładki',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Przebuduj',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Nowa zakładka',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'Czy na pewno chcesz usunąć tę zakładkę?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Tytuł',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Licznik',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Opis',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Zakładka została pomyślnie utworzona',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Zakładka została pomyślnie zaktualizowana',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Zakładka została pomyślnie usunięta',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Wystąpił błąd podczas tworzenia zakładki',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Wystąpił błąd podczas aktualizacji zakładki',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Wystąpił błąd podczas usuwania zakładki',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Dodaj do bieżącej rozmowy',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1089,6 +1161,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Szukaj wiadomości',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Jeśli liczba zakładek jest nieprawidłowa, przebuduj informacje o zakładkach. Liczba zakładek zostanie ponownie obliczona, a dane przywrócone do prawidłowego stanu.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Ogólne',

View file

@ -90,6 +90,20 @@ export default {
com_ui_unarchive: 'разархивировать',
com_ui_unarchive_error: 'Nie udało się odtworzyć rozmowy z archiwum',
com_ui_more_options: 'Еще',
com_ui_bookmarks: 'Закладки',
com_ui_bookmarks_rebuild: 'Перестроить',
com_ui_bookmarks_new: 'Новая закладка',
com_ui_bookmark_delete_confirm: 'Вы уверены, что хотите удалить э<><D18D>у закладку?',
com_ui_bookmarks_title: 'Заголовок',
com_ui_bookmarks_count: 'Количество',
com_ui_bookmarks_description: 'Описание',
com_ui_bookmarks_create_success: 'Закладка успешно создана',
com_ui_bookmarks_update_success: 'Закладка успешно обновлена',
com_ui_bookmarks_delete_success: 'Закладка успешно удалена',
com_ui_bookmarks_create_error: 'Произошла ошибка при создании закладки',
com_ui_bookmarks_update_error: 'Произошла ошибка при обновлении закладки',
com_ui_bookmarks_delete_error: 'Произошла ошибка при удалении закладки',
com_ui_bookmarks_add_to_conversation: 'Добавить в текущий разговор',
com_auth_error_login:
'Не удалось войти с предоставленной информацией. Пожалуйста, проверьте ваши учетные данные и попробуйте снова.',
com_auth_error_login_rl:
@ -357,6 +371,8 @@ export default {
com_nav_help_faq: 'Помощь и Вопросы',
com_nav_settings: 'Настройки',
com_nav_search_placeholder: 'Поиск сообщений',
com_nav_info_bookmarks_rebuild:
'Если количество закладок некорректно, пожалуйста, перестройте информацию о закладках. Количество закладок будет пересчитано, и данные будут восстановлены до правильного состояния.',
com_nav_setting_general: 'Общие',
com_nav_setting_beta: 'Бета-функции',
com_nav_setting_data: 'Управление данными',
@ -431,6 +447,7 @@ export default {
com_files_number_selected: 'Выбрано {0} из {1} файл(а/ов)',
com_sidepanel_parameters: 'Параметры',
com_sidepanel_hide_panel: 'Скрыть панель',
com_sidepanel_conversation_tags: 'Закладки',
com_assistants_capabilities: 'Возможности',
com_assistants_image_vision: 'Анализ изображений',
com_assistants_search_name: 'Поиск ассистентов по имени',
@ -925,6 +942,62 @@ export const comparisons = {
english: 'More',
translated: 'Еще',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Закладки',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Перестроить',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Новая закладка',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'Вы уверены, что хотите удалить эу закладку?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Заголовок',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Количество',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Описание',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Закладка успешно создана',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Закладка успешно обновлена',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Закладка успешно удалена',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Произошла ошибка при создании закладки',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Произошла ошибка при обновлении закладки',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Произошла ошибка при удалении закладки',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Добавить в текущий разговор',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1876,6 +1949,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Поиск сообщений',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Если количество закладок некорректно, пожалуйста, перестройте информацию о закладках. Количество закладок будет пересчитано, и данные будут восстановлены до правильного состояния.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Общие',
@ -2143,6 +2222,10 @@ export const comparisons = {
english: 'Hide Panel',
translated: 'Скрыть панель',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: 'Закладки',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: 'Возможности',

View file

@ -76,6 +76,20 @@ export default {
com_ui_unarchive: 'Avarkivera',
com_ui_unarchive_error: 'Kunde inte avarkivera chatt',
com_ui_more_options: 'Mer',
com_ui_bookmarks: 'Bokmärken',
com_ui_bookmarks_rebuild: 'Återuppbygg',
com_ui_bookmarks_new: 'Nytt Bokmärke',
com_ui_bookmark_delete_confirm: 'Är du säker på att du vill ta bort detta bokmärke?',
com_ui_bookmarks_title: 'Titel',
com_ui_bookmarks_count: 'Antal',
com_ui_bookmarks_description: 'Beskrivning',
com_ui_bookmarks_create_success: 'Bokmärke skapat framgångsrikt',
com_ui_bookmarks_update_success: 'Bokmärke uppdaterat framgångsrikt',
com_ui_bookmarks_delete_success: 'Bokm<6B><6D>rke raderat framgångsrikt',
com_ui_bookmarks_create_error: 'Ett fel uppstod vid skapandet av bokmärket',
com_ui_bookmarks_update_error: 'Ett fel uppstod vid uppdateringen av bokmärket',
com_ui_bookmarks_delete_error: 'Ett fel uppstod vid raderingen av bokmärket',
com_ui_bookmarks_add_to_conversation: 'Lägg till i nuvarande konversation',
com_auth_error_login:
'Kunde inte logga in med den angivna informationen. Kontrollera dina uppgifter och försök igen.',
com_auth_error_login_rl:
@ -295,6 +309,8 @@ export default {
com_nav_help_faq: 'Hjälp & Vanliga frågor',
com_nav_settings: 'Inställningar',
com_nav_search_placeholder: 'Sök meddelanden',
com_nav_info_bookmarks_rebuild:
'Om antalet bokmärken är felaktigt, vänligen återuppbygg informationen om bokmärkena. Antalet bokmärken kommer att omberäknas och data återställs till sitt korrekta tillstånd.',
com_nav_setting_general: 'Allmänt',
com_nav_setting_data: 'Datakontroller',
};
@ -589,6 +605,62 @@ export const comparisons = {
english: 'More',
translated: 'Mer',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Bokmärken',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Återuppbygg',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Nytt Bokmärke',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'Är du säker på att du vill ta bort detta bokmärke?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Titel',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Antal',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Beskrivning',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Bokmärke skapat framgångsrikt',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Bokmärke uppdaterat framgångsrikt',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Bokmrke raderat framgångsrikt',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Ett fel uppstod vid skapandet av bokmärket',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Ett fel uppstod vid uppdateringen av bokmärket',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Ett fel uppstod vid raderingen av bokmärket',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Lägg till i nuvarande konversation',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1380,6 +1452,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Sök meddelanden',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Om antalet bokmärken är felaktigt, vänligen återuppbygg informationen om bokmärkena. Antalet bokmärken kommer att omberäknas och data återställs till sitt korrekta tillstånd.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Allmänt',

File diff suppressed because it is too large Load diff

View file

@ -78,6 +78,20 @@ export default {
com_ui_unarchive: 'Bỏ lưu trữ',
com_ui_unarchive_error: 'Không thể bỏ lưu trữ cuộc trò chuyện',
com_ui_more_options: 'Thêm',
com_ui_bookmarks: 'Dấu trang',
com_ui_bookmarks_rebuild: 'Xây dựng lại',
com_ui_bookmarks_new: 'Dấu trang mới',
com_ui_bookmark_delete_confirm: 'Bạn có chắc chắn muốn xóa dấu trang này không?',
com_ui_bookmarks_title: 'Tiêu đề',
com_ui_bookmarks_count: 'Số lượng',
com_ui_bookmarks_description: 'Mô tả',
com_ui_bookmarks_create_success: 'Tạo dấu trang thành công',
com_ui_bookmarks_update_success: 'Cập nhật dấu trang thành công',
com_ui_bookmarks_delete_success: 'Xóa dấu trang thành công',
com_ui_bookmarks_create_error: 'Có lỗi xảy ra khi tạo dấu trang',
com_ui_bookmarks_update_error: 'Có lỗi xảy ra khi cập nhật dấu trang',
com_ui_bookmarks_delete_error: 'Có lỗi xảy ra khi x<><78>a dấu trang',
com_ui_bookmarks_add_to_conversation: 'Thêm vào cuộc hội thoại hiện tại',
com_auth_error_login:
'Không thể đăng nhập với thông tin được cung cấp. Vui lòng kiểm tra thông tin đăng nhập và thử lại.',
com_auth_error_login_rl:
@ -293,6 +307,8 @@ export default {
com_nav_help_faq: 'Trợ giúp & Câu hỏi thường gặp',
com_nav_settings: 'Cài đặt',
com_nav_search_placeholder: 'Tìm kiếm tin nhắn',
com_nav_info_bookmarks_rebuild:
'Nếu số lượng dấu trang không chính xác, vui lòng xây dựng lại thông tin dấu trang. Số lượng dấu trang sẽ được tính lại và dữ liệu sẽ được khôi phục về trạng thái chính xác.',
com_nav_setting_general: 'Chung',
com_nav_setting_data: 'Kiểm soát dữ liệu',
};
@ -588,6 +604,62 @@ export const comparisons = {
english: 'More',
translated: 'Thêm',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: 'Dấu trang',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: 'Xây dựng lại',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: 'Dấu trang mới',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: 'Bạn có chắc chắn muốn xóa dấu trang này không?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: 'Tiêu đề',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: 'Số lượng',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: 'Mô tả',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: 'Tạo dấu trang thành công',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: 'Cập nhật dấu trang thành công',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: 'Xóa dấu trang thành công',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: 'Có lỗi xảy ra khi tạo dấu trang',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: 'Có lỗi xảy ra khi cập nhật dấu trang',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: 'Có lỗi xảy ra khi xa dấu trang',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: 'Thêm vào cuộc hội thoại hiện tại',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1361,6 +1433,12 @@ export const comparisons = {
english: 'Search messages',
translated: 'Tìm kiếm tin nhắn',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'Nếu số lượng dấu trang không chính xác, vui lòng xây dựng lại thông tin dấu trang. Số lượng dấu trang sẽ được tính lại và dữ liệu sẽ được khôi phục về trạng thái chính xác.',
},
com_nav_setting_general: {
english: 'General',
translated: 'Chung',

View file

@ -11,6 +11,7 @@ export default {
com_sidepanel_hide_panel: '隐藏侧边栏',
com_sidepanel_attach_files: '附加文件',
com_sidepanel_manage_files: '管理文件',
com_sidepanel_conversation_tags: '书签',
com_assistants_capabilities: '功能',
com_assistants_knowledge: '知识',
com_assistants_knowledge_info: '如果您在“知识”中上传文件,与助手的对话可能包括文件内容。',
@ -159,6 +160,20 @@ export default {
com_ui_upload_delay: '上传 "{0}" 时比预期花了更长时间。 文件正在进行检索索引,请稍候。',
com_ui_privacy_policy: '隐私政策',
com_ui_terms_of_service: '服务政策',
com_ui_bookmarks: '书签',
com_ui_bookmarks_rebuild: '重建',
com_ui_bookmarks_new: '新书签',
com_ui_bookmark_delete_confirm: '你确定要删除这个书签吗?',
com_ui_bookmarks_title: '标题',
com_ui_bookmarks_count: '计数',
com_ui_bookmarks_description: '描述',
com_ui_bookmarks_create_success: '书签创建成功',
com_ui_bookmarks_update_success: '书签更新成功',
com_ui_bookmarks_delete_success: '书签删除成功',
com_ui_bookmarks_create_error: '创建书签时出错',
com_ui_bookmarks_update_error: '更新书签时出错',
com_ui_bookmarks_delete_error: '删除书签时出错',
com_ui_bookmarks_add_to_conversation: '添加到当前对话',
com_auth_error_login: '无法登录,请确认提供的账户密码正确,并重新尝试。',
com_auth_error_login_rl: '尝试登录次数过多,请稍后再试。',
com_auth_error_login_ban: '根据我们的服务规则,您的帐号被暂时禁用。',
@ -431,6 +446,8 @@ export default {
com_nav_help_faq: '帮助',
com_nav_settings: '设置',
com_nav_search_placeholder: '搜索对话及对话内容',
com_nav_info_bookmarks_rebuild:
'如果书签计数不正确,请重新构建书签信息。书签计数将被重新计算,数据将恢复到其正确状态。',
com_nav_setting_general: '通用',
com_nav_setting_beta: '实验特性',
com_nav_setting_data: '数据管理',
@ -572,6 +589,10 @@ export const comparisons = {
english: 'Manage Files',
translated: '管理文件',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: '书签',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: '功能',
@ -1169,6 +1190,62 @@ export const comparisons = {
english: 'Terms of service',
translated: '服务政策',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: '书签',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: '重建',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: '新书签',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: '你确定要删除这个书签吗?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: '标题',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: '计数',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: '描述',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: '书签创建成功',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: '书签更新成功',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: '书签删除成功',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: '创建书签时出错',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: '更新书签时出错',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: '删除书签时出错',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: '添加到当前对话',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -2209,6 +2286,12 @@ export const comparisons = {
english: 'Search messages',
translated: '搜索对话及对话内容',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'如果书签计数不正确,请重新构建书签信息。书签计数将被重新计算,数据将恢复到其正确状态。',
},
com_nav_setting_general: {
english: 'General',
translated: '通用',

View file

@ -71,6 +71,20 @@ export default {
com_ui_unarchive: '取消封存',
com_ui_unarchive_error: '取消封存對話時發生錯誤',
com_ui_more_options: '更多',
com_ui_bookmarks: '書籤',
com_ui_bookmarks_rebuild: '重建',
com_ui_bookmarks_new: '新書籤',
com_ui_bookmark_delete_confirm: '你確定要刪除這個書籤嗎?',
com_ui_bookmarks_title: '標題',
com_ui_bookmarks_count: '計數',
com_ui_bookmarks_description: '描述',
com_ui_bookmarks_create_success: '書籤創建成功',
com_ui_bookmarks_update_success: '書籤更新成功',
com_ui_bookmarks_delete_success: '書籤刪除成功',
com_ui_bookmarks_create_error: '創建書籤時出錯',
com_ui_bookmarks_update_error: '更新書籤時出錯',
com_ui_bookmarks_delete_error: '刪除書籤時出錯',
com_ui_bookmarks_add_to_conversation: '添加到當前對話',
com_auth_error_login: '無法使用提供的資訊登入。請檢查您的登入資訊後重試。',
com_auth_error_login_rl: '短時間內嘗試登入的次數過多。請稍後再試。',
com_auth_error_login_ban: '由於違反我們的服務條款,您的帳號已被暫時停用。',
@ -281,6 +295,8 @@ export default {
com_nav_help_faq: '說明與常見問題',
com_nav_settings: '設定',
com_nav_search_placeholder: '搜尋訊息',
com_nav_info_bookmarks_rebuild:
'如果書籤計數不正確,請重新構建書籤信息。書籤計數將被重新計算,數據將恢復到其正確狀態。',
com_nav_setting_general: '一般',
com_nav_setting_data: '資料控制',
/* The following are AI translated */
@ -328,6 +344,7 @@ export default {
com_sidepanel_hide_panel: '隱藏側邊選單',
com_sidepanel_attach_files: '附加檔案',
com_sidepanel_manage_files: '管理檔案',
com_sidepanel_conversation_tags: '書籤',
com_assistants_capabilities: '功能',
com_assistants_knowledge: '知識',
com_assistants_knowledge_info: '如果您在「知識」下上傳檔案,與您的助理的對話可能會包含檔案內容。',
@ -830,6 +847,62 @@ export const comparisons = {
english: 'More',
translated: '更多',
},
com_ui_bookmarks: {
english: 'Bookmarks',
translated: '書籤',
},
com_ui_bookmarks_rebuild: {
english: 'Rebuild',
translated: '重建',
},
com_ui_bookmarks_new: {
english: 'New Bookmark',
translated: '新書籤',
},
com_ui_bookmark_delete_confirm: {
english: 'Are you sure you want to delete this bookmark?',
translated: '你確定要刪除這個書籤嗎?',
},
com_ui_bookmarks_title: {
english: 'Title',
translated: '標題',
},
com_ui_bookmarks_count: {
english: 'Count',
translated: '計數',
},
com_ui_bookmarks_description: {
english: 'Description',
translated: '描述',
},
com_ui_bookmarks_create_success: {
english: 'Bookmark created successfully',
translated: '書籤創建成功',
},
com_ui_bookmarks_update_success: {
english: 'Bookmark updated successfully',
translated: '書籤更新成功',
},
com_ui_bookmarks_delete_success: {
english: 'Bookmark deleted successfully',
translated: '書籤刪除成功',
},
com_ui_bookmarks_create_error: {
english: 'There was an error creating the bookmark',
translated: '創建書籤時出錯',
},
com_ui_bookmarks_update_error: {
english: 'There was an error updating the bookmark',
translated: '更新書籤時出錯',
},
com_ui_bookmarks_delete_error: {
english: 'There was an error deleting the bookmark',
translated: '刪除書籤時出錯',
},
com_ui_bookmarks_add_to_conversation: {
english: 'Add to current conversation',
translated: '添加到當前對話',
},
com_auth_error_login: {
english:
'Unable to login with the information provided. Please check your credentials and try again.',
@ -1626,6 +1699,12 @@ export const comparisons = {
english: 'Search messages',
translated: '搜尋訊息',
},
com_nav_info_bookmarks_rebuild: {
english:
'If the bookmark count is incorrect, please rebuild the bookmark information. The bookmark count will be recalculated and the data will be restored to its correct state.',
translated:
'如果書籤計數不正確,請重新構建書籤信息。書籤計數將被重新計算,數據將恢復到其正確狀態。',
},
com_nav_setting_general: {
english: 'General',
translated: '一般',
@ -1806,6 +1885,10 @@ export const comparisons = {
english: 'Manage Files',
translated: '管理檔案',
},
com_sidepanel_conversation_tags: {
english: 'Bookmarks',
translated: '書籤',
},
com_assistants_capabilities: {
english: 'Capabilities',
translated: '功能',

View file

@ -8,7 +8,7 @@ import {
useSetRecoilState,
useRecoilCallback,
} from 'recoil';
import { LocalStorageKeys } from 'librechat-data-provider';
import { LocalStorageKeys, Constants } from 'librechat-data-provider';
import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
import type { TOptionSettings, ExtendedFile } from '~/common';
import { storeEndpointSettings, logger } from '~/utils';
@ -27,6 +27,14 @@ const submissionKeysAtom = atom<(string | number)[]>({
const latestMessageFamily = atomFamily<TMessage | null, string | number | null>({
key: 'latestMessageByIndex',
default: null,
effects: [
({ onSet, node }) => {
onSet(async (newValue) => {
const key = Number(node.key.split(Constants.COMMON_DIVIDER)[1]);
logger.log('Recoil Effect: Setting latestMessage', { key, newValue });
});
},
] as const,
});
const submissionByIndex = atomFamily<TSubmission | null, string | number>({
@ -41,7 +49,7 @@ const latestMessageKeysSelector = selector<(string | number)[]>({
return keys.filter((key) => get(latestMessageFamily(key)) !== null);
},
set: ({ set }, newKeys) => {
logger.log('setting latestMessageKeys', newKeys);
logger.log('setting latestMessageKeys', { newKeys });
set(latestMessageKeysAtom, newKeys);
},
});
@ -279,19 +287,22 @@ function useClearSubmissionState() {
return clearAllSubmissions;
}
function useClearLatestMessages() {
function useClearLatestMessages(context?: string) {
const clearAllLatestMessages = useRecoilCallback(
({ reset, set, snapshot }) =>
async (skipFirst?: boolean) => {
const latestMessageKeys = await snapshot.getPromise(latestMessageKeysSelector);
logger.log('latestMessageKeys', latestMessageKeys);
logger.log('[clearAllLatestMessages] latestMessageKeys', latestMessageKeys);
if (context) {
logger.log(`[clearAllLatestMessages] context: ${context}`);
}
for (const key of latestMessageKeys) {
if (skipFirst && key == 0) {
continue;
}
logger.log('resetting latest message', key);
logger.log(`[clearAllLatestMessages] resetting latest message; key: ${key}`);
reset(latestMessageFamily(key));
}

View file

@ -0,0 +1,225 @@
import type { TConversationTagsResponse } from 'librechat-data-provider';
import { updateConversationTag } from './conversationTags';
describe('ConversationTag Utilities', () => {
let conversations: TConversationTagsResponse;
beforeEach(() => {
conversations = [
{
tag: 'saved',
count: 1,
position: 0,
description: 'description1',
updatedAt: '2023-04-01T12:00:00Z',
createdAt: '2023-04-01T12:00:00Z',
user: 'user1',
},
{
tag: 'tag1',
count: 1,
position: 1,
description: 'description1',
updatedAt: '2023-04-01T12:00:00Z',
createdAt: '2023-04-01T12:00:00Z',
user: 'user1',
},
{
tag: 'tag2',
count: 20,
position: 2,
description: 'description2',
updatedAt: new Date().toISOString(),
createdAt: '2023-04-01T12:00:00Z',
user: 'user1',
},
{
tag: 'tag3',
count: 30,
position: 3,
description: 'description3',
updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
user: 'user1',
},
{
tag: 'tag4',
count: 40,
position: 4,
description: 'description4',
updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
user: 'user1',
},
{
tag: 'tag5',
count: 50,
position: 5,
description: 'description5',
updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
user: 'user1',
},
];
});
describe('updateConversationTag', () => {
it('updates the first tag correctly', () => {
const updated = updateConversationTag(
conversations,
{ tag: 'tag1-new', description: 'description1-new' },
{
...conversations[1],
tag: 'tag1-new',
description: 'description1-new',
},
'tag1',
);
expect(updated[0].tag).toBe('saved');
expect(updated[0].position).toBe(0);
expect(updated[1].tag).toBe('tag1-new');
expect(updated[1].description).toBe('description1-new');
expect(updated[1].position).toBe(1);
expect(updated[2].tag).toBe('tag2');
expect(updated[2].position).toBe(2);
expect(updated[3].tag).toBe('tag3');
expect(updated[3].position).toBe(3);
expect(updated[4].tag).toBe('tag4');
expect(updated[4].position).toBe(4);
expect(updated[5].tag).toBe('tag5');
expect(updated[5].position).toBe(5);
});
});
it('updates the third tag correctly', () => {
const updated = updateConversationTag(
conversations,
{ tag: 'tag3-new', description: 'description3-new' },
{
...conversations[3],
tag: 'tag3-new',
description: 'description3-new',
},
'tag3',
);
expect(updated[0].tag).toBe('saved');
expect(updated[0].position).toBe(0);
expect(updated[1].tag).toBe('tag1');
expect(updated[1].position).toBe(1);
expect(updated[2].tag).toBe('tag2');
expect(updated[2].position).toBe(2);
expect(updated[3].tag).toBe('tag3-new');
expect(updated[3].description).toBe('description3-new');
expect(updated[3].position).toBe(3);
expect(updated[4].tag).toBe('tag4');
expect(updated[4].position).toBe(4);
expect(updated[5].tag).toBe('tag5');
expect(updated[5].position).toBe(5);
});
it('updates the order of other tags if the order of the tags is moving up', () => {
const updated = updateConversationTag(
conversations,
// move tag3 to the second position
{ position: 2 },
{
...conversations[3],
position: 2,
},
'tag3',
);
expect(updated[0].tag).toBe('saved');
expect(updated[0].position).toBe(0);
expect(updated[1].tag).toBe('tag1');
expect(updated[1].position).toBe(1);
expect(updated[2].tag).toBe('tag3');
expect(updated[2].position).toBe(2);
expect(updated[3].tag).toBe('tag2');
expect(updated[3].position).toBe(3);
expect(updated[4].tag).toBe('tag4');
expect(updated[4].position).toBe(4);
expect(updated[5].tag).toBe('tag5');
expect(updated[5].position).toBe(5);
});
it('updates the order of other tags if the order of the tags is moving down', () => {
const updated = updateConversationTag(
conversations,
// move tag3 to the last position
{ position: 5 },
{
...conversations[3],
position: 5,
},
'tag3',
);
expect(updated[0].tag).toBe('saved');
expect(updated[0].position).toBe(0);
expect(updated[1].tag).toBe('tag1');
expect(updated[1].position).toBe(1);
expect(updated[2].tag).toBe('tag2');
expect(updated[2].position).toBe(2);
expect(updated[3].tag).toBe('tag4');
expect(updated[3].position).toBe(3);
expect(updated[4].tag).toBe('tag5');
expect(updated[4].position).toBe(4);
expect(updated[5].tag).toBe('tag3');
expect(updated[5].position).toBe(5);
});
it('updates the order of other tags if new tag is added', () => {
const updated = updateConversationTag(
conversations,
{ tag: 'newtag', description: 'newDescription' },
{
tag: 'newtag',
description: 'newDescription',
position: 1,
updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
user: 'user1',
count: 30,
},
// no tag tag specified
);
expect(updated[0].tag).toBe('saved');
expect(updated[0].position).toBe(0);
expect(updated[1].tag).toBe('newtag');
expect(updated[1].description).toBe('newDescription');
expect(updated[1].position).toBe(1);
expect(updated[2].tag).toBe('tag1');
expect(updated[2].position).toBe(2);
expect(updated[3].tag).toBe('tag2');
expect(updated[3].position).toBe(3);
expect(updated[4].tag).toBe('tag3');
expect(updated[4].position).toBe(4);
expect(updated[5].tag).toBe('tag4');
expect(updated[5].position).toBe(5);
expect(updated[6].tag).toBe('tag5');
expect(updated[6].position).toBe(6);
});
it('returns a new array for new tag if no tags exist', () => {
const updated = updateConversationTag(
[],
{ tag: 'newtag', description: 'newDescription' },
{
tag: 'saved',
description: 'newDescription',
position: 0,
updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
user: 'user1',
count: 30,
},
// no tag tag specified
);
expect(updated.length).toBe(1);
expect(updated[0].tag).toBe('saved');
expect(updated[0].position).toBe(0);
});
});

View file

@ -0,0 +1,55 @@
import {
TConversationTagRequest,
TConversationTagResponse,
TConversationTagsResponse,
} from 'librechat-data-provider';
export const updateConversationTag = (
queryCache: TConversationTagsResponse,
request: TConversationTagRequest,
response: TConversationTagResponse,
tag?: string,
): TConversationTagsResponse => {
if (queryCache.length === 0) {
return [response];
}
const oldData = queryCache.find((t) => t.tag === tag);
if (!oldData) {
// When a new tag is added, it is positioned at the top of the list.
return [queryCache[0], response, ...queryCache.slice(1)].map((t, index) => ({
...t,
position: index,
}));
}
const oldPosition = oldData.position;
const newPosition = response.position;
// Remove the updated data from the array
const filteredData = queryCache.filter((t) => t.tag !== tag);
if (newPosition === undefined || oldPosition === newPosition) {
// If the position hasn't changed, just replace the updated tag
return queryCache.map((t) => (t.tag === tag ? response : t));
}
// If the position has changed, update the position of the tag
const newData = [
...filteredData.slice(0, newPosition),
response,
...filteredData.slice(newPosition),
];
if (newPosition > oldPosition) {
// moving down
for (let i = oldPosition; i < newPosition; i++) {
newData[i].position = i;
}
} else {
// moving up
for (let i = newPosition + 1; i < newData.length; i++) {
newData[i].position = i;
}
}
return newData;
};

View file

@ -145,25 +145,37 @@ export const updateConversation = (
);
};
export const updateConvoFields: ConversationUpdater = (
export const updateConvoFields = (
data: ConversationData,
updatedConversation: Partial<TConversation> & Pick<TConversation, 'conversationId'>,
keepPosition = false,
): ConversationData => {
const newData = JSON.parse(JSON.stringify(data));
const { pageIndex, index } = findPageForConversation(
newData,
updatedConversation as { conversationId: string },
);
if (pageIndex !== -1 && index !== -1) {
const deleted = newData.pages[pageIndex].conversations.splice(index, 1);
const oldConversation = deleted[0] as TConversation;
const oldConversation = newData.pages[pageIndex].conversations[index] as TConversation;
newData.pages[0].conversations.unshift({
...oldConversation,
...updatedConversation,
updatedAt: new Date().toISOString(),
});
/**
* Do not change the position of the conversation if the tags are updated.
*/
if (keepPosition) {
const updatedConvo = {
...oldConversation,
...updatedConversation,
};
newData.pages[pageIndex].conversations[index] = updatedConvo;
} else {
const updatedConvo = {
...oldConversation,
...updatedConversation,
updatedAt: new Date().toISOString(),
};
newData.pages[pageIndex].conversations.splice(index, 1);
newData.pages[0].conversations.unshift(updatedConvo);
}
}
return newData;

View file

@ -1,13 +1,17 @@
import { ContentTypes } from 'librechat-data-provider';
import { ContentTypes, Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
export const getLengthAndFirstFiveChars = (str?: string) => {
const length = str ? str.length : 0;
const firstFiveChars = str ? str.substring(0, 5) : '';
return `${length}${firstFiveChars}`;
export const getLengthAndLastTenChars = (str?: string): string => {
if (!str) {
return '0';
}
const length = str.length;
const lastTenChars = str.slice(-10);
return `${length}${lastTenChars}`;
};
export const getLatestText = (message?: TMessage | null) => {
export const getLatestText = (message?: TMessage | null, includeIndex?: boolean) => {
if (!message) {
return '';
}
@ -18,9 +22,24 @@ export const getLatestText = (message?: TMessage | null) => {
for (let i = message.content.length - 1; i >= 0; i--) {
const part = message.content[i];
if (part.type === ContentTypes.TEXT && part[ContentTypes.TEXT]?.value?.length > 0) {
return part[ContentTypes.TEXT].value;
const text = part[ContentTypes.TEXT].value;
if (includeIndex) {
return `${text}-${i}`;
} else {
return text;
}
}
}
}
return '';
};
export const getTextKey = (message?: TMessage | null, convoId?: string | null) => {
if (!message) {
return '';
}
const text = getLatestText(message, true);
return `${message.messageId ?? ''}${Constants.COMMON_DIVIDER}${getLengthAndLastTenChars(text)}${
Constants.COMMON_DIVIDER
}${message.conversationId ?? convoId}`;
};