mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🎨 refactor: UI stlye (#4438)
* feat: Refactor ChatForm and StopButton components for improved styling and localization * feat: Refactor AudioRecorder, ChatForm, AttachFile, and SendButton components for improved styling and layout * feat: Add RevokeAllKeys component and update styling for buttons and inputs * feat: Refactor ClearChats component and update ClearConvos functionality for improved clarity and user experience * feat: Remove ClearConvos component and update related imports and functionality in Avatar and DeleteCacheButton components * feat: Rename DeleteCacheButton to DeleteCache and update related imports; enhance confirmation message in localization * feat: Update ChatForm layout for RTL support and improve component structure * feat: Adjust ChatForm layout for improved RTL support and alignment * feat: Refactor Bookmark components to use new UI elements and improve styling * feat: Update FileSearch and ShareAgent components for improved button styling and layout * feat: Update ChatForm and TextareaHeader styles for improved UI consistency * feat: Refactor Nav components for improved styling and layout adjustments * feat: Update button sizes and padding for improved UI consistency across chat components * feat: Remove ClearChatsButton test file as part of code cleanup
This commit is contained in:
parent
20fb7f05ae
commit
8f3de7d11f
38 changed files with 471 additions and 564 deletions
|
|
@ -2,10 +2,9 @@ import React, { useRef, Dispatch, SetStateAction } from 'react';
|
|||
import { TConversationTag, TConversation } from 'librechat-data-provider';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useConversationTagMutation } from '~/data-provider';
|
||||
import { OGDialog, Button, Spinner } from '~/components';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { OGDialog } from '~/components/ui';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import BookmarkForm from './BookmarkForm';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { logger } from '~/utils';
|
||||
|
|
@ -75,6 +74,7 @@ const BookmarkEditDialog = ({
|
|||
<OGDialogTemplate
|
||||
title="Bookmark"
|
||||
showCloseButton={false}
|
||||
className="w-11/12 md:max-w-2xl"
|
||||
main={
|
||||
<BookmarkForm
|
||||
tags={tags}
|
||||
|
|
@ -86,14 +86,14 @@ const BookmarkEditDialog = ({
|
|||
/>
|
||||
}
|
||||
buttons={
|
||||
<button
|
||||
<Button
|
||||
variant="submit"
|
||||
type="submit"
|
||||
disabled={mutation.isLoading}
|
||||
onClick={handleSubmitForm}
|
||||
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
||||
>
|
||||
{mutation.isLoading ? <Spinner /> : localize('com_ui_save')}
|
||||
</button>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialog>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type {
|
|||
TConversationTagRequest,
|
||||
} from 'librechat-data-provider';
|
||||
import { cn, removeFocusOutlines, defaultTextProps, logger } from '~/utils';
|
||||
import { Checkbox, Label, TextareaAutosize } from '~/components/ui';
|
||||
import { Checkbox, Label, TextareaAutosize, Input } from '~/components';
|
||||
import { useBookmarkContext } from '~/Providers/BookmarkContext';
|
||||
import { useConversationTagMutation } from '~/data-provider';
|
||||
import { useToastContext } from '~/Providers';
|
||||
|
|
@ -100,7 +100,7 @@ const BookmarkForm = ({
|
|||
<Label htmlFor="bookmark-tag" className="text-left text-sm font-medium">
|
||||
{localize('com_ui_bookmarks_title')}
|
||||
</Label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
id="bookmark-tag"
|
||||
aria-label="Bookmark"
|
||||
|
|
@ -119,17 +119,12 @@ const BookmarkForm = ({
|
|||
},
|
||||
})}
|
||||
aria-invalid={!!errors.tag}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
placeholder=" "
|
||||
placeholder="Bookmark"
|
||||
/>
|
||||
{errors.tag && <span className="text-sm text-red-500">{errors.tag.message}</span>}
|
||||
</div>
|
||||
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<div className="mt-4 grid w-full items-center gap-2">
|
||||
<Label htmlFor="bookmark-description" className="text-left text-sm font-medium">
|
||||
{localize('com_ui_bookmarks_description')}
|
||||
</Label>
|
||||
|
|
@ -143,8 +138,7 @@ const BookmarkForm = ({
|
|||
id="bookmark-description"
|
||||
disabled={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2',
|
||||
'flex h-10 max-h-[250px] min-h-[100px] w-full resize-none rounded-lg border border-input bg-transparent px-3 py-2 text-sm ring-offset-background focus-visible:outline-none',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -80,10 +80,8 @@ export default function AudioRecorder({
|
|||
onClick={isListening ? handleStopRecording : handleStartRecording}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'absolute flex h-[30px] w-[30px] items-center justify-center rounded-lg p-0.5 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700',
|
||||
isRTL
|
||||
? 'bottom-1.5 left-4 md:bottom-3 md:left-12'
|
||||
: 'bottom-1.5 right-12 md:bottom-3 md:right-12',
|
||||
'absolute flex size-[35px] items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover',
|
||||
isRTL ? 'bottom-2 left-2' : 'bottom-2 right-2',
|
||||
)}
|
||||
description={localize('com_ui_use_micrphone')}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
/>
|
||||
)}
|
||||
<PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} />
|
||||
<div className="bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border dark:border-gray-600 dark:text-white [&:has(textarea:focus)]:border-gray-300 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)] dark:[&:has(textarea:focus)]:border-gray-500">
|
||||
<div className="transitional-all relative flex w-full flex-grow flex-col overflow-hidden rounded-3xl bg-surface-tertiary text-text-primary duration-200">
|
||||
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||
<FileFormWrapper disableInputs={disableInputs}>
|
||||
{endpoint && (
|
||||
|
|
@ -181,7 +181,7 @@ const ChatForm = ({ index = 0 }) => {
|
|||
endpointSupportsFiles && !isUploadDisabled
|
||||
? 'pl-10 md:pl-[55px]'
|
||||
: 'pl-3 md:pl-4',
|
||||
'm-0 w-full resize-none border-0 bg-transparent py-[10px] placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 ',
|
||||
'md:py-3.5- m-0 w-full resize-none bg-surface-tertiary py-[13px] placeholder-black/50 dark:placeholder-white/50 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]',
|
||||
SpeechToText && !isRTL ? 'pr-20 md:pr-[85px]' : 'pr-10 md:pr-12',
|
||||
'max-h-[65vh] md:max-h-[75vh]',
|
||||
removeFocusRings,
|
||||
|
|
@ -189,22 +189,6 @@ const ChatForm = ({ index = 0 }) => {
|
|||
/>
|
||||
)}
|
||||
</FileFormWrapper>
|
||||
{(isSubmitting || isSubmittingAdded) && (showStopButton || showStopAdded) ? (
|
||||
<StopButton
|
||||
stop={handleStopGenerating}
|
||||
setShowStopButton={setShowStopButton}
|
||||
isRTL={isRTL}
|
||||
/>
|
||||
) : (
|
||||
endpoint && (
|
||||
<SendButton
|
||||
ref={submitButtonRef}
|
||||
control={methods.control}
|
||||
isRTL={isRTL}
|
||||
disabled={!!(filesLoading || isSubmitting || disableInputs)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{SpeechToText && (
|
||||
<AudioRecorder
|
||||
disabled={!!disableInputs}
|
||||
|
|
@ -216,6 +200,25 @@ const ChatForm = ({ index = 0 }) => {
|
|||
)}
|
||||
{TextToSpeech && automaticPlayback && <StreamAudio index={index} />}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'mb-[5px] ml-[8px] flex flex-col items-end justify-end',
|
||||
isRTL && 'order-first mr-[8px]',
|
||||
)}
|
||||
style={{ alignSelf: 'flex-end' }}
|
||||
>
|
||||
{(isSubmitting || isSubmittingAdded) && (showStopButton || showStopAdded) ? (
|
||||
<StopButton stop={handleStopGenerating} setShowStopButton={setShowStopButton} />
|
||||
) : (
|
||||
endpoint && (
|
||||
<SendButton
|
||||
ref={submitButtonRef}
|
||||
control={methods.control}
|
||||
disabled={!!(filesLoading || isSubmitting || disableInputs)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -17,21 +17,15 @@ const AttachFile = ({
|
|||
const isUploadDisabled = disabled ?? false;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute',
|
||||
isRTL
|
||||
? 'bottom-2 right-14 md:bottom-3.5 md:right-3'
|
||||
: 'bottom-2 left-2 md:bottom-3.5 md:left-4',
|
||||
)}
|
||||
>
|
||||
<FileUpload handleFileChange={handleFileChange} className="flex">
|
||||
<TooltipAnchor
|
||||
id="audio-recorder"
|
||||
disabled={isUploadDisabled}
|
||||
aria-label={localize('com_sidepanel_attach_files')}
|
||||
className="btn relative text-black focus:outline-none focus:ring-2 focus:ring-border-xheavy focus:ring-opacity-50 dark:text-white"
|
||||
style={{ padding: 0 }}
|
||||
disabled={isUploadDisabled}
|
||||
className={cn(
|
||||
'absolute flex size-[35px] items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover',
|
||||
isRTL ? 'bottom-2 right-2' : 'bottom-2 left-2',
|
||||
)}
|
||||
description={localize('com_sidepanel_attach_files')}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
|
|
@ -39,7 +33,6 @@ const AttachFile = ({
|
|||
</div>
|
||||
</TooltipAnchor>
|
||||
</FileUpload>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,19 +13,17 @@ import AttachFile from './AttachFile';
|
|||
import FileRow from './FileRow';
|
||||
import store from '~/store';
|
||||
|
||||
function FileFormWrapper({ children, disableInputs } : {
|
||||
function FileFormWrapper({
|
||||
children,
|
||||
disableInputs,
|
||||
}: {
|
||||
disableInputs: boolean;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const { handleFileChange, abortUpload } = useFileHandling();
|
||||
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
|
||||
|
||||
const {
|
||||
files,
|
||||
setFiles,
|
||||
conversation,
|
||||
setFilesLoading,
|
||||
} = useChatContext();
|
||||
const { files, setFiles, conversation, setFilesLoading } = useChatContext();
|
||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
|
|
@ -33,11 +31,14 @@ function FileFormWrapper({ children, disableInputs } : {
|
|||
const isRTL = chatDirection === 'rtl';
|
||||
|
||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
||||
const endpointFileConfig = fileConfig.endpoints[_endpoint ?? ''] as EndpointFileConfig | undefined;
|
||||
const endpointFileConfig = fileConfig.endpoints[_endpoint ?? ''] as
|
||||
| EndpointFileConfig
|
||||
| undefined;
|
||||
const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? _endpoint ?? ''] ?? false;
|
||||
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
|
||||
|
||||
return (<>
|
||||
return (
|
||||
<>
|
||||
<FileRow
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
|
|
@ -45,18 +46,15 @@ function FileFormWrapper({ children, disableInputs } : {
|
|||
setFilesLoading={setFilesLoading}
|
||||
isRTL={isRTL}
|
||||
Wrapper={({ children }) => (
|
||||
<div className="mx-2 mt-2 flex flex-wrap gap-2 px-2.5 md:pl-0 md:pr-4">
|
||||
{children}
|
||||
</div>
|
||||
<div className="mx-2 mt-2 flex flex-wrap gap-2 px-2.5 md:pl-0 md:pr-4">{children}</div>
|
||||
)}
|
||||
/>
|
||||
{children}
|
||||
{endpointSupportsFiles && !isUploadDisabled && <AttachFile
|
||||
isRTL={isRTL}
|
||||
disabled={disableInputs}
|
||||
handleFileChange={handleFileChange}
|
||||
/>}
|
||||
</>);
|
||||
{endpointSupportsFiles && !isUploadDisabled && (
|
||||
<AttachFile isRTL={isRTL} disabled={disableInputs} handleFileChange={handleFileChange} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(FileFormWrapper);
|
||||
|
|
@ -9,12 +9,10 @@ import { cn } from '~/utils';
|
|||
type SendButtonProps = {
|
||||
disabled: boolean;
|
||||
control: Control<{ text: string }>;
|
||||
isRTL: boolean;
|
||||
};
|
||||
|
||||
const SubmitButton = React.memo(
|
||||
forwardRef(
|
||||
(props: { disabled: boolean; isRTL: boolean }, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
forwardRef((props: { disabled: boolean }, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<TooltipAnchor
|
||||
|
|
@ -26,10 +24,7 @@ const SubmitButton = React.memo(
|
|||
id="send-button"
|
||||
disabled={props.disabled}
|
||||
className={cn(
|
||||
'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:cursor-not-allowed disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
|
||||
props.isRTL
|
||||
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
|
||||
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',
|
||||
'rounded-full bg-text-primary p-2 text-text-primary outline-offset-4 transition-all duration-200 disabled:cursor-not-allowed disabled:text-text-secondary disabled:opacity-10',
|
||||
)}
|
||||
data-testid="send-button"
|
||||
type="submit"
|
||||
|
|
@ -41,14 +36,13 @@ const SubmitButton = React.memo(
|
|||
}
|
||||
></TooltipAnchor>
|
||||
);
|
||||
},
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const SendButton = React.memo(
|
||||
forwardRef((props: SendButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
const data = useWatch({ control: props.control });
|
||||
return <SubmitButton ref={ref} disabled={props.disabled || !data.text} isRTL={props.isRTL} />;
|
||||
return <SubmitButton ref={ref} disabled={props.disabled || !data.text} />;
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,37 @@
|
|||
import { TooltipAnchor } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function StopButton({ stop, setShowStopButton, isRTL }) {
|
||||
export default function StopButton({ stop, setShowStopButton }) {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute',
|
||||
isRTL ? 'bottom-3 left-2 md:bottom-4 md:left-4' : 'bottom-3 right-2 md:bottom-4 md:right-4',
|
||||
)}
|
||||
>
|
||||
<TooltipAnchor
|
||||
description={localize('com_nav_stop_generating')}
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
className="border-gizmo-gray-900 rounded-full border-2 p-1 dark:border-gray-200"
|
||||
aria-label="Stop generating"
|
||||
className={cn(
|
||||
'rounded-full bg-text-primary p-1.5 text-text-primary outline-offset-4 transition-all duration-200 disabled:cursor-not-allowed disabled:text-text-secondary disabled:opacity-10',
|
||||
)}
|
||||
aria-label={localize('com_nav_stop_generating')}
|
||||
onClick={(e) => {
|
||||
setShowStopButton(false);
|
||||
stop(e);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
className="text-gizmo-gray-900 h-2 w-2 dark:text-gray-200"
|
||||
height="16"
|
||||
width="16"
|
||||
className="icon-lg text-surface-primary"
|
||||
>
|
||||
<path
|
||||
d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2z"
|
||||
strokeWidth="0"
|
||||
></path>
|
||||
<rect x="7" y="7" width="10" height="10" rx="1.25" fill="currentColor"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
></TooltipAnchor>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default function TextareaHeader({
|
|||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="divide-token-border-light m-1.5 flex flex-col divide-y overflow-hidden rounded-b-lg rounded-t-2xl bg-surface-primary-contrast">
|
||||
<div className="divide-token-border-light m-1.5 flex flex-col divide-y overflow-hidden rounded-b-lg rounded-t-2xl bg-surface-secondary-alt">
|
||||
<AddedConvo addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const InputWithLabel: FC<InputWithLabelProps> = forwardRef((props, ref) => {
|
|||
)}
|
||||
<br />
|
||||
</div>
|
||||
<div className="h-1" />
|
||||
<Input
|
||||
id={id}
|
||||
data-testid={`input-${id}`}
|
||||
|
|
@ -36,12 +37,7 @@ const InputWithLabel: FC<InputWithLabelProps> = forwardRef((props, ref) => {
|
|||
onChange={onChange}
|
||||
ref={ref}
|
||||
placeholder={`${localize('com_endpoint_config_value')} ${label}`}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||
removeFocusOutlines,
|
||||
inputClassName,
|
||||
)}
|
||||
className={cn('flex h-10 max-h-10 w-full resize-none px-3 py-2')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { useForm, FormProvider } from 'react-hook-form';
|
|||
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { TDialogProps } from '~/common';
|
||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { RevokeKeysButton } from '~/components/Nav';
|
||||
import { Dialog, Dropdown } from '~/components/ui';
|
||||
import { OGDialog, Dropdown } from '~/components/ui';
|
||||
import { useUserKey, useLocalize } from '~/hooks';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import CustomConfig from './CustomEndpoint';
|
||||
|
|
@ -160,10 +160,11 @@ const SetKeyDialog = ({
|
|||
const config = endpointsConfig?.[endpoint];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||
<OGDialogTemplate
|
||||
title={`${localize('com_endpoint_config_key_for')} ${alternateName[endpoint] ?? endpoint}`}
|
||||
className="w-11/12 max-w-[650px] sm:w-3/4 md:w-3/4 lg:w-3/4"
|
||||
showCancelButton={false}
|
||||
main={
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<small className="text-red-600">
|
||||
|
|
@ -172,7 +173,7 @@ const SetKeyDialog = ({
|
|||
: `${localize('com_endpoint_config_key_encryption')} ${new Date(
|
||||
expiryTime ?? 0,
|
||||
).toLocaleString()}`}
|
||||
</small>{' '}
|
||||
</small>
|
||||
<Dropdown
|
||||
label="Expires "
|
||||
value={expiresAtLabel}
|
||||
|
|
@ -180,6 +181,7 @@ const SetKeyDialog = ({
|
|||
options={expirationOptions.map((option) => option.label)}
|
||||
sizeClasses="w-[185px]"
|
||||
/>
|
||||
<div className="mt-2" />
|
||||
<FormProvider {...methods}>
|
||||
<EndpointComponent
|
||||
userKey={userKey}
|
||||
|
|
@ -197,14 +199,18 @@ const SetKeyDialog = ({
|
|||
}
|
||||
selection={{
|
||||
selectHandler: submit,
|
||||
selectClasses: 'bg-green-500 hover:bg-green-600 dark:hover:bg-green-600 text-white',
|
||||
selectClasses: 'btn btn-primary',
|
||||
selectText: localize('com_ui_submit'),
|
||||
}}
|
||||
leftButtons={
|
||||
<RevokeKeysButton endpoint={endpoint} showText={false} disabled={!expiryTime} />
|
||||
<RevokeKeysButton
|
||||
endpoint={endpoint}
|
||||
disabled={!expiryTime}
|
||||
setDialogOpen={onOpenChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
</OGDialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function AccountSettings() {
|
|||
<Select.Select
|
||||
aria-label={localize('com_nav_account_settings')}
|
||||
data-testid="nav-user"
|
||||
className="duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-xl p-2 text-sm transition-all duration-200 ease-in-out hover:bg-accent"
|
||||
className="mt-text-sm flex h-auto w-full items-center gap-2 rounded-xl p-2 text-sm transition-all duration-200 ease-in-out hover:bg-accent"
|
||||
>
|
||||
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
|
||||
<div className="relative flex">
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
|
|||
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-hover',
|
||||
open ? 'bg-surface-hover' : '',
|
||||
isSmallScreen ? 'h-14 rounded-2xl' : '',
|
||||
isSmallScreen ? 'h-12' : '',
|
||||
)}
|
||||
data-testid="bookmark-menu"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
|
||||
import { useLocalize, useConversation, useConversations } from '~/hooks';
|
||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||
import { ClearChatsButton } from './SettingsTabs';
|
||||
import { Dialog } from '~/components/ui';
|
||||
|
||||
const ClearConvos = ({ open, onOpenChange }) => {
|
||||
const { newConversation } = useConversation();
|
||||
const { refreshConversations } = useConversations();
|
||||
const clearConvosMutation = useClearConversationsMutation();
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const localize = useLocalize();
|
||||
|
||||
// Clear all conversations
|
||||
const clearConvos = () => {
|
||||
if (confirmClear) {
|
||||
clearConvosMutation.mutate(
|
||||
{},
|
||||
{
|
||||
onSuccess: () => {
|
||||
newConversation();
|
||||
refreshConversations();
|
||||
},
|
||||
},
|
||||
);
|
||||
setConfirmClear(false);
|
||||
} else {
|
||||
setConfirmClear(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={localize('com_nav_clear_conversation')}
|
||||
className="w-11/12 max-w-[650px] sm:w-3/4 md:w-3/4 lg:w-3/4"
|
||||
headerClassName="border-none"
|
||||
description={localize('com_nav_clear_conversation_confirm_message')}
|
||||
buttons={
|
||||
<ClearChatsButton
|
||||
showText={false}
|
||||
confirmClear={confirmClear}
|
||||
onClick={clearConvos}
|
||||
className="w-[77px]"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearConvos;
|
||||
|
|
@ -168,22 +168,9 @@ const Nav = ({
|
|||
onMouseLeave={handleMouseLeave}
|
||||
ref={containerRef}
|
||||
>
|
||||
{isSmallScreen == true ? (
|
||||
<div className="pt-3.5">
|
||||
{isSearchEnabled === true && (
|
||||
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
|
||||
)}
|
||||
{hasAccessToBookmarks === true && (
|
||||
<BookmarkNav
|
||||
tags={tags}
|
||||
setTags={setTags}
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<NewChat
|
||||
toggleNav={itemToggleNav}
|
||||
isSmallScreen={isSmallScreen}
|
||||
subHeaders={
|
||||
<>
|
||||
{isSearchEnabled === true && (
|
||||
|
|
@ -197,7 +184,6 @@ const Nav = ({
|
|||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Conversations
|
||||
conversations={conversations}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
|||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { useLocalize, useNewConvo } from '~/hooks';
|
||||
import { NewChatIcon } from '~/components/svg';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const NewChatButtonIcon = ({ conversation }: { conversation: TConversation | null }) => {
|
||||
|
|
@ -57,10 +58,12 @@ export default function NewChat({
|
|||
index = 0,
|
||||
toggleNav,
|
||||
subHeaders,
|
||||
isSmallScreen,
|
||||
}: {
|
||||
index?: number;
|
||||
toggleNav: () => void;
|
||||
subHeaders?: React.ReactNode;
|
||||
isSmallScreen: boolean;
|
||||
}) {
|
||||
/** Note: this component needs an explicit index passed if using more than one */
|
||||
const { newConversation: newConvo } = useNewConvo(index);
|
||||
|
|
@ -86,7 +89,10 @@ export default function NewChat({
|
|||
tabIndex={0}
|
||||
data-testid="nav-new-chat"
|
||||
onClick={clickHandler}
|
||||
className="group flex h-10 items-center gap-2 rounded-lg px-2 font-medium transition-colors duration-200 hover:bg-surface-hover"
|
||||
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} />
|
||||
|
|
|
|||
|
|
@ -76,11 +76,11 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
<div className={cn('fixed inset-0 flex w-screen items-center justify-center p-4')}>
|
||||
<DialogPanel
|
||||
className={cn(
|
||||
'min-h-[600px] overflow-hidden rounded-xl rounded-b-lg bg-background pb-6 shadow-2xl backdrop-blur-2xl animate-in sm:rounded-lg md:min-h-[373px] md:w-[680px]',
|
||||
'min-h-[600px] overflow-hidden rounded-xl rounded-b-lg bg-background pb-6 shadow-2xl backdrop-blur-2xl animate-in sm:rounded-2xl md:min-h-[373px] md:w-[680px]',
|
||||
)}
|
||||
>
|
||||
<DialogTitle
|
||||
className="mb-3 flex items-center justify-between border-b border-border-medium p-6 pb-5 text-left"
|
||||
className="mb-1 flex items-center justify-between p-6 pb-5 text-left"
|
||||
as="div"
|
||||
>
|
||||
<h2 className="text-lg font-medium leading-6 text-text-primary">
|
||||
|
|
|
|||
|
|
@ -5,16 +5,17 @@ import AvatarEditor from 'react-avatar-editor';
|
|||
import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
|
||||
import type { TUser } from 'librechat-data-provider';
|
||||
import {
|
||||
Slider,
|
||||
Button,
|
||||
Spinner,
|
||||
OGDialog,
|
||||
OGDialogContent,
|
||||
OGDialogHeader,
|
||||
OGDialogTitle,
|
||||
OGDialogTrigger,
|
||||
Slider,
|
||||
} from '~/components/ui';
|
||||
} from '~/components';
|
||||
import { useUploadAvatarMutation, useGetFileConfig } from '~/data-provider';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import { cn, formatBytes } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
|
@ -130,10 +131,7 @@ function Avatar() {
|
|||
</OGDialogTrigger>
|
||||
</div>
|
||||
|
||||
<OGDialogContent
|
||||
className={cn('bg-surface-tertiary text-text-primary shadow-2xl md:h-auto md:w-[450px]')}
|
||||
style={{ borderRadius: '12px' }}
|
||||
>
|
||||
<OGDialogContent className="w-11/12 max-w-sm" style={{ borderRadius: '12px' }}>
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle className="text-lg font-medium leading-6 text-text-primary">
|
||||
{image ? localize('com_ui_preview') : localize('com_ui_upload_image')}
|
||||
|
|
@ -174,10 +172,10 @@ function Avatar() {
|
|||
<RotateCw className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
className={cn(
|
||||
'mt-4 flex items-center rounded px-4 py-2 text-white transition-colors hover:bg-green-600 hover:text-gray-200',
|
||||
isUploading ? 'cursor-not-allowed bg-green-600' : 'bg-green-500',
|
||||
'btn btn-primary mt-4 flex w-full hover:bg-green-600',
|
||||
isUploading ? 'cursor-not-allowed opacity-90' : '',
|
||||
)}
|
||||
onClick={handleUpload}
|
||||
disabled={isUploading}
|
||||
|
|
@ -188,24 +186,21 @@ function Avatar() {
|
|||
<Upload className="mr-2 h-5 w-5" />
|
||||
)}
|
||||
{localize('com_ui_upload')}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="flex h-64 w-64 flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="flex h-64 w-11/12 flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-transparent dark:border-gray-600"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
<FileImage className="mb-4 h-12 w-12 text-gray-400" />
|
||||
<FileImage className="mb-4 size-12 text-gray-400" />
|
||||
<p className="mb-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{localize('com_ui_drag_drop')}
|
||||
</p>
|
||||
<button
|
||||
onClick={openFileDialog}
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
|
||||
>
|
||||
<Button variant="secondary" onClick={openFileDialog}>
|
||||
{localize('com_ui_select_file')}
|
||||
</button>
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import { LockIcon, Trash } from 'lucide-react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, Input } from '~/components/ui';
|
||||
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
Spinner,
|
||||
OGDialog,
|
||||
OGDialogContent,
|
||||
OGDialogTrigger,
|
||||
OGDialogHeader,
|
||||
OGDialogTitle,
|
||||
} from '~/components';
|
||||
import { useDeleteUserMutation } from '~/data-provider';
|
||||
import { Spinner, LockIcon } from '~/components/svg';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import DangerButton from '../DangerButton';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolean }) => {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -15,14 +23,8 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
|
|||
});
|
||||
|
||||
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [deleteInput, setDeleteInput] = useState('');
|
||||
const [emailInput, setEmailInput] = useState('');
|
||||
const [isLocked, setIsLocked] = useState(true);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteUser = () => {
|
||||
if (!isLocked) {
|
||||
deleteUser(undefined);
|
||||
|
|
@ -30,47 +32,38 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
|
|||
};
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(newEmailInput: string, newDeleteInput: string) => {
|
||||
(newEmailInput: string) => {
|
||||
const isEmailCorrect =
|
||||
newEmailInput.trim().toLowerCase() === user?.email?.trim().toLowerCase();
|
||||
const isDeleteInputCorrect = newDeleteInput === 'DELETE';
|
||||
setIsLocked(!(isEmailCorrect && isDeleteInputCorrect));
|
||||
newEmailInput.trim().toLowerCase() === user?.email.trim().toLowerCase();
|
||||
setIsLocked(!isEmailCorrect);
|
||||
},
|
||||
[user?.email],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OGDialog open={isDialogOpen} onOpenChange={setDialogOpen}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{localize('com_nav_delete_account')}</span>
|
||||
<label>
|
||||
<DangerButton
|
||||
id={'delete-user-account'}
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
actionTextCode="com_ui_delete"
|
||||
className={cn(
|
||||
'btn relative border-none bg-red-500 text-white hover:bg-red-700 dark:hover:bg-red-700',
|
||||
)}
|
||||
confirmClear={false}
|
||||
infoTextCode={''}
|
||||
dataTestIdInitial={''}
|
||||
dataTestIdConfirm={''}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={() => setDialogOpen(false)}>
|
||||
<DialogContent
|
||||
className={cn('shadow-2xl md:h-[500px] md:w-[450px]')}
|
||||
style={{ borderRadius: '12px', padding: '20px' }}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-medium leading-6">
|
||||
{localize('com_ui_delete')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
</div>
|
||||
<OGDialogContent className="w-11/12 max-w-2xl">
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle className="text-lg font-medium leading-6">
|
||||
{localize('com_nav_delete_account_confirm')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mb-20 text-sm text-black dark:text-white">
|
||||
<ul>
|
||||
</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<div className="mb-8 text-sm text-black dark:text-white">
|
||||
<ul className="font-semibold text-amber-600">
|
||||
<li>{localize('com_nav_delete_warning')}</li>
|
||||
<li>{localize('com_nav_delete_data_info')}</li>
|
||||
</ul>
|
||||
|
|
@ -80,28 +73,14 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
|
|||
{renderInput(
|
||||
localize('com_nav_delete_account_email_placeholder'),
|
||||
'email-confirm-input',
|
||||
user?.email || '',
|
||||
(e) => {
|
||||
setEmailInput(e.target.value);
|
||||
handleInputChange(e.target.value, deleteInput);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
{renderInput(
|
||||
localize('com_nav_delete_account_confirm_placeholder'),
|
||||
'delete-confirm-input',
|
||||
'',
|
||||
(e) => {
|
||||
setDeleteInput(e.target.value);
|
||||
handleInputChange(emailInput, e.target.value);
|
||||
},
|
||||
user?.email ?? '',
|
||||
(e) => handleInputChange(e.target.value),
|
||||
)}
|
||||
</div>
|
||||
{renderDeleteButton(handleDeleteUser, isDeleting, isLocked, localize)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -113,17 +92,10 @@ const renderInput = (
|
|||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
|
||||
) => (
|
||||
<div className="mb-4">
|
||||
<label className="mb-1 block text-sm font-medium text-black dark:text-white">{label}</label>
|
||||
<Input
|
||||
id={id}
|
||||
onChange={onChange}
|
||||
placeholder={value}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'h-10 max-h-10 w-full max-w-full rounded-md bg-white px-3 py-2',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium text-black dark:text-white" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
<Input id={id} onChange={onChange} placeholder={value} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -135,12 +107,8 @@ const renderDeleteButton = (
|
|||
) => (
|
||||
<button
|
||||
className={cn(
|
||||
'mt-4 flex w-full items-center justify-center rounded-lg px-4 py-2 transition-colors duration-200',
|
||||
isLocked
|
||||
? 'cursor-not-allowed bg-gray-200 text-gray-300 dark:bg-gray-500 dark:text-gray-600'
|
||||
: isDeleting
|
||||
? 'cursor-not-allowed bg-gray-100 text-gray-700 dark:bg-gray-400 dark:text-gray-700'
|
||||
: 'bg-red-700 text-white hover:bg-red-800 ',
|
||||
'mt-4 flex w-full items-center justify-center rounded-lg bg-surface-tertiary px-4 py-2 transition-all duration-200',
|
||||
isLocked ? 'cursor-not-allowed opacity-30' : 'bg-destructive text-destructive-foreground',
|
||||
)}
|
||||
onClick={handleDeleteUser}
|
||||
disabled={isDeleting || isLocked}
|
||||
|
|
@ -153,12 +121,12 @@ const renderDeleteButton = (
|
|||
<>
|
||||
{isLocked ? (
|
||||
<>
|
||||
<LockIcon />
|
||||
<LockIcon className="size-5" />
|
||||
<span className="ml-2">{localize('com_ui_locked')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LockIcon />
|
||||
<Trash className="size-5" />
|
||||
<span className="ml-2">{localize('com_nav_delete_account_button')}</span>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,58 @@
|
|||
import type { TDangerButtonProps } from '~/common';
|
||||
import DangerButton from '../DangerButton';
|
||||
import React, { useState } from 'react';
|
||||
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
|
||||
import { Label, Button, OGDialog, OGDialogTrigger, Spinner } from '~/components';
|
||||
import { useConversation, useConversations, useLocalize } from '~/hooks';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
|
||||
export const ClearChats = () => {
|
||||
const localize = useLocalize();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { newConversation } = useConversation();
|
||||
const { refreshConversations } = useConversations();
|
||||
const clearConvosMutation = useClearConversationsMutation();
|
||||
|
||||
const clearConvos = () => {
|
||||
clearConvosMutation.mutate(
|
||||
{},
|
||||
{
|
||||
onSuccess: () => {
|
||||
newConversation();
|
||||
refreshConversations();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const ClearChatsButton = ({
|
||||
confirmClear,
|
||||
className = '',
|
||||
showText = true,
|
||||
mutation,
|
||||
onClick,
|
||||
}: Pick<
|
||||
TDangerButtonProps,
|
||||
'confirmClear' | 'mutation' | 'className' | 'showText' | 'onClick'
|
||||
>) => {
|
||||
return (
|
||||
<DangerButton
|
||||
id="clearConvosBtn"
|
||||
mutation={mutation}
|
||||
confirmClear={confirmClear}
|
||||
className={className}
|
||||
showText={showText}
|
||||
infoTextCode="com_nav_clear_all_chats"
|
||||
actionTextCode="com_ui_clear"
|
||||
confirmActionTextCode="com_nav_confirm_clear"
|
||||
dataTestIdInitial="clear-convos-initial"
|
||||
dataTestIdConfirm="clear-convos-confirm"
|
||||
onClick={onClick}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-light">{localize('com_nav_clear_all_chats')}</Label>
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{localize('com_ui_delete')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_nav_confirm_clear')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<Label className="text-left text-sm font-medium">
|
||||
{localize('com_nav_clear_conversation_confirm_message')}
|
||||
</Label>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: clearConvos,
|
||||
selectClasses:
|
||||
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
||||
selectText: clearConvosMutation.isLoading ? <Spinner /> : localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import 'test/matchMedia.mock';
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { ClearChatsButton } from './ClearChats';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
describe('ClearChatsButton', () => {
|
||||
let mockOnClick;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnClick = jest.fn();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const { getByText } = render(
|
||||
<RecoilRoot>
|
||||
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
expect(getByText('Clear all chats')).toBeInTheDocument();
|
||||
expect(getByText('Clear')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders confirm clear when confirmClear is true', () => {
|
||||
const { getByText } = render(
|
||||
<RecoilRoot>
|
||||
<ClearChatsButton confirmClear={true} showText={true} onClick={mockOnClick} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
expect(getByText('Confirm Clear')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClick when the button is clicked', () => {
|
||||
const { getByText } = render(
|
||||
<RecoilRoot>
|
||||
<ClearChatsButton confirmClear={false} showText={true} onClick={mockOnClick} />
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('Clear'));
|
||||
|
||||
expect(mockOnClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
|
||||
import { useConversation, useConversations, useOnClickOutside } from '~/hooks';
|
||||
import { RevokeKeysButton } from './RevokeKeysButton';
|
||||
import { DeleteCacheButton } from './DeleteCacheButton';
|
||||
import ImportConversations from './ImportConversations';
|
||||
import { ClearChatsButton } from './ClearChats';
|
||||
import { RevokeAllKeys } from './RevokeAllKeys';
|
||||
import { DeleteCache } from './DeleteCache';
|
||||
import { useOnClickOutside } from '~/hooks';
|
||||
import { ClearChats } from './ClearChats';
|
||||
import SharedLinks from './SharedLinks';
|
||||
|
||||
function Data() {
|
||||
|
|
@ -12,28 +11,6 @@ function Data() {
|
|||
const [confirmClearConvos, setConfirmClearConvos] = useState(false);
|
||||
useOnClickOutside(dataTabRef, () => confirmClearConvos && setConfirmClearConvos(false), []);
|
||||
|
||||
const { newConversation } = useConversation();
|
||||
const { refreshConversations } = useConversations();
|
||||
const clearConvosMutation = useClearConversationsMutation();
|
||||
|
||||
const clearConvos = () => {
|
||||
if (confirmClearConvos) {
|
||||
console.log('Clearing conversations...');
|
||||
setConfirmClearConvos(false);
|
||||
clearConvosMutation.mutate(
|
||||
{},
|
||||
{
|
||||
onSuccess: () => {
|
||||
newConversation();
|
||||
refreshConversations();
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
setConfirmClearConvos(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
|
|
@ -43,18 +20,13 @@ function Data() {
|
|||
<SharedLinks />
|
||||
</div>
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<RevokeKeysButton all={true} />
|
||||
<RevokeAllKeys />
|
||||
</div>
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<DeleteCacheButton />
|
||||
<DeleteCache />
|
||||
</div>
|
||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||
<ClearChatsButton
|
||||
confirmClear={confirmClearConvos}
|
||||
onClick={clearConvos}
|
||||
showText={true}
|
||||
mutation={clearConvosMutation}
|
||||
/>
|
||||
<ClearChats />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
65
client/src/components/Nav/SettingsTabs/Data/DeleteCache.tsx
Normal file
65
client/src/components/Nav/SettingsTabs/Data/DeleteCache.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { Label, Button, OGDialog, OGDialogTrigger, Spinner } from '~/components';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useOnClickOutside, useLocalize } from '~/hooks';
|
||||
|
||||
export const DeleteCache = ({ disabled = false }: { disabled?: boolean }) => {
|
||||
const localize = useLocalize();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isCacheEmpty, setIsCacheEmpty] = useState(true);
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const contentRef = useRef(null);
|
||||
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
|
||||
|
||||
const checkCache = useCallback(async () => {
|
||||
const cache = await caches.open('tts-responses');
|
||||
const keys = await cache.keys();
|
||||
setIsCacheEmpty(keys.length === 0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkCache();
|
||||
}, [checkCache]);
|
||||
|
||||
const revokeAllUserKeys = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
const cache = await caches.open('tts-responses');
|
||||
await cache.keys().then((keys) => Promise.all(keys.map((key) => cache.delete(key))));
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-light">{localize('com_nav_delete_cache_storage')}</Label>
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={disabled || isCacheEmpty}
|
||||
>
|
||||
{localize('com_ui_delete')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_nav_confirm_clear')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<Label className="text-left text-sm font-medium">
|
||||
{localize('com_nav_clear_cache_confirm_message')}
|
||||
</Label>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: revokeAllUserKeys,
|
||||
selectClasses:
|
||||
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
||||
selectText: isLoading ? <Spinner /> : localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useOnClickOutside } from '~/hooks';
|
||||
import DangerButton from '../DangerButton';
|
||||
|
||||
export const DeleteCacheButton = ({
|
||||
showText = true,
|
||||
disabled = false,
|
||||
}: {
|
||||
showText?: boolean;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const [isCacheEmpty, setIsCacheEmpty] = useState(true);
|
||||
const contentRef = useRef(null);
|
||||
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
|
||||
|
||||
const checkCache = useCallback(async () => {
|
||||
const cache = await caches.open('tts-responses');
|
||||
const keys = await cache.keys();
|
||||
setIsCacheEmpty(keys.length === 0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkCache();
|
||||
}, [confirmClear]);
|
||||
|
||||
const revokeAllUserKeys = useCallback(async () => {
|
||||
if (confirmClear) {
|
||||
const cache = await caches.open('tts-responses');
|
||||
await cache.keys().then((keys) => Promise.all(keys.map((key) => cache.delete(key))));
|
||||
|
||||
setConfirmClear(false);
|
||||
} else {
|
||||
setConfirmClear(true);
|
||||
}
|
||||
}, [confirmClear]);
|
||||
|
||||
return (
|
||||
<DangerButton
|
||||
ref={contentRef}
|
||||
showText={showText}
|
||||
onClick={revokeAllUserKeys}
|
||||
disabled={disabled || isCacheEmpty}
|
||||
confirmClear={confirmClear}
|
||||
id={'delete-cache'}
|
||||
actionTextCode={'com_ui_delete'}
|
||||
infoTextCode={'com_nav_delete_cache_storage'}
|
||||
infoDescriptionCode={'com_nav_info_delete_cache_storage'}
|
||||
dataTestIdInitial={'delete-cache-initial'}
|
||||
dataTestIdConfirm={'delete-cache-confirm'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import { RevokeKeysButton } from './RevokeKeysButton';
|
||||
import { Label } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export const RevokeAllKeys = () => {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-light">{localize('com_ui_revoke_info')}</Label>
|
||||
<RevokeKeysButton all={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,64 +2,77 @@ import {
|
|||
useRevokeAllUserKeysMutation,
|
||||
useRevokeUserKeyMutation,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { useOnClickOutside } from '~/hooks';
|
||||
import DangerButton from '../DangerButton';
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Label, OGDialog, OGDialogTrigger, Spinner } from '~/components';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export const RevokeKeysButton = ({
|
||||
showText = true,
|
||||
endpoint = '',
|
||||
all = false,
|
||||
disabled = false,
|
||||
setDialogOpen,
|
||||
}: {
|
||||
showText?: boolean;
|
||||
endpoint?: string;
|
||||
all?: boolean;
|
||||
disabled?: boolean;
|
||||
setDialogOpen?: (open: boolean) => void;
|
||||
}) => {
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const localize = useLocalize();
|
||||
const [open, setOpen] = useState(false);
|
||||
const revokeKeyMutation = useRevokeUserKeyMutation(endpoint);
|
||||
const revokeKeysMutation = useRevokeAllUserKeysMutation();
|
||||
|
||||
const contentRef = useRef(null);
|
||||
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
|
||||
|
||||
const revokeAllUserKeys = useCallback(() => {
|
||||
if (confirmClear) {
|
||||
revokeKeysMutation.mutate({});
|
||||
setConfirmClear(false);
|
||||
} else {
|
||||
setConfirmClear(true);
|
||||
}
|
||||
}, [confirmClear, revokeKeysMutation]);
|
||||
|
||||
const revokeUserKey = useCallback(() => {
|
||||
if (!endpoint) {
|
||||
const handleSuccess = () => {
|
||||
if (!setDialogOpen) {
|
||||
return;
|
||||
} else if (confirmClear) {
|
||||
revokeKeyMutation.mutate({});
|
||||
setConfirmClear(false);
|
||||
} else {
|
||||
setConfirmClear(true);
|
||||
}
|
||||
}, [confirmClear, revokeKeyMutation, endpoint]);
|
||||
|
||||
const onClick = all ? revokeAllUserKeys : revokeUserKey;
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
if (all) {
|
||||
revokeKeysMutation.mutate({});
|
||||
} else {
|
||||
revokeKeyMutation.mutate({}, { onSuccess: handleSuccess });
|
||||
}
|
||||
};
|
||||
|
||||
const dialogTitle = all
|
||||
? localize('com_ui_revoke_keys')
|
||||
: localize('com_ui_revoke_key_endpoint', endpoint);
|
||||
|
||||
const dialogMessage = all
|
||||
? localize('com_ui_revoke_keys_confirm')
|
||||
: localize('com_ui_revoke_key_confirm');
|
||||
|
||||
const isLoading = revokeKeyMutation.isLoading || revokeKeysMutation.isLoading;
|
||||
|
||||
return (
|
||||
<DangerButton
|
||||
ref={contentRef}
|
||||
showText={showText}
|
||||
onClick={onClick}
|
||||
<OGDialog open={open} onOpenChange={setOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={disabled}
|
||||
confirmClear={confirmClear}
|
||||
id={'revoke-all-user-keys'}
|
||||
actionTextCode={'com_ui_revoke'}
|
||||
infoTextCode={'com_ui_revoke_info'}
|
||||
infoDescriptionCode={'com_nav_info_revoke'}
|
||||
dataTestIdInitial={'revoke-all-keys-initial'}
|
||||
dataTestIdConfirm={'revoke-all-keys-confirm'}
|
||||
mutation={all ? revokeKeysMutation : revokeKeyMutation}
|
||||
>
|
||||
{localize('com_ui_revoke')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={dialogTitle}
|
||||
className="max-w-[450px]"
|
||||
main={<Label className="text-left text-sm font-medium">{dialogMessage}</Label>}
|
||||
selection={{
|
||||
selectHandler: onClick,
|
||||
selectClasses:
|
||||
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
||||
selectText: isLoading ? <Spinner /> : localize('com_ui_revoke'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
export * from './ExportConversation';
|
||||
export * from './SettingsTabs/';
|
||||
export { default as ClearConvos } from './ClearConvos';
|
||||
export { default as MobileNav } from './MobileNav';
|
||||
export { default as Nav } from './Nav';
|
||||
export { default as NavLink } from './NavLink';
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export default function FileSearch({
|
|||
<button
|
||||
type="button"
|
||||
disabled={!agent_id || fileSearchChecked === false}
|
||||
className="btn btn-neutral border-token-border-light relative h-8 rounded-lg font-medium"
|
||||
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-1">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Permissions } from 'librechat-data-provider';
|
|||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import type { TStartupConfig, AgentUpdateParams } from 'librechat-data-provider';
|
||||
import {
|
||||
Button,
|
||||
Switch,
|
||||
OGDialog,
|
||||
OGDialogTitle,
|
||||
|
|
@ -146,7 +147,7 @@ export default function ShareAgent({
|
|||
</div>
|
||||
</button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogContent className="w-1/4 border-border-light bg-surface-primary-alt text-text-secondary">
|
||||
<OGDialogContent className="w-11/12 md:max-w-xl">
|
||||
<OGDialogTitle>
|
||||
{localize(
|
||||
'com_ui_share_var',
|
||||
|
|
@ -255,13 +256,14 @@ export default function ShareAgent({
|
|||
</div>
|
||||
<div className="flex justify-end">
|
||||
<OGDialogClose asChild>
|
||||
<button
|
||||
<Button
|
||||
variant="submit"
|
||||
size="sm"
|
||||
type="submit"
|
||||
disabled={isSubmitting || isFetching}
|
||||
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
||||
>
|
||||
{localize('com_ui_save')}
|
||||
</button>
|
||||
</Button>
|
||||
</OGDialogClose>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ const BookmarkPanel = () => {
|
|||
<BookmarkTable />
|
||||
<div className="flex justify-between gap-2">
|
||||
<BookmarkEditDialog context="BookmarkPanel" open={open} setOpen={setOpen} />
|
||||
<Button variant="outline" className="w-full text-sm" onClick={() => setOpen(!open)}>
|
||||
<BookmarkPlusIcon className="mr-1 size-4" />
|
||||
<Button variant="outline" className="w-full gap-2 text-sm" onClick={() => setOpen(!open)}>
|
||||
<BookmarkPlusIcon className="size-4" />
|
||||
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ const buttonVariants = cva(
|
|||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline:
|
||||
'text-text-primary border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
submit: 'bg-surface-submit text-text-primary hover:bg-surface-submit/90',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...pr
|
|||
return (
|
||||
<input
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-border-light bg-transparent px-3 py-2 text-sm placeholder:text-text-tertiary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-50',
|
||||
'flex h-10 w-full rounded-lg border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className ?? '',
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { cn } from '~/utils/';
|
|||
type SelectionProps = {
|
||||
selectHandler?: () => void;
|
||||
selectClasses?: string;
|
||||
selectText?: string;
|
||||
selectText?: string | ReactNode;
|
||||
};
|
||||
|
||||
type DialogTemplateProps = {
|
||||
|
|
@ -81,7 +81,7 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
|||
onClick={selectHandler}
|
||||
className={`${
|
||||
selectClasses ?? defaultSelect
|
||||
} inline-flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm`}
|
||||
} flex h-10 w-full items-center justify-center rounded-lg border-none px-4 py-2 text-sm`}
|
||||
>
|
||||
{selectText}
|
||||
</OGDialogClose>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'max-w-11/12 fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 bg-background p-6 text-text-primary shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
'max-w-11/12 fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl bg-background p-6 text-text-primary shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -280,6 +280,10 @@ export default {
|
|||
com_ui_clear: 'Clear',
|
||||
com_ui_revoke: 'Revoke',
|
||||
com_ui_revoke_info: 'Revoke all user provided credentials',
|
||||
com_ui_revoke_keys: 'Revoke Keys',
|
||||
com_ui_revoke_keys_confirm: 'Are you sure you want to revoke all keys?',
|
||||
com_ui_revoke_key_endpoint: 'Revoke Key for {0}',
|
||||
com_ui_revoke_key_confirm: 'Are you sure you want to revoke this key?',
|
||||
com_ui_import_conversation: 'Import',
|
||||
com_ui_nothing_found: 'Nothing found',
|
||||
com_ui_go_to_conversation: 'Go to conversation',
|
||||
|
|
@ -717,10 +721,12 @@ export default {
|
|||
com_nav_auto_send_prompts: 'Auto-send Prompts',
|
||||
com_nav_always_make_prod: 'Always make new versions production',
|
||||
com_nav_clear_all_chats: 'Clear all chats',
|
||||
com_nav_clear_cache_confirm_message: 'Are you sure you want to clear the cache?',
|
||||
com_nav_confirm_clear: 'Confirm Clear',
|
||||
com_nav_close_sidebar: 'Close sidebar',
|
||||
com_nav_open_sidebar: 'Open sidebar',
|
||||
com_nav_send_message: 'Send message',
|
||||
com_nav_stop_generating: 'Stop generating',
|
||||
com_nav_log_out: 'Log out',
|
||||
com_nav_user: 'USER',
|
||||
com_nav_archived_chats: 'Archived chats',
|
||||
|
|
|
|||
|
|
@ -18,6 +18,17 @@
|
|||
--gray-800: #212121;
|
||||
--gray-850: #171717;
|
||||
--gray-900: #0d0d0d;
|
||||
--green-50: #ecfdf5;
|
||||
--green-100: #d1fae5;
|
||||
--green-200: #a7f3d0;
|
||||
--green-300: #6ee7b7;
|
||||
--green-400: #34d399;
|
||||
--green-500: #10b981;
|
||||
--green-600: #059669;
|
||||
--green-700: #047857;
|
||||
--green-800: #065f46;
|
||||
--green-900: #064e3b;
|
||||
--green-950: #022c22;
|
||||
--gizmo-gray-500: #999;
|
||||
--gizmo-gray-600: #666;
|
||||
--gizmo-gray-950: #0f0f0f;
|
||||
|
|
@ -42,9 +53,11 @@ html {
|
|||
--surface-primary-alt: var(--gray-50);
|
||||
--surface-primary-contrast: var(--gray-100);
|
||||
--surface-secondary: var(--gray-50);
|
||||
--surface-secondary-alt: var(--gray-300);
|
||||
--surface-tertiary: var(--gray-100);
|
||||
--surface-tertiary-alt: var(--white);
|
||||
--surface-dialog: var(--white);
|
||||
--surface-submit: var(--green-500);
|
||||
--border-light: var(--gray-200);
|
||||
--border-medium-alt: var(--gray-300);
|
||||
--border-medium: var(--gray-300);
|
||||
|
|
@ -90,9 +103,11 @@ html {
|
|||
--surface-primary-alt: var(--gray-850);
|
||||
--surface-primary-contrast: var(--gray-850);
|
||||
--surface-secondary: var(--gray-800);
|
||||
--surface-secondary-alt: var(--gray-800);
|
||||
--surface-tertiary: var(--gray-700);
|
||||
--surface-tertiary-alt: var(--gray-700);
|
||||
--surface-dialog: var(--gray-850);
|
||||
--surface-submit: var(--green-600);
|
||||
--border-light: var(--gray-700);
|
||||
--border-medium-alt: var(--gray-600);
|
||||
--border-medium: var(--gray-600);
|
||||
|
|
@ -112,7 +127,7 @@ html {
|
|||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive: 0 62.8% 40.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
|
|
@ -2351,7 +2366,7 @@ button.scroll-convo {
|
|||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
color: black;
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.25);
|
||||
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 0.25);
|
||||
}
|
||||
|
||||
.tooltip:where(.dark, .dark *) {
|
||||
|
|
|
|||
|
|
@ -75,9 +75,11 @@ module.exports = {
|
|||
'surface-primary-alt': 'var(--surface-primary-alt)',
|
||||
'surface-primary-contrast': 'var(--surface-primary-contrast)',
|
||||
'surface-secondary': 'var(--surface-secondary)',
|
||||
'surface-secondary-alt': 'var(--surface-secondary-alt)',
|
||||
'surface-tertiary': 'var(--surface-tertiary)',
|
||||
'surface-tertiary-alt': 'var(--surface-tertiary-alt)',
|
||||
'surface-dialog': 'var(--surface-dialog)',
|
||||
'surface-submit': 'var(--surface-submit)',
|
||||
'border-light': 'var(--border-light)',
|
||||
'border-medium': 'var(--border-medium)',
|
||||
'border-medium-alt': 'var(--border-medium-alt)',
|
||||
|
|
|
|||
|
|
@ -314,7 +314,10 @@ export const getFileConfig = (): Promise<f.FileConfig> => {
|
|||
return request.get(`${endpoints.files()}/config`);
|
||||
};
|
||||
|
||||
export const uploadImage = (data: FormData, signal?: AbortSignal | null): Promise<f.TFileUpload> => {
|
||||
export const uploadImage = (
|
||||
data: FormData,
|
||||
signal?: AbortSignal | null,
|
||||
): Promise<f.TFileUpload> => {
|
||||
const requestConfig = signal ? { signal } : undefined;
|
||||
return request.postMultiPart(endpoints.images(), data, requestConfig);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue