Compare commits

...

17 Commits

Author SHA1 Message Date
Danny Avila
a54e2beeac fix: add condition to skip processing files with a fileIdentifier metadata 2025-04-27 02:38:43 -04:00
Danny Avila
96e3236820 fix: reset invalid tokenCount to 0 and log a warning in saveMessage function 2025-04-27 02:38:34 -04:00
Danny Avila
e81853623f fix: add condition to prevent queryClient updates for specific file paths (api downloads) 2025-04-27 02:21:23 -04:00
Danny Avila
c18b5c95d4 fix: open new chat in new tab when clicking with CTRL/meta 2025-04-27 02:17:37 -04:00
Danny Avila
17e2e0eee5 feat: add functionality to open conversation in a new tab on navigation 2025-04-27 01:55:42 -04:00
Danny Avila
e8f8ab0ba0 fix: remove redundant background classes for KnownEndpoints.xai in UnknownIcon component 2025-04-27 01:55:01 -04:00
Danny Avila
3b93224fb0 fix: update className in Landing component for improved dark mode styling 2025-04-27 01:52:55 -04:00
Danny Avila
2b1e5cd8e9 feat: add XAIcon component and integrate it into UnknownIcon for endpoint handling 2025-04-27 01:52:42 -04:00
Danny Avila
bd3f5f1e77 fix: add optional chaining to prevent runtime errors in various components 2025-04-27 01:40:49 -04:00
Danny Avila
670f9b8daf fix: add optional chaining to file type checks in DragDropModal and Columns to prevent runtime errors 2025-04-27 01:37:25 -04:00
Danny Avila
f868bb7df7 fix: message handling in useChatHelpers and useEventHandlers to prevent hard "load" of messages upon navigation 2025-04-27 01:20:43 -04:00
Danny Avila
a9db09fc04 refactor: Update createChatSearchParams to handle conversation.spec and improve NewChat component's preset handling 2025-04-27 01:04:27 -04:00
Danny Avila
0e68f67cac refactor: Simplify preset creation in useNewConvo and ChatRoute by utilizing getModelSpecPreset 2025-04-27 01:04:02 -04:00
Danny Avila
00cff20b08 fix: createChatSearchParams endpoint handling for agents/assistants 2025-04-27 00:25:53 -04:00
Danny Avila
a772b7368b chore: Remove unused type import and NavToggle component from Nav 2025-04-27 00:10:37 -04:00
Danny Avila
84a1a16c3a refactor: Enhance navigation and chat components with new sidebar and button functionalities that free up space in the nav 2025-04-27 00:09:41 -04:00
Matt Burnett
cd4a3bd061 feat: Implement search parameter updates and enhance chat component styles
- Added `useUpdateSearchParams` hook to manage URL search parameters dynamically.
- Updated `Landing`, `NewChat`, and various parameter components to utilize the new hook for better state management.
- Enhanced styling for dark mode support in chat components.
- Refactored click handlers in `NewChat` to create chat search parameters for navigation.
- Improved accessibility and usability in `DynamicCombobox`, `DynamicInput`, `DynamicSlider`, `DynamicSwitch`, and `DynamicTags` components.
- Introduced `createChatSearchParams` utility to streamline query parameter creation for chat sessions.

remove icon borders in dark mode
2025-04-26 20:18:32 -04:00
48 changed files with 915 additions and 326 deletions

View File

@@ -418,6 +418,9 @@ class AnthropicClient extends BaseClient {
this.contextHandlers?.processFile(file);
continue;
}
if (file.metadata?.fileIdentifier) {
continue;
}
orderedMessages[i].tokenCount += this.calculateImageTokenCost({
width: file.width,

View File

@@ -318,6 +318,9 @@ class GoogleClient extends BaseClient {
this.contextHandlers?.processFile(file);
continue;
}
if (file.metadata?.fileIdentifier) {
continue;
}
}
this.augmentedPrompt = await this.contextHandlers.createContext();

View File

@@ -455,6 +455,9 @@ class OpenAIClient extends BaseClient {
this.contextHandlers?.processFile(file);
continue;
}
if (file.metadata?.fileIdentifier) {
continue;
}
orderedMessages[i].tokenCount += this.calculateImageTokenCost({
width: file.width,

View File

@@ -61,6 +61,14 @@ async function saveMessage(req, params, metadata) {
update.expiredAt = null;
}
if (update.tokenCount != null && isNaN(update.tokenCount)) {
logger.warn(
`Resetting invalid \`tokenCount\` for message \`${params.messageId}\`: ${update.tokenCount}`,
);
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
update.tokenCount = 0;
}
const message = await Message.findOneAndUpdate(
{ messageId: params.messageId, user: req.user.id },
update,
@@ -97,7 +105,9 @@ async function saveMessage(req, params, metadata) {
};
} catch (findError) {
// If the findOne also fails, log it but don't crash
logger.warn(`Could not retrieve existing message with ID ${params.messageId}: ${findError.message}`);
logger.warn(
`Could not retrieve existing message with ID ${params.messageId}: ${findError.message}`,
);
return {
...params,
messageId: params.messageId,

View File

@@ -364,7 +364,9 @@ class AgentClient extends BaseClient {
this.contextHandlers?.processFile(file);
continue;
}
if (file.metadata?.fileIdentifier) {
continue;
}
// orderedMessages[i].tokenCount += this.calculateImageTokenCost({
// width: file.width,
// height: file.height,

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor" class="bg-white"><path d="m3.005 8.858 8.783 12.544h3.904L6.908 8.858zM6.905 15.825 3 21.402h3.907l1.951-2.788zM16.585 2l-6.75 9.64 1.953 2.79L20.492 2zM17.292 7.965v13.437h3.2V3.395z"></path></svg>

Before

Width:  |  Height:  |  Size: 315 B

View File

@@ -508,7 +508,10 @@ export interface ModelItemProps {
className?: string;
}
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };
export type ContextType = {
navVisible: boolean;
setNavVisible: React.Dispatch<React.SetStateAction<boolean>>;
};
export interface SwitcherProps {
endpoint?: t.EModelEndpoint | null;

View File

@@ -3,7 +3,7 @@ import { useOutletContext } from 'react-router-dom';
import { getConfigDefaults, PermissionTypes, Permissions } from 'librechat-data-provider';
import type { ContextType } from '~/common';
import ModelSelector from './Menus/Endpoints/ModelSelector';
import { PresetsMenu, HeaderNewChat } from './Menus';
import { PresetsMenu, HeaderNewChat, OpenSidebar } from './Menus';
import { useGetStartupConfig } from '~/data-provider';
import ExportAndShareMenu from './ExportAndShareMenu';
import { useMediaQuery, useHasAccess } from '~/hooks';
@@ -15,7 +15,7 @@ const defaultInterface = getConfigDefaults().interface;
export default function Header() {
const { data: startupConfig } = useGetStartupConfig();
const { navVisible } = useOutletContext<ContextType>();
const { navVisible, setNavVisible } = useOutletContext<ContextType>();
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
@@ -37,6 +37,7 @@ export default function Header() {
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold text-text-primary dark:bg-gray-800">
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
<div className="mx-2 flex items-center gap-2">
{!navVisible && <OpenSidebar setNavVisible={setNavVisible} />}
{!navVisible && <HeaderNewChat />}
{<ModelSelector startupConfig={startupConfig} />}
{interfaceConfig.presets === true && interfaceConfig.modelSelect && <PresetsMenu />}

View File

@@ -34,7 +34,7 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
label: localize('com_ui_upload_image_input'),
value: undefined,
icon: <ImageUpIcon className="icon-md" />,
condition: files.every((file) => file.type.startsWith('image/')),
condition: files.every((file) => file.type?.startsWith('image/')),
},
];
for (const capability of capabilities) {

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { ArrowUpDown, Database } from 'lucide-react';
import { FileSources, FileContext } from 'librechat-data-provider';
@@ -68,7 +69,7 @@ export const columns: ColumnDef<TFile>[] = [
},
cell: ({ row }) => {
const file = row.original;
if (file.type.startsWith('image')) {
if (file.type?.startsWith('image')) {
return (
<div className="flex gap-2">
<ImagePreview
@@ -76,7 +77,7 @@ export const columns: ColumnDef<TFile>[] = [
className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
source={file.source}
/>
<span className="self-center truncate ">{file.filename}</span>
<span className="self-center truncate">{file.filename}</span>
</div>
);
}
@@ -212,4 +213,4 @@ export const columns: ColumnDef<TFile>[] = [
return `${value}${suffix}`;
},
},
];
];

View File

@@ -9,7 +9,7 @@ import { useLocalize, useAuthContext } from '~/hooks';
import { getIconEndpoint, getEntity } from '~/utils';
const containerClassName =
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black';
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white dark:bg-presentation dark:text-white text-black dark:after:shadow-none ';
function getTextSizeClass(text: string | undefined | null) {
if (!text) {
@@ -159,7 +159,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
endpointsConfig={endpointsConfig}
containerClassName={containerClassName}
context="landing"
className="h-2/3 w-2/3"
className="h-2/3 w-2/3 text-black dark:text-white"
size={41}
/>
{startupConfig?.showBirthdayIcon && (

View File

@@ -1,36 +1,43 @@
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import { useMediaQuery, useLocalize } from '~/hooks';
import { Button, NewChatIcon } from '~/components';
import { TooltipAnchor, Button } from '~/components/ui';
import { NewChatIcon } from '~/components/svg';
import { useChatContext } from '~/Providers';
import { useLocalize } from '~/hooks';
export default function HeaderNewChat() {
const localize = useLocalize();
const queryClient = useQueryClient();
const { conversation, newConversation } = useChatContext();
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const localize = useLocalize();
if (isSmallScreen) {
return null;
}
const clickHandler: React.MouseEventHandler<HTMLButtonElement> = (e) => {
if (e.button === 0 && (e.ctrlKey || e.metaKey)) {
window.open('/c/new', '_blank');
return;
}
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
newConversation();
};
return (
<Button
size="icon"
variant="outline"
data-testid="wide-header-new-chat-button"
aria-label={localize('com_ui_new_chat')}
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover"
onClick={() => {
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
newConversation();
}}
>
<NewChatIcon />
</Button>
<TooltipAnchor
description={localize('com_ui_new_chat')}
render={
<Button
size="icon"
variant="outline"
data-testid="wide-header-new-chat-button"
aria-label={localize('com_ui_new_chat')}
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover max-md:hidden"
onClick={clickHandler}
>
<NewChatIcon />
</Button>
}
/>
);
}

View File

@@ -0,0 +1,33 @@
import { TooltipAnchor, Button } from '~/components/ui';
import { Sidebar } from '~/components/svg';
import { useLocalize } from '~/hooks';
export default function OpenSidebar({
setNavVisible,
}: {
setNavVisible: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const localize = useLocalize();
return (
<TooltipAnchor
description={localize('com_nav_open_sidebar')}
render={
<Button
size="icon"
variant="outline"
data-testid="open-sidebar-button"
aria-label={localize('com_nav_open_sidebar')}
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover max-md:hidden"
onClick={() =>
setNavVisible((prev) => {
localStorage.setItem('navVisible', JSON.stringify(!prev));
return !prev;
})
}
>
<Sidebar />
</Button>
}
/>
);
}

View File

@@ -1,2 +1,3 @@
export { default as PresetsMenu } from './PresetsMenu';
export { default as OpenSidebar } from './OpenSidebar';
export { default as HeaderNewChat } from './HeaderNewChat';

View File

@@ -150,7 +150,7 @@ export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => {
return (
<a
href={filepath.startsWith('files/') ? `/api/${filepath}` : `/api/files/${filepath}`}
href={filepath?.startsWith('files/') ? `/api/${filepath}` : `/api/files/${filepath}`}
{...props}
>
{children}

View File

@@ -102,6 +102,9 @@ export default function Conversation({
const handleNavigation = (ctrlOrMetaKey: boolean) => {
if (ctrlOrMetaKey) {
toggleNav();
const baseUrl = window.location.origin;
const path = `/c/${conversationId}`;
window.open(baseUrl + path, '_blank');
return;
}

View File

@@ -63,7 +63,6 @@ export default function EndpointIcon({
isCreatedByUser={false}
chatGptLabel={undefined}
modelLabel={undefined}
jailbreak={undefined}
/>
);
}

View File

@@ -1,10 +1,12 @@
import { type FC } from 'react';
import { useMemo } from 'react';
import type { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { Menu, MenuButton, MenuItems } from '@headlessui/react';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import { BookmarkContext } from '~/Providers/BookmarkContext';
import { useGetConversationTags } from '~/data-provider';
import BookmarkNavItems from './BookmarkNavItems';
import { TooltipAnchor } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import store from '~/store';
@@ -19,33 +21,41 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
const localize = useLocalize();
const { data } = useGetConversationTags();
const conversation = useRecoilValue(store.conversationByIndex(0));
const label = useMemo(
() => (tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')),
[tags, localize],
);
return (
<Menu as="div" className="group relative">
{({ open }) => (
<>
<MenuButton
className={cn(
'mt-text-sm flex h-10 w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors duration-200 hover:bg-surface-active-alt',
open ? 'bg-surface-active-alt' : '',
isSmallScreen ? 'h-12' : '',
)}
data-testid="bookmark-menu"
>
<div className="h-7 w-7 flex-shrink-0">
<div className="relative flex h-full items-center justify-center text-text-primary">
{tags.length > 0 ? (
<BookmarkFilledIcon className="h-4 w-4" aria-hidden="true" />
) : (
<BookmarkIcon className="h-4 w-4" aria-hidden="true" />
<TooltipAnchor
description={label}
render={
<MenuButton
id="bookmark-menu-button"
aria-label={localize('com_ui_bookmarks')}
className={cn(
'flex items-center justify-center',
'size-10 border-none text-text-primary hover:bg-accent hover:text-accent-foreground',
'rounded-xl border-none p-2 hover:bg-surface-hover',
open ? 'bg-surface-hover' : '',
)}
</div>
</div>
<div className="grow overflow-hidden whitespace-nowrap text-left text-sm font-medium text-text-primary">
{tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')}
</div>
</MenuButton>
<MenuItems className="absolute left-0 top-full z-[100] mt-1 w-full translate-y-0 overflow-hidden rounded-lg bg-surface-secondary p-1.5 shadow-lg outline-none">
data-testid="bookmark-menu"
>
{tags.length > 0 ? (
<BookmarkFilledIcon className="icon-lg text-text-primary" aria-hidden="true" />
) : (
<BookmarkIcon className="icon-lg text-text-primary" aria-hidden="true" />
)}
</MenuButton>
}
/>
<MenuItems
anchor="bottom"
className="absolute left-0 top-full z-[100] mt-1 w-60 translate-y-0 overflow-hidden rounded-lg bg-surface-secondary p-1.5 shadow-lg outline-none"
>
{data && conversation && (
<BookmarkContext.Provider value={{ bookmarks: data.filter((tag) => tag.count > 0) }}>
<BookmarkNavItems

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { TConversation, ConversationListResponse } from 'librechat-data-provider';
import type { ConversationListResponse } from 'librechat-data-provider';
import type { InfiniteQueryObserverResult } from '@tanstack/react-query';
import {
useLocalize,
@@ -13,7 +13,6 @@ import {
} from '~/hooks';
import { useConversationsInfiniteQuery } from '~/data-provider';
import { Conversations } from '~/components/Conversations';
import NavToggle from './NavToggle';
import SearchBar from './SearchBar';
import NewChat from './NewChat';
import { cn } from '~/utils';
@@ -152,20 +151,21 @@ const Nav = memo(
}, [isFetchingNextPage, computedHasNextPage, fetchNextPage]);
const subHeaders = useMemo(
() => (
<>
{search.enabled === true && <SearchBar isSmallScreen={isSmallScreen} />}
{hasAccessToBookmarks && (
<>
<div className="mt-1.5" />
<Suspense fallback={null}>
<BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} />
</Suspense>
</>
)}
</>
),
[search.enabled, hasAccessToBookmarks, isSmallScreen, tags, setTags],
() => search.enabled === true && <SearchBar isSmallScreen={isSmallScreen} />,
[search.enabled, isSmallScreen],
);
const headerButtons = useMemo(
() =>
hasAccessToBookmarks && (
<>
<div className="mt-1.5" />
<Suspense fallback={null}>
<BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} />
</Suspense>
</>
),
[hasAccessToBookmarks, tags, isSmallScreen],
);
const [isSearchLoading, setIsSearchLoading] = useState(
@@ -212,9 +212,10 @@ const Nav = memo(
>
<div className="flex flex-1 flex-col" ref={outerContainerRef}>
<MemoNewChat
toggleNav={itemToggleNav}
isSmallScreen={isSmallScreen}
subHeaders={subHeaders}
toggleNav={toggleNavVisible}
headerButtons={headerButtons}
isSmallScreen={isSmallScreen}
/>
<Conversations
conversations={conversations}
@@ -235,15 +236,6 @@ const Nav = memo(
</div>
</div>
</div>
<NavToggle
isHovering={isToggleHovering}
setIsHovering={setIsToggleHovering}
onToggle={toggleNavVisible}
navVisible={navVisible}
className="fixed left-0 top-1/2 z-40 hidden md:flex"
/>
{isSmallScreen && <NavMask navVisible={navVisible} toggleNavVisible={toggleNavVisible} />}
</>
);

View File

@@ -1,138 +1,99 @@
import React, { useMemo, useCallback } from 'react';
import { Search } from 'lucide-react';
import React, { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import type { TConversation, TMessage } from 'librechat-data-provider';
import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { useGetEndpointsQuery } from '~/data-provider';
import type { TMessage, TStartupConfig } from 'librechat-data-provider';
import { createChatSearchParams, getDefaultModelSpec, getModelSpecPreset } from '~/utils';
import { NewChatIcon, MobileSidebar, Sidebar } from '~/components/svg';
import { TooltipAnchor, Button } from '~/components/ui';
import { useLocalize, useNewConvo } from '~/hooks';
import { icons } from '~/hooks/Endpoint/Icons';
import { NewChatIcon } from '~/components/svg';
import { cn } from '~/utils';
import store from '~/store';
const NewChatButtonIcon = React.memo(({ conversation }: { conversation: TConversation | null }) => {
const { data: endpointsConfig } = useGetEndpointsQuery();
const search = useRecoilValue(store.search);
const searchQuery = search.debouncedQuery;
const computedIcon = useMemo(() => {
if (searchQuery) {
return null;
}
let { endpoint = '' } = conversation ?? {};
const iconURL = conversation?.iconURL ?? '';
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointType, endpointIconURL });
const Icon = icons[iconKey];
return { iconURL, endpoint, endpointType, endpointIconURL, Icon };
}, [searchQuery, conversation, endpointsConfig]);
if (searchQuery) {
return (
<div className="shadow-stroke relative flex h-7 w-7 items-center justify-center rounded-full bg-white text-black dark:bg-white">
<Search className="h-5 w-5" />
</div>
);
}
if (!computedIcon) {
return null;
}
const { iconURL, endpoint, endpointIconURL, Icon } = computedIcon;
return (
<div className="h-7 w-7 flex-shrink-0">
{iconURL && iconURL.includes('http') ? (
<ConvoIconURL
iconURL={iconURL}
modelLabel={conversation?.chatGptLabel ?? conversation?.modelLabel ?? ''}
endpointIconURL={iconURL}
context="nav"
/>
) : (
<div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black">
{endpoint && Icon && (
<Icon
size={41}
context="nav"
className="h-2/3 w-2/3"
endpoint={endpoint}
iconURL={endpointIconURL}
/>
)}
</div>
)}
</div>
);
});
export default function NewChat({
index = 0,
toggleNav,
subHeaders,
isSmallScreen,
headerButtons,
}: {
index?: number;
toggleNav: () => void;
isSmallScreen?: boolean;
subHeaders?: React.ReactNode;
isSmallScreen: boolean;
headerButtons?: React.ReactNode;
}) {
const queryClient = useQueryClient();
/** Note: this component needs an explicit index passed if using more than one */
const { newConversation: newConvo } = useNewConvo(index);
const navigate = useNavigate();
const localize = useLocalize();
const defaultPreset = useRecoilValue(store.defaultPreset);
const { conversation } = store.useCreateConversationAtom(index);
const clickHandler = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
if (event.button === 0 && !(event.ctrlKey || event.metaKey)) {
event.preventDefault();
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
newConvo();
navigate('/c/new');
const clickHandler: React.MouseEventHandler<HTMLButtonElement> = useCallback(
(e) => {
if (e.button === 0 && (e.ctrlKey || e.metaKey)) {
window.open('/c/new', '_blank');
return;
}
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
const defaultSpec = getDefaultModelSpec(startupConfig);
const preset = defaultSpec != null ? getModelSpecPreset(defaultSpec) : defaultPreset;
const params = createChatSearchParams(preset ?? conversation);
const newRoute = params.size > 0 ? `/c/new?${params.toString()}` : '/c/new';
newConvo();
navigate(newRoute);
if (isSmallScreen) {
toggleNav();
}
},
[queryClient, conversation, newConvo, navigate, toggleNav],
[queryClient, conversation, newConvo, navigate, toggleNav, defaultPreset, isSmallScreen],
);
return (
<div className="sticky left-0 right-0 top-0 z-50 bg-surface-primary-alt pt-3.5">
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
<a
href="/"
tabIndex={0}
data-testid="nav-new-chat-button"
onClick={clickHandler}
className={cn(
'group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover',
isSmallScreen ? 'h-14' : '',
)}
aria-label={localize('com_ui_new_chat')}
>
<NewChatButtonIcon conversation={conversation} />
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-sm text-text-primary">
{localize('com_ui_new_chat')}
</div>
<div className="flex gap-3">
<span className="flex items-center" data-state="closed">
<NewChatIcon className="size-5" />
</span>
</div>
</a>
<>
<div className="h-header-height xs:pe-3 flex items-center justify-between py-2">
<TooltipAnchor
description={localize('com_nav_close_sidebar')}
render={
<Button
size="icon"
variant="outline"
data-testid="close-sidebar-button"
aria-label={localize('com_nav_close_sidebar')}
className="rounded-xl border-none bg-transparent p-2 hover:bg-surface-hover"
onClick={toggleNav}
>
<Sidebar className="max-md:hidden" />
<MobileSidebar className="md:hidden" />
</Button>
}
/>
<div className="flex">
{headerButtons}
<TooltipAnchor
description={localize('com_ui_new_chat')}
render={
<Button
size="icon"
variant="outline"
data-testid="nav-new-chat-button"
aria-label={localize('com_ui_new_chat')}
className="rounded-xl border-none bg-transparent p-2 hover:bg-surface-hover"
onClick={clickHandler}
>
<NewChatIcon />
</Button>
}
/>
</div>
</div>
{subHeaders != null ? subHeaders : null}
</div>
</>
);
}

View File

@@ -24,7 +24,7 @@ export default function GroupSidePanel({
} & ReturnType<typeof usePromptGroupsNav>) {
const location = useLocation();
const isSmallerScreen = useMediaQuery('(max-width: 1024px)');
const isChatRoute = useMemo(() => location.pathname.startsWith('/c/'), [location.pathname]);
const isChatRoute = useMemo(() => location.pathname?.startsWith('/c/'), [location.pathname]);
return (
<div

View File

@@ -8,7 +8,7 @@ export default function PanelFileCell({ row }: { row: Row<TFile | undefined> })
const file = row.original;
return (
<div className="flex w-full items-center gap-2">
{file?.type.startsWith('image') === true ? (
{file?.type?.startsWith('image') === true ? (
<ImagePreview
url={file.filepath}
className="h-10 w-10 flex-shrink-0"

View File

@@ -3,7 +3,7 @@ import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, HoverCard, HoverCardTrigger } from '~/components/ui';
import ControlCombobox from '~/components/ui/ControlCombobox';
import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
import { TranslationKeys, useLocalize, useParameterEffects, useUpdateSearchParams } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@@ -33,6 +33,7 @@ function DynamicCombobox({
}: DynamicSettingProps & { isCollapsed?: boolean; SelectIcon?: React.ReactNode }) {
const localize = useLocalize();
const { preset } = useChatContext();
const updateSearchParams = useUpdateSearchParams();
const [inputValue, setInputValue] = useState<string | null>(null);
const selectedValue = useMemo(() => {
@@ -59,8 +60,10 @@ function DynamicCombobox({
} else {
setOption(settingKey)(value);
}
updateSearchParams({ [settingKey]: value });
},
[optionType, setOption, settingKey],
[optionType, setOption, settingKey, updateSearchParams],
);
useParameterEffects({
@@ -93,7 +96,7 @@ function DynamicCombobox({
htmlFor={`${settingKey}-dynamic-combobox`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}
{labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue})
@@ -105,10 +108,14 @@ function DynamicCombobox({
<ControlCombobox
displayValue={selectedValue}
selectPlaceholder={
selectPlaceholderCode === true ? localize(selectPlaceholder as TranslationKeys) : selectPlaceholder
selectPlaceholderCode === true
? localize(selectPlaceholder as TranslationKeys)
: selectPlaceholder
}
searchPlaceholder={
searchPlaceholderCode === true ? localize(searchPlaceholder as TranslationKeys) : searchPlaceholder
searchPlaceholderCode === true
? localize(searchPlaceholder as TranslationKeys)
: searchPlaceholder
}
isCollapsed={isCollapsed}
ariaLabel={settingKey}
@@ -120,7 +127,11 @@ function DynamicCombobox({
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
description={
descriptionCode
? (localize(description as TranslationKeys) ?? description)
: description
}
side={ESide.Left}
/>
)}

View File

@@ -1,6 +1,12 @@
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { useLocalize, useDebouncedInput, useParameterEffects, TranslationKeys } from '~/hooks';
import {
useLocalize,
useDebouncedInput,
useParameterEffects,
TranslationKeys,
useUpdateSearchParams,
} from '~/hooks';
import { Label, Input, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
@@ -26,6 +32,7 @@ function DynamicInput({
}: DynamicSettingProps) {
const localize = useLocalize();
const { preset } = useChatContext();
const updateSearchParams = useUpdateSearchParams();
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<string | number>({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
@@ -47,6 +54,7 @@ function DynamicInput({
const value = e.target.value;
if (type !== 'number') {
setInputValue(e);
updateSearchParams({ [settingKey]: value });
return;
}
@@ -55,6 +63,7 @@ function DynamicInput({
} else if (!isNaN(Number(value))) {
setInputValue(e, true);
}
updateSearchParams({ [settingKey]: value });
};
return (

View File

@@ -2,7 +2,13 @@ import { useMemo, useCallback } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Slider, HoverCard, Input, InputNumber, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useDebouncedInput, useParameterEffects, TranslationKeys } from '~/hooks';
import {
useLocalize,
useDebouncedInput,
useParameterEffects,
TranslationKeys,
useUpdateSearchParams,
} from '~/hooks';
import { cn, defaultTextProps, optionText } from '~/utils';
import { ESide, defaultDebouncedDelay } from '~/common';
import { useChatContext } from '~/Providers';
@@ -31,6 +37,7 @@ function DynamicSlider({
() => (!range && options && options.length > 0) ?? false,
[options, range],
);
const updateSearchParams = useUpdateSearchParams();
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<string | number>({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
@@ -60,20 +67,26 @@ function DynamicSlider({
const enumToNumeric = useMemo(() => {
if (isEnum && options) {
return options.reduce((acc, mapping, index) => {
acc[mapping] = index;
return acc;
}, {} as Record<string, number>);
return options.reduce(
(acc, mapping, index) => {
acc[mapping] = index;
return acc;
},
{} as Record<string, number>,
);
}
return {};
}, [isEnum, options]);
const valueToEnumOption = useMemo(() => {
if (isEnum && options) {
return options.reduce((acc, option, index) => {
acc[index] = option;
return acc;
}, {} as Record<number, string>);
return options.reduce(
(acc, option, index) => {
acc[index] = option;
return acc;
},
{} as Record<number, string>,
);
}
return {};
}, [isEnum, options]);
@@ -85,8 +98,10 @@ function DynamicSlider({
} else {
setInputValue(value);
}
updateSearchParams({ [settingKey]: value.toString() });
},
[isEnum, setInputValue, valueToEnumOption],
[isEnum, setInputValue, valueToEnumOption, updateSearchParams, settingKey],
);
const max = useMemo(() => {
@@ -117,7 +132,7 @@ function DynamicSlider({
htmlFor={`${settingKey}-dynamic-setting`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
{labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue})
@@ -132,7 +147,7 @@ function DynamicSlider({
onChange={(value) => setInputValue(Number(value))}
max={range ? range.max : (options?.length ?? 0) - 1}
min={range ? range.min : 0}
step={range ? range.step ?? 1 : 1}
step={range ? (range.step ?? 1) : 1}
controls={false}
className={cn(
defaultTextProps,
@@ -164,19 +179,23 @@ function DynamicSlider({
value={[
isEnum
? enumToNumeric[(selectedValue as number) ?? '']
: (inputValue as number) ?? (defaultValue as number),
: ((inputValue as number) ?? (defaultValue as number)),
]}
onValueChange={(value) => handleValueChange(value[0])}
onDoubleClick={() => setInputValue(defaultValue as string | number)}
max={max}
min={range ? range.min : 0}
step={range ? range.step ?? 1 : 1}
step={range ? (range.step ?? 1) : 1}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
description={
descriptionCode
? (localize(description as TranslationKeys) ?? description)
: description
}
side={ESide.Left}
/>
)}

View File

@@ -2,7 +2,7 @@ import { useState, useMemo } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Switch, HoverCard, HoverCardTrigger } from '~/components/ui';
import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
import { TranslationKeys, useLocalize, useParameterEffects, useUpdateSearchParams } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@@ -23,6 +23,7 @@ function DynamicSwitch({
}: DynamicSettingProps) {
const localize = useLocalize();
const { preset } = useChatContext();
const updateSearchParams = useUpdateSearchParams();
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
useParameterEffects({
preset,
@@ -47,6 +48,7 @@ function DynamicSwitch({
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
setInputValue(checked);
updateSearchParams({ [settingKey]: checked.toString() });
return;
}
setOption(settingKey)(checked);
@@ -65,7 +67,7 @@ function DynamicSwitch({
htmlFor={`${settingKey}-dynamic-switch`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
{labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}:{' '}
@@ -84,7 +86,11 @@ function DynamicSwitch({
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
description={
descriptionCode
? (localize(description as TranslationKeys) ?? description)
: description
}
side={ESide.Left}
/>
)}

View File

@@ -3,8 +3,8 @@ import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui';
import { useChatContext, useToastContext } from '~/Providers';
import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
import { cn, defaultTextProps } from '~/utils';
import { TranslationKeys, useLocalize, useParameterEffects, useUpdateSearchParams } from '~/hooks';
import { cn } from '~/utils';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@@ -30,6 +30,7 @@ function DynamicTags({
const localize = useLocalize();
const { preset } = useChatContext();
const { showToast } = useToastContext();
const updateSearchParams = useUpdateSearchParams();
const inputRef = useRef<HTMLInputElement>(null);
const [tagText, setTagText] = useState<string>('');
const [tags, setTags] = useState<string[] | undefined>(
@@ -41,11 +42,13 @@ function DynamicTags({
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
setTags(update);
updateSearchParams({ [settingKey]: update.join(',') });
return;
}
setOption(settingKey)(update);
updateSearchParams({ [settingKey]: update.join(',') });
},
[optionType, setOption, settingKey],
[optionType, setOption, settingKey, updateSearchParams],
);
const onTagClick = useCallback(() => {
@@ -75,7 +78,7 @@ function DynamicTags({
if (minTags != null && currentTags.length <= minTags) {
showToast({
message: localize('com_ui_min_tags',{ 0: minTags + '' }),
message: localize('com_ui_min_tags', { 0: minTags + '' }),
status: 'warning',
});
return;
@@ -126,7 +129,7 @@ function DynamicTags({
htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
{labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
(
@@ -174,7 +177,11 @@ function DynamicTags({
}
}}
onChange={(e) => setTagText(e.target.value)}
placeholder={placeholderCode ? localize(placeholder as TranslationKeys) ?? placeholder : placeholder}
placeholder={
placeholderCode
? (localize(placeholder as TranslationKeys) ?? placeholder)
: placeholder
}
className={cn('flex h-10 max-h-10 border-none bg-surface-secondary px-3 py-2')}
/>
</div>
@@ -182,7 +189,11 @@ function DynamicTags({
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
description={
descriptionCode
? (localize(description as TranslationKeys) ?? description)
: description
}
side={descriptionSide as ESide}
/>
)}

View File

@@ -1,6 +1,6 @@
import React from 'react';
export default function AzureMinimalistIcon() {
export default function AnthropicMinimalIcon() {
return (
<svg
stroke="currentColor"

View File

@@ -0,0 +1,19 @@
export default function MobileSidebar({ className }: { className?: string }) {
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="M3 8C3 7.44772 3.44772 7 4 7H20C20.5523 7 21 7.44772 21 8C21 8.55228 20.5523 9 20 9H4C3.44772 9 3 8.55228 3 8ZM3 16C3 15.4477 3.44772 15 4 15H14C14.5523 15 15 15.4477 15 16C15 16.5523 14.5523 17 14 17H4C3.44772 17 3 16.5523 3 16Z"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -1,43 +0,0 @@
export default function Panel({ open = false }) {
const openPanel = (
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="3" x2="9" y2="21"></line>
</svg>
);
const closePanel = (
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="3" x2="9" y2="21"></line>
</svg>
);
if (open) {
return openPanel;
} else {
return closePanel;
}
}

View File

@@ -0,0 +1,19 @@
export default function Sidebar({ className }: { className?: string }) {
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="M8.85719 3H15.1428C16.2266 2.99999 17.1007 2.99998 17.8086 3.05782C18.5375 3.11737 19.1777 3.24318 19.77 3.54497C20.7108 4.02433 21.4757 4.78924 21.955 5.73005C22.2568 6.32234 22.3826 6.96253 22.4422 7.69138C22.5 8.39925 22.5 9.27339 22.5 10.3572V13.6428C22.5 14.7266 22.5 15.6008 22.4422 16.3086C22.3826 17.0375 22.2568 17.6777 21.955 18.27C21.4757 19.2108 20.7108 19.9757 19.77 20.455C19.1777 20.7568 18.5375 20.8826 17.8086 20.9422C17.1008 21 16.2266 21 15.1428 21H8.85717C7.77339 21 6.89925 21 6.19138 20.9422C5.46253 20.8826 4.82234 20.7568 4.23005 20.455C3.28924 19.9757 2.52433 19.2108 2.04497 18.27C1.74318 17.6777 1.61737 17.0375 1.55782 16.3086C1.49998 15.6007 1.49999 14.7266 1.5 13.6428V10.3572C1.49999 9.27341 1.49998 8.39926 1.55782 7.69138C1.61737 6.96253 1.74318 6.32234 2.04497 5.73005C2.52433 4.78924 3.28924 4.02433 4.23005 3.54497C4.82234 3.24318 5.46253 3.11737 6.19138 3.05782C6.89926 2.99998 7.77341 2.99999 8.85719 3ZM6.35424 5.05118C5.74907 5.10062 5.40138 5.19279 5.13803 5.32698C4.57354 5.6146 4.1146 6.07354 3.82698 6.63803C3.69279 6.90138 3.60062 7.24907 3.55118 7.85424C3.50078 8.47108 3.5 9.26339 3.5 10.4V13.6C3.5 14.7366 3.50078 15.5289 3.55118 16.1458C3.60062 16.7509 3.69279 17.0986 3.82698 17.362C4.1146 17.9265 4.57354 18.3854 5.13803 18.673C5.40138 18.8072 5.74907 18.8994 6.35424 18.9488C6.97108 18.9992 7.76339 19 8.9 19H9.5V5H8.9C7.76339 5 6.97108 5.00078 6.35424 5.05118ZM11.5 5V19H15.1C16.2366 19 17.0289 18.9992 17.6458 18.9488C18.2509 18.8994 18.5986 18.8072 18.862 18.673C19.4265 18.3854 19.8854 17.9265 20.173 17.362C20.3072 17.0986 20.3994 16.7509 20.4488 16.1458C20.4992 15.5289 20.5 14.7366 20.5 13.6V10.4C20.5 9.26339 20.4992 8.47108 20.4488 7.85424C20.3994 7.24907 20.3072 6.90138 20.173 6.63803C19.8854 6.07354 19.4265 5.6146 18.862 5.32698C18.5986 5.19279 18.2509 5.10062 17.6458 5.05118C17.0289 5.00078 16.2366 5 15.1 5H11.5ZM5 8.5C5 7.94772 5.44772 7.5 6 7.5H7C7.55229 7.5 8 7.94772 8 8.5C8 9.05229 7.55229 9.5 7 9.5H6C5.44772 9.5 5 9.05229 5 8.5ZM5 12C5 11.4477 5.44772 11 6 11H7C7.55229 11 8 11.4477 8 12C8 12.5523 7.55229 13 7 13H6C5.44772 13 5 12.5523 5 12Z"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -0,0 +1,16 @@
import React from 'react';
export default function XAIcon({ className = '' }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
fill="currentColor"
className={className}
>
<path d="m3.005 8.858 8.783 12.544h3.904L6.908 8.858zM6.905 15.825 3 21.402h3.907l1.951-2.788zM16.585 2l-6.75 9.64 1.953 2.79L20.492 2zM17.292 7.965v13.437h3.2V3.395z"></path>
</svg>
);
}

View File

@@ -4,7 +4,8 @@ export { default as Plugin } from './Plugin';
export { default as GPTIcon } from './GPTIcon';
export { default as EditIcon } from './EditIcon';
export { default as DataIcon } from './DataIcon';
export { default as Panel } from './Panel';
export { default as Sidebar } from './Sidebar';
export { default as MobileSidebar } from './MobileSidebar';
export { default as Spinner } from './Spinner';
export { default as Clipboard } from './Clipboard';
export { default as CheckMark } from './CheckMark';
@@ -56,3 +57,4 @@ export { default as SpeechIcon } from './SpeechIcon';
export { default as SaveIcon } from './SaveIcon';
export { default as CircleHelpIcon } from './CircleHelpIcon';
export { default as BedrockIcon } from './BedrockIcon';
export { default as XAIcon } from './XAIcon';

View File

@@ -33,7 +33,8 @@ const ModelParameters: React.FC<ModelParametersProps> = ({
const rangeRef = useRef<HTMLInputElement>(null);
const id = `model-parameter-${ariaLabel.toLowerCase().replace(/\s+/g, '-')}`;
const displayLabel = label.startsWith('com_') ? localize(label as TranslationKeys) : label;
const displayLabel =
label && label.startsWith('com_') ? localize(label as TranslationKeys) : label;
const getDecimalPlaces = (num: number) => {
const match = ('' + num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);

View File

@@ -25,6 +25,7 @@ import store, { useGetEphemeralAgent } from '~/store';
import { getArtifactsMode } from '~/utils/artifacts';
import { getEndpointField, logger } from '~/utils';
import useUserKey from '~/hooks/Input/useUserKey';
import { useNavigate } from 'react-router-dom';
const logChatRequest = (request: Record<string, unknown>) => {
logger.log('=====================================\nAsk function called with:');
@@ -69,6 +70,7 @@ export default function useChatFunctions({
const codeArtifacts = useRecoilValue(store.codeArtifacts);
const includeShadcnui = useRecoilValue(store.includeShadcnui);
const customPromptMode = useRecoilValue(store.customPromptMode);
const navigate = useNavigate();
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
const setFilesToDelete = useSetFilesToDelete();
@@ -146,6 +148,7 @@ export default function useChatFunctions({
parentMessageId = Constants.NO_PARENT;
currentMessages = [];
conversationId = null;
navigate('/c/new');
}
const targetParentMessageId = isRegenerate ? messageId : latestMessage?.parentMessageId;

View File

@@ -23,10 +23,10 @@ export default function useChatHelpers(index = 0, paramId?: string) {
const { conversation, setConversation } = useCreateConversationAtom(index);
const { conversationId } = conversation ?? {};
const queryParam = paramId === 'new' ? paramId : conversationId ?? paramId ?? '';
const queryParam = paramId === 'new' ? paramId : (conversationId ?? paramId ?? '');
/* Messages: here simply to fetch, don't export and use `getMessages()` instead */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { data: _messages } = useGetMessagesByConvoId(conversationId ?? '', {
enabled: isAuthenticated,
});
@@ -41,7 +41,7 @@ export default function useChatHelpers(index = 0, paramId?: string) {
const setMessages = useCallback(
(messages: TMessage[]) => {
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, queryParam], messages);
if (queryParam === 'new') {
if (queryParam === 'new' && conversationId && conversationId !== 'new') {
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, conversationId], messages);
}
},

View File

@@ -1,6 +1,6 @@
import { memo } from 'react';
import { EModelEndpoint, KnownEndpoints } from 'librechat-data-provider';
import { CustomMinimalIcon } from '~/components/svg';
import { CustomMinimalIcon, XAIcon } from '~/components/svg';
import { IconContext } from '~/common';
import { cn } from '~/utils';
@@ -20,7 +20,6 @@ const knownEndpointAssets = {
[KnownEndpoints.shuttleai]: '/assets/shuttleai.png',
[KnownEndpoints['together.ai']]: '/assets/together.png',
[KnownEndpoints.unify]: '/assets/unify.webp',
[KnownEndpoints.xai]: '/assets/xai.svg',
};
const knownEndpointClasses = {
@@ -29,9 +28,6 @@ const knownEndpointClasses = {
},
[KnownEndpoints.xai]: {
[IconContext.landing]: 'p-2',
[IconContext.menuItem]: 'bg-white',
[IconContext.message]: 'bg-white',
[IconContext.nav]: 'bg-white',
},
};
@@ -72,6 +68,18 @@ function UnknownIcon({
const currentEndpoint = endpoint.toLowerCase();
if (currentEndpoint === KnownEndpoints.xai) {
return (
<XAIcon
className={getKnownClass({
currentEndpoint,
context: context,
className,
})}
/>
);
}
if (iconURL) {
return <img className={className} src={iconURL} alt={`${endpoint} Icon`} />;
}

View File

@@ -73,7 +73,7 @@ export default function useQueryParams({
const attemptsRef = useRef(0);
const processedRef = useRef(false);
const methods = useChatFormContext();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const getDefaultConversation = useDefaultConvo();
const modularChat = useRecoilValue(store.modularChat);
const availableTools = useRecoilValue(store.availableTools);
@@ -222,8 +222,12 @@ export default function useQueryParams({
/** Clean up URL parameters after successful processing */
const success = () => {
const newUrl = window.location.pathname;
window.history.replaceState({}, '', newUrl);
const currentParams = new URLSearchParams(searchParams.toString());
currentParams.delete('prompt');
currentParams.delete('q');
currentParams.delete('submit');
setSearchParams(currentParams, { replace: true });
processedRef.current = true;
console.log('Parameters processed successfully');
clearInterval(intervalId);
@@ -254,5 +258,14 @@ export default function useQueryParams({
return () => {
clearInterval(intervalId);
};
}, [searchParams, methods, textAreaRef, newQueryConvo, newConversation, submitMessage]);
}, [
searchParams,
methods,
textAreaRef,
newQueryConvo,
newConversation,
submitMessage,
setSearchParams,
queryClient,
]);
}

View File

@@ -10,7 +10,7 @@ export default function useAttachmentHandler(queryClient?: QueryClient) {
return ({ data }: { data: TAttachment; submission: EventSubmission }) => {
const { messageId } = data;
if (queryClient) {
if (queryClient && !data?.filepath?.startsWith('/api/files')) {
queryClient.setQueryData([QueryKeys.files], (oldData: TAttachment[] | undefined) => {
return [data, ...(oldData || [])];
});

View File

@@ -1,7 +1,7 @@
import { v4 } from 'uuid';
import { useCallback, useRef } from 'react';
import { useSetRecoilState } from 'recoil';
import { useParams } from 'react-router-dom';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import {
QueryKeys,
@@ -172,6 +172,8 @@ export default function useEventHandlers({
const { announcePolite } = useLiveAnnouncer();
const applyAgentTemplate = useApplyNewAgentTemplate();
const setAbortScroll = useSetRecoilState(store.abortScroll);
const navigate = useNavigate();
const location = useLocation();
const lastAnnouncementTimeRef = useRef(Date.now());
const { conversationId: paramId } = useParams();
@@ -421,6 +423,7 @@ export default function useEventHandlers({
announcePolite,
setConversation,
resetLatestMessage,
applyAgentTemplate,
],
);
@@ -449,12 +452,20 @@ export default function useEventHandlers({
announcePolite({ message: getAllContentText(responseMessage) });
/* Update messages; if assistants endpoint, client doesn't receive responseMessage */
let finalMessages: TMessage[] = [];
if (runMessages) {
setMessages([...runMessages]);
finalMessages = [...runMessages];
} else if (isRegenerate && responseMessage) {
setMessages([...messages, responseMessage]);
finalMessages = [...messages, responseMessage];
} else if (requestMessage != null && responseMessage != null) {
setMessages([...messages, requestMessage, responseMessage]);
finalMessages = [...messages, requestMessage, responseMessage];
}
if (finalMessages.length > 0) {
setMessages(finalMessages);
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation.conversationId],
finalMessages,
);
}
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
@@ -476,8 +487,8 @@ export default function useEventHandlers({
}
if (setConversation && isAddedRequest !== true) {
if (window.location.pathname === '/c/new') {
window.history.pushState({}, '', '/c/' + conversation.conversationId);
if (location.pathname === '/c/new') {
navigate(`/c/${conversation.conversationId}`, { replace: true });
}
setConversation((prevState) => {
@@ -502,16 +513,18 @@ export default function useEventHandlers({
setIsSubmitting(false);
},
[
genTitle,
queryClient,
getMessages,
setMessages,
setCompleted,
isAddedRequest,
announcePolite,
setConversation,
setIsSubmitting,
setShowStopButton,
setCompleted,
getMessages,
announcePolite,
genTitle,
setConversation,
isAddedRequest,
setIsSubmitting,
setMessages,
queryClient,
location.pathname,
navigate,
],
);
@@ -599,7 +612,7 @@ export default function useEventHandlers({
setIsSubmitting(false);
return;
},
[setMessages, paramId, setIsSubmitting, setCompleted, newConversation],
[setCompleted, setMessages, paramId, newConversation, setIsSubmitting, getMessages],
);
const abortConversation = useCallback(
@@ -698,7 +711,15 @@ export default function useEventHandlers({
setIsSubmitting(false);
}
},
[token, setIsSubmitting, finalHandler, cancelHandler, setMessages, newConversation],
[
finalHandler,
newConversation,
setIsSubmitting,
token,
cancelHandler,
getMessages,
setMessages,
],
);
return {

View File

@@ -35,3 +35,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 useUpdateSearchParams } from './useUpdateSearchParams';

View File

@@ -22,9 +22,10 @@ import {
getEndpointField,
buildDefaultConvo,
getDefaultEndpoint,
getModelSpecPreset,
getDefaultModelSpec,
getModelSpecIconURL,
updateLastSelectedModel,
createChatSearchParams,
} from '~/utils';
import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import useAssistantListMap from './Assistants/useAssistantListMap';
@@ -164,7 +165,13 @@ const useNewConvo = (index = 0) => {
if (appTitle) {
document.title = appTitle;
}
navigate(`/c/${Constants.NEW_CONVO}`);
const params = createChatSearchParams(conversation);
const newRoute =
params.size > 0
? `/c/${Constants.NEW_CONVO}?${params.toString()}`
: `/c/${Constants.NEW_CONVO}`;
navigate(newRoute);
}
clearTimeout(timeoutIdRef.current);
@@ -231,11 +238,7 @@ const useNewConvo = (index = 0) => {
(startupConfig.interface?.modelSelect ?? true) !== true) &&
defaultModelSpec
) {
preset = {
...defaultModelSpec.preset,
iconURL: getModelSpecIconURL(defaultModelSpec),
spec: defaultModelSpec.name,
} as TConversation;
preset = getModelSpecPreset(defaultModelSpec);
}
if (conversation.conversationId === 'new' && !modelsData) {

View File

@@ -0,0 +1,40 @@
import { useCallback, useRef } from 'react';
import { useSearchParams, useLocation } from 'react-router-dom';
import { createChatSearchParams } from '~/utils';
export const useUpdateSearchParams = () => {
const lastParamsRef = useRef<Record<string, string>>({});
const [, setSearchParams] = useSearchParams();
const location = useLocation();
const updateSearchParams = useCallback(
(record: Record<string, string> | null) => {
if (record == null || location.pathname !== '/c/new') {
return;
}
setSearchParams(
(params) => {
const currentParams = Object.fromEntries(params.entries());
const newSearchParams = createChatSearchParams(record);
const newParams = Object.fromEntries(newSearchParams.entries());
const mergedParams = { ...currentParams, ...newParams };
// If the new params are the same as the last params, don't update the search params
if (JSON.stringify(lastParamsRef.current) === JSON.stringify(newParams)) {
return currentParams;
}
lastParamsRef.current = { ...newParams };
return mergedParams;
},
{ replace: true },
);
},
[setSearchParams, location.pathname],
);
return updateSearchParams;
};
export default useUpdateSearchParams;

View File

@@ -9,8 +9,8 @@ import {
useGetStartupConfig,
useGetEndpointsQuery,
} from '~/data-provider';
import { getDefaultModelSpec, getModelSpecPreset, logger } from '~/utils';
import { useNewConvo, useAppStartup, useAssistantListMap } from '~/hooks';
import { getDefaultModelSpec, getModelSpecIconURL, logger } from '~/utils';
import { ToolCallsMapProvider } from '~/Providers';
import ChatView from '~/components/Chat/ChatView';
import useAuthRedirect from './useAuthRedirect';
@@ -65,15 +65,7 @@ export default function ChatRoute() {
newConversation({
modelsData: modelsQuery.data,
template: conversation ? conversation : undefined,
...(spec
? {
preset: {
...spec.preset,
iconURL: getModelSpecIconURL(spec),
spec: spec.name,
},
}
: {}),
...(spec ? { preset: getModelSpecPreset(spec) } : {}),
});
hasSetConversation.current = true;
@@ -97,15 +89,7 @@ export default function ChatRoute() {
newConversation({
modelsData: modelsQuery.data,
template: conversation ? conversation : undefined,
...(spec
? {
preset: {
...spec.preset,
iconURL: getModelSpecIconURL(spec),
spec: spec.name,
},
}
: {}),
...(spec ? { preset: getModelSpecPreset(spec) } : {}),
});
hasSetConversation.current = true;
} else if (

View File

@@ -0,0 +1,310 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { TConversation, TPreset } from 'librechat-data-provider';
import createChatSearchParams from './createChatSearchParams';
describe('createChatSearchParams', () => {
describe('conversation inputs', () => {
it('handles basic conversation properties', () => {
const conversation: Partial<TConversation> = {
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
temperature: 0.7,
};
const result = createChatSearchParams(conversation as TConversation);
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.get('model')).toBe('gpt-4');
expect(result.get('temperature')).toBe('0.7');
});
it('applies only the endpoint property when other conversation fields are absent', () => {
const endpointOnly = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
} as TConversation);
expect(endpointOnly.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(endpointOnly.has('model')).toBe(false);
expect(endpointOnly.has('endpoint')).toBe(true);
});
it('applies only the model property when other conversation fields are absent', () => {
const modelOnly = createChatSearchParams({ model: 'gpt-4' } as TConversation);
expect(modelOnly.has('endpoint')).toBe(false);
expect(modelOnly.get('model')).toBe('gpt-4');
expect(modelOnly.has('model')).toBe(true);
});
it('used to include endpoint and model with assistant_id but now only includes assistant_id', () => {
const withAssistantId = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
assistant_id: 'asst_123',
temperature: 0.7,
} as TConversation);
expect(withAssistantId.get('assistant_id')).toBe('asst_123');
expect(withAssistantId.has('endpoint')).toBe(false);
expect(withAssistantId.has('model')).toBe(false);
expect(withAssistantId.has('temperature')).toBe(false);
});
it('used to include endpoint and model with agent_id but now only includes agent_id', () => {
const withAgentId = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
agent_id: 'agent_123',
temperature: 0.7,
} as TConversation);
expect(withAgentId.get('agent_id')).toBe('agent_123');
expect(withAgentId.has('endpoint')).toBe(false);
expect(withAgentId.has('model')).toBe(false);
expect(withAgentId.has('temperature')).toBe(false);
});
it('when assistant_id is present, only include that param (excluding endpoint, model, etc)', () => {
const withAssistantId = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
assistant_id: 'asst_123',
temperature: 0.7,
} as TConversation);
expect(withAssistantId.get('assistant_id')).toBe('asst_123');
expect(withAssistantId.has('endpoint')).toBe(false);
expect(withAssistantId.has('model')).toBe(false);
expect(withAssistantId.has('temperature')).toBe(false);
expect([...withAssistantId.entries()].length).toBe(1);
});
it('when agent_id is present, only include that param (excluding endpoint, model, etc)', () => {
const withAgentId = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
agent_id: 'agent_123',
temperature: 0.7,
} as TConversation);
expect(withAgentId.get('agent_id')).toBe('agent_123');
expect(withAgentId.has('endpoint')).toBe(false);
expect(withAgentId.has('model')).toBe(false);
expect(withAgentId.has('temperature')).toBe(false);
expect([...withAgentId.entries()].length).toBe(1);
});
it('handles stop arrays correctly by joining with commas', () => {
const withStopArray = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
stop: ['stop1', 'stop2'],
} as TConversation);
expect(withStopArray.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(withStopArray.get('model')).toBe('gpt-4');
expect(withStopArray.get('stop')).toBe('stop1,stop2');
});
it('filters out non-supported array properties', () => {
const withOtherArray = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
otherArrayProp: ['value1', 'value2'],
} as any);
expect(withOtherArray.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(withOtherArray.get('model')).toBe('gpt-4');
expect(withOtherArray.has('otherArrayProp')).toBe(false);
});
it('includes empty arrays in output params', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
stop: [],
});
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.has('stop')).toBe(true);
expect(result.get('stop')).toBe('');
});
it('handles non-stop arrays correctly in paramMap', () => {
const conversation: any = {
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
top_p: ['0.7', '0.8'],
};
const result = createChatSearchParams(conversation);
const expectedJson = JSON.stringify(['0.7', '0.8']);
expect(result.get('top_p')).toBe(expectedJson);
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.get('model')).toBe('gpt-4');
});
it('includes empty non-stop arrays as serialized empty arrays', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
temperature: 0.7,
top_p: [],
} as any);
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.get('model')).toBe('gpt-4');
expect(result.get('temperature')).toBe('0.7');
expect(result.has('top_p')).toBe(true);
expect(result.get('top_p')).toBe('[]');
});
it('excludes parameters with null or undefined values from the output', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
temperature: 0.7,
top_p: undefined,
presence_penalty: undefined,
frequency_penalty: null,
} as any);
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.get('model')).toBe('gpt-4');
expect(result.get('temperature')).toBe('0.7');
expect(result.has('top_p')).toBe(false);
expect(result.has('presence_penalty')).toBe(false);
expect(result.has('frequency_penalty')).toBe(false);
expect(result).toBeDefined();
});
it('handles float parameter values correctly', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.google,
model: 'gemini-pro',
frequency_penalty: 0.25,
temperature: 0.75,
});
expect(result.get('endpoint')).toBe(EModelEndpoint.google);
expect(result.get('model')).toBe('gemini-pro');
expect(result.get('frequency_penalty')).toBe('0.25');
expect(result.get('temperature')).toBe('0.75');
});
it('handles integer parameter values correctly', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.google,
model: 'gemini-pro',
topK: 40,
maxOutputTokens: 2048,
});
expect(result.get('endpoint')).toBe(EModelEndpoint.google);
expect(result.get('model')).toBe('gemini-pro');
expect(result.get('topK')).toBe('40');
expect(result.get('maxOutputTokens')).toBe('2048');
});
});
describe('preset inputs', () => {
it('handles preset objects correctly', () => {
const preset: Partial<TPreset> = {
endpoint: EModelEndpoint.google,
model: 'gemini-pro',
temperature: 0.5,
topP: 0.8,
};
const result = createChatSearchParams(preset as TPreset);
expect(result.get('endpoint')).toBe(EModelEndpoint.google);
expect(result.get('model')).toBe('gemini-pro');
expect(result.get('temperature')).toBe('0.5');
expect(result.get('topP')).toBe('0.8');
});
});
describe('record inputs', () => {
it('includes allowed parameters from Record inputs', () => {
const record: Record<string, any> = {
endpoint: EModelEndpoint.anthropic,
model: 'claude-2',
temperature: '0.8',
top_p: '0.95',
extraParam: 'should-not-be-included',
invalidParam1: 'value1',
invalidParam2: 'value2',
};
const result = createChatSearchParams(record);
expect(result.get('endpoint')).toBe(EModelEndpoint.anthropic);
expect(result.get('model')).toBe('claude-2');
expect(result.get('temperature')).toBe('0.8');
expect(result.get('top_p')).toBe('0.95');
});
it('excludes disallowed parameters from Record inputs', () => {
const record: Record<string, any> = {
endpoint: EModelEndpoint.anthropic,
model: 'claude-2',
extraParam: 'should-not-be-included',
invalidParam1: 'value1',
invalidParam2: 'value2',
};
const result = createChatSearchParams(record);
expect(result.has('extraParam')).toBe(false);
expect(result.has('invalidParam1')).toBe(false);
expect(result.has('invalidParam2')).toBe(false);
expect(result.toString().includes('invalidParam')).toBe(false);
expect(result.toString().includes('extraParam')).toBe(false);
});
it('includes valid values from Record inputs', () => {
const record: Record<string, any> = {
temperature: '0.7',
top_p: null,
frequency_penalty: undefined,
};
const result = createChatSearchParams(record);
expect(result.get('temperature')).toBe('0.7');
});
it('excludes null or undefined values from Record inputs', () => {
const record: Record<string, any> = {
temperature: '0.7',
top_p: null,
frequency_penalty: undefined,
};
const result = createChatSearchParams(record);
expect(result.has('top_p')).toBe(false);
expect(result.has('frequency_penalty')).toBe(false);
});
it('handles generic object without endpoint or model properties', () => {
const customObject = {
temperature: '0.5',
top_p: '0.7',
customProperty: 'value',
};
const result = createChatSearchParams(customObject);
expect(result.get('temperature')).toBe('0.5');
expect(result.get('top_p')).toBe('0.7');
expect(result.has('customProperty')).toBe(false);
});
});
describe('edge cases', () => {
it('returns an empty URLSearchParams instance when input is null', () => {
const result = createChatSearchParams(null);
expect(result.toString()).toBe('');
expect(result instanceof URLSearchParams).toBe(true);
});
it('returns an empty URLSearchParams instance for an empty object input', () => {
const result = createChatSearchParams({});
expect(result.toString()).toBe('');
expect(result instanceof URLSearchParams).toBe(true);
});
});
});

View File

@@ -0,0 +1,93 @@
import { isAgentsEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider';
import type { TConversation, TPreset } from 'librechat-data-provider';
export default function createChatSearchParams(
input: TConversation | TPreset | Record<string, string> | null,
): URLSearchParams {
if (input == null) {
return new URLSearchParams();
}
const params = new URLSearchParams();
const allowedParams = [
'endpoint',
'model',
'temperature',
'presence_penalty',
'frequency_penalty',
'stop',
'top_p',
'max_tokens',
'topP',
'topK',
'maxOutputTokens',
'promptCache',
'region',
'maxTokens',
'agent_id',
'assistant_id',
];
if (input && typeof input === 'object' && !('endpoint' in input) && !('model' in input)) {
Object.entries(input as Record<string, string>).forEach(([key, value]) => {
if (value != null && allowedParams.includes(key)) {
params.set(key, value);
}
});
return params;
}
const conversation = input as TConversation | TPreset;
const endpoint = conversation.endpoint;
if (conversation.spec) {
return new URLSearchParams({ spec: conversation.spec });
}
if (
isAgentsEndpoint(endpoint) &&
conversation.agent_id &&
conversation.agent_id !== Constants.EPHEMERAL_AGENT_ID
) {
return new URLSearchParams({ agent_id: String(conversation.agent_id) });
} else if (isAssistantsEndpoint(endpoint) && conversation.assistant_id) {
return new URLSearchParams({ assistant_id: String(conversation.assistant_id) });
} else if (isAgentsEndpoint(endpoint) && !conversation.agent_id) {
return params;
} else if (isAssistantsEndpoint(endpoint) && !conversation.assistant_id) {
return params;
}
if (endpoint) {
params.set('endpoint', endpoint);
}
if (conversation.model) {
params.set('model', conversation.model);
}
const paramMap = {
temperature: conversation.temperature,
presence_penalty: conversation.presence_penalty,
frequency_penalty: conversation.frequency_penalty,
stop: conversation.stop,
top_p: conversation.top_p,
max_tokens: conversation.max_tokens,
topP: conversation.topP,
topK: conversation.topK,
maxOutputTokens: conversation.maxOutputTokens,
promptCache: conversation.promptCache,
region: conversation.region,
maxTokens: conversation.maxTokens,
};
return Object.entries(paramMap).reduce((params, [key, value]) => {
if (value != null) {
if (Array.isArray(value)) {
params.set(key, key === 'stop' ? value.join(',') : JSON.stringify(value));
} else {
params.set(key, String(value));
}
}
return params;
}, params);
}

View File

@@ -203,6 +203,17 @@ export function getDefaultModelSpec(startupConfig?: t.TStartupConfig) {
return list?.find((spec) => spec.name === lastConversationSetup.spec);
}
export function getModelSpecPreset(modelSpec?: t.TModelSpec) {
if (!modelSpec) {
return;
}
return {
...modelSpec.preset,
spec: modelSpec.name,
iconURL: getModelSpecIconURL(modelSpec),
};
}
/** Gets the default spec iconURL by order or definition.
*
* First, the admin defined default, then last selected spec, followed by first spec

View File

@@ -22,6 +22,7 @@ export { default as getLoginError } from './getLoginError';
export { default as cleanupPreset } from './cleanupPreset';
export { default as buildDefaultConvo } from './buildDefaultConvo';
export { default as getDefaultEndpoint } from './getDefaultEndpoint';
export { default as createChatSearchParams } from './createChatSearchParams';
export const languages = [
'java',