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 { TConversationTag, TConversation } from 'librechat-data-provider';
|
||||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { useConversationTagMutation } from '~/data-provider';
|
import { useConversationTagMutation } from '~/data-provider';
|
||||||
|
import { OGDialog, Button, Spinner } from '~/components';
|
||||||
import { NotificationSeverity } from '~/common';
|
import { NotificationSeverity } from '~/common';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { OGDialog } from '~/components/ui';
|
|
||||||
import { Spinner } from '~/components/svg';
|
|
||||||
import BookmarkForm from './BookmarkForm';
|
import BookmarkForm from './BookmarkForm';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { logger } from '~/utils';
|
import { logger } from '~/utils';
|
||||||
|
|
@ -75,6 +74,7 @@ const BookmarkEditDialog = ({
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
title="Bookmark"
|
title="Bookmark"
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
|
className="w-11/12 md:max-w-2xl"
|
||||||
main={
|
main={
|
||||||
<BookmarkForm
|
<BookmarkForm
|
||||||
tags={tags}
|
tags={tags}
|
||||||
|
|
@ -86,14 +86,14 @@ const BookmarkEditDialog = ({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
buttons={
|
buttons={
|
||||||
<button
|
<Button
|
||||||
|
variant="submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
onClick={handleSubmitForm}
|
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')}
|
{mutation.isLoading ? <Spinner /> : localize('com_ui_save')}
|
||||||
</button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</OGDialog>
|
</OGDialog>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
TConversationTagRequest,
|
TConversationTagRequest,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { cn, removeFocusOutlines, defaultTextProps, logger } from '~/utils';
|
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 { useBookmarkContext } from '~/Providers/BookmarkContext';
|
||||||
import { useConversationTagMutation } from '~/data-provider';
|
import { useConversationTagMutation } from '~/data-provider';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
|
|
@ -100,7 +100,7 @@ const BookmarkForm = ({
|
||||||
<Label htmlFor="bookmark-tag" className="text-left text-sm font-medium">
|
<Label htmlFor="bookmark-tag" className="text-left text-sm font-medium">
|
||||||
{localize('com_ui_bookmarks_title')}
|
{localize('com_ui_bookmarks_title')}
|
||||||
</Label>
|
</Label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="bookmark-tag"
|
id="bookmark-tag"
|
||||||
aria-label="Bookmark"
|
aria-label="Bookmark"
|
||||||
|
|
@ -119,17 +119,12 @@ const BookmarkForm = ({
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
aria-invalid={!!errors.tag}
|
aria-invalid={!!errors.tag}
|
||||||
className={cn(
|
placeholder="Bookmark"
|
||||||
defaultTextProps,
|
|
||||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
|
||||||
removeFocusOutlines,
|
|
||||||
)}
|
|
||||||
placeholder=" "
|
|
||||||
/>
|
/>
|
||||||
{errors.tag && <span className="text-sm text-red-500">{errors.tag.message}</span>}
|
{errors.tag && <span className="text-sm text-red-500">{errors.tag.message}</span>}
|
||||||
</div>
|
</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">
|
<Label htmlFor="bookmark-description" className="text-left text-sm font-medium">
|
||||||
{localize('com_ui_bookmarks_description')}
|
{localize('com_ui_bookmarks_description')}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
@ -143,8 +138,7 @@ const BookmarkForm = ({
|
||||||
id="bookmark-description"
|
id="bookmark-description"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
'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',
|
||||||
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2',
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,8 @@ export default function AudioRecorder({
|
||||||
onClick={isListening ? handleStopRecording : handleStartRecording}
|
onClick={isListening ? handleStopRecording : handleStartRecording}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
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',
|
'absolute flex size-[35px] items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover',
|
||||||
isRTL
|
isRTL ? 'bottom-2 left-2' : 'bottom-2 right-2',
|
||||||
? 'bottom-1.5 left-4 md:bottom-3 md:left-12'
|
|
||||||
: 'bottom-1.5 right-12 md:bottom-3 md:right-12',
|
|
||||||
)}
|
)}
|
||||||
description={localize('com_ui_use_micrphone')}
|
description={localize('com_ui_use_micrphone')}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} />
|
<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} />
|
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||||
<FileFormWrapper disableInputs={disableInputs}>
|
<FileFormWrapper disableInputs={disableInputs}>
|
||||||
{endpoint && (
|
{endpoint && (
|
||||||
|
|
@ -181,7 +181,7 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
endpointSupportsFiles && !isUploadDisabled
|
endpointSupportsFiles && !isUploadDisabled
|
||||||
? 'pl-10 md:pl-[55px]'
|
? 'pl-10 md:pl-[55px]'
|
||||||
: 'pl-3 md:pl-4',
|
: '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',
|
SpeechToText && !isRTL ? 'pr-20 md:pr-[85px]' : 'pr-10 md:pr-12',
|
||||||
'max-h-[65vh] md:max-h-[75vh]',
|
'max-h-[65vh] md:max-h-[75vh]',
|
||||||
removeFocusRings,
|
removeFocusRings,
|
||||||
|
|
@ -189,22 +189,6 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</FileFormWrapper>
|
</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 && (
|
{SpeechToText && (
|
||||||
<AudioRecorder
|
<AudioRecorder
|
||||||
disabled={!!disableInputs}
|
disabled={!!disableInputs}
|
||||||
|
|
@ -216,6 +200,25 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
)}
|
)}
|
||||||
{TextToSpeech && automaticPlayback && <StreamAudio index={index} />}
|
{TextToSpeech && automaticPlayback && <StreamAudio index={index} />}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,15 @@ const AttachFile = ({
|
||||||
const isUploadDisabled = disabled ?? false;
|
const isUploadDisabled = disabled ?? false;
|
||||||
|
|
||||||
return (
|
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">
|
<FileUpload handleFileChange={handleFileChange} className="flex">
|
||||||
<TooltipAnchor
|
<TooltipAnchor
|
||||||
id="audio-recorder"
|
id="audio-recorder"
|
||||||
disabled={isUploadDisabled}
|
|
||||||
aria-label={localize('com_sidepanel_attach_files')}
|
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"
|
disabled={isUploadDisabled}
|
||||||
style={{ padding: 0 }}
|
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')}
|
description={localize('com_sidepanel_attach_files')}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-center gap-2">
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
|
|
@ -39,7 +33,6 @@ const AttachFile = ({
|
||||||
</div>
|
</div>
|
||||||
</TooltipAnchor>
|
</TooltipAnchor>
|
||||||
</FileUpload>
|
</FileUpload>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,17 @@ import AttachFile from './AttachFile';
|
||||||
import FileRow from './FileRow';
|
import FileRow from './FileRow';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
function FileFormWrapper({ children, disableInputs } : {
|
function FileFormWrapper({
|
||||||
|
children,
|
||||||
|
disableInputs,
|
||||||
|
}: {
|
||||||
disableInputs: boolean;
|
disableInputs: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { handleFileChange, abortUpload } = useFileHandling();
|
const { handleFileChange, abortUpload } = useFileHandling();
|
||||||
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
|
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
|
||||||
|
|
||||||
const {
|
const { files, setFiles, conversation, setFilesLoading } = useChatContext();
|
||||||
files,
|
|
||||||
setFiles,
|
|
||||||
conversation,
|
|
||||||
setFilesLoading,
|
|
||||||
} = useChatContext();
|
|
||||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||||
select: (data) => mergeFileConfig(data),
|
select: (data) => mergeFileConfig(data),
|
||||||
});
|
});
|
||||||
|
|
@ -33,11 +31,14 @@ function FileFormWrapper({ children, disableInputs } : {
|
||||||
const isRTL = chatDirection === 'rtl';
|
const isRTL = chatDirection === 'rtl';
|
||||||
|
|
||||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
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 endpointSupportsFiles: boolean = supportsFiles[endpointType ?? _endpoint ?? ''] ?? false;
|
||||||
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
|
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
|
||||||
|
|
||||||
return (<>
|
return (
|
||||||
|
<>
|
||||||
<FileRow
|
<FileRow
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
|
|
@ -45,18 +46,15 @@ function FileFormWrapper({ children, disableInputs } : {
|
||||||
setFilesLoading={setFilesLoading}
|
setFilesLoading={setFilesLoading}
|
||||||
isRTL={isRTL}
|
isRTL={isRTL}
|
||||||
Wrapper={({ children }) => (
|
Wrapper={({ children }) => (
|
||||||
<div className="mx-2 mt-2 flex flex-wrap gap-2 px-2.5 md:pl-0 md:pr-4">
|
<div className="mx-2 mt-2 flex flex-wrap gap-2 px-2.5 md:pl-0 md:pr-4">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
{endpointSupportsFiles && !isUploadDisabled && <AttachFile
|
{endpointSupportsFiles && !isUploadDisabled && (
|
||||||
isRTL={isRTL}
|
<AttachFile isRTL={isRTL} disabled={disableInputs} handleFileChange={handleFileChange} />
|
||||||
disabled={disableInputs}
|
)}
|
||||||
handleFileChange={handleFileChange}
|
</>
|
||||||
/>}
|
);
|
||||||
</>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(FileFormWrapper);
|
export default memo(FileFormWrapper);
|
||||||
|
|
@ -9,12 +9,10 @@ import { cn } from '~/utils';
|
||||||
type SendButtonProps = {
|
type SendButtonProps = {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
control: Control<{ text: string }>;
|
control: Control<{ text: string }>;
|
||||||
isRTL: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SubmitButton = React.memo(
|
const SubmitButton = React.memo(
|
||||||
forwardRef(
|
forwardRef((props: { disabled: boolean }, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||||
(props: { disabled: boolean; isRTL: boolean }, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
return (
|
return (
|
||||||
<TooltipAnchor
|
<TooltipAnchor
|
||||||
|
|
@ -26,10 +24,7 @@ const SubmitButton = React.memo(
|
||||||
id="send-button"
|
id="send-button"
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
className={cn(
|
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',
|
'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',
|
||||||
props.isRTL
|
|
||||||
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
|
|
||||||
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',
|
|
||||||
)}
|
)}
|
||||||
data-testid="send-button"
|
data-testid="send-button"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
@ -41,14 +36,13 @@ const SubmitButton = React.memo(
|
||||||
}
|
}
|
||||||
></TooltipAnchor>
|
></TooltipAnchor>
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const SendButton = React.memo(
|
const SendButton = React.memo(
|
||||||
forwardRef((props: SendButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
forwardRef((props: SendButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||||
const data = useWatch({ control: props.control });
|
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';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
export default function StopButton({ stop, setShowStopButton, isRTL }) {
|
export default function StopButton({ stop, setShowStopButton }) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<TooltipAnchor
|
||||||
className={cn(
|
description={localize('com_nav_stop_generating')}
|
||||||
'absolute',
|
render={
|
||||||
isRTL ? 'bottom-3 left-2 md:bottom-4 md:left-4' : 'bottom-3 right-2 md:bottom-4 md:right-4',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="border-gizmo-gray-900 rounded-full border-2 p-1 dark:border-gray-200"
|
className={cn(
|
||||||
aria-label="Stop generating"
|
'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) => {
|
onClick={(e) => {
|
||||||
setShowStopButton(false);
|
setShowStopButton(false);
|
||||||
stop(e);
|
stop(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
className="icon-lg text-surface-primary"
|
||||||
fill="currentColor"
|
|
||||||
className="text-gizmo-gray-900 h-2 w-2 dark:text-gray-200"
|
|
||||||
height="16"
|
|
||||||
width="16"
|
|
||||||
>
|
>
|
||||||
<path
|
<rect x="7" y="7" width="10" height="10" rx="1.25" fill="currentColor"></rect>
|
||||||
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>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
|
></TooltipAnchor>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default function TextareaHeader({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
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} />
|
<AddedConvo addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ const InputWithLabel: FC<InputWithLabelProps> = forwardRef((props, ref) => {
|
||||||
)}
|
)}
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-1" />
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
data-testid={`input-${id}`}
|
data-testid={`input-${id}`}
|
||||||
|
|
@ -36,12 +37,7 @@ const InputWithLabel: FC<InputWithLabelProps> = forwardRef((props, ref) => {
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
placeholder={`${localize('com_endpoint_config_value')} ${label}`}
|
placeholder={`${localize('com_endpoint_config_value')} ${label}`}
|
||||||
className={cn(
|
className={cn('flex h-10 max-h-10 w-full resize-none px-3 py-2')}
|
||||||
defaultTextProps,
|
|
||||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
|
||||||
removeFocusOutlines,
|
|
||||||
inputClassName,
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import { useForm, FormProvider } from 'react-hook-form';
|
||||||
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
import type { TDialogProps } from '~/common';
|
import type { TDialogProps } from '~/common';
|
||||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { RevokeKeysButton } from '~/components/Nav';
|
import { RevokeKeysButton } from '~/components/Nav';
|
||||||
import { Dialog, Dropdown } from '~/components/ui';
|
import { OGDialog, Dropdown } from '~/components/ui';
|
||||||
import { useUserKey, useLocalize } from '~/hooks';
|
import { useUserKey, useLocalize } from '~/hooks';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import CustomConfig from './CustomEndpoint';
|
import CustomConfig from './CustomEndpoint';
|
||||||
|
|
@ -160,10 +160,11 @@ const SetKeyDialog = ({
|
||||||
const config = endpointsConfig?.[endpoint];
|
const config = endpointsConfig?.[endpoint];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogTemplate
|
<OGDialogTemplate
|
||||||
title={`${localize('com_endpoint_config_key_for')} ${alternateName[endpoint] ?? endpoint}`}
|
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"
|
className="w-11/12 max-w-[650px] sm:w-3/4 md:w-3/4 lg:w-3/4"
|
||||||
|
showCancelButton={false}
|
||||||
main={
|
main={
|
||||||
<div className="grid w-full items-center gap-2">
|
<div className="grid w-full items-center gap-2">
|
||||||
<small className="text-red-600">
|
<small className="text-red-600">
|
||||||
|
|
@ -172,7 +173,7 @@ const SetKeyDialog = ({
|
||||||
: `${localize('com_endpoint_config_key_encryption')} ${new Date(
|
: `${localize('com_endpoint_config_key_encryption')} ${new Date(
|
||||||
expiryTime ?? 0,
|
expiryTime ?? 0,
|
||||||
).toLocaleString()}`}
|
).toLocaleString()}`}
|
||||||
</small>{' '}
|
</small>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
label="Expires "
|
label="Expires "
|
||||||
value={expiresAtLabel}
|
value={expiresAtLabel}
|
||||||
|
|
@ -180,6 +181,7 @@ const SetKeyDialog = ({
|
||||||
options={expirationOptions.map((option) => option.label)}
|
options={expirationOptions.map((option) => option.label)}
|
||||||
sizeClasses="w-[185px]"
|
sizeClasses="w-[185px]"
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-2" />
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<EndpointComponent
|
<EndpointComponent
|
||||||
userKey={userKey}
|
userKey={userKey}
|
||||||
|
|
@ -197,14 +199,18 @@ const SetKeyDialog = ({
|
||||||
}
|
}
|
||||||
selection={{
|
selection={{
|
||||||
selectHandler: submit,
|
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'),
|
selectText: localize('com_ui_submit'),
|
||||||
}}
|
}}
|
||||||
leftButtons={
|
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
|
<Select.Select
|
||||||
aria-label={localize('com_nav_account_settings')}
|
aria-label={localize('com_nav_account_settings')}
|
||||||
data-testid="nav-user"
|
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="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
|
||||||
className={cn(
|
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',
|
'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' : '',
|
open ? 'bg-surface-hover' : '',
|
||||||
isSmallScreen ? 'h-14 rounded-2xl' : '',
|
isSmallScreen ? 'h-12' : '',
|
||||||
)}
|
)}
|
||||||
data-testid="bookmark-menu"
|
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}
|
onMouseLeave={handleMouseLeave}
|
||||||
ref={containerRef}
|
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
|
<NewChat
|
||||||
toggleNav={itemToggleNav}
|
toggleNav={itemToggleNav}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
subHeaders={
|
subHeaders={
|
||||||
<>
|
<>
|
||||||
{isSearchEnabled === true && (
|
{isSearchEnabled === true && (
|
||||||
|
|
@ -197,7 +184,6 @@ const Nav = ({
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<Conversations
|
<Conversations
|
||||||
conversations={conversations}
|
conversations={conversations}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
|
||||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||||
import { useLocalize, useNewConvo } from '~/hooks';
|
import { useLocalize, useNewConvo } from '~/hooks';
|
||||||
import { NewChatIcon } from '~/components/svg';
|
import { NewChatIcon } from '~/components/svg';
|
||||||
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const NewChatButtonIcon = ({ conversation }: { conversation: TConversation | null }) => {
|
const NewChatButtonIcon = ({ conversation }: { conversation: TConversation | null }) => {
|
||||||
|
|
@ -57,10 +58,12 @@ export default function NewChat({
|
||||||
index = 0,
|
index = 0,
|
||||||
toggleNav,
|
toggleNav,
|
||||||
subHeaders,
|
subHeaders,
|
||||||
|
isSmallScreen,
|
||||||
}: {
|
}: {
|
||||||
index?: number;
|
index?: number;
|
||||||
toggleNav: () => void;
|
toggleNav: () => void;
|
||||||
subHeaders?: React.ReactNode;
|
subHeaders?: React.ReactNode;
|
||||||
|
isSmallScreen: boolean;
|
||||||
}) {
|
}) {
|
||||||
/** Note: this component needs an explicit index passed if using more than one */
|
/** Note: this component needs an explicit index passed if using more than one */
|
||||||
const { newConversation: newConvo } = useNewConvo(index);
|
const { newConversation: newConvo } = useNewConvo(index);
|
||||||
|
|
@ -86,7 +89,10 @@ export default function NewChat({
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
data-testid="nav-new-chat"
|
data-testid="nav-new-chat"
|
||||||
onClick={clickHandler}
|
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')}
|
aria-label={localize('com_ui_new_chat')}
|
||||||
>
|
>
|
||||||
<NewChatButtonIcon conversation={conversation} />
|
<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')}>
|
<div className={cn('fixed inset-0 flex w-screen items-center justify-center p-4')}>
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
className={cn(
|
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
|
<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"
|
as="div"
|
||||||
>
|
>
|
||||||
<h2 className="text-lg font-medium leading-6 text-text-primary">
|
<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 { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider';
|
||||||
import type { TUser } from 'librechat-data-provider';
|
import type { TUser } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
|
Slider,
|
||||||
|
Button,
|
||||||
|
Spinner,
|
||||||
OGDialog,
|
OGDialog,
|
||||||
OGDialogContent,
|
OGDialogContent,
|
||||||
OGDialogHeader,
|
OGDialogHeader,
|
||||||
OGDialogTitle,
|
OGDialogTitle,
|
||||||
OGDialogTrigger,
|
OGDialogTrigger,
|
||||||
Slider,
|
} from '~/components';
|
||||||
} from '~/components/ui';
|
|
||||||
import { useUploadAvatarMutation, useGetFileConfig } from '~/data-provider';
|
import { useUploadAvatarMutation, useGetFileConfig } from '~/data-provider';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { Spinner } from '~/components/svg';
|
|
||||||
import { cn, formatBytes } from '~/utils';
|
import { cn, formatBytes } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -130,10 +131,7 @@ function Avatar() {
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OGDialogContent
|
<OGDialogContent className="w-11/12 max-w-sm" style={{ borderRadius: '12px' }}>
|
||||||
className={cn('bg-surface-tertiary text-text-primary shadow-2xl md:h-auto md:w-[450px]')}
|
|
||||||
style={{ borderRadius: '12px' }}
|
|
||||||
>
|
|
||||||
<OGDialogHeader>
|
<OGDialogHeader>
|
||||||
<OGDialogTitle className="text-lg font-medium leading-6 text-text-primary">
|
<OGDialogTitle className="text-lg font-medium leading-6 text-text-primary">
|
||||||
{image ? localize('com_ui_preview') : localize('com_ui_upload_image')}
|
{image ? localize('com_ui_preview') : localize('com_ui_upload_image')}
|
||||||
|
|
@ -174,10 +172,10 @@ function Avatar() {
|
||||||
<RotateCw className="h-5 w-5" />
|
<RotateCw className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-4 flex items-center rounded px-4 py-2 text-white transition-colors hover:bg-green-600 hover:text-gray-200',
|
'btn btn-primary mt-4 flex w-full hover:bg-green-600',
|
||||||
isUploading ? 'cursor-not-allowed bg-green-600' : 'bg-green-500',
|
isUploading ? 'cursor-not-allowed opacity-90' : '',
|
||||||
)}
|
)}
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
|
|
@ -188,24 +186,21 @@ function Avatar() {
|
||||||
<Upload className="mr-2 h-5 w-5" />
|
<Upload className="mr-2 h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
{localize('com_ui_upload')}
|
{localize('com_ui_upload')}
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<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}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
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">
|
<p className="mb-2 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
{localize('com_ui_drag_drop')}
|
{localize('com_ui_drag_drop')}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<Button variant="secondary" onClick={openFileDialog}>
|
||||||
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"
|
|
||||||
>
|
|
||||||
{localize('com_ui_select_file')}
|
{localize('com_ui_select_file')}
|
||||||
</button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
|
import { LockIcon, Trash } from 'lucide-react';
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, Input } from '~/components/ui';
|
import {
|
||||||
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
|
Input,
|
||||||
|
Button,
|
||||||
|
Spinner,
|
||||||
|
OGDialog,
|
||||||
|
OGDialogContent,
|
||||||
|
OGDialogTrigger,
|
||||||
|
OGDialogHeader,
|
||||||
|
OGDialogTitle,
|
||||||
|
} from '~/components';
|
||||||
import { useDeleteUserMutation } from '~/data-provider';
|
import { useDeleteUserMutation } from '~/data-provider';
|
||||||
import { Spinner, LockIcon } from '~/components/svg';
|
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import DangerButton from '../DangerButton';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolean }) => {
|
const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolean }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -15,14 +23,8 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
const [deleteInput, setDeleteInput] = useState('');
|
|
||||||
const [emailInput, setEmailInput] = useState('');
|
|
||||||
const [isLocked, setIsLocked] = useState(true);
|
const [isLocked, setIsLocked] = useState(true);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
|
||||||
setDialogOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDeleteUser = () => {
|
const handleDeleteUser = () => {
|
||||||
if (!isLocked) {
|
if (!isLocked) {
|
||||||
deleteUser(undefined);
|
deleteUser(undefined);
|
||||||
|
|
@ -30,47 +32,38 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = useCallback(
|
const handleInputChange = useCallback(
|
||||||
(newEmailInput: string, newDeleteInput: string) => {
|
(newEmailInput: string) => {
|
||||||
const isEmailCorrect =
|
const isEmailCorrect =
|
||||||
newEmailInput.trim().toLowerCase() === user?.email?.trim().toLowerCase();
|
newEmailInput.trim().toLowerCase() === user?.email.trim().toLowerCase();
|
||||||
const isDeleteInputCorrect = newDeleteInput === 'DELETE';
|
setIsLocked(!isEmailCorrect);
|
||||||
setIsLocked(!(isEmailCorrect && isDeleteInputCorrect));
|
|
||||||
},
|
},
|
||||||
[user?.email],
|
[user?.email],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<OGDialog open={isDialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>{localize('com_nav_delete_account')}</span>
|
<span>{localize('com_nav_delete_account')}</span>
|
||||||
<label>
|
<OGDialogTrigger asChild>
|
||||||
<DangerButton
|
<Button
|
||||||
id={'delete-user-account'}
|
variant="destructive"
|
||||||
|
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||||
|
onClick={() => setDialogOpen(true)}
|
||||||
disabled={disabled}
|
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>
|
{localize('com_ui_delete')}
|
||||||
<DialogTitle className="text-lg font-medium leading-6">
|
</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')}
|
{localize('com_nav_delete_account_confirm')}
|
||||||
</DialogTitle>
|
</OGDialogTitle>
|
||||||
</DialogHeader>
|
</OGDialogHeader>
|
||||||
<div className="mb-20 text-sm text-black dark:text-white">
|
<div className="mb-8 text-sm text-black dark:text-white">
|
||||||
<ul>
|
<ul className="font-semibold text-amber-600">
|
||||||
<li>{localize('com_nav_delete_warning')}</li>
|
<li>{localize('com_nav_delete_warning')}</li>
|
||||||
<li>{localize('com_nav_delete_data_info')}</li>
|
<li>{localize('com_nav_delete_data_info')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -80,28 +73,14 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
|
||||||
{renderInput(
|
{renderInput(
|
||||||
localize('com_nav_delete_account_email_placeholder'),
|
localize('com_nav_delete_account_email_placeholder'),
|
||||||
'email-confirm-input',
|
'email-confirm-input',
|
||||||
user?.email || '',
|
user?.email ?? '',
|
||||||
(e) => {
|
(e) => handleInputChange(e.target.value),
|
||||||
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);
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{renderDeleteButton(handleDeleteUser, isDeleting, isLocked, localize)}
|
{renderDeleteButton(handleDeleteUser, isDeleting, isLocked, localize)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</OGDialogContent>
|
||||||
</Dialog>
|
</OGDialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -113,17 +92,10 @@ const renderInput = (
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
|
||||||
) => (
|
) => (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-black dark:text-white">{label}</label>
|
<label className="mb-1 block text-sm font-medium text-black dark:text-white" htmlFor={id}>
|
||||||
<Input
|
{label}
|
||||||
id={id}
|
</label>
|
||||||
onChange={onChange}
|
<Input id={id} onChange={onChange} placeholder={value} />
|
||||||
placeholder={value}
|
|
||||||
className={cn(
|
|
||||||
defaultTextProps,
|
|
||||||
'h-10 max-h-10 w-full max-w-full rounded-md bg-white px-3 py-2',
|
|
||||||
removeFocusOutlines,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -135,12 +107,8 @@ const renderDeleteButton = (
|
||||||
) => (
|
) => (
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-4 flex w-full items-center justify-center rounded-lg px-4 py-2 transition-colors duration-200',
|
'mt-4 flex w-full items-center justify-center rounded-lg bg-surface-tertiary px-4 py-2 transition-all duration-200',
|
||||||
isLocked
|
isLocked ? 'cursor-not-allowed opacity-30' : 'bg-destructive text-destructive-foreground',
|
||||||
? '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 ',
|
|
||||||
)}
|
)}
|
||||||
onClick={handleDeleteUser}
|
onClick={handleDeleteUser}
|
||||||
disabled={isDeleting || isLocked}
|
disabled={isDeleting || isLocked}
|
||||||
|
|
@ -153,12 +121,12 @@ const renderDeleteButton = (
|
||||||
<>
|
<>
|
||||||
{isLocked ? (
|
{isLocked ? (
|
||||||
<>
|
<>
|
||||||
<LockIcon />
|
<LockIcon className="size-5" />
|
||||||
<span className="ml-2">{localize('com_ui_locked')}</span>
|
<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>
|
<span className="ml-2">{localize('com_nav_delete_account_button')}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,58 @@
|
||||||
import type { TDangerButtonProps } from '~/common';
|
import React, { useState } from 'react';
|
||||||
import DangerButton from '../DangerButton';
|
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 (
|
return (
|
||||||
<DangerButton
|
<div className="flex items-center justify-between">
|
||||||
id="clearConvosBtn"
|
<Label className="font-light">{localize('com_nav_clear_all_chats')}</Label>
|
||||||
mutation={mutation}
|
<OGDialog open={open} onOpenChange={setOpen}>
|
||||||
confirmClear={confirmClear}
|
<OGDialogTrigger asChild>
|
||||||
className={className}
|
<Button
|
||||||
showText={showText}
|
variant="destructive"
|
||||||
infoTextCode="com_nav_clear_all_chats"
|
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||||
actionTextCode="com_ui_clear"
|
onClick={() => setOpen(true)}
|
||||||
confirmActionTextCode="com_nav_confirm_clear"
|
>
|
||||||
dataTestIdInitial="clear-convos-initial"
|
{localize('com_ui_delete')}
|
||||||
dataTestIdConfirm="clear-convos-confirm"
|
</Button>
|
||||||
onClick={onClick}
|
</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 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 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';
|
import SharedLinks from './SharedLinks';
|
||||||
|
|
||||||
function Data() {
|
function Data() {
|
||||||
|
|
@ -12,28 +11,6 @@ function Data() {
|
||||||
const [confirmClearConvos, setConfirmClearConvos] = useState(false);
|
const [confirmClearConvos, setConfirmClearConvos] = useState(false);
|
||||||
useOnClickOutside(dataTabRef, () => confirmClearConvos && setConfirmClearConvos(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 (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
<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">
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
|
|
@ -43,18 +20,13 @@ function Data() {
|
||||||
<SharedLinks />
|
<SharedLinks />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
<RevokeKeysButton all={true} />
|
<RevokeAllKeys />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
<DeleteCacheButton />
|
<DeleteCache />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
|
||||||
<ClearChatsButton
|
<ClearChats />
|
||||||
confirmClear={confirmClearConvos}
|
|
||||||
onClick={clearConvos}
|
|
||||||
showText={true}
|
|
||||||
mutation={clearConvosMutation}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</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,
|
useRevokeAllUserKeysMutation,
|
||||||
useRevokeUserKeyMutation,
|
useRevokeUserKeyMutation,
|
||||||
} from 'librechat-data-provider/react-query';
|
} from 'librechat-data-provider/react-query';
|
||||||
import React, { useState, useCallback, useRef } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useOnClickOutside } from '~/hooks';
|
import { Button, Label, OGDialog, OGDialogTrigger, Spinner } from '~/components';
|
||||||
import DangerButton from '../DangerButton';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
export const RevokeKeysButton = ({
|
export const RevokeKeysButton = ({
|
||||||
showText = true,
|
|
||||||
endpoint = '',
|
endpoint = '',
|
||||||
all = false,
|
all = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
setDialogOpen,
|
||||||
}: {
|
}: {
|
||||||
showText?: boolean;
|
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
all?: boolean;
|
all?: boolean;
|
||||||
disabled?: 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 revokeKeyMutation = useRevokeUserKeyMutation(endpoint);
|
||||||
const revokeKeysMutation = useRevokeAllUserKeysMutation();
|
const revokeKeysMutation = useRevokeAllUserKeysMutation();
|
||||||
|
|
||||||
const contentRef = useRef(null);
|
const handleSuccess = () => {
|
||||||
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
|
if (!setDialogOpen) {
|
||||||
|
|
||||||
const revokeAllUserKeys = useCallback(() => {
|
|
||||||
if (confirmClear) {
|
|
||||||
revokeKeysMutation.mutate({});
|
|
||||||
setConfirmClear(false);
|
|
||||||
} else {
|
|
||||||
setConfirmClear(true);
|
|
||||||
}
|
|
||||||
}, [confirmClear, revokeKeysMutation]);
|
|
||||||
|
|
||||||
const revokeUserKey = useCallback(() => {
|
|
||||||
if (!endpoint) {
|
|
||||||
return;
|
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 (
|
return (
|
||||||
<DangerButton
|
<OGDialog open={open} onOpenChange={setOpen}>
|
||||||
ref={contentRef}
|
<OGDialogTrigger asChild>
|
||||||
showText={showText}
|
<Button
|
||||||
onClick={onClick}
|
variant="destructive"
|
||||||
|
className="flex items-center justify-center rounded-lg transition-colors duration-200"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
confirmClear={confirmClear}
|
>
|
||||||
id={'revoke-all-user-keys'}
|
{localize('com_ui_revoke')}
|
||||||
actionTextCode={'com_ui_revoke'}
|
</Button>
|
||||||
infoTextCode={'com_ui_revoke_info'}
|
</OGDialogTrigger>
|
||||||
infoDescriptionCode={'com_nav_info_revoke'}
|
<OGDialogTemplate
|
||||||
dataTestIdInitial={'revoke-all-keys-initial'}
|
showCloseButton={false}
|
||||||
dataTestIdConfirm={'revoke-all-keys-confirm'}
|
title={dialogTitle}
|
||||||
mutation={all ? revokeKeysMutation : revokeKeyMutation}
|
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 './ExportConversation';
|
||||||
export * from './SettingsTabs/';
|
export * from './SettingsTabs/';
|
||||||
export { default as ClearConvos } from './ClearConvos';
|
|
||||||
export { default as MobileNav } from './MobileNav';
|
export { default as MobileNav } from './MobileNav';
|
||||||
export { default as Nav } from './Nav';
|
export { default as Nav } from './Nav';
|
||||||
export { default as NavLink } from './NavLink';
|
export { default as NavLink } from './NavLink';
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ export default function FileSearch({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!agent_id || fileSearchChecked === false}
|
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}
|
onClick={handleButtonClick}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-center gap-1">
|
<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 { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
import type { TStartupConfig, AgentUpdateParams } from 'librechat-data-provider';
|
import type { TStartupConfig, AgentUpdateParams } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Switch,
|
Switch,
|
||||||
OGDialog,
|
OGDialog,
|
||||||
OGDialogTitle,
|
OGDialogTitle,
|
||||||
|
|
@ -146,7 +147,7 @@ export default function ShareAgent({
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</OGDialogTrigger>
|
</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>
|
<OGDialogTitle>
|
||||||
{localize(
|
{localize(
|
||||||
'com_ui_share_var',
|
'com_ui_share_var',
|
||||||
|
|
@ -255,13 +256,14 @@ export default function ShareAgent({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<OGDialogClose asChild>
|
<OGDialogClose asChild>
|
||||||
<button
|
<Button
|
||||||
|
variant="submit"
|
||||||
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting || isFetching}
|
disabled={isSubmitting || isFetching}
|
||||||
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
|
||||||
>
|
>
|
||||||
{localize('com_ui_save')}
|
{localize('com_ui_save')}
|
||||||
</button>
|
</Button>
|
||||||
</OGDialogClose>
|
</OGDialogClose>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ const BookmarkPanel = () => {
|
||||||
<BookmarkTable />
|
<BookmarkTable />
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<BookmarkEditDialog context="BookmarkPanel" open={open} setOpen={setOpen} />
|
<BookmarkEditDialog context="BookmarkPanel" open={open} setOpen={setOpen} />
|
||||||
<Button variant="outline" className="w-full text-sm" onClick={() => setOpen(!open)}>
|
<Button variant="outline" className="w-full gap-2 text-sm" onClick={() => setOpen(!open)}>
|
||||||
<BookmarkPlusIcon className="mr-1 size-4" />
|
<BookmarkPlusIcon className="size-4" />
|
||||||
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
|
<div className="break-all">{localize('com_ui_bookmarks_new')}</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,13 @@ const buttonVariants = cva(
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
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:
|
outline:
|
||||||
'text-text-primary border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
'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',
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
submit: 'bg-surface-submit text-text-primary hover:bg-surface-submit/90',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-10 px-4 py-2',
|
default: 'h-10 px-4 py-2',
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...pr
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
className={cn(
|
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 ?? '',
|
className ?? '',
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { cn } from '~/utils/';
|
||||||
type SelectionProps = {
|
type SelectionProps = {
|
||||||
selectHandler?: () => void;
|
selectHandler?: () => void;
|
||||||
selectClasses?: string;
|
selectClasses?: string;
|
||||||
selectText?: string;
|
selectText?: string | ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DialogTemplateProps = {
|
type DialogTemplateProps = {
|
||||||
|
|
@ -81,7 +81,7 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
||||||
onClick={selectHandler}
|
onClick={selectHandler}
|
||||||
className={`${
|
className={`${
|
||||||
selectClasses ?? defaultSelect
|
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}
|
{selectText}
|
||||||
</OGDialogClose>
|
</OGDialogClose>
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,10 @@ export default {
|
||||||
com_ui_clear: 'Clear',
|
com_ui_clear: 'Clear',
|
||||||
com_ui_revoke: 'Revoke',
|
com_ui_revoke: 'Revoke',
|
||||||
com_ui_revoke_info: 'Revoke all user provided credentials',
|
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_import_conversation: 'Import',
|
||||||
com_ui_nothing_found: 'Nothing found',
|
com_ui_nothing_found: 'Nothing found',
|
||||||
com_ui_go_to_conversation: 'Go to conversation',
|
com_ui_go_to_conversation: 'Go to conversation',
|
||||||
|
|
@ -717,10 +721,12 @@ export default {
|
||||||
com_nav_auto_send_prompts: 'Auto-send Prompts',
|
com_nav_auto_send_prompts: 'Auto-send Prompts',
|
||||||
com_nav_always_make_prod: 'Always make new versions production',
|
com_nav_always_make_prod: 'Always make new versions production',
|
||||||
com_nav_clear_all_chats: 'Clear all chats',
|
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_confirm_clear: 'Confirm Clear',
|
||||||
com_nav_close_sidebar: 'Close sidebar',
|
com_nav_close_sidebar: 'Close sidebar',
|
||||||
com_nav_open_sidebar: 'Open sidebar',
|
com_nav_open_sidebar: 'Open sidebar',
|
||||||
com_nav_send_message: 'Send message',
|
com_nav_send_message: 'Send message',
|
||||||
|
com_nav_stop_generating: 'Stop generating',
|
||||||
com_nav_log_out: 'Log out',
|
com_nav_log_out: 'Log out',
|
||||||
com_nav_user: 'USER',
|
com_nav_user: 'USER',
|
||||||
com_nav_archived_chats: 'Archived chats',
|
com_nav_archived_chats: 'Archived chats',
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,17 @@
|
||||||
--gray-800: #212121;
|
--gray-800: #212121;
|
||||||
--gray-850: #171717;
|
--gray-850: #171717;
|
||||||
--gray-900: #0d0d0d;
|
--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-500: #999;
|
||||||
--gizmo-gray-600: #666;
|
--gizmo-gray-600: #666;
|
||||||
--gizmo-gray-950: #0f0f0f;
|
--gizmo-gray-950: #0f0f0f;
|
||||||
|
|
@ -42,9 +53,11 @@ html {
|
||||||
--surface-primary-alt: var(--gray-50);
|
--surface-primary-alt: var(--gray-50);
|
||||||
--surface-primary-contrast: var(--gray-100);
|
--surface-primary-contrast: var(--gray-100);
|
||||||
--surface-secondary: var(--gray-50);
|
--surface-secondary: var(--gray-50);
|
||||||
|
--surface-secondary-alt: var(--gray-300);
|
||||||
--surface-tertiary: var(--gray-100);
|
--surface-tertiary: var(--gray-100);
|
||||||
--surface-tertiary-alt: var(--white);
|
--surface-tertiary-alt: var(--white);
|
||||||
--surface-dialog: var(--white);
|
--surface-dialog: var(--white);
|
||||||
|
--surface-submit: var(--green-500);
|
||||||
--border-light: var(--gray-200);
|
--border-light: var(--gray-200);
|
||||||
--border-medium-alt: var(--gray-300);
|
--border-medium-alt: var(--gray-300);
|
||||||
--border-medium: var(--gray-300);
|
--border-medium: var(--gray-300);
|
||||||
|
|
@ -90,9 +103,11 @@ html {
|
||||||
--surface-primary-alt: var(--gray-850);
|
--surface-primary-alt: var(--gray-850);
|
||||||
--surface-primary-contrast: var(--gray-850);
|
--surface-primary-contrast: var(--gray-850);
|
||||||
--surface-secondary: var(--gray-800);
|
--surface-secondary: var(--gray-800);
|
||||||
|
--surface-secondary-alt: var(--gray-800);
|
||||||
--surface-tertiary: var(--gray-700);
|
--surface-tertiary: var(--gray-700);
|
||||||
--surface-tertiary-alt: var(--gray-700);
|
--surface-tertiary-alt: var(--gray-700);
|
||||||
--surface-dialog: var(--gray-850);
|
--surface-dialog: var(--gray-850);
|
||||||
|
--surface-submit: var(--green-600);
|
||||||
--border-light: var(--gray-700);
|
--border-light: var(--gray-700);
|
||||||
--border-medium-alt: var(--gray-600);
|
--border-medium-alt: var(--gray-600);
|
||||||
--border-medium: var(--gray-600);
|
--border-medium: var(--gray-600);
|
||||||
|
|
@ -112,7 +127,7 @@ html {
|
||||||
--muted-foreground: 0 0% 63.9%;
|
--muted-foreground: 0 0% 63.9%;
|
||||||
--accent: 0 0% 14.9%;
|
--accent: 0 0% 14.9%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 40.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 0 0% 14.9%;
|
--border: 0 0% 14.9%;
|
||||||
--input: 0 0% 14.9%;
|
--input: 0 0% 14.9%;
|
||||||
|
|
@ -2351,7 +2366,7 @@ button.scroll-convo {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
color: black;
|
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 *) {
|
.tooltip:where(.dark, .dark *) {
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,11 @@ module.exports = {
|
||||||
'surface-primary-alt': 'var(--surface-primary-alt)',
|
'surface-primary-alt': 'var(--surface-primary-alt)',
|
||||||
'surface-primary-contrast': 'var(--surface-primary-contrast)',
|
'surface-primary-contrast': 'var(--surface-primary-contrast)',
|
||||||
'surface-secondary': 'var(--surface-secondary)',
|
'surface-secondary': 'var(--surface-secondary)',
|
||||||
|
'surface-secondary-alt': 'var(--surface-secondary-alt)',
|
||||||
'surface-tertiary': 'var(--surface-tertiary)',
|
'surface-tertiary': 'var(--surface-tertiary)',
|
||||||
'surface-tertiary-alt': 'var(--surface-tertiary-alt)',
|
'surface-tertiary-alt': 'var(--surface-tertiary-alt)',
|
||||||
'surface-dialog': 'var(--surface-dialog)',
|
'surface-dialog': 'var(--surface-dialog)',
|
||||||
|
'surface-submit': 'var(--surface-submit)',
|
||||||
'border-light': 'var(--border-light)',
|
'border-light': 'var(--border-light)',
|
||||||
'border-medium': 'var(--border-medium)',
|
'border-medium': 'var(--border-medium)',
|
||||||
'border-medium-alt': 'var(--border-medium-alt)',
|
'border-medium-alt': 'var(--border-medium-alt)',
|
||||||
|
|
|
||||||
|
|
@ -314,7 +314,10 @@ export const getFileConfig = (): Promise<f.FileConfig> => {
|
||||||
return request.get(`${endpoints.files()}/config`);
|
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;
|
const requestConfig = signal ? { signal } : undefined;
|
||||||
return request.postMultiPart(endpoints.images(), data, requestConfig);
|
return request.postMultiPart(endpoints.images(), data, requestConfig);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue