mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02: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 { useMediaQuery, useHasAccess } from '~/hooks';
|
||||
import BookmarkMenu from './Menus/BookmarkMenu';
|
||||
import { TemporaryChat } from './TemporaryChat';
|
||||
import AddMultiConvo from './AddMultiConvo';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
export default function Header() {
|
||||
|
@ -42,13 +44,21 @@ export default function Header() {
|
|||
{hasAccessToBookmarks === true && <BookmarkMenu />}
|
||||
{hasAccessToMultiConvo === true && <AddMultiConvo />}
|
||||
{isSmallScreen && (
|
||||
<ExportAndShareMenu
|
||||
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
|
||||
/>
|
||||
<>
|
||||
<ExportAndShareMenu
|
||||
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
|
||||
/>
|
||||
<TemporaryChat />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isSmallScreen && (
|
||||
<ExportAndShareMenu isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false} />
|
||||
<div className="flex items-center gap-2">
|
||||
<ExportAndShareMenu
|
||||
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
|
||||
/>
|
||||
<TemporaryChat />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Empty div for spacing */}
|
||||
|
|
|
@ -9,9 +9,9 @@ import React, {
|
|||
} from 'react';
|
||||
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import type { BadgeItem } from '~/common';
|
||||
import { useChatBadges } from '~/hooks';
|
||||
import { Badge } from '~/components/ui';
|
||||
import { BadgeItem } from '~/common';
|
||||
import store from '~/store';
|
||||
|
||||
interface BadgeRowProps {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, useRef, useMemo, useEffect, useState, useCallback } from 'react';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import { Constants, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import {
|
||||
useChatContext,
|
||||
useChatFormContext,
|
||||
|
@ -50,6 +50,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
const centerFormOnLanding = useRecoilValue(store.centerFormOnLanding);
|
||||
const isTemporary = useRecoilValue(store.isTemporary);
|
||||
|
||||
const [badges, setBadges] = useRecoilState(store.chatBadges);
|
||||
const [isEditingBadges, setIsEditingBadges] = useRecoilState(store.isEditingBadges);
|
||||
|
@ -180,7 +181,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
const baseClasses = useMemo(
|
||||
() =>
|
||||
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]',
|
||||
isMoreThanThreeRows ? 'pl-5' : 'px-5',
|
||||
),
|
||||
|
@ -191,9 +192,13 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
<form
|
||||
onSubmit={methods.handleSubmit(submitMessage)}
|
||||
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',
|
||||
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">
|
||||
|
@ -219,8 +224,11 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
|||
<div
|
||||
onClick={handleContainerClick}
|
||||
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',
|
||||
isTemporary
|
||||
? 'border-violet-800/60 bg-violet-950/10'
|
||||
: 'border-border-light bg-surface-chat',
|
||||
)}
|
||||
>
|
||||
<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 store from '~/store';
|
||||
|
||||
const badgeConfig = [
|
||||
{
|
||||
id: '1',
|
||||
icon: MessageCircleDashed,
|
||||
label: 'com_ui_temporary',
|
||||
atom: store.isTemporary,
|
||||
},
|
||||
interface ChatBadgeConfig {
|
||||
id: string;
|
||||
icon: typeof Box;
|
||||
label: string;
|
||||
atom?: any;
|
||||
}
|
||||
|
||||
const badgeConfig: ReadonlyArray<ChatBadgeConfig> = [
|
||||
// {
|
||||
// id: '2',
|
||||
// id: '1',
|
||||
// icon: Box,
|
||||
// label: 'com_ui_artifacts',
|
||||
// atom: store.codeArtifacts,
|
||||
// },
|
||||
// TODO: add more badges here (missing store atoms)
|
||||
] as const;
|
||||
];
|
||||
|
||||
export default function useChatBadges(): BadgeItem[] {
|
||||
const localize = useLocalize();
|
||||
|
|
|
@ -50,6 +50,7 @@ const useNewConvo = (index = 0) => {
|
|||
const assistantsListMap = useAssistantListMap();
|
||||
const { pauseGlobalAudio } = usePauseGlobalAudio(index);
|
||||
const saveDrafts = useRecoilValue<boolean>(store.saveDrafts);
|
||||
const resetBadges = useResetChatBadges();
|
||||
|
||||
const { mutateAsync } = useDeleteFilesMutation({
|
||||
onSuccess: () => {
|
||||
|
@ -198,7 +199,7 @@ const useNewConvo = (index = 0) => {
|
|||
} = {}) {
|
||||
pauseGlobalAudio();
|
||||
if (!saveBadgesState) {
|
||||
useResetChatBadges();
|
||||
resetBadges();
|
||||
}
|
||||
|
||||
const templateConvoId = _template.conversationId ?? '';
|
||||
|
@ -278,6 +279,7 @@ const useNewConvo = (index = 0) => {
|
|||
files,
|
||||
setFiles,
|
||||
mutateAsync,
|
||||
resetBadges,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -810,7 +810,7 @@
|
|||
"com_ui_storage": "Storage",
|
||||
"com_ui_submit": "Submit",
|
||||
"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_of_service": "Terms of service",
|
||||
"com_ui_thinking": "Thinking...",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue