Compare commits
1 Commits
chart-1.9.
...
feat/bette
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ec685e44 |
@@ -1,4 +1,4 @@
|
||||
# v0.8.1-rc2
|
||||
# v0.8.1-rc1
|
||||
|
||||
# Base node image
|
||||
FROM node:20-alpine AS node
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Dockerfile.multi
|
||||
# v0.8.1-rc2
|
||||
# v0.8.1-rc1
|
||||
|
||||
# Base for all builds
|
||||
FROM node:20-alpine AS base-min
|
||||
|
||||
@@ -1213,8 +1213,8 @@ class BaseClient {
|
||||
this.options.req,
|
||||
attachments,
|
||||
{
|
||||
provider: this.options.agent?.provider ?? this.options.endpoint,
|
||||
endpoint: this.options.agent?.endpoint ?? this.options.endpoint,
|
||||
provider: this.options.agent?.provider,
|
||||
endpoint: this.options.agent?.endpoint,
|
||||
useResponsesApi: this.options.agent?.model_parameters?.useResponsesApi,
|
||||
},
|
||||
getStrategyFunctions,
|
||||
@@ -1231,8 +1231,8 @@ class BaseClient {
|
||||
this.options.req,
|
||||
attachments,
|
||||
{
|
||||
provider: this.options.agent?.provider ?? this.options.endpoint,
|
||||
endpoint: this.options.agent?.endpoint ?? this.options.endpoint,
|
||||
provider: this.options.agent?.provider,
|
||||
endpoint: this.options.agent?.endpoint,
|
||||
},
|
||||
getStrategyFunctions,
|
||||
);
|
||||
@@ -1246,8 +1246,8 @@ class BaseClient {
|
||||
this.options.req,
|
||||
attachments,
|
||||
{
|
||||
provider: this.options.agent?.provider ?? this.options.endpoint,
|
||||
endpoint: this.options.agent?.endpoint ?? this.options.endpoint,
|
||||
provider: this.options.agent?.provider,
|
||||
endpoint: this.options.agent?.endpoint,
|
||||
},
|
||||
getStrategyFunctions,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/backend",
|
||||
"version": "v0.8.1-rc2",
|
||||
"version": "v0.8.1-rc1",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** v0.8.1-rc2 */
|
||||
/** v0.8.1-rc1 */
|
||||
module.exports = {
|
||||
roots: ['<rootDir>/src'],
|
||||
testEnvironment: 'jsdom',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.8.1-rc2",
|
||||
"version": "v0.8.1-rc1",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -72,7 +72,7 @@ const BookmarkForm = ({
|
||||
}
|
||||
const allTags =
|
||||
queryClient.getQueryData<TConversationTag[]>([QueryKeys.conversationTags]) ?? [];
|
||||
if (allTags.some((tag) => tag.tag === data.tag && tag.tag !== bookmark?.tag)) {
|
||||
if (allTags.some((tag) => tag.tag === data.tag)) {
|
||||
showToast({
|
||||
message: localize('com_ui_bookmarks_create_exists'),
|
||||
status: 'warning',
|
||||
|
||||
@@ -1,499 +0,0 @@
|
||||
import React, { createRef } from 'react';
|
||||
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import BookmarkForm from '../BookmarkForm';
|
||||
import type { TConversationTag } from 'librechat-data-provider';
|
||||
|
||||
const mockMutate = jest.fn();
|
||||
const mockShowToast = jest.fn();
|
||||
const mockGetQueryData = jest.fn();
|
||||
const mockSetOpen = jest.fn();
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useLocalize: () => (key: string, params?: Record<string, unknown>) => {
|
||||
const translations: Record<string, string> = {
|
||||
com_ui_bookmarks_title: 'Title',
|
||||
com_ui_bookmarks_description: 'Description',
|
||||
com_ui_bookmarks_edit: 'Edit Bookmark',
|
||||
com_ui_bookmarks_new: 'New Bookmark',
|
||||
com_ui_bookmarks_create_exists: 'This bookmark already exists',
|
||||
com_ui_bookmarks_add_to_conversation: 'Add to current conversation',
|
||||
com_ui_bookmarks_tag_exists: 'A bookmark with this title already exists',
|
||||
com_ui_field_required: 'This field is required',
|
||||
com_ui_field_max_length: `${params?.field || 'Field'} must be less than ${params?.length || 0} characters`,
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/client', () => {
|
||||
const ActualReact = jest.requireActual<typeof import('react')>('react');
|
||||
return {
|
||||
Checkbox: ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
checked: boolean;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
value: string;
|
||||
}) =>
|
||||
ActualReact.createElement('input', {
|
||||
type: 'checkbox',
|
||||
checked,
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => onCheckedChange(e.target.checked),
|
||||
value,
|
||||
...props,
|
||||
}),
|
||||
Label: ({ children, ...props }: { children: React.ReactNode }) =>
|
||||
ActualReact.createElement('label', props, children),
|
||||
TextareaAutosize: ActualReact.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
>((props, ref) => ActualReact.createElement('textarea', { ref, ...props })),
|
||||
Input: ActualReact.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||
(props, ref) => ActualReact.createElement('input', { ref, ...props }),
|
||||
),
|
||||
useToastContext: () => ({
|
||||
showToast: mockShowToast,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('~/Providers/BookmarkContext', () => ({
|
||||
useBookmarkContext: () => ({
|
||||
bookmarks: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQueryClient: () => ({
|
||||
getQueryData: mockGetQueryData,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/utils', () => ({
|
||||
cn: (...classes: (string | undefined | null | boolean)[]) => classes.filter(Boolean).join(' '),
|
||||
logger: {
|
||||
log: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const createMockBookmark = (overrides?: Partial<TConversationTag>): TConversationTag => ({
|
||||
_id: 'bookmark-1',
|
||||
user: 'user-1',
|
||||
tag: 'Test Bookmark',
|
||||
description: 'Test description',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
count: 1,
|
||||
position: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockMutation = (isLoading = false) => ({
|
||||
mutate: mockMutate,
|
||||
isLoading,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
data: undefined,
|
||||
error: null,
|
||||
reset: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
status: 'idle' as const,
|
||||
variables: undefined,
|
||||
context: undefined,
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
isPaused: false,
|
||||
isIdle: true,
|
||||
submittedAt: 0,
|
||||
});
|
||||
|
||||
describe('BookmarkForm - Bookmark Editing', () => {
|
||||
const formRef = createRef<HTMLFormElement>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetQueryData.mockReturnValue([]);
|
||||
});
|
||||
|
||||
describe('Editing only the description (tag unchanged)', () => {
|
||||
it('should allow submitting when only the description is changed', async () => {
|
||||
const existingBookmark = createMockBookmark({
|
||||
tag: 'My Bookmark',
|
||||
description: 'Original description',
|
||||
});
|
||||
|
||||
mockGetQueryData.mockReturnValue([existingBookmark]);
|
||||
|
||||
render(
|
||||
<BookmarkForm
|
||||
bookmark={existingBookmark}
|
||||
mutation={
|
||||
createMockMutation() as ReturnType<
|
||||
typeof import('~/data-provider').useConversationTagMutation
|
||||
>
|
||||
}
|
||||
setOpen={mockSetOpen}
|
||||
formRef={formRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
const descriptionInput = screen.getByRole('textbox', { name: /description/i });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(descriptionInput, { target: { value: 'Updated description' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.submit(formRef.current!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tag: 'My Bookmark',
|
||||
description: 'Updated description',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(mockShowToast).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should not submit when both tag and description are unchanged', async () => {
|
||||
const existingBookmark = createMockBookmark({
|
||||
tag: 'My Bookmark',
|
||||
description: 'Same description',
|
||||
});
|
||||
|
||||
mockGetQueryData.mockReturnValue([existingBookmark]);
|
||||
|
||||
render(
|
||||
<BookmarkForm
|
||||
bookmark={existingBookmark}
|
||||
mutation={
|
||||
createMockMutation() as ReturnType<
|
||||
typeof import('~/data-provider').useConversationTagMutation
|
||||
>
|
||||
}
|
||||
setOpen={mockSetOpen}
|
||||
formRef={formRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.submit(formRef.current!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutate).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Renaming a tag to an existing tag (should show error)', () => {
|
||||
it('should show error toast when renaming to an existing tag name (via allTags)', async () => {
|
||||
const existingBookmark = createMockBookmark({
|
||||
tag: 'Original Tag',
|
||||
description: 'Description',
|
||||
});
|
||||
|
||||
const otherBookmark = createMockBookmark({
|
||||
_id: 'bookmark-2',
|
||||
tag: 'Existing Tag',
|
||||
description: 'Other description',
|
||||
});
|
||||
|
||||
mockGetQueryData.mockReturnValue([existingBookmark, otherBookmark]);
|
||||
|
||||
render(
|
||||
<BookmarkForm
|
||||
bookmark={existingBookmark}
|
||||
mutation={
|
||||
createMockMutation() as ReturnType<
|
||||
typeof import('~/data-provider').useConversationTagMutation
|
||||
>
|
||||
}
|
||||
setOpen={mockSetOpen}
|
||||
formRef={formRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tagInput = screen.getByLabelText('Edit Bookmark');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(tagInput, { target: { value: 'Existing Tag' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.submit(formRef.current!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith({
|
||||
message: 'This bookmark already exists',
|
||||
status: 'warning',
|
||||
});
|
||||
});
|
||||
expect(mockMutate).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show error toast when renaming to an existing tag name (via tags prop)', async () => {
|
||||
const existingBookmark = createMockBookmark({
|
||||
tag: 'Original Tag',
|
||||
description: 'Description',
|
||||
});
|
||||
|
||||
mockGetQueryData.mockReturnValue([existingBookmark]);
|
||||
|
||||
render(
|
||||
<BookmarkForm
|
||||
tags={['Existing Tag', 'Another Tag']}
|
||||
bookmark={existingBookmark}
|
||||
mutation={
|
||||
createMockMutation() as ReturnType<
|
||||
typeof import('~/data-provider').useConversationTagMutation
|
||||
>
|
||||
}
|
||||
setOpen={mockSetOpen}
|
||||
formRef={formRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tagInput = screen.getByLabelText('Edit Bookmark');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(tagInput, { target: { value: 'Existing Tag' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.submit(formRef.current!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith({
|
||||
message: 'This bookmark already exists',
|
||||
status: 'warning',
|
||||
});
|
||||
});
|
||||
expect(mockMutate).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Renaming a tag to a new tag (should succeed)', () => {
|
||||
it('should allow renaming to a completely new tag name', async () => {
|
||||
const existingBookmark = createMockBookmark({
|
||||
tag: 'Original Tag',
|
||||
description: 'Description',
|
||||
});
|
||||
|
||||
mockGetQueryData.mockReturnValue([existingBookmark]);
|
||||
|
||||
render(
|
||||
<BookmarkForm
|
||||
bookmark={existingBookmark}
|
||||
mutation={
|
||||
createMockMutation() as ReturnType<
|
||||
typeof import('~/data-provider').useConversationTagMutation
|
||||
>
|
||||
}
|
||||
setOpen={mockSetOpen}
|
||||
formRef={formRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tagInput = screen.getByLabelText('Edit Bookmark');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(tagInput, { target: { value: 'Brand New Tag' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.submit(formRef.current!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tag: 'Brand New Tag',
|
||||
description: 'Description',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(mockShowToast).not.toHaveBeenCalled();
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should allow keeping the same tag name when editing (not trigger duplicate error)', async () => {
|
||||
const existingBookmark = createMockBookmark({
|
||||
tag: 'My Bookmark',
|
||||
description: 'Original description',
|
||||
});
|
||||
|
||||
mockGetQueryData.mockReturnValue([existingBookmark]);
|
||||
|
||||
render(
|
||||
<BookmarkForm
|
||||
bookmark={existingBookmark}
|
||||
mutation={
|
||||
createMockMutation() as ReturnType<
|
||||
typeof import('~/data-provider').useConversationTagMutation
|
||||
>
|
||||
}
|
||||
setOpen={mockSetOpen}
|
||||
formRef={formRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
const descriptionInput = screen.getByRole('textbox', { name: /description/i });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(descriptionInput, { target: { value: 'New description' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.submit(formRef.current!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tag: 'My Bookmark',
|
||||
description: 'New description',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(mockShowToast).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation interaction between different data sources', () => {
|
||||
it('should check both tags prop and allTags query data for duplicates', async () => {
|
||||
const existingBookmark = createMockBookmark({
|
||||
tag: 'Original Tag',
|
||||
description: 'Description',
|
||||
});
|
||||
|
||||
const queryDataBookmark = createMockBookmark({
|
||||
_id: 'bookmark-query',
|
||||
tag: 'Query Data Tag',
|
||||
});
|
||||
|
||||
mockGetQueryData.mockReturnValue([existingBookmark, queryDataBookmark]);
|
||||
|
||||
render(
|
||||
<BookmarkForm
|
||||
tags={['Props Tag']}
|
||||
bookmark={existingBookmark}
|
||||
mutation={
|
||||
createMockMutation() as ReturnType<
|
||||
typeof import('~/data-provider').useConversationTagMutation
|
||||
>
|
||||
}
|
||||
setOpen={mockSetOpen}
|
||||
formRef={formRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tagInput = screen.getByLabelText('Edit Bookmark');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(tagInput, { target: { value: 'Props Tag' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.submit(formRef.current!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith({
|
||||
message: 'This bookmark already exists',
|
||||
status: 'warning',
|
||||
});
|
||||
});
|
||||
expect(mockMutate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not trigger mutation when mutation is loading', async () => {
|
||||
const existingBookmark = createMockBookmark({
|
||||
tag: 'My Bookmark',
|
||||
description: 'Description',
|
||||
});
|
||||
|
||||
mockGetQueryData.mockReturnValue([existingBookmark]);
|
||||
|
||||
render(
|
||||
<BookmarkForm
|
||||
bookmark={existingBookmark}
|
||||
mutation={
|
||||
createMockMutation(true) as ReturnType<
|
||||
typeof import('~/data-provider').useConversationTagMutation
|
||||
>
|
||||
}
|
||||
setOpen={mockSetOpen}
|
||||
formRef={formRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
const descriptionInput = screen.getByRole('textbox', { name: /description/i });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(descriptionInput, { target: { value: 'Updated description' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.submit(formRef.current!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty allTags gracefully', async () => {
|
||||
const existingBookmark = createMockBookmark({
|
||||
tag: 'My Bookmark',
|
||||
description: 'Description',
|
||||
});
|
||||
|
||||
mockGetQueryData.mockReturnValue(null);
|
||||
|
||||
render(
|
||||
<BookmarkForm
|
||||
bookmark={existingBookmark}
|
||||
mutation={
|
||||
createMockMutation() as ReturnType<
|
||||
typeof import('~/data-provider').useConversationTagMutation
|
||||
>
|
||||
}
|
||||
setOpen={mockSetOpen}
|
||||
formRef={formRef}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tagInput = screen.getByLabelText('Edit Bookmark');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(tagInput, { target: { value: 'New Tag' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.submit(formRef.current!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tag: 'New Tag',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -260,50 +260,37 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||
<FileFormChat conversation={conversation} />
|
||||
{endpoint && (
|
||||
<div className={cn('flex', isRTL ? 'flex-row-reverse' : 'flex-row')}>
|
||||
<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%)',
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
<div className="flex flex-col items-start justify-start pr-2.5 pt-1.5">
|
||||
/>
|
||||
<div className="flex flex-col items-start justify-start pt-1.5">
|
||||
<CollapseChat
|
||||
isCollapsed={isCollapsed}
|
||||
isScrollable={isMoreThanThreeRows}
|
||||
|
||||
@@ -150,7 +150,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||
return (
|
||||
<CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
|
||||
{({ registerChild }) => (
|
||||
<div ref={registerChild} style={style}>
|
||||
<div ref={registerChild} style={style} className="px-1">
|
||||
{rendering}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -133,7 +133,9 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
||||
<div
|
||||
className={cn(
|
||||
'group relative flex h-12 w-full items-center rounded-lg transition-colors duration-200 md:h-9',
|
||||
isActiveConvo ? 'bg-surface-active-alt' : 'hover:bg-surface-active-alt',
|
||||
isActiveConvo
|
||||
? 'bg-surface-active-alt outline outline-2 outline-offset-[-2px]'
|
||||
: 'hover:bg-surface-active-alt',
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={renaming ? -1 : 0}
|
||||
|
||||
@@ -55,11 +55,6 @@ export function DeleteConversationDialog({
|
||||
}
|
||||
setMenuOpen?.(false);
|
||||
retainView();
|
||||
showToast({
|
||||
message: localize('com_ui_convo_delete_success'),
|
||||
severity: NotificationSeverity.SUCCESS,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
|
||||
@@ -93,11 +93,6 @@ 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({
|
||||
|
||||
@@ -799,7 +799,6 @@
|
||||
"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",
|
||||
|
||||
@@ -1487,26 +1487,6 @@ 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%;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// v0.8.1-rc2
|
||||
// v0.8.1-rc1
|
||||
// See .env.test.example for an example of the '.env.test' file.
|
||||
require('dotenv').config({ path: './e2e/.env.test' });
|
||||
|
||||
@@ -15,7 +15,7 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.9.3
|
||||
version: 1.9.2
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
@@ -23,7 +23,7 @@ version: 1.9.3
|
||||
# It is recommended to use it with quotes.
|
||||
|
||||
# renovate: image=ghcr.io/danny-avila/librechat
|
||||
appVersion: "v0.8.1-rc2"
|
||||
appVersion: "v0.8.1-rc1"
|
||||
|
||||
home: https://www.librechat.ai
|
||||
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "LibreChat",
|
||||
"version": "v0.8.1-rc2",
|
||||
"version": "v0.8.1-rc1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "LibreChat",
|
||||
"version": "v0.8.1-rc2",
|
||||
"version": "v0.8.1-rc1",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"api",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"api": {
|
||||
"name": "@librechat/backend",
|
||||
"version": "v0.8.1-rc2",
|
||||
"version": "v0.8.1-rc1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0",
|
||||
@@ -455,7 +455,7 @@
|
||||
},
|
||||
"client": {
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.8.1-rc2",
|
||||
"version": "v0.8.1-rc1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.15",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "LibreChat",
|
||||
"version": "v0.8.1-rc2",
|
||||
"version": "v0.8.1-rc1",
|
||||
"description": "",
|
||||
"workspaces": [
|
||||
"api",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/api",
|
||||
"version": "1.6.0",
|
||||
"version": "1.5.0",
|
||||
"type": "commonjs",
|
||||
"description": "MCP services for LibreChat",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -36,16 +36,30 @@ const OPENID_TOKEN_FIELDS = [
|
||||
|
||||
export function extractOpenIDTokenInfo(user: IUser | null | undefined): OpenIDTokenInfo | null {
|
||||
if (!user) {
|
||||
logger.debug('[extractOpenIDTokenInfo] No user provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug(
|
||||
'[extractOpenIDTokenInfo] User provider:',
|
||||
user.provider,
|
||||
'openidId:',
|
||||
user.openidId,
|
||||
);
|
||||
|
||||
if (user.provider !== 'openid' && !user.openidId) {
|
||||
logger.debug('[extractOpenIDTokenInfo] User not authenticated via OpenID');
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenInfo: OpenIDTokenInfo = {};
|
||||
|
||||
logger.debug(
|
||||
'[extractOpenIDTokenInfo] Checking for federatedTokens in user object:',
|
||||
'federatedTokens' in user,
|
||||
);
|
||||
|
||||
if ('federatedTokens' in user && isFederatedTokens(user.federatedTokens)) {
|
||||
const tokens = user.federatedTokens;
|
||||
logger.debug('[extractOpenIDTokenInfo] Found federatedTokens:', {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/client",
|
||||
"version": "0.4.0",
|
||||
"version": "0.3.2",
|
||||
"description": "React components for LibreChat",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.8.100",
|
||||
"version": "0.8.020",
|
||||
"description": "data services for librechat apps",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
|
||||
@@ -1585,7 +1585,7 @@ export enum TTSProviders {
|
||||
/** Enum for app-wide constants */
|
||||
export enum Constants {
|
||||
/** Key for the app's version. */
|
||||
VERSION = 'v0.8.1-rc2',
|
||||
VERSION = 'v0.8.1-rc1',
|
||||
/** Key for the Custom Config's version (librechat.yaml). */
|
||||
CONFIG_VERSION = '1.3.1',
|
||||
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/data-schemas",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.23",
|
||||
"description": "Mongoose schemas and models for LibreChat",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
|
||||
Reference in New Issue
Block a user