mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
💬 fix: Temporary Chat PR's broken components and improved UI (#5705)
* 💬 fix: Temporary Chat PR's broken components and improved UI * 💬 fix: bring back hover effect on AudioRecorder button * style: adjust position of Mention component popover * refactor: PromptsCommand typing and style position * refactor: virtualize mention UI --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
63afb317c6
commit
70e410f38b
11 changed files with 196 additions and 86 deletions
|
|
@ -13,7 +13,6 @@ export default function AudioRecorder({
|
||||||
methods,
|
methods,
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
isTemporary = false,
|
|
||||||
}: {
|
}: {
|
||||||
isRTL: boolean;
|
isRTL: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
@ -21,7 +20,6 @@ export default function AudioRecorder({
|
||||||
methods: ReturnType<typeof useChatFormContext>;
|
methods: ReturnType<typeof useChatFormContext>;
|
||||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
isTemporary?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const { setValue, reset } = methods;
|
const { setValue, reset } = methods;
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -78,11 +76,7 @@ export default function AudioRecorder({
|
||||||
if (isLoading === true) {
|
if (isLoading === true) {
|
||||||
return <Spinner className="stroke-gray-700 dark:stroke-gray-300" />;
|
return <Spinner className="stroke-gray-700 dark:stroke-gray-300" />;
|
||||||
}
|
}
|
||||||
return (
|
return <ListeningIcon className="stroke-gray-700 dark:stroke-gray-300" />;
|
||||||
<ListeningIcon
|
|
||||||
className={cn(isTemporary ? 'stroke-white' : 'stroke-gray-700 dark:stroke-gray-300')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { cn, removeFocusRings, checkIfScrollable } from '~/utils';
|
||||||
import FileFormWrapper from './Files/FileFormWrapper';
|
import FileFormWrapper from './Files/FileFormWrapper';
|
||||||
import { TextareaAutosize } from '~/components/ui';
|
import { TextareaAutosize } from '~/components/ui';
|
||||||
import { useGetFileConfig } from '~/data-provider';
|
import { useGetFileConfig } from '~/data-provider';
|
||||||
|
import { TemporaryChat } from './TemporaryChat';
|
||||||
import TextareaHeader from './TextareaHeader';
|
import TextareaHeader from './TextareaHeader';
|
||||||
import PromptsCommand from './PromptsCommand';
|
import PromptsCommand from './PromptsCommand';
|
||||||
import AudioRecorder from './AudioRecorder';
|
import AudioRecorder from './AudioRecorder';
|
||||||
|
|
@ -47,7 +48,7 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
||||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||||
const isTemporary = useRecoilValue(store.isTemporary);
|
const [isTemporaryChat, setIsTemporaryChat] = useRecoilState<boolean>(store.isTemporary);
|
||||||
|
|
||||||
const isSearching = useRecoilValue(store.isSearching);
|
const isSearching = useRecoilValue(store.isSearching);
|
||||||
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
||||||
|
|
@ -145,11 +146,8 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
const isUploadDisabled: boolean = endpointFileConfig?.disabled ?? false;
|
const isUploadDisabled: boolean = endpointFileConfig?.disabled ?? false;
|
||||||
|
|
||||||
const baseClasses = cn(
|
const baseClasses = cn(
|
||||||
'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)]',
|
'md:py-3.5 m-0 w-full resize-none py-[13px] bg-surface-tertiary placeholder-black/50 dark:placeholder-white/50 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]',
|
||||||
isCollapsed ? 'max-h-[52px]' : 'max-h-[65vh] md:max-h-[75vh]',
|
isCollapsed ? 'max-h-[52px]' : 'max-h-[65vh] md:max-h-[75vh]',
|
||||||
isTemporary
|
|
||||||
? 'bg-gray-600 text-white placeholder-white/20'
|
|
||||||
: 'bg-surface-tertiary placeholder-black/50 dark:placeholder-white/50',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const uploadActive = endpointSupportsFiles && !isUploadDisabled;
|
const uploadActive = endpointSupportsFiles && !isUploadDisabled;
|
||||||
|
|
@ -185,12 +183,11 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} />
|
<PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} />
|
||||||
<div
|
<div className="transitional-all relative flex w-full flex-grow flex-col overflow-hidden rounded-3xl bg-surface-tertiary text-text-primary duration-200">
|
||||||
className={cn(
|
<TemporaryChat
|
||||||
'transitional-all relative flex w-full flex-grow flex-col overflow-hidden rounded-3xl text-text-primary ',
|
isTemporaryChat={isTemporaryChat}
|
||||||
isTemporary ? 'text-white' : 'duration-200',
|
setIsTemporaryChat={setIsTemporaryChat}
|
||||||
)}
|
/>
|
||||||
>
|
|
||||||
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||||
<FileFormWrapper disableInputs={disableInputs}>
|
<FileFormWrapper disableInputs={disableInputs}>
|
||||||
{endpoint && (
|
{endpoint && (
|
||||||
|
|
@ -243,7 +240,6 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
textAreaRef={textAreaRef}
|
textAreaRef={textAreaRef}
|
||||||
disabled={!!disableInputs}
|
disabled={!!disableInputs}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
isTemporary={isTemporary}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{TextToSpeech && automaticPlayback && <StreamAudio index={index} />}
|
{TextToSpeech && automaticPlayback && <StreamAudio index={index} />}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { AutoSizer, List } from 'react-virtualized';
|
||||||
import { EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { SetterOrUpdater } from 'recoil';
|
import type { SetterOrUpdater } from 'recoil';
|
||||||
import type { MentionOption, ConvoGenerator } from '~/common';
|
import type { MentionOption, ConvoGenerator } from '~/common';
|
||||||
|
|
@ -9,6 +10,8 @@ import { useLocalize, useCombobox } from '~/hooks';
|
||||||
import { removeCharIfLast } from '~/utils';
|
import { removeCharIfLast } from '~/utils';
|
||||||
import MentionItem from './MentionItem';
|
import MentionItem from './MentionItem';
|
||||||
|
|
||||||
|
const ROW_HEIGHT = 40;
|
||||||
|
|
||||||
export default function Mention({
|
export default function Mention({
|
||||||
setShowMentionPopover,
|
setShowMentionPopover,
|
||||||
newConversation,
|
newConversation,
|
||||||
|
|
@ -121,8 +124,41 @@ export default function Mention({
|
||||||
currentActiveItem?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
currentActiveItem?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||||
}, [type, activeIndex]);
|
}, [type, activeIndex]);
|
||||||
|
|
||||||
|
const rowRenderer = ({
|
||||||
|
index,
|
||||||
|
key,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
index: number;
|
||||||
|
key: string;
|
||||||
|
style: React.CSSProperties;
|
||||||
|
}) => {
|
||||||
|
const mention = matches[index] as MentionOption;
|
||||||
|
return (
|
||||||
|
<MentionItem
|
||||||
|
type={type}
|
||||||
|
index={index}
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
timeoutRef.current = null;
|
||||||
|
handleSelect(mention);
|
||||||
|
}}
|
||||||
|
name={mention.label ?? ''}
|
||||||
|
icon={mention.icon}
|
||||||
|
description={mention.description}
|
||||||
|
isActive={index === activeIndex}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-16 z-10 w-full space-y-2">
|
<div className="absolute bottom-14 z-10 w-full space-y-2">
|
||||||
<div className="popover border-token-border-light rounded-2xl border bg-white p-2 shadow-lg dark:bg-gray-700">
|
<div className="popover border-token-border-light rounded-2xl border bg-white p-2 shadow-lg dark:bg-gray-700">
|
||||||
<input
|
<input
|
||||||
// The user expects focus to transition to the input field when the popover is opened
|
// The user expects focus to transition to the input field when the popover is opened
|
||||||
|
|
@ -167,27 +203,20 @@ export default function Mention({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="max-h-40 overflow-y-auto">
|
<div className="max-h-40">
|
||||||
{(matches as MentionOption[]).map((mention, index) => (
|
<AutoSizer disableHeight>
|
||||||
<MentionItem
|
{({ width }) => (
|
||||||
type={type}
|
<List
|
||||||
index={index}
|
width={width}
|
||||||
key={`${mention.value}-${index}`}
|
overscanRowCount={5}
|
||||||
onClick={(e) => {
|
rowHeight={ROW_HEIGHT}
|
||||||
e.preventDefault();
|
rowCount={matches.length}
|
||||||
e.stopPropagation();
|
rowRenderer={rowRenderer}
|
||||||
if (timeoutRef.current) {
|
scrollToIndex={activeIndex}
|
||||||
clearTimeout(timeoutRef.current);
|
height={Math.min(matches.length * ROW_HEIGHT, 160)}
|
||||||
}
|
/>
|
||||||
timeoutRef.current = null;
|
)}
|
||||||
handleSelect(mention);
|
</AutoSizer>
|
||||||
}}
|
|
||||||
name={mention.label ?? ''}
|
|
||||||
icon={mention.icon}
|
|
||||||
description={mention.description}
|
|
||||||
isActive={index === activeIndex}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export interface MentionItemProps {
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MentionItem({
|
export default function MentionItem({
|
||||||
|
|
@ -19,21 +20,28 @@ export default function MentionItem({
|
||||||
icon,
|
icon,
|
||||||
isActive,
|
isActive,
|
||||||
description,
|
description,
|
||||||
|
style,
|
||||||
type = 'mention',
|
type = 'mention',
|
||||||
}: MentionItemProps) {
|
}: MentionItemProps) {
|
||||||
return (
|
return (
|
||||||
<button tabIndex={index} onClick={onClick} id={`${type}-item-${index}`} className="w-full">
|
<button
|
||||||
|
tabIndex={index}
|
||||||
|
onClick={onClick}
|
||||||
|
id={`${type}-item-${index}`}
|
||||||
|
className="w-full"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-token-text-primary bg-token-main-surface-secondary group flex h-10 items-center gap-2 rounded-lg px-2 text-sm font-medium hover:bg-surface-secondary',
|
'text-token-text-primary bg-token-main-surface-secondary group flex h-10 items-center gap-2 rounded-lg px-2 text-sm font-medium hover:bg-surface-secondary',
|
||||||
isActive ? 'bg-surface-active' : 'bg-transparent',
|
isActive === true ? 'bg-surface-active' : 'bg-transparent',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center">{icon}</div>
|
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center">{icon}</div>
|
||||||
<div className="flex min-w-0 flex-grow items-center justify-between">
|
<div className="flex min-w-0 flex-grow items-center justify-between">
|
||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
<span className="font-medium">{name}</span>
|
<span className="font-medium">{name}</span>
|
||||||
{description ? (
|
{description != null && description ? (
|
||||||
<span className="text-token-text-tertiary ml-2 text-sm font-light">
|
<span className="text-token-text-tertiary ml-2 text-sm font-light">
|
||||||
{description}
|
{description}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
|
import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
|
||||||
|
import { AutoSizer, List } from 'react-virtualized';
|
||||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
import type { TPromptGroup } from 'librechat-data-provider';
|
import type { TPromptGroup } from 'librechat-data-provider';
|
||||||
|
|
@ -42,6 +43,8 @@ const PopoverContainer = memo(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ROW_HEIGHT = 40;
|
||||||
|
|
||||||
function PromptsCommand({
|
function PromptsCommand({
|
||||||
index,
|
index,
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
|
|
@ -63,8 +66,10 @@ function PromptsCommand({
|
||||||
const mappedArray = data.map((group) => ({
|
const mappedArray = data.map((group) => ({
|
||||||
id: group._id,
|
id: group._id,
|
||||||
value: group.command ?? group.name,
|
value: group.command ?? group.name,
|
||||||
label: `${group.command ? `/${group.command} - ` : ''}${group.name}: ${
|
label: `${group.command != null && group.command ? `/${group.command} - ` : ''}${
|
||||||
group.oneliner?.length ? group.oneliner : group.productionPrompt?.prompt ?? ''
|
group.name
|
||||||
|
}: ${
|
||||||
|
(group.oneliner?.length ?? 0) > 0 ? group.oneliner : group.productionPrompt?.prompt ?? ''
|
||||||
}`,
|
}`,
|
||||||
icon: <CategoryIcon category={group.category ?? ''} className="h-5 w-5" />,
|
icon: <CategoryIcon category={group.category ?? ''} className="h-5 w-5" />,
|
||||||
}));
|
}));
|
||||||
|
|
@ -85,12 +90,12 @@ function PromptsCommand({
|
||||||
const [variableGroup, setVariableGroup] = useState<TPromptGroup | null>(null);
|
const [variableGroup, setVariableGroup] = useState<TPromptGroup | null>(null);
|
||||||
const setShowPromptsPopover = useSetRecoilState(store.showPromptsPopoverFamily(index));
|
const setShowPromptsPopover = useSetRecoilState(store.showPromptsPopoverFamily(index));
|
||||||
|
|
||||||
const prompts = useMemo(() => data?.promptGroups ?? [], [data]);
|
const prompts = useMemo(() => data?.promptGroups, [data]);
|
||||||
const promptsMap = useMemo(() => data?.promptsMap ?? {}, [data]);
|
const promptsMap = useMemo(() => data?.promptsMap, [data]);
|
||||||
|
|
||||||
const { open, setOpen, searchValue, setSearchValue, matches } = useCombobox({
|
const { open, setOpen, searchValue, setSearchValue, matches } = useCombobox({
|
||||||
value: '',
|
value: '',
|
||||||
options: prompts,
|
options: prompts ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
|
|
@ -107,22 +112,20 @@ function PromptsCommand({
|
||||||
removeCharIfLast(textAreaRef.current, commandChar);
|
removeCharIfLast(textAreaRef.current, commandChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidPrompt = mention && promptsMap && promptsMap[mention.id];
|
const group = promptsMap?.[mention.id];
|
||||||
|
if (!group) {
|
||||||
if (!isValidPrompt) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = promptsMap[mention.id];
|
|
||||||
const hasVariables = detectVariables(group.productionPrompt?.prompt ?? '');
|
const hasVariables = detectVariables(group.productionPrompt?.prompt ?? '');
|
||||||
if (group && hasVariables) {
|
if (hasVariables) {
|
||||||
if (e && e.key === 'Tab') {
|
if (e && e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
setVariableGroup(group);
|
setVariableGroup(group);
|
||||||
setVariableDialogOpen(true);
|
setVariableDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
} else if (group) {
|
} else {
|
||||||
submitPrompt(group.productionPrompt?.prompt ?? '');
|
submitPrompt(group.productionPrompt?.prompt ?? '');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -154,6 +157,37 @@ function PromptsCommand({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rowRenderer = ({
|
||||||
|
index,
|
||||||
|
key,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
index: number;
|
||||||
|
key: string;
|
||||||
|
style: React.CSSProperties;
|
||||||
|
}) => {
|
||||||
|
const mention = matches[index] as PromptOption;
|
||||||
|
return (
|
||||||
|
<MentionItem
|
||||||
|
index={index}
|
||||||
|
type="prompt"
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
onClick={() => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
timeoutRef.current = null;
|
||||||
|
handleSelect(mention);
|
||||||
|
}}
|
||||||
|
name={mention.label ?? ''}
|
||||||
|
icon={mention.icon}
|
||||||
|
description={mention.description}
|
||||||
|
isActive={index === activeIndex}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverContainer
|
<PopoverContainer
|
||||||
index={index}
|
index={index}
|
||||||
|
|
@ -161,7 +195,7 @@ function PromptsCommand({
|
||||||
variableGroup={variableGroup}
|
variableGroup={variableGroup}
|
||||||
setVariableDialogOpen={setVariableDialogOpen}
|
setVariableDialogOpen={setVariableDialogOpen}
|
||||||
>
|
>
|
||||||
<div className="absolute bottom-16 z-10 w-full space-y-2">
|
<div className="absolute bottom-14 z-10 w-full space-y-2">
|
||||||
<div className="popover border-token-border-light rounded-2xl border bg-surface-tertiary-alt p-2 shadow-lg">
|
<div className="popover border-token-border-light rounded-2xl border bg-surface-tertiary-alt p-2 shadow-lg">
|
||||||
<input
|
<input
|
||||||
// The user expects focus to transition to the input field when the popover is opened
|
// The user expects focus to transition to the input field when the popover is opened
|
||||||
|
|
@ -213,24 +247,23 @@ function PromptsCommand({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoading && open) {
|
if (!isLoading && open) {
|
||||||
return (matches as PromptOption[]).map((mention, index) => (
|
return (
|
||||||
<MentionItem
|
<div className="max-h-40">
|
||||||
index={index}
|
<AutoSizer disableHeight>
|
||||||
type="prompt"
|
{({ width }) => (
|
||||||
key={`${mention.value}-${index}`}
|
<List
|
||||||
onClick={() => {
|
width={width}
|
||||||
if (timeoutRef.current) {
|
overscanRowCount={5}
|
||||||
clearTimeout(timeoutRef.current);
|
rowHeight={ROW_HEIGHT}
|
||||||
}
|
rowCount={matches.length}
|
||||||
timeoutRef.current = null;
|
rowRenderer={rowRenderer}
|
||||||
handleSelect(mention);
|
scrollToIndex={activeIndex}
|
||||||
}}
|
height={Math.min(matches.length * ROW_HEIGHT, 160)}
|
||||||
name={mention.label ?? ''}
|
/>
|
||||||
icon={mention.icon}
|
)}
|
||||||
description={mention.description}
|
</AutoSizer>
|
||||||
isActive={index === activeIndex}
|
</div>
|
||||||
/>
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})()}
|
})()}
|
||||||
|
|
|
||||||
38
client/src/components/Chat/Input/TemporaryChat.tsx
Normal file
38
client/src/components/Chat/Input/TemporaryChat.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { MessageCircleDashed, X } from 'lucide-react';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
interface TemporaryChatProps {
|
||||||
|
isTemporaryChat: boolean;
|
||||||
|
setIsTemporaryChat: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemporaryChat = ({ isTemporaryChat, setIsTemporaryChat }: TemporaryChatProps) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
if (!isTemporaryChat) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="divide-token-border-light m-1.5 flex flex-col divide-y overflow-hidden rounded-b-lg rounded-t-2xl bg-surface-secondary-alt">
|
||||||
|
<div className="flex items-start gap-4 py-2.5 pl-3 pr-1.5 text-sm">
|
||||||
|
<span className="mt-0 flex h-6 w-6 flex-shrink-0 items-center justify-center">
|
||||||
|
<div className="icon-md">
|
||||||
|
<MessageCircleDashed className="icon-md" />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span className="text-token-text-secondary line-clamp-3 flex-1 py-0.5 font-semibold">
|
||||||
|
{localize('com_ui_temporary_chat')}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="text-token-text-secondary flex-shrink-0"
|
||||||
|
type="button"
|
||||||
|
aria-label="Close temporary chat"
|
||||||
|
onClick={() => setIsTemporaryChat(false)}
|
||||||
|
>
|
||||||
|
<X className="pr-1" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -4,15 +4,16 @@ import { MessageCircleDashed } from 'lucide-react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { Constants, getConfigDefaults } from 'librechat-data-provider';
|
import { Constants, getConfigDefaults } from 'librechat-data-provider';
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
import temporaryStore from '~/store/temporary';
|
|
||||||
import { Switch } from '~/components/ui';
|
import { Switch } from '~/components/ui';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export const TemporaryChat = () => {
|
export const TemporaryChat = () => {
|
||||||
|
const localize = useLocalize();
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
const defaultInterface = getConfigDefaults().interface;
|
const defaultInterface = getConfigDefaults().interface;
|
||||||
const [isTemporary, setIsTemporary] = useRecoilState(temporaryStore.isTemporary);
|
const [isTemporary, setIsTemporary] = useRecoilState(store.isTemporary);
|
||||||
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
|
||||||
const conversationId = conversation?.conversationId ?? '';
|
const conversationId = conversation?.conversationId ?? '';
|
||||||
const interfaceConfig = useMemo(
|
const interfaceConfig = useMemo(
|
||||||
|
|
@ -20,7 +21,7 @@ export const TemporaryChat = () => {
|
||||||
[startupConfig],
|
[startupConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!interfaceConfig.temporaryChat) {
|
if (interfaceConfig.temporaryChat === false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,20 +40,21 @@ export const TemporaryChat = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky bottom-0 border-t border-gray-200 bg-white px-6 py-4 dark:border-gray-700 dark:bg-gray-700">
|
<div className="sticky bottom-0 border-none bg-surface-tertiary px-6 py-4 ">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className={cn('flex flex-1 items-center gap-2', isActiveConvo && 'opacity-40')}>
|
<div className={cn('flex flex-1 items-center gap-2', isActiveConvo && 'opacity-40')}>
|
||||||
<MessageCircleDashed className="icon-sm" />
|
<MessageCircleDashed className="icon-sm" />
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">Temporary Chat</span>
|
<span className="text-sm text-text-primary">{localize('com_ui_temporary_chat')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex items-center">
|
<div className="ml-auto flex items-center">
|
||||||
<Switch
|
<Switch
|
||||||
id="enableUserMsgMarkdown"
|
id="temporary-chat-switch"
|
||||||
checked={isTemporary}
|
checked={isTemporary}
|
||||||
onCheckedChange={onClick}
|
onCheckedChange={onClick}
|
||||||
disabled={isActiveConvo}
|
disabled={isActiveConvo}
|
||||||
className="ml-4"
|
className="ml-4"
|
||||||
data-testid="enableUserMsgMarkdown"
|
aria-label="Toggle temporary chat"
|
||||||
|
data-testid="temporary-chat-switch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -463,6 +463,7 @@ export default {
|
||||||
com_ui_shared_link_delete_success: 'Successfully deleted shared link',
|
com_ui_shared_link_delete_success: 'Successfully deleted shared link',
|
||||||
com_ui_shared_link_bulk_delete_success: 'Successfully deleted shared links',
|
com_ui_shared_link_bulk_delete_success: 'Successfully deleted shared links',
|
||||||
com_ui_search: 'Search',
|
com_ui_search: 'Search',
|
||||||
|
com_ui_temporary_chat: 'Temporary Chat',
|
||||||
com_auth_error_login:
|
com_auth_error_login:
|
||||||
'Unable to login with the information provided. Please check your credentials and try again.',
|
'Unable to login with the information provided. Please check your credentials and try again.',
|
||||||
com_auth_error_login_rl:
|
com_auth_error_login_rl:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import { atom } from 'recoil';
|
import { atomWithLocalStorage } from '~/store/utils';
|
||||||
|
|
||||||
const isTemporary = atom<boolean>({
|
const isTemporary = atomWithLocalStorage('isTemporary', false);
|
||||||
key: 'isTemporary',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
isTemporary,
|
isTemporary,
|
||||||
|
|
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "^4.9.1",
|
"@axe-core/playwright": "^4.9.1",
|
||||||
"@playwright/test": "^1.38.1",
|
"@playwright/test": "^1.38.1",
|
||||||
|
"@types/react-virtualized": "^9.22.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
|
@ -15126,6 +15127,16 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-virtualized": {
|
||||||
|
"version": "9.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.22.0.tgz",
|
||||||
|
"integrity": "sha512-JL/YCCFZ123za//cj10Apk54F0UGFMrjOE0QHTuXt1KBMFrzLOGv9/x6Uc/pZ0Gaf4o6w61Fostvlw0DwuPXig==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "^4.9.1",
|
"@axe-core/playwright": "^4.9.1",
|
||||||
"@playwright/test": "^1.38.1",
|
"@playwright/test": "^1.38.1",
|
||||||
|
"@types/react-virtualized": "^9.22.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue