mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
💬 feat: move Temporary Chat to the Header (#6646)
* 🚀 feat: Add Temporary Chat feature with badge toggle functionality
* style: update header button
* fix: Integrate resetChatBadges functionality into useNewConvo hook following rules of react
* fix: Adjust margin logic in ChatForm for better layout handling on existing conversations
* fix: Refine margin logic in ChatForm to improve layout during message submission
* fix: Update TemporaryChat component to not render when message is submitting
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
a5154e1349
commit
cd7cdaa703
7 changed files with 111 additions and 21 deletions
|
|
@ -8,7 +8,9 @@ import { useGetStartupConfig } from '~/data-provider';
|
||||||
import ExportAndShareMenu from './ExportAndShareMenu';
|
import ExportAndShareMenu from './ExportAndShareMenu';
|
||||||
import { useMediaQuery, useHasAccess } from '~/hooks';
|
import { useMediaQuery, useHasAccess } from '~/hooks';
|
||||||
import BookmarkMenu from './Menus/BookmarkMenu';
|
import BookmarkMenu from './Menus/BookmarkMenu';
|
||||||
|
import { TemporaryChat } from './TemporaryChat';
|
||||||
import AddMultiConvo from './AddMultiConvo';
|
import AddMultiConvo from './AddMultiConvo';
|
||||||
|
|
||||||
const defaultInterface = getConfigDefaults().interface;
|
const defaultInterface = getConfigDefaults().interface;
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
|
@ -42,13 +44,21 @@ export default function Header() {
|
||||||
{hasAccessToBookmarks === true && <BookmarkMenu />}
|
{hasAccessToBookmarks === true && <BookmarkMenu />}
|
||||||
{hasAccessToMultiConvo === true && <AddMultiConvo />}
|
{hasAccessToMultiConvo === true && <AddMultiConvo />}
|
||||||
{isSmallScreen && (
|
{isSmallScreen && (
|
||||||
<ExportAndShareMenu
|
<>
|
||||||
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
|
<ExportAndShareMenu
|
||||||
/>
|
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
|
||||||
|
/>
|
||||||
|
<TemporaryChat />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isSmallScreen && (
|
{!isSmallScreen && (
|
||||||
<ExportAndShareMenu isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false} />
|
<div className="flex items-center gap-2">
|
||||||
|
<ExportAndShareMenu
|
||||||
|
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
|
||||||
|
/>
|
||||||
|
<TemporaryChat />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Empty div for spacing */}
|
{/* Empty div for spacing */}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ import React, {
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
import type { BadgeItem } from '~/common';
|
||||||
import { useChatBadges } from '~/hooks';
|
import { useChatBadges } from '~/hooks';
|
||||||
import { Badge } from '~/components/ui';
|
import { Badge } from '~/components/ui';
|
||||||
import { BadgeItem } from '~/common';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
interface BadgeRowProps {
|
interface BadgeRowProps {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { memo, useRef, useMemo, useEffect, useState, useCallback } from 'react';
|
import { memo, useRef, useMemo, useEffect, useState, useCallback } from 'react';
|
||||||
import { useWatch } from 'react-hook-form';
|
import { useWatch } from 'react-hook-form';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
import { Constants, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
useChatContext,
|
useChatContext,
|
||||||
useChatFormContext,
|
useChatFormContext,
|
||||||
|
|
@ -50,6 +50,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||||
const centerFormOnLanding = useRecoilValue(store.centerFormOnLanding);
|
const centerFormOnLanding = useRecoilValue(store.centerFormOnLanding);
|
||||||
|
const isTemporary = useRecoilValue(store.isTemporary);
|
||||||
|
|
||||||
const [badges, setBadges] = useRecoilState(store.chatBadges);
|
const [badges, setBadges] = useRecoilState(store.chatBadges);
|
||||||
const [isEditingBadges, setIsEditingBadges] = useRecoilState(store.isEditingBadges);
|
const [isEditingBadges, setIsEditingBadges] = useRecoilState(store.isEditingBadges);
|
||||||
|
|
@ -180,7 +181,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
const baseClasses = useMemo(
|
const baseClasses = useMemo(
|
||||||
() =>
|
() =>
|
||||||
cn(
|
cn(
|
||||||
'md:py-3.5 m-0 w-full resize-none py-[13px] bg-surface-chat 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] placeholder-black/50 bg-transparent dark:placeholder-white/50 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]',
|
||||||
isCollapsed ? 'max-h-[52px]' : 'max-h-[45vh] md:max-h-[55vh]',
|
isCollapsed ? 'max-h-[52px]' : 'max-h-[45vh] md:max-h-[55vh]',
|
||||||
isMoreThanThreeRows ? 'pl-5' : 'px-5',
|
isMoreThanThreeRows ? 'pl-5' : 'px-5',
|
||||||
),
|
),
|
||||||
|
|
@ -191,9 +192,13 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
<form
|
<form
|
||||||
onSubmit={methods.handleSubmit(submitMessage)}
|
onSubmit={methods.handleSubmit(submitMessage)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'mx-auto flex flex-row gap-3 transition-all duration-200 sm:px-2',
|
'mx-auto flex flex-row gap-3 sm:px-2',
|
||||||
maximizeChatSpace ? 'w-full max-w-full' : 'md:max-w-3xl xl:max-w-4xl',
|
maximizeChatSpace ? 'w-full max-w-full' : 'md:max-w-3xl xl:max-w-4xl',
|
||||||
centerFormOnLanding ? 'sm:mb-28' : 'sm:mb-10',
|
centerFormOnLanding &&
|
||||||
|
(!conversation?.conversationId || conversation?.conversationId === Constants.NEW_CONVO) &&
|
||||||
|
!isSubmitting
|
||||||
|
? 'transition-all duration-200 sm:mb-28'
|
||||||
|
: 'sm:mb-10',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative flex h-full flex-1 items-stretch md:flex-col">
|
<div className="relative flex h-full flex-1 items-stretch md:flex-col">
|
||||||
|
|
@ -219,8 +224,11 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
<div
|
<div
|
||||||
onClick={handleContainerClick}
|
onClick={handleContainerClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex w-full flex-grow flex-col overflow-hidden rounded-t-3xl border border-border-medium bg-surface-chat pb-4 text-text-primary transition-all duration-200 sm:rounded-3xl sm:pb-0',
|
'relative flex w-full flex-grow flex-col overflow-hidden rounded-t-3xl border pb-4 text-text-primary transition-all duration-200 sm:rounded-3xl sm:pb-0',
|
||||||
isTextAreaFocused ? 'shadow-lg' : 'shadow-md',
|
isTextAreaFocused ? 'shadow-lg' : 'shadow-md',
|
||||||
|
isTemporary
|
||||||
|
? 'border-violet-800/60 bg-violet-950/10'
|
||||||
|
: 'border-border-light bg-surface-chat',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||||
|
|
|
||||||
69
client/src/components/Chat/TemporaryChat.tsx
Normal file
69
client/src/components/Chat/TemporaryChat.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { MessageCircleDashed } from 'lucide-react';
|
||||||
|
import { useRecoilState, useRecoilCallback } from 'recoil';
|
||||||
|
import { TooltipAnchor } from '~/components/ui';
|
||||||
|
import { useChatContext } from '~/Providers';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
export function TemporaryChat() {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const [isTemporary, setIsTemporary] = useRecoilState(store.isTemporary);
|
||||||
|
const { conversation, isSubmitting } = useChatContext();
|
||||||
|
|
||||||
|
const temporaryBadge = {
|
||||||
|
id: 'temporary',
|
||||||
|
icon: MessageCircleDashed,
|
||||||
|
label: 'com_ui_temporary' as const,
|
||||||
|
atom: store.isTemporary,
|
||||||
|
isAvailable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBadgeToggle = useRecoilCallback(
|
||||||
|
() => () => {
|
||||||
|
setIsTemporary(!isTemporary);
|
||||||
|
},
|
||||||
|
[isTemporary],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(Array.isArray(conversation?.messages) && conversation.messages.length >= 1) ||
|
||||||
|
isSubmitting
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-wrap items-center gap-2">
|
||||||
|
<div className="badge-icon h-full">
|
||||||
|
<TooltipAnchor
|
||||||
|
description={localize(temporaryBadge.label)}
|
||||||
|
render={
|
||||||
|
<motion.button
|
||||||
|
onClick={handleBadgeToggle}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex size-10 flex-shrink-0 items-center justify-center rounded-lg border border-border-light text-text-primary transition-all ease-in-out hover:bg-surface-tertiary',
|
||||||
|
isTemporary
|
||||||
|
? 'bg-surface-active shadow-md'
|
||||||
|
: 'bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md',
|
||||||
|
'active:scale-95 active:shadow-inner',
|
||||||
|
)}
|
||||||
|
transition={{ type: 'tween', duration: 0.1, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
{temporaryBadge.icon && (
|
||||||
|
<temporaryBadge.icon
|
||||||
|
className={cn(
|
||||||
|
'relative h-5 w-5 md:h-4 md:w-4',
|
||||||
|
!temporaryBadge.label && 'mx-auto',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,21 +5,22 @@ import type { BadgeItem } from '~/common';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const badgeConfig = [
|
interface ChatBadgeConfig {
|
||||||
{
|
id: string;
|
||||||
id: '1',
|
icon: typeof Box;
|
||||||
icon: MessageCircleDashed,
|
label: string;
|
||||||
label: 'com_ui_temporary',
|
atom?: any;
|
||||||
atom: store.isTemporary,
|
}
|
||||||
},
|
|
||||||
|
const badgeConfig: ReadonlyArray<ChatBadgeConfig> = [
|
||||||
// {
|
// {
|
||||||
// id: '2',
|
// id: '1',
|
||||||
// icon: Box,
|
// icon: Box,
|
||||||
// label: 'com_ui_artifacts',
|
// label: 'com_ui_artifacts',
|
||||||
// atom: store.codeArtifacts,
|
// atom: store.codeArtifacts,
|
||||||
// },
|
// },
|
||||||
// TODO: add more badges here (missing store atoms)
|
// TODO: add more badges here (missing store atoms)
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
export default function useChatBadges(): BadgeItem[] {
|
export default function useChatBadges(): BadgeItem[] {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ const useNewConvo = (index = 0) => {
|
||||||
const assistantsListMap = useAssistantListMap();
|
const assistantsListMap = useAssistantListMap();
|
||||||
const { pauseGlobalAudio } = usePauseGlobalAudio(index);
|
const { pauseGlobalAudio } = usePauseGlobalAudio(index);
|
||||||
const saveDrafts = useRecoilValue<boolean>(store.saveDrafts);
|
const saveDrafts = useRecoilValue<boolean>(store.saveDrafts);
|
||||||
|
const resetBadges = useResetChatBadges();
|
||||||
|
|
||||||
const { mutateAsync } = useDeleteFilesMutation({
|
const { mutateAsync } = useDeleteFilesMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -198,7 +199,7 @@ const useNewConvo = (index = 0) => {
|
||||||
} = {}) {
|
} = {}) {
|
||||||
pauseGlobalAudio();
|
pauseGlobalAudio();
|
||||||
if (!saveBadgesState) {
|
if (!saveBadgesState) {
|
||||||
useResetChatBadges();
|
resetBadges();
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateConvoId = _template.conversationId ?? '';
|
const templateConvoId = _template.conversationId ?? '';
|
||||||
|
|
@ -278,6 +279,7 @@ const useNewConvo = (index = 0) => {
|
||||||
files,
|
files,
|
||||||
setFiles,
|
setFiles,
|
||||||
mutateAsync,
|
mutateAsync,
|
||||||
|
resetBadges,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -810,7 +810,7 @@
|
||||||
"com_ui_storage": "Storage",
|
"com_ui_storage": "Storage",
|
||||||
"com_ui_submit": "Submit",
|
"com_ui_submit": "Submit",
|
||||||
"com_ui_teach_or_explain": "Learning",
|
"com_ui_teach_or_explain": "Learning",
|
||||||
"com_ui_temporary": "Temporary",
|
"com_ui_temporary": "Temporary Chat",
|
||||||
"com_ui_terms_and_conditions": "Terms and Conditions",
|
"com_ui_terms_and_conditions": "Terms and Conditions",
|
||||||
"com_ui_terms_of_service": "Terms of service",
|
"com_ui_terms_of_service": "Terms of service",
|
||||||
"com_ui_thinking": "Thinking...",
|
"com_ui_thinking": "Thinking...",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue