mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 00:15:23 +02:00
Merge branch 'main' into style/clean-copied-text
This commit is contained in:
commit
1336b13639
15 changed files with 146 additions and 89 deletions
|
|
@ -1213,8 +1213,8 @@ class BaseClient {
|
|||
this.options.req,
|
||||
attachments,
|
||||
{
|
||||
provider: this.options.agent?.provider,
|
||||
endpoint: this.options.agent?.endpoint,
|
||||
provider: this.options.agent?.provider ?? this.options.endpoint,
|
||||
endpoint: this.options.agent?.endpoint ?? this.options.endpoint,
|
||||
useResponsesApi: this.options.agent?.model_parameters?.useResponsesApi,
|
||||
},
|
||||
getStrategyFunctions,
|
||||
|
|
@ -1231,8 +1231,8 @@ class BaseClient {
|
|||
this.options.req,
|
||||
attachments,
|
||||
{
|
||||
provider: this.options.agent?.provider,
|
||||
endpoint: this.options.agent?.endpoint,
|
||||
provider: this.options.agent?.provider ?? this.options.endpoint,
|
||||
endpoint: this.options.agent?.endpoint ?? this.options.endpoint,
|
||||
},
|
||||
getStrategyFunctions,
|
||||
);
|
||||
|
|
@ -1246,8 +1246,8 @@ class BaseClient {
|
|||
this.options.req,
|
||||
attachments,
|
||||
{
|
||||
provider: this.options.agent?.provider,
|
||||
endpoint: this.options.agent?.endpoint,
|
||||
provider: this.options.agent?.provider ?? this.options.endpoint,
|
||||
endpoint: this.options.agent?.endpoint ?? this.options.endpoint,
|
||||
},
|
||||
getStrategyFunctions,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import BookmarkMenu from './Menus/BookmarkMenu';
|
|||
import { TemporaryChat } from './TemporaryChat';
|
||||
import AddMultiConvo from './AddMultiConvo';
|
||||
import { useHasAccess } from '~/hooks';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
|
|
@ -38,24 +39,24 @@ export default function Header() {
|
|||
return (
|
||||
<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-1 flex items-center gap-2">
|
||||
<div
|
||||
className={`flex items-center gap-2 ${
|
||||
!isSmallScreen ? 'transition-all duration-200 ease-in-out' : ''
|
||||
} ${
|
||||
!navVisible
|
||||
? 'translate-x-0 opacity-100'
|
||||
: 'pointer-events-none translate-x-[-100px] opacity-0'
|
||||
}`}
|
||||
>
|
||||
<OpenSidebar setNavVisible={setNavVisible} className="max-md:hidden" />
|
||||
<HeaderNewChat />
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center gap-2 ${
|
||||
!isSmallScreen ? 'transition-all duration-200 ease-in-out' : ''
|
||||
} ${!navVisible ? 'translate-x-0' : 'translate-x-[-100px]'}`}
|
||||
>
|
||||
<div className="mx-1 flex items-center">
|
||||
<AnimatePresence initial={false}>
|
||||
{!navVisible && (
|
||||
<motion.div
|
||||
className={`flex items-center gap-2`}
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'auto', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
key="header-buttons"
|
||||
>
|
||||
<OpenSidebar setNavVisible={setNavVisible} className="max-md:hidden" />
|
||||
<HeaderNewChat />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={navVisible ? 'flex items-center gap-2' : 'ml-2 flex items-center gap-2'}>
|
||||
<ModelSelector startupConfig={startupConfig} />
|
||||
{interfaceConfig.presets === true && interfaceConfig.modelSelect && <PresetsMenu />}
|
||||
{hasAccessToBookmarks === true && <BookmarkMenu />}
|
||||
|
|
|
|||
|
|
@ -260,37 +260,50 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
<FileFormChat conversation={conversation} />
|
||||
{endpoint && (
|
||||
<div className={cn('flex', isRTL ? 'flex-row-reverse' : 'flex-row')}>
|
||||
<TextareaAutosize
|
||||
{...registerProps}
|
||||
ref={(e) => {
|
||||
ref(e);
|
||||
(textAreaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = e;
|
||||
}}
|
||||
disabled={disableInputs || isNotAppendable}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
id={mainTextareaId}
|
||||
tabIndex={0}
|
||||
data-testid="text-input"
|
||||
rows={1}
|
||||
onFocus={() => {
|
||||
handleFocusOrClick();
|
||||
setIsTextAreaFocused(true);
|
||||
}}
|
||||
onBlur={setIsTextAreaFocused.bind(null, false)}
|
||||
aria-label={localize('com_ui_message_input')}
|
||||
onClick={handleFocusOrClick}
|
||||
style={{ height: 44, overflowY: 'auto' }}
|
||||
className={cn(
|
||||
baseClasses,
|
||||
removeFocusRings,
|
||||
'transition-[max-height] duration-200 disabled:cursor-not-allowed',
|
||||
<div className="relative flex-1">
|
||||
<TextareaAutosize
|
||||
{...registerProps}
|
||||
ref={(e) => {
|
||||
ref(e);
|
||||
(textAreaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current =
|
||||
e;
|
||||
}}
|
||||
disabled={disableInputs || isNotAppendable}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
id={mainTextareaId}
|
||||
tabIndex={0}
|
||||
data-testid="text-input"
|
||||
rows={1}
|
||||
onFocus={() => {
|
||||
handleFocusOrClick();
|
||||
setIsTextAreaFocused(true);
|
||||
}}
|
||||
onBlur={setIsTextAreaFocused.bind(null, false)}
|
||||
aria-label={localize('com_ui_message_input')}
|
||||
onClick={handleFocusOrClick}
|
||||
style={{ height: 44, overflowY: 'auto' }}
|
||||
className={cn(
|
||||
baseClasses,
|
||||
removeFocusRings,
|
||||
'scrollbar-hover transition-[max-height] duration-200 disabled:cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
{isCollapsed && (
|
||||
<div
|
||||
className="pointer-events-none absolute bottom-0 left-0 right-0 h-10 transition-all duration-200"
|
||||
style={{
|
||||
backdropFilter: 'blur(2px)',
|
||||
WebkitMaskImage: 'linear-gradient(to top, black 15%, transparent 75%)',
|
||||
maskImage: 'linear-gradient(to top, black 15%, transparent 75%)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col items-start justify-start pt-1.5">
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start pr-2.5 pt-1.5">
|
||||
<CollapseChat
|
||||
isCollapsed={isCollapsed}
|
||||
isScrollable={isMoreThanThreeRows}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useState, useId, useRef, memo, useCallback, useMemo } from 'react';
|
|||
import * as Menu from '@ariakit/react/menu';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { DropdownPopup, Spinner, useToastContext } from '@librechat/client';
|
||||
import { Ellipsis, Share2, Copy, Archive, Pen, Trash } from 'lucide-react';
|
||||
import { Ellipsis, Share2, CopyPlus, Archive, Pen, Trash } from 'lucide-react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import {
|
||||
useDuplicateConversationMutation,
|
||||
|
|
@ -151,7 +151,7 @@ function ConvoOptions({
|
|||
icon: isDuplicateLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<Copy className="icon-sm mr-2 text-text-primary" />
|
||||
<CopyPlus className="icon-sm mr-2 text-text-primary" />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@ export function DeleteConversationDialog({
|
|||
}
|
||||
setMenuOpen?.(false);
|
||||
retainView();
|
||||
showToast({
|
||||
message: localize('com_ui_convo_delete_success'),
|
||||
severity: NotificationSeverity.SUCCESS,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ import { clearMessagesCache } from '~/utils';
|
|||
import store from '~/store';
|
||||
|
||||
export default function MobileNav({
|
||||
navVisible,
|
||||
setNavVisible,
|
||||
}: {
|
||||
navVisible: boolean;
|
||||
setNavVisible: Dispatch<SetStateAction<boolean>>;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -25,7 +27,7 @@ export default function MobileNav({
|
|||
type="button"
|
||||
data-testid="mobile-header-new-chat-button"
|
||||
aria-label={localize('com_nav_open_sidebar')}
|
||||
className="m-1 inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover"
|
||||
className={`m-1 inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover ${navVisible ? 'invisible' : ''}`}
|
||||
onClick={() =>
|
||||
setNavVisible((prev) => {
|
||||
localStorage.setItem('navVisible', JSON.stringify(!prev));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useMediaQuery } from '@librechat/client';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||
|
|
@ -190,22 +191,21 @@ const Nav = memo(
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-testid="nav"
|
||||
className={cn(
|
||||
'nav active max-w-[320px] flex-shrink-0 transform overflow-x-hidden bg-surface-primary-alt transition-all duration-200 ease-in-out',
|
||||
'md:max-w-[260px]',
|
||||
)}
|
||||
style={{
|
||||
width: navVisible ? navWidth : '0px',
|
||||
transform: navVisible ? 'translateX(0)' : 'translateX(-100%)',
|
||||
}}
|
||||
>
|
||||
<div className="h-full w-[320px] md:w-[260px]">
|
||||
<div className="flex h-full flex-col">
|
||||
<div
|
||||
className={`flex h-full flex-col transition-opacity duration-200 ease-in-out ${navVisible ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
<AnimatePresence initial={false}>
|
||||
{navVisible && (
|
||||
<motion.div
|
||||
data-testid="nav"
|
||||
className={cn(
|
||||
'nav active max-w-[320px] flex-shrink-0 overflow-x-hidden bg-surface-primary-alt',
|
||||
'md:max-w-[260px]',
|
||||
)}
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: navWidth }}
|
||||
exit={{ width: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
key="nav"
|
||||
>
|
||||
<div className="h-full w-[320px] md:w-[260px]">
|
||||
<div className="flex h-full flex-col">
|
||||
<nav
|
||||
id="chat-history-nav"
|
||||
|
|
@ -235,9 +235,9 @@ const Nav = memo(
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{isSmallScreen && <NavMask navVisible={navVisible} toggleNavVisible={toggleNavVisible} />}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ export default function ArchivedChatsTable({
|
|||
onSuccess: async () => {
|
||||
setIsDeleteOpen(false);
|
||||
await refetch();
|
||||
showToast({
|
||||
message: localize('com_ui_convo_delete_success'),
|
||||
severity: NotificationSeverity.SUCCESS,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
showToast({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CopyIcon } from 'lucide-react';
|
||||
import { CopyPlus } from 'lucide-react';
|
||||
import { useToastContext, Button } from '@librechat/client';
|
||||
import { useDuplicateAgentMutation } from '~/data-provider';
|
||||
import { isEphemeralAgent } from '~/common';
|
||||
|
|
@ -41,7 +41,7 @@ export default function DuplicateAgent({ agent_id }: { agent_id: string }) {
|
|||
onClick={handleDuplicate}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2 text-primary">
|
||||
<CopyIcon className="size-4" />
|
||||
<CopyPlus className="size-4" />
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -147,11 +147,13 @@ const SidePanelGroup = memo(
|
|||
{artifacts != null && isSmallScreen && (
|
||||
<div className="fixed inset-0 z-[100]">{artifacts}</div>
|
||||
)}
|
||||
<button
|
||||
aria-label="Close right side panel"
|
||||
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
||||
onClick={handleClosePanel}
|
||||
/>
|
||||
{!hideSidePanel && interfaceConfig.sidePanel === true && (
|
||||
<button
|
||||
aria-label="Close right side panel"
|
||||
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
||||
onClick={handleClosePanel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -799,6 +799,7 @@
|
|||
"com_ui_continue_oauth": "Continue with OAuth",
|
||||
"com_ui_controls": "Controls",
|
||||
"com_ui_convo_delete_error": "Failed to delete conversation",
|
||||
"com_ui_convo_delete_success": "Conversation successfully deleted",
|
||||
"com_ui_copied": "Copied!",
|
||||
"com_ui_copied_to_clipboard": "Copied to clipboard",
|
||||
"com_ui_copy_code": "Copy code",
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export default function Root() {
|
|||
<div className="relative z-0 flex h-full w-full overflow-hidden">
|
||||
<Nav navVisible={navVisible} setNavVisible={setNavVisible} />
|
||||
<div className="relative flex h-full max-w-full flex-1 flex-col overflow-hidden">
|
||||
<MobileNav setNavVisible={setNavVisible} />
|
||||
<MobileNav navVisible={navVisible} setNavVisible={setNavVisible} />
|
||||
<Outlet context={{ navVisible, setNavVisible } satisfies ContextType} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1487,6 +1487,26 @@ button {
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Show scrollbar only on hover */
|
||||
.scrollbar-hover {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
.scrollbar-hover:hover {
|
||||
scrollbar-color: var(--border-medium) transparent;
|
||||
}
|
||||
|
||||
.scrollbar-hover::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
transition: background-color 0.3s ease 0.5s;
|
||||
}
|
||||
|
||||
.scrollbar-hover:hover::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border-medium);
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -50,8 +50,9 @@ import { parseTextNative, parseText } from './text';
|
|||
import fs, { ReadStream } from 'fs';
|
||||
import axios from 'axios';
|
||||
import FormData from 'form-data';
|
||||
import { generateShortLivedToken } from '../crypto/jwt';
|
||||
import { readFileAsString } from '../utils';
|
||||
import type { ServerRequest } from '~/types';
|
||||
import { generateShortLivedToken } from '~/crypto/jwt';
|
||||
import { readFileAsString } from '~/utils';
|
||||
|
||||
const mockedFs = fs as jest.Mocked<typeof fs>;
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
|
@ -77,7 +78,7 @@ describe('text', () => {
|
|||
|
||||
const mockReq = {
|
||||
user: { id: 'user123' },
|
||||
};
|
||||
} as ServerRequest;
|
||||
|
||||
const mockFileId = 'file123';
|
||||
|
||||
|
|
@ -228,6 +229,13 @@ describe('text', () => {
|
|||
file_id: mockFileId,
|
||||
});
|
||||
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'http://rag-api.test/text',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
timeout: 300000,
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
text: '',
|
||||
bytes: 0,
|
||||
|
|
@ -278,7 +286,7 @@ describe('text', () => {
|
|||
});
|
||||
|
||||
const result = await parseText({
|
||||
req: { user: undefined },
|
||||
req: { user: undefined } as ServerRequest,
|
||||
file: mockFile,
|
||||
file_id: mockFileId,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export async function parseText({
|
|||
accept: 'application/json',
|
||||
...formHeaders,
|
||||
},
|
||||
timeout: 30000,
|
||||
timeout: 300000,
|
||||
});
|
||||
|
||||
const responseData = response.data;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue