Compare commits
17 Commits
feature/cl
...
mawburn-sc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a54e2beeac | ||
|
|
96e3236820 | ||
|
|
e81853623f | ||
|
|
c18b5c95d4 | ||
|
|
17e2e0eee5 | ||
|
|
e8f8ab0ba0 | ||
|
|
3b93224fb0 | ||
|
|
2b1e5cd8e9 | ||
|
|
bd3f5f1e77 | ||
|
|
670f9b8daf | ||
|
|
f868bb7df7 | ||
|
|
a9db09fc04 | ||
|
|
0e68f67cac | ||
|
|
00cff20b08 | ||
|
|
a772b7368b | ||
|
|
84a1a16c3a | ||
|
|
cd4a3bd061 |
@@ -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,
|
||||
|
||||
@@ -318,6 +318,9 @@ class GoogleClient extends BaseClient {
|
||||
this.contextHandlers?.processFile(file);
|
||||
continue;
|
||||
}
|
||||
if (file.metadata?.fileIdentifier) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
this.augmentedPrompt = await this.contextHandlers.createContext();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`;
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
33
client/src/components/Chat/Menus/OpenSidebar.tsx
Normal file
33
client/src/components/Chat/Menus/OpenSidebar.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as PresetsMenu } from './PresetsMenu';
|
||||
export { default as OpenSidebar } from './OpenSidebar';
|
||||
export { default as HeaderNewChat } from './HeaderNewChat';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ export default function EndpointIcon({
|
||||
isCreatedByUser={false}
|
||||
chatGptLabel={undefined}
|
||||
modelLabel={undefined}
|
||||
jailbreak={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function AzureMinimalistIcon() {
|
||||
export default function AnthropicMinimalIcon() {
|
||||
return (
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
|
||||
19
client/src/components/svg/MobileSidebar.tsx
Normal file
19
client/src/components/svg/MobileSidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
19
client/src/components/svg/Sidebar.tsx
Normal file
19
client/src/components/svg/Sidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
client/src/components/svg/XAIcon.tsx
Normal file
16
client/src/components/svg/XAIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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+))?$/);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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`} />;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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 || [])];
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
40
client/src/hooks/useUpdateSearchParams.ts
Normal file
40
client/src/hooks/useUpdateSearchParams.ts
Normal 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;
|
||||
@@ -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 (
|
||||
|
||||
310
client/src/utils/createChatSearchParams.spec.ts
Normal file
310
client/src/utils/createChatSearchParams.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
93
client/src/utils/createChatSearchParams.ts
Normal file
93
client/src/utils/createChatSearchParams.ts
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user