mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 10:50:14 +01:00
🌿 feat: Fork Messages/Conversations (#2617)
* typedef for ImportBatchBuilder * feat: first pass, fork conversations * feat: fork - getMessagesUpToTargetLevel * fix: additional tests and fix getAllMessagesUpToParent * chore: arrow function return * refactor: fork 3 options * chore: remove unused genbuttons * chore: remove unused hover buttons code * feat: fork first pass * wip: fork remember setting * style: user icon * chore: move clear chats to data tab * WIP: fork UI options * feat: data-provider fork types/services/vars and use generic MutationOptions * refactor: use single param for fork option, use enum, fix mongo errors, use Date.now(), add records flag for testing, use endpoint from original convo and messages, pass originalConvo to finishConversation * feat: add fork mutation hook and consolidate type imports * refactor: use enum * feat: first pass, fork mutation * chore: add enum for target level fork option * chore: add enum for target level fork option * show toast when checking remember selection * feat: splitAtTarget * feat: split at target option * feat: navigate to new fork, show toasts, set result query data * feat: hover info for all fork options * refactor: add Messages settings tab * fix(Fork): remember text info * ci: test for single message and is target edge case * feat: additional tests for getAllMessagesUpToParent * ci: additional tests and cycle detection for getMessagesUpToTargetLevel * feat: circular dependency checks for getAllMessagesUpToParent * fix: getMessagesUpToTargetLevel circular dep. check * ci: more tests for getMessagesForConversation * style: hover text for checkbox fork items * refactor: add statefulness to conversation import
This commit is contained in:
parent
c8baceac76
commit
25fceb78b7
37 changed files with 1831 additions and 523 deletions
|
|
@ -1,86 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { useMediaQuery, useGenerationsByLatest } from '~/hooks';
|
||||
import Regenerate from '~/components/Input/Generations/Regenerate';
|
||||
import Continue from '~/components/Input/Generations/Continue';
|
||||
import Stop from '~/components/Input/Generations/Stop';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
type GenerationButtonsProps = {
|
||||
endpoint: string;
|
||||
showPopover?: boolean;
|
||||
opacityClass?: string;
|
||||
};
|
||||
|
||||
export default function GenerationButtons({
|
||||
endpoint,
|
||||
showPopover = false,
|
||||
opacityClass = 'full-opacity',
|
||||
}: GenerationButtonsProps) {
|
||||
const {
|
||||
getMessages,
|
||||
isSubmitting,
|
||||
latestMessage,
|
||||
handleContinue,
|
||||
handleRegenerate,
|
||||
handleStopGenerating,
|
||||
} = useChatContext();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const { continueSupported, regenerateEnabled } = useGenerationsByLatest({
|
||||
endpoint,
|
||||
message: latestMessage as TMessage,
|
||||
isSubmitting,
|
||||
latestMessage,
|
||||
});
|
||||
|
||||
const [userStopped, setUserStopped] = useState(false);
|
||||
const messages = getMessages();
|
||||
|
||||
const handleStop = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setUserStopped(true);
|
||||
handleStopGenerating(e);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
if (userStopped) {
|
||||
timer = setTimeout(() => {
|
||||
setUserStopped(false);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [userStopped]);
|
||||
|
||||
if (isSmallScreen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let button: React.ReactNode = null;
|
||||
|
||||
if (isSubmitting) {
|
||||
button = <Stop onClick={handleStop} />;
|
||||
} else if (userStopped || continueSupported) {
|
||||
button = <Continue onClick={handleContinue} />;
|
||||
} else if (messages && messages.length > 0 && regenerateEnabled) {
|
||||
button = <Regenerate onClick={handleRegenerate} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-0 right-0 z-[62]">
|
||||
<div className="grow" />
|
||||
<div className="flex items-center md:items-end">
|
||||
<div
|
||||
className={cn('option-buttons', showPopover ? '' : opacityClass)}
|
||||
data-projection-id="173"
|
||||
>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { EModelEndpoint } from 'librechat-data-provider';
|
|||
import type { TConversation, TMessage } from 'librechat-data-provider';
|
||||
import { Clipboard, CheckMark, EditIcon, RegenerateIcon, ContinueIcon } from '~/components/svg';
|
||||
import { useGenerationsByLatest, useLocalize } from '~/hooks';
|
||||
import { Fork } from '~/components/Conversations';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
type THoverButtons = {
|
||||
|
|
@ -34,13 +35,14 @@ export default function HoverButtons({
|
|||
const { endpoint: _endpoint, endpointType } = conversation ?? {};
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const { hideEditButton, regenerateEnabled, continueSupported } = useGenerationsByLatest({
|
||||
isEditing,
|
||||
isSubmitting,
|
||||
message,
|
||||
endpoint: endpoint ?? '',
|
||||
latestMessage,
|
||||
});
|
||||
const { hideEditButton, regenerateEnabled, continueSupported, forkingSupported } =
|
||||
useGenerationsByLatest({
|
||||
isEditing,
|
||||
isSubmitting,
|
||||
message,
|
||||
endpoint: endpoint ?? '',
|
||||
latestMessage,
|
||||
});
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -113,6 +115,13 @@ export default function HoverButtons({
|
|||
<ContinueIcon className="h-4 w-4 hover:text-gray-700 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" />
|
||||
</button>
|
||||
) : null}
|
||||
<Fork
|
||||
isLast={isLast}
|
||||
messageId={message.messageId}
|
||||
conversationId={conversation.conversationId}
|
||||
forkingSupported={forkingSupported}
|
||||
latestMessage={latestMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
331
client/src/components/Conversations/Fork.tsx
Normal file
331
client/src/components/Conversations/Fork.tsx
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { GitFork, InfoIcon } from 'lucide-react';
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { ForkOptions, TMessage } from 'librechat-data-provider';
|
||||
import { GitCommit, GitBranchPlus, ListTree } from 'lucide-react';
|
||||
import {
|
||||
Checkbox,
|
||||
HoverCard,
|
||||
HoverCardTrigger,
|
||||
HoverCardPortal,
|
||||
HoverCardContent,
|
||||
} from '~/components/ui';
|
||||
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
|
||||
import { useToastContext, useChatContext } from '~/Providers';
|
||||
import { useLocalize, useNavigateToConvo } from '~/hooks';
|
||||
import { useForkConvoMutation } from '~/data-provider';
|
||||
import { ESide } from '~/common';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
interface PopoverButtonProps {
|
||||
children: React.ReactNode;
|
||||
setting: string;
|
||||
onClick: (setting: string) => void;
|
||||
setActiveSetting: React.Dispatch<React.SetStateAction<string>>;
|
||||
sideOffset?: number;
|
||||
timeoutRef: React.MutableRefObject<NodeJS.Timeout | null>;
|
||||
hoverInfo?: React.ReactNode;
|
||||
hoverTitle?: React.ReactNode;
|
||||
hoverDescription?: React.ReactNode;
|
||||
}
|
||||
|
||||
const optionLabels = {
|
||||
[ForkOptions.DIRECT_PATH]: 'com_ui_fork_visible',
|
||||
[ForkOptions.INCLUDE_BRANCHES]: 'com_ui_fork_branches',
|
||||
[ForkOptions.TARGET_LEVEL]: 'com_ui_fork_all_target',
|
||||
default: 'com_ui_fork_from_message',
|
||||
};
|
||||
|
||||
const PopoverButton: React.FC<PopoverButtonProps> = ({
|
||||
children,
|
||||
setting,
|
||||
onClick,
|
||||
setActiveSetting,
|
||||
sideOffset = 30,
|
||||
timeoutRef,
|
||||
hoverInfo,
|
||||
hoverTitle,
|
||||
hoverDescription,
|
||||
}) => {
|
||||
return (
|
||||
<HoverCard openDelay={200}>
|
||||
<Popover.Close
|
||||
onClick={() => onClick(setting)}
|
||||
onMouseEnter={() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
setActiveSetting(optionLabels[setting]);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setActiveSetting(optionLabels.default);
|
||||
}, 175);
|
||||
}}
|
||||
className="mx-1 max-w-14 flex-1 rounded-lg border-2 bg-white transition duration-300 ease-in-out hover:bg-black dark:border-gray-400 dark:bg-gray-700/95 dark:text-gray-400 hover:dark:border-gray-200 hover:dark:text-gray-200"
|
||||
type="button"
|
||||
>
|
||||
{children}
|
||||
</Popover.Close>
|
||||
{(hoverInfo || hoverTitle || hoverDescription) && (
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent
|
||||
side="right"
|
||||
className="z-[999] w-80 dark:bg-gray-700"
|
||||
sideOffset={sideOffset}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="flex flex-col gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
{hoverInfo && hoverInfo}
|
||||
{hoverTitle && <span className="flex flex-wrap gap-1 font-bold">{hoverTitle}</span>}
|
||||
{hoverDescription && hoverDescription}
|
||||
</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
)}
|
||||
</HoverCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Fork({
|
||||
isLast,
|
||||
messageId,
|
||||
conversationId,
|
||||
forkingSupported,
|
||||
latestMessage,
|
||||
}: {
|
||||
isLast?: boolean;
|
||||
messageId: string;
|
||||
conversationId: string | null;
|
||||
forkingSupported?: boolean;
|
||||
latestMessage: TMessage | null;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { index } = useChatContext();
|
||||
const { showToast } = useToastContext();
|
||||
const [remember, setRemember] = useState(false);
|
||||
const { navigateToConvo } = useNavigateToConvo(index);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [forkSetting, setForkSetting] = useRecoilState(store.forkSetting);
|
||||
const [activeSetting, setActiveSetting] = useState(optionLabels.default);
|
||||
const [splitAtTarget, setSplitAtTarget] = useRecoilState(store.splitAtTarget);
|
||||
const [rememberGlobal, setRememberGlobal] = useRecoilState(store.rememberForkOption);
|
||||
const forkConvo = useForkConvoMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
navigateToConvo(data.conversation);
|
||||
showToast({
|
||||
message: localize('com_ui_fork_success'),
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
},
|
||||
onMutate: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_fork_processing'),
|
||||
status: 'info',
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_fork_error'),
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!forkingSupported || !conversationId || !messageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onClick = (option: string) => {
|
||||
if (remember) {
|
||||
setRememberGlobal(true);
|
||||
setForkSetting(option);
|
||||
}
|
||||
|
||||
forkConvo.mutate({
|
||||
messageId,
|
||||
conversationId,
|
||||
option,
|
||||
splitAtTarget,
|
||||
latestMessageId: latestMessage?.messageId,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
'hover-button active rounded-md p-1 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible ',
|
||||
'data-[state=open]:active data-[state=open]:bg-gray-200 data-[state=open]:text-gray-700 data-[state=open]:dark:bg-gray-700 data-[state=open]:dark:text-gray-200',
|
||||
!isLast ? 'data-[state=open]:opacity-100 md:opacity-0 md:group-hover:opacity-100' : '',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (rememberGlobal) {
|
||||
e.preventDefault();
|
||||
forkConvo.mutate({
|
||||
messageId,
|
||||
splitAtTarget,
|
||||
conversationId,
|
||||
option: forkSetting,
|
||||
latestMessageId: latestMessage?.messageId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
title={localize('com_ui_continue')}
|
||||
>
|
||||
<GitFork className="h-4 w-4 hover:text-gray-700 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<div dir="ltr">
|
||||
<Popover.Content
|
||||
side="top"
|
||||
role="menu"
|
||||
className="bg-token-surface-primary flex min-h-[120px] min-w-[215px] flex-col gap-3 overflow-hidden rounded-lg bg-white p-2 px-3 shadow-lg dark:bg-gray-700/95"
|
||||
style={{ outline: 'none', pointerEvents: 'auto', boxSizing: 'border-box' }}
|
||||
tabIndex={-1}
|
||||
sideOffset={5}
|
||||
align="center"
|
||||
>
|
||||
<div className="flex h-6 w-full items-center justify-center text-sm dark:text-gray-200">
|
||||
{localize(activeSetting)}
|
||||
<HoverCard openDelay={50}>
|
||||
<HoverCardTrigger asChild>
|
||||
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent
|
||||
side="right"
|
||||
className="z-[999] w-80 dark:bg-gray-700"
|
||||
sideOffset={19}
|
||||
>
|
||||
<div className="flex flex-col gap-2 space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<span>{localize('com_ui_fork_info_1')}</span>
|
||||
<span>{localize('com_ui_fork_info_2')}</span>
|
||||
<span>
|
||||
{localize('com_ui_fork_info_3', localize('com_ui_fork_split_target'))}
|
||||
</span>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</HoverCard>
|
||||
</div>
|
||||
<div className="flex h-full w-full items-center justify-center gap-1">
|
||||
<PopoverButton
|
||||
sideOffset={155}
|
||||
setActiveSetting={setActiveSetting}
|
||||
timeoutRef={timeoutRef}
|
||||
onClick={onClick}
|
||||
setting={ForkOptions.DIRECT_PATH}
|
||||
hoverTitle={
|
||||
<>
|
||||
<GitCommit className="h-5 w-5 rotate-90" />
|
||||
{localize(optionLabels[ForkOptions.DIRECT_PATH])}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_visible')}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<GitCommit className="h-full w-full rotate-90 p-2" />
|
||||
</HoverCardTrigger>
|
||||
</PopoverButton>
|
||||
<PopoverButton
|
||||
sideOffset={90}
|
||||
setActiveSetting={setActiveSetting}
|
||||
timeoutRef={timeoutRef}
|
||||
onClick={onClick}
|
||||
setting={ForkOptions.INCLUDE_BRANCHES}
|
||||
hoverTitle={
|
||||
<>
|
||||
<GitBranchPlus className="h-4 w-4 rotate-180" />
|
||||
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_branches')}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<GitBranchPlus className="h-full w-full rotate-180 p-2" />
|
||||
</HoverCardTrigger>
|
||||
</PopoverButton>
|
||||
<PopoverButton
|
||||
sideOffset={25}
|
||||
setActiveSetting={setActiveSetting}
|
||||
timeoutRef={timeoutRef}
|
||||
onClick={onClick}
|
||||
setting={ForkOptions.TARGET_LEVEL}
|
||||
hoverTitle={
|
||||
<>
|
||||
<ListTree className="h-5 w-5" />
|
||||
{`${localize(optionLabels[ForkOptions.TARGET_LEVEL])} (${localize(
|
||||
'com_endpoint_default',
|
||||
)})`}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_target')}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<ListTree className="h-full w-full p-2" />
|
||||
</HoverCardTrigger>
|
||||
</PopoverButton>
|
||||
</div>
|
||||
<HoverCard openDelay={50}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="flex h-6 w-full items-center justify-start text-sm dark:text-gray-300 dark:hover:text-gray-200">
|
||||
<Checkbox
|
||||
checked={splitAtTarget}
|
||||
onCheckedChange={(checked: boolean) => setSplitAtTarget(checked)}
|
||||
className="m-2 transition duration-300 ease-in-out"
|
||||
/>
|
||||
{localize('com_ui_fork_split_target')}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
side={ESide.Right}
|
||||
description="com_ui_fork_info_start"
|
||||
langCode={true}
|
||||
sideOffset={20}
|
||||
/>
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={50}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="flex h-6 w-full items-center justify-start text-sm dark:text-gray-300 dark:hover:text-gray-200">
|
||||
<Checkbox
|
||||
checked={remember}
|
||||
onCheckedChange={(checked: boolean) => {
|
||||
if (checked) {
|
||||
showToast({
|
||||
message: localize('com_ui_fork_remember_checked'),
|
||||
status: 'info',
|
||||
});
|
||||
}
|
||||
setRemember(checked);
|
||||
}}
|
||||
className="m-2 transition duration-300 ease-in-out"
|
||||
/>
|
||||
{localize('com_ui_fork_remember')}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
side={ESide.Right}
|
||||
description="com_ui_fork_info_remember"
|
||||
langCode={true}
|
||||
sideOffset={20}
|
||||
/>
|
||||
</HoverCard>
|
||||
</Popover.Content>
|
||||
</div>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export { default as Fork } from './Fork';
|
||||
export { default as Pages } from './Pages';
|
||||
export { default as Conversation } from './Conversation';
|
||||
export { default as RenameButton } from './RenameButton';
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { cn, removeFocusOutlines } from '~/utils/';
|
||||
|
||||
type GenerationButtonsProps = {
|
||||
showPopover: boolean;
|
||||
opacityClass: string;
|
||||
};
|
||||
|
||||
export default function GenerationButtons({ showPopover, opacityClass }: GenerationButtonsProps) {
|
||||
return (
|
||||
<div className="absolute bottom-4 right-0 z-[62]">
|
||||
<div className="grow"></div>
|
||||
<div className="flex items-center md:items-end">
|
||||
<div
|
||||
className={cn('option-buttons', showPopover ? '' : opacityClass)}
|
||||
data-projection-id="173"
|
||||
>
|
||||
{/* <button
|
||||
className={cn(
|
||||
'custom-btn btn-neutral relative -z-0 whitespace-nowrap border-0 md:border',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-3 w-3 flex-shrink-0"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline points="1 4 1 10 7 10"></polyline>
|
||||
<polyline points="23 20 23 14 17 14"></polyline>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
|
||||
</svg>
|
||||
Regenerate
|
||||
</div>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import type { TConversation, TMessage } from 'librechat-data-provider';
|
||||
import { Clipboard, CheckMark, EditIcon, RegenerateIcon, ContinueIcon } from '~/components/svg';
|
||||
import { useGenerations, useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
type THoverButtons = {
|
||||
isEditing: boolean;
|
||||
enterEdit: (cancel?: boolean) => void;
|
||||
copyToClipboard: (setIsCopied: React.Dispatch<React.SetStateAction<boolean>>) => void;
|
||||
conversation: TConversation | null;
|
||||
isSubmitting: boolean;
|
||||
message: TMessage;
|
||||
regenerate: () => void;
|
||||
handleContinue: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
};
|
||||
|
||||
export default function HoverButtons({
|
||||
isEditing,
|
||||
enterEdit,
|
||||
copyToClipboard,
|
||||
conversation,
|
||||
isSubmitting,
|
||||
message,
|
||||
regenerate,
|
||||
handleContinue,
|
||||
}: THoverButtons) {
|
||||
const localize = useLocalize();
|
||||
const { endpoint } = conversation ?? {};
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const { hideEditButton, regenerateEnabled, continueSupported } = useGenerations({
|
||||
isEditing,
|
||||
isSubmitting,
|
||||
message,
|
||||
endpoint: endpoint ?? '',
|
||||
});
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isCreatedByUser } = message;
|
||||
|
||||
const onEdit = () => {
|
||||
if (isEditing) {
|
||||
return enterEdit(true);
|
||||
}
|
||||
enterEdit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="visible mt-2 flex justify-center gap-3 self-end text-gray-400 md:gap-4 lg:absolute lg:right-0 lg:top-0 lg:mt-0 lg:translate-x-full lg:gap-1 lg:self-center lg:pl-2">
|
||||
<button
|
||||
className={cn(
|
||||
'hover-button rounded-md p-1 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible',
|
||||
isCreatedByUser ? '' : 'active',
|
||||
hideEditButton ? 'opacity-0' : '',
|
||||
isEditing ? 'active bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200' : '',
|
||||
)}
|
||||
onClick={onEdit}
|
||||
type="button"
|
||||
title={localize('com_ui_edit')}
|
||||
disabled={hideEditButton}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
<button
|
||||
className={cn(
|
||||
'hover-button rounded-md p-1 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible',
|
||||
isCreatedByUser ? '' : 'active',
|
||||
)}
|
||||
onClick={() => copyToClipboard(setIsCopied)}
|
||||
type="button"
|
||||
title={
|
||||
isCopied ? localize('com_ui_copied_to_clipboard') : localize('com_ui_copy_to_clipboard')
|
||||
}
|
||||
>
|
||||
{isCopied ? <CheckMark /> : <Clipboard />}
|
||||
</button>
|
||||
{regenerateEnabled ? (
|
||||
<button
|
||||
className="hover-button active rounded-md p-1 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible"
|
||||
onClick={regenerate}
|
||||
type="button"
|
||||
title={localize('com_ui_regenerate')}
|
||||
>
|
||||
<RegenerateIcon className="hover:text-gray-700 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" />
|
||||
</button>
|
||||
) : null}
|
||||
{continueSupported ? (
|
||||
<button
|
||||
className="hover-button active rounded-md p-1 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible "
|
||||
onClick={handleContinue}
|
||||
type="button"
|
||||
title={localize('com_ui_continue')}
|
||||
>
|
||||
<ContinueIcon className="h-4 w-4 hover:text-gray-700 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -79,11 +79,11 @@ function NavLinks() {
|
|||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgb(121, 137, 255)',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
|
||||
}}
|
||||
className="relative flex h-8 w-8 items-center justify-center rounded-full p-1 text-white"
|
||||
className="relative flex items-center justify-center rounded-full p-1 text-white"
|
||||
>
|
||||
<UserIcon />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import type { TDialogProps } from '~/common';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
|
||||
import { GearIcon, DataIcon, UserIcon, ExperimentIcon } from '~/components/svg';
|
||||
import { General, Beta, Data, Account } from './SettingsTabs';
|
||||
import { General, Messages, Beta, Data, Account } from './SettingsTabs';
|
||||
import { useMediaQuery, useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
|
@ -54,6 +55,20 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
<GearIcon />
|
||||
{localize('com_nav_setting_general')}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className={cn(
|
||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
||||
isSmallScreen
|
||||
? 'flex-1 flex-col items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
|
||||
: 'bg-white radix-state-active:bg-gray-200',
|
||||
isSmallScreen ? '' : 'dark:bg-gray-700',
|
||||
)}
|
||||
value={SettingsTabValues.MESSAGES}
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
<MessageSquare className="icon-sm" />
|
||||
{localize('com_endpoint_messages')}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className={cn(
|
||||
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
|
||||
|
|
@ -98,6 +113,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
|
|||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<General />
|
||||
<Messages />
|
||||
<Beta />
|
||||
<Data />
|
||||
<Account />
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const DangerButton = (props: TDangerButtonProps, ref: ForwardedRef<HTMLButtonEle
|
|||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
' btn btn-danger relative border-none bg-red-700 text-white hover:bg-red-800 dark:hover:bg-red-800',
|
||||
' btn btn-danger relative min-w-[70px] border-none bg-red-700 text-white hover:bg-red-800 dark:hover:bg-red-800',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
29
client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx
Normal file
29
client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type { TDangerButtonProps } from '~/common';
|
||||
import DangerButton from '../DangerButton';
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@ 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 './General';
|
||||
import { ClearChatsButton } from './ClearChats';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
describe('ClearChatsButton', () => {
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import {
|
||||
useRevokeAllUserKeysMutation,
|
||||
useRevokeUserKeyMutation,
|
||||
useRevokeAllUserKeysMutation,
|
||||
useClearConversationsMutation,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { useOnClickOutside } from '~/hooks';
|
||||
import DangerButton from '../DangerButton';
|
||||
import { useConversation, useConversations, useOnClickOutside } from '~/hooks';
|
||||
import ImportConversations from './ImportConversations';
|
||||
import { ClearChatsButton } from './ClearChats';
|
||||
import DangerButton from '../DangerButton';
|
||||
|
||||
export const RevokeKeysButton = ({
|
||||
showText = true,
|
||||
|
|
@ -20,42 +22,43 @@ export const RevokeKeysButton = ({
|
|||
all?: boolean;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
const revokeKeyMutation = useRevokeUserKeyMutation(endpoint);
|
||||
const revokeKeysMutation = useRevokeAllUserKeysMutation();
|
||||
const [confirmRevoke, setConfirmRevoke] = useState(false);
|
||||
|
||||
const contentRef = useRef(null);
|
||||
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
|
||||
const revokeKeysMutation = useRevokeAllUserKeysMutation();
|
||||
const revokeKeyMutation = useRevokeUserKeyMutation(endpoint);
|
||||
|
||||
const revokeContentRef = useRef(null);
|
||||
useOnClickOutside(revokeContentRef, () => confirmRevoke && setConfirmRevoke(false), []);
|
||||
|
||||
const revokeAllUserKeys = useCallback(() => {
|
||||
if (confirmClear) {
|
||||
if (confirmRevoke) {
|
||||
revokeKeysMutation.mutate({});
|
||||
setConfirmClear(false);
|
||||
setConfirmRevoke(false);
|
||||
} else {
|
||||
setConfirmClear(true);
|
||||
setConfirmRevoke(true);
|
||||
}
|
||||
}, [confirmClear, revokeKeysMutation]);
|
||||
}, [confirmRevoke, revokeKeysMutation]);
|
||||
|
||||
const revokeUserKey = useCallback(() => {
|
||||
if (!endpoint) {
|
||||
return;
|
||||
} else if (confirmClear) {
|
||||
} else if (confirmRevoke) {
|
||||
revokeKeyMutation.mutate({});
|
||||
setConfirmClear(false);
|
||||
setConfirmRevoke(false);
|
||||
} else {
|
||||
setConfirmClear(true);
|
||||
setConfirmRevoke(true);
|
||||
}
|
||||
}, [confirmClear, revokeKeyMutation, endpoint]);
|
||||
}, [confirmRevoke, revokeKeyMutation, endpoint]);
|
||||
|
||||
const onClick = all ? revokeAllUserKeys : revokeUserKey;
|
||||
|
||||
return (
|
||||
<DangerButton
|
||||
ref={contentRef}
|
||||
ref={revokeContentRef}
|
||||
showText={showText}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
confirmClear={confirmClear}
|
||||
confirmClear={confirmRevoke}
|
||||
id={'revoke-all-user-keys'}
|
||||
actionTextCode={'com_ui_revoke'}
|
||||
infoTextCode={'com_ui_revoke_info'}
|
||||
|
|
@ -67,18 +70,54 @@ export const RevokeKeysButton = ({
|
|||
};
|
||||
|
||||
function Data() {
|
||||
const dataTabRef = useRef(null);
|
||||
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 (
|
||||
<Tabs.Content
|
||||
value={SettingsTabValues.DATA}
|
||||
role="tabpanel"
|
||||
className="w-full md:min-h-[300px]"
|
||||
ref={dataTabRef}
|
||||
>
|
||||
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<RevokeKeysButton all={true} />
|
||||
<ImportConversations />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<ImportConversations />
|
||||
<RevokeKeysButton all={true} />
|
||||
</div>
|
||||
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<ClearChatsButton
|
||||
confirmClear={confirmClearConvos}
|
||||
onClick={clearConvos}
|
||||
showText={true}
|
||||
mutation={clearConvosMutation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { useState } from 'react';
|
||||
import { Import } from 'lucide-react';
|
||||
import { cn } from '~/utils';
|
||||
import { useUploadConversationsMutation } from '~/data-provider';
|
||||
import { useLocalize, useConversations } from '~/hooks';
|
||||
import { useState } from 'react';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
function ImportConversations() {
|
||||
const localize = useLocalize();
|
||||
|
||||
const { showToast } = useToastContext();
|
||||
const [, setErrors] = useState<string[]>([]);
|
||||
const [allowImport, setAllowImport] = useState(true);
|
||||
const setError = (error: string) => setErrors((prevErrors) => [...prevErrors, error]);
|
||||
const { refreshConversations } = useConversations();
|
||||
|
||||
|
|
@ -17,9 +19,11 @@ function ImportConversations() {
|
|||
onSuccess: () => {
|
||||
refreshConversations();
|
||||
showToast({ message: localize('com_ui_import_conversation_success') });
|
||||
setAllowImport(true);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error: ', error);
|
||||
setAllowImport(true);
|
||||
setError(
|
||||
(error as { response: { data: { message?: string } } })?.response?.data?.message ??
|
||||
'An error occurred while uploading the file.',
|
||||
|
|
@ -33,6 +37,9 @@ function ImportConversations() {
|
|||
showToast({ message: localize('com_ui_import_conversation_error'), status: 'error' });
|
||||
}
|
||||
},
|
||||
onMutate: () => {
|
||||
setAllowImport(false);
|
||||
},
|
||||
});
|
||||
|
||||
const startUpload = async (file: File) => {
|
||||
|
|
@ -43,8 +50,6 @@ function ImportConversations() {
|
|||
};
|
||||
|
||||
const handleFiles = async (_file: File) => {
|
||||
console.log('Handling files...');
|
||||
|
||||
/* Process files */
|
||||
try {
|
||||
await startUpload(_file);
|
||||
|
|
@ -55,7 +60,6 @@ function ImportConversations() {
|
|||
};
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
console.log('file change');
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
handleFiles(file);
|
||||
|
|
@ -67,12 +71,17 @@ function ImportConversations() {
|
|||
<span>{localize('com_ui_import_conversation_info')}</span>
|
||||
<label
|
||||
htmlFor={'import-conversations-file'}
|
||||
className="flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal transition-colors hover:bg-gray-100 hover:text-green-700 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-green-500"
|
||||
className="flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-3 text-xs font-medium font-normal transition-colors hover:bg-gray-100 hover:text-green-700 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-green-500"
|
||||
>
|
||||
<Import className="mr-1 flex w-[22px] items-center stroke-1" />
|
||||
{allowImport ? (
|
||||
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" />
|
||||
) : (
|
||||
<Spinner className="mr-1 w-4" />
|
||||
)}
|
||||
<span>{localize('com_ui_import_conversation')}</span>
|
||||
<input
|
||||
id={'import-conversations-file'}
|
||||
disabled={!allowImport}
|
||||
value=""
|
||||
type="file"
|
||||
className={cn('hidden')}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,11 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import React, { useState, useContext, useCallback, useRef } from 'react';
|
||||
import { useClearConversationsMutation } from 'librechat-data-provider/react-query';
|
||||
import {
|
||||
ThemeContext,
|
||||
useLocalize,
|
||||
useOnClickOutside,
|
||||
useConversation,
|
||||
useConversations,
|
||||
useLocalStorage,
|
||||
} from '~/hooks';
|
||||
import React, { useContext, useCallback, useRef } from 'react';
|
||||
import type { TDangerButtonProps } from '~/common';
|
||||
import { ThemeContext, useLocalize, useLocalStorage } from '~/hooks';
|
||||
import HideSidePanelSwitch from './HideSidePanelSwitch';
|
||||
import AutoScrollSwitch from './AutoScrollSwitch';
|
||||
import SendMessageKeyEnter from './EnterToSend';
|
||||
import ShowCodeSwitch from './ShowCodeSwitch';
|
||||
import { Dropdown } from '~/components/ui';
|
||||
import DangerButton from '../DangerButton';
|
||||
import store from '~/store';
|
||||
|
|
@ -119,33 +109,11 @@ export const LangSelector = ({
|
|||
|
||||
function General() {
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
const clearConvosMutation = useClearConversationsMutation();
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
|
||||
const [langcode, setLangcode] = useRecoilState(store.lang);
|
||||
const [selectedLang, setSelectedLang] = useLocalStorage('selectedLang', langcode);
|
||||
const { newConversation } = useConversation();
|
||||
const { refreshConversations } = useConversations();
|
||||
|
||||
const contentRef = useRef(null);
|
||||
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
|
||||
|
||||
const clearConvos = () => {
|
||||
if (confirmClear) {
|
||||
console.log('Clearing conversations...');
|
||||
setConfirmClear(false);
|
||||
clearConvosMutation.mutate(
|
||||
{},
|
||||
{
|
||||
onSuccess: () => {
|
||||
newConversation();
|
||||
refreshConversations();
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
setConfirmClear(true);
|
||||
}
|
||||
};
|
||||
|
||||
const changeTheme = useCallback(
|
||||
(value: string) => {
|
||||
|
|
@ -183,28 +151,14 @@ function General() {
|
|||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<LangSelector langcode={selectedLang} onChange={changeLang} />
|
||||
</div>
|
||||
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<AutoScrollSwitch />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<SendMessageKeyEnter />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<ShowCodeSwitch />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<HideSidePanelSwitch />
|
||||
</div>
|
||||
{/* Clear Chats should be last */}
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<ClearChatsButton
|
||||
confirmClear={confirmClear}
|
||||
onClick={clearConvos}
|
||||
showText={true}
|
||||
mutation={clearConvosMutation}
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
</div> */}
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,4 +30,4 @@ export default function SendMessageKeyEnter({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { useRecoilState } from 'recoil';
|
||||
import { ForkOptions } from 'librechat-data-provider';
|
||||
import { Dropdown, Switch } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export const ForkSettings = () => {
|
||||
const localize = useLocalize();
|
||||
const [forkSetting, setForkSetting] = useRecoilState(store.forkSetting);
|
||||
const [splitAtTarget, setSplitAtTarget] = useRecoilState(store.splitAtTarget);
|
||||
const [remember, setRemember] = useRecoilState<boolean>(store.rememberForkOption);
|
||||
|
||||
const forkOptions = [
|
||||
{ value: ForkOptions.DIRECT_PATH, display: localize('com_ui_fork_visible') },
|
||||
{ value: ForkOptions.INCLUDE_BRANCHES, display: localize('com_ui_fork_branches') },
|
||||
{ value: ForkOptions.TARGET_LEVEL, display: localize('com_ui_fork_all_target') },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div> {localize('com_ui_fork_change_default')} </div>
|
||||
<Dropdown
|
||||
value={forkSetting}
|
||||
onChange={setForkSetting}
|
||||
options={forkOptions}
|
||||
width={200}
|
||||
testId="fork-setting-dropdown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div> {localize('com_ui_fork_default')} </div>
|
||||
<Switch
|
||||
id="rememberForkOption"
|
||||
checked={remember}
|
||||
onCheckedChange={setRemember}
|
||||
className="ml-4 mt-2"
|
||||
data-testid="rememberForkOption"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div> {localize('com_ui_fork_split_target_setting')} </div>
|
||||
<Switch
|
||||
id="splitAtTarget"
|
||||
checked={splitAtTarget}
|
||||
onCheckedChange={setSplitAtTarget}
|
||||
className="ml-4 mt-2"
|
||||
data-testid="splitAtTarget"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
28
client/src/components/Nav/SettingsTabs/Messages/Messages.tsx
Normal file
28
client/src/components/Nav/SettingsTabs/Messages/Messages.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { memo } from 'react';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { SettingsTabValues } from 'librechat-data-provider';
|
||||
import SendMessageKeyEnter from './EnterToSend';
|
||||
import ShowCodeSwitch from './ShowCodeSwitch';
|
||||
import { ForkSettings } from './ForkSettings';
|
||||
|
||||
function Messages() {
|
||||
return (
|
||||
<Tabs.Content
|
||||
value={SettingsTabValues.MESSAGES}
|
||||
role="tabpanel"
|
||||
className="w-full md:min-h-[300px]"
|
||||
>
|
||||
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-50">
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<SendMessageKeyEnter />
|
||||
</div>
|
||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||
<ShowCodeSwitch />
|
||||
</div>
|
||||
<ForkSettings />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Messages);
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export { default as General } from './General/General';
|
||||
export { default as Messages } from './Messages/Messages';
|
||||
export { ClearChatsButton } from './General/General';
|
||||
export { default as Data } from './Data/Data';
|
||||
export { default as Beta } from './Beta/Beta';
|
||||
|
|
|
|||
|
|
@ -6,15 +6,20 @@ import { ESide } from '~/common';
|
|||
type TOptionHoverProps = {
|
||||
description: string;
|
||||
langCode?: boolean;
|
||||
sideOffset?: number;
|
||||
side: ESide;
|
||||
};
|
||||
|
||||
function OptionHover({ side, description, langCode }: TOptionHoverProps) {
|
||||
function OptionHover({ side, description, langCode, sideOffset = 30 }: TOptionHoverProps) {
|
||||
const localize = useLocalize();
|
||||
const text = langCode ? localize(description) : description;
|
||||
return (
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent side={side} className="z-[999] w-80 dark:bg-gray-700" sideOffset={30}>
|
||||
<HoverCardContent
|
||||
side={side}
|
||||
className="z-[999] w-80 dark:bg-gray-700"
|
||||
sideOffset={sideOffset}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{text}</p>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue