💬 feat: Temporary Chats (#5493)

* feat: add expiredAt property to Conversation and Message models

Added `expiredAt` property to both Conversation and Message schemas.
Configured `expireAfterSeconds` index in MongoDB to automatically delete documents after a specified period.

* feat(data-provider): add isTemporary and expiredAt properties to support temporary chats

Added `isTemporary` property to TPayload and TSubmission for API calls for temporary chat.
Additionally, added `expiredAt` property to `tConversationSchema` to determine if a chat is temporary.

* feat: implement isTemporary state management

Add Recoil state for tracking temporary conversations, update event handlers to respect temporary chat status

* feat: add configuration to interfaceconfig to hide the temporary chat switch

* feat: add Temporary Chat UI with switch and modify related behaviors

- Added a Temporary Chat switch button at the end of dropdown lists in each model.
- Updated the form background color to black when Temporary Chat is enabled.
- Modified Navigation to exclude Temporary Chats from the chat list.

* fix: exclude Temporary Chats from search results

Updated the getConvosQueried query to ensure that Temporary Chats are not included in the search results.

* fix: hide bookmark button for Temporary Chats

Updated the UI to ensure that the bookmark button is not displayed when a chat is as Temporary Chat.

* chore: update isTemporary state management in ChatRoute

* chore: fix to pass the tests
This commit is contained in:
Yuichi Oneda 2025-02-06 08:11:47 -08:00 committed by GitHub
parent 5f9543f6fc
commit 8c404ae056
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 185 additions and 13 deletions

View file

@ -96,6 +96,14 @@ module.exports = {
update.conversationId = newConversationId; update.conversationId = newConversationId;
} }
if (req.body.isTemporary) {
const expiredAt = new Date();
expiredAt.setDate(expiredAt.getDate() + 30);
update.expiredAt = expiredAt;
} else {
update.expiredAt = null;
}
/** Note: the resulting Model object is necessary for Meilisearch operations */ /** Note: the resulting Model object is necessary for Meilisearch operations */
const conversation = await Conversation.findOneAndUpdate( const conversation = await Conversation.findOneAndUpdate(
{ conversationId, user: req.user.id }, { conversationId, user: req.user.id },
@ -143,6 +151,9 @@ module.exports = {
if (Array.isArray(tags) && tags.length > 0) { if (Array.isArray(tags) && tags.length > 0) {
query.tags = { $in: tags }; query.tags = { $in: tags };
} }
query.$and = [{ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] }];
try { try {
const totalConvos = (await Conversation.countDocuments(query)) || 1; const totalConvos = (await Conversation.countDocuments(query)) || 1;
const totalPages = Math.ceil(totalConvos / pageSize); const totalPages = Math.ceil(totalConvos / pageSize);
@ -172,6 +183,7 @@ module.exports = {
Conversation.findOne({ Conversation.findOne({
user, user,
conversationId: convo.conversationId, conversationId: convo.conversationId,
$or: [{ expiredAt: { $exists: false } }, { expiredAt: null }],
}).lean(), }).lean(),
), ),
); );

View file

@ -52,6 +52,15 @@ async function saveMessage(req, params, metadata) {
user: req.user.id, user: req.user.id,
messageId: params.newMessageId || params.messageId, messageId: params.newMessageId || params.messageId,
}; };
if (req?.body?.isTemporary) {
const expiredAt = new Date();
expiredAt.setDate(expiredAt.getDate() + 30);
update.expiredAt = expiredAt;
} else {
update.expiredAt = null;
}
const message = await Message.findOneAndUpdate( const message = await Message.findOneAndUpdate(
{ messageId: params.messageId, user: req.user.id }, { messageId: params.messageId, user: req.user.id },
update, update,

View file

@ -37,6 +37,9 @@ const convoSchema = mongoose.Schema(
files: { files: {
type: [String], type: [String],
}, },
expiredAt: {
type: Date,
},
}, },
{ timestamps: true }, { timestamps: true },
); );
@ -50,6 +53,8 @@ if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
}); });
} }
// Create TTL index
convoSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 });
convoSchema.index({ createdAt: 1, updatedAt: 1 }); convoSchema.index({ createdAt: 1, updatedAt: 1 });
convoSchema.index({ conversationId: 1, user: 1 }, { unique: true }); convoSchema.index({ conversationId: 1, user: 1 }, { unique: true });

View file

@ -134,6 +134,9 @@ const messageSchema = mongoose.Schema(
default: undefined, default: undefined,
}, },
*/ */
expiredAt: {
type: Date,
},
}, },
{ timestamps: true }, { timestamps: true },
); );
@ -146,7 +149,7 @@ if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
primaryKey: 'messageId', primaryKey: 'messageId',
}); });
} }
messageSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 });
messageSchema.index({ createdAt: 1 }); messageSchema.index({ createdAt: 1 });
messageSchema.index({ messageId: 1, user: 1 }, { unique: true }); messageSchema.index({ messageId: 1, user: 1 }, { unique: true });

View file

@ -33,6 +33,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
prompts: interfaceConfig?.prompts ?? defaults.prompts, prompts: interfaceConfig?.prompts ?? defaults.prompts,
multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo, multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo,
agents: interfaceConfig?.agents ?? defaults.agents, agents: interfaceConfig?.agents ?? defaults.agents,
temporaryChat: interfaceConfig?.temporaryChat ?? defaults.temporaryChat,
}); });
await updateAccessPermissions(roleName, { await updateAccessPermissions(roleName, {

View file

@ -13,6 +13,7 @@ export default function AudioRecorder({
methods, methods,
textAreaRef, textAreaRef,
isSubmitting, isSubmitting,
isTemporary = false,
}: { }: {
isRTL: boolean; isRTL: boolean;
disabled: boolean; disabled: boolean;
@ -20,6 +21,7 @@ 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();
@ -76,7 +78,11 @@ 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 <ListeningIcon className="stroke-gray-700 dark:stroke-gray-300" />; return (
<ListeningIcon
className={cn(isTemporary ? 'stroke-white' : 'stroke-gray-700 dark:stroke-gray-300')}
/>
);
}; };
return ( return (

View file

@ -47,6 +47,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 isSearching = useRecoilValue(store.isSearching); const isSearching = useRecoilValue(store.isSearching);
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index)); const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
@ -146,6 +147,9 @@ const ChatForm = ({ index = 0 }) => {
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 bg-surface-tertiary py-[13px] 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;
@ -181,7 +185,12 @@ const ChatForm = ({ index = 0 }) => {
/> />
)} )}
<PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} /> <PromptsCommand index={index} textAreaRef={textAreaRef} submitPrompt={submitPrompt} />
<div className="transitional-all relative flex w-full flex-grow flex-col overflow-hidden rounded-3xl bg-surface-tertiary text-text-primary duration-200"> <div
className={cn(
'transitional-all relative flex w-full flex-grow flex-col overflow-hidden rounded-3xl text-text-primary ',
isTemporary ? 'text-white' : 'duration-200',
)}
>
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} /> <TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
<FileFormWrapper disableInputs={disableInputs}> <FileFormWrapper disableInputs={disableInputs}>
{endpoint && ( {endpoint && (
@ -234,6 +243,7 @@ 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} />}

View file

@ -28,6 +28,7 @@ const BookmarkMenu: FC = () => {
const conversationId = conversation?.conversationId ?? ''; const conversationId = conversation?.conversationId ?? '';
const updateConvoTags = useBookmarkSuccess(conversationId); const updateConvoTags = useBookmarkSuccess(conversationId);
const tags = conversation?.tags; const tags = conversation?.tags;
const isTemporary = conversation?.expiredAt != null;
const menuId = useId(); const menuId = useId();
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
@ -139,6 +140,10 @@ const BookmarkMenu: FC = () => {
return null; return null;
} }
if (isTemporary) {
return null;
}
const renderButtonContent = () => { const renderButtonContent = () => {
if (mutation.isLoading) { if (mutation.isLoading) {
return <Spinner aria-label="Spinner" />; return <Spinner aria-label="Spinner" />;

View file

@ -1,6 +1,7 @@
import { SelectDropDown, SelectDropDownPop } from '~/components/ui'; import { SelectDropDown, SelectDropDownPop } from '~/components/ui';
import type { TModelSelectProps } from '~/common'; import type { TModelSelectProps } from '~/common';
import { cn, cardStyle } from '~/utils/'; import { cn, cardStyle } from '~/utils/';
import { TemporaryChat } from './TemporaryChat';
export default function Anthropic({ export default function Anthropic({
conversation, conversation,
@ -19,8 +20,9 @@ export default function Anthropic({
showLabel={false} showLabel={false}
className={cn( className={cn(
cardStyle, cardStyle,
'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer', 'z-50 flex h-[40px] w-48 min-w-48 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer',
)} )}
footer={<TemporaryChat />}
/> />
); );
} }

View file

@ -1,5 +1,6 @@
import { SelectDropDown, SelectDropDownPop } from '~/components/ui'; import { SelectDropDown, SelectDropDownPop } from '~/components/ui';
import type { TModelSelectProps } from '~/common'; import type { TModelSelectProps } from '~/common';
import { TemporaryChat } from './TemporaryChat';
import { cn, cardStyle } from '~/utils/'; import { cn, cardStyle } from '~/utils/';
export default function ChatGPT({ export default function ChatGPT({
@ -26,8 +27,9 @@ export default function ChatGPT({
showLabel={false} showLabel={false}
className={cn( className={cn(
cardStyle, cardStyle,
'min-w-48 z-50 flex h-[40px] w-60 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer', 'z-50 flex h-[40px] w-60 min-w-48 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer',
)} )}
footer={<TemporaryChat />}
/> />
); );
} }

View file

@ -1,5 +1,6 @@
import { SelectDropDown, SelectDropDownPop } from '~/components/ui'; import { SelectDropDown, SelectDropDownPop } from '~/components/ui';
import type { TModelSelectProps } from '~/common'; import type { TModelSelectProps } from '~/common';
import { TemporaryChat } from './TemporaryChat';
import { cn, cardStyle } from '~/utils/'; import { cn, cardStyle } from '~/utils/';
export default function Google({ export default function Google({
@ -19,8 +20,9 @@ export default function Google({
showLabel={false} showLabel={false}
className={cn( className={cn(
cardStyle, cardStyle,
'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer', 'z-50 flex h-[40px] w-48 min-w-48 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer',
)} )}
footer={<TemporaryChat />}
/> />
); );
} }

View file

@ -1,5 +1,6 @@
import { SelectDropDown, SelectDropDownPop } from '~/components/ui'; import { SelectDropDown, SelectDropDownPop } from '~/components/ui';
import type { TModelSelectProps } from '~/common'; import type { TModelSelectProps } from '~/common';
import { TemporaryChat } from './TemporaryChat';
import { cn, cardStyle } from '~/utils/'; import { cn, cardStyle } from '~/utils/';
export default function OpenAI({ export default function OpenAI({
@ -19,8 +20,9 @@ export default function OpenAI({
showLabel={false} showLabel={false}
className={cn( className={cn(
cardStyle, cardStyle,
'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 hover:cursor-pointer', 'z-50 flex h-[40px] w-48 min-w-48 flex-none items-center justify-center px-4 hover:cursor-pointer',
)} )}
footer={<TemporaryChat />}
/> />
); );
} }

View file

@ -0,0 +1,61 @@
import { useMemo } from 'react';
import { MessageCircleDashed } from 'lucide-react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { Constants, getConfigDefaults } from 'librechat-data-provider';
import temporaryStore from '~/store/temporary';
import { Switch } from '~/components/ui';
import { cn } from '~/utils';
import store from '~/store';
export const TemporaryChat = () => {
const { data: startupConfig } = useGetStartupConfig();
const defaultInterface = getConfigDefaults().interface;
const [isTemporary, setIsTemporary] = useRecoilState(temporaryStore.isTemporary);
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
const conversationId = conversation?.conversationId ?? '';
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
);
if (!interfaceConfig.temporaryChat) {
return null;
}
const isActiveConvo = Boolean(
conversation &&
conversationId &&
conversationId !== Constants.NEW_CONVO &&
conversationId !== 'search',
);
const onClick = () => {
if (isActiveConvo) {
return;
}
setIsTemporary(!isTemporary);
};
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="flex items-center">
<div className={cn('flex flex-1 items-center gap-2', isActiveConvo && 'opacity-40')}>
<MessageCircleDashed className="icon-sm" />
<span className="text-sm text-gray-700 dark:text-gray-300">Temporary Chat</span>
</div>
<div className="ml-auto flex items-center">
<Switch
id="enableUserMsgMarkdown"
checked={isTemporary}
onCheckedChange={onClick}
disabled={isActiveConvo}
className="ml-4"
data-testid="enableUserMsgMarkdown"
/>
</div>
</div>
</div>
);
};

View file

@ -18,6 +18,7 @@ type SelectDropDownProps = {
showLabel?: boolean; showLabel?: boolean;
iconSide?: 'left' | 'right'; iconSide?: 'left' | 'right';
renderOption?: () => React.ReactNode; renderOption?: () => React.ReactNode;
footer?: React.ReactNode;
}; };
function SelectDropDownPop({ function SelectDropDownPop({
@ -28,6 +29,7 @@ function SelectDropDownPop({
showAbove = false, showAbove = false,
showLabel = true, showLabel = true,
emptyTitle = false, emptyTitle = false,
footer,
}: SelectDropDownProps) { }: SelectDropDownProps) {
const localize = useLocalize(); const localize = useLocalize();
const transitionProps = { className: 'top-full mt-3' }; const transitionProps = { className: 'top-full mt-3' };
@ -78,9 +80,6 @@ function SelectDropDownPop({
'min-w-[75px] font-normal', 'min-w-[75px] font-normal',
)} )}
> >
{/* {!showLabel && !emptyTitle && (
<span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span>
)} */}
{typeof value !== 'string' && value ? value.label ?? '' : value ?? ''} {typeof value !== 'string' && value ? value.label ?? '' : value ?? ''}
</span> </span>
</span> </span>
@ -124,6 +123,7 @@ function SelectDropDownPop({
/> />
); );
})} })}
{footer}
</Content> </Content>
</Portal> </Portal>
</div> </div>

View file

@ -71,6 +71,7 @@ export default function useChatFunctions({
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index)); const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
const setFilesToDelete = useSetFilesToDelete(); const setFilesToDelete = useSetFilesToDelete();
const getSender = useGetSender(); const getSender = useGetSender();
const isTemporary = useRecoilValue(store.isTemporary);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { getExpiry } = useUserKey(conversation?.endpoint ?? ''); const { getExpiry } = useUserKey(conversation?.endpoint ?? '');
@ -293,6 +294,7 @@ export default function useChatFunctions({
isContinued, isContinued,
isRegenerate, isRegenerate,
initialResponse, initialResponse,
isTemporary,
}; };
if (isRegenerate) { if (isRegenerate) {

View file

@ -275,7 +275,7 @@ export default function useEventHandlers({
const createdHandler = useCallback( const createdHandler = useCallback(
(data: TResData, submission: EventSubmission) => { (data: TResData, submission: EventSubmission) => {
const { messages, userMessage, isRegenerate = false } = submission; const { messages, userMessage, isRegenerate = false, isTemporary = false } = submission;
const initialResponse = { const initialResponse = {
...submission.initialResponse, ...submission.initialResponse,
parentMessageId: userMessage.messageId, parentMessageId: userMessage.messageId,
@ -317,6 +317,9 @@ export default function useEventHandlers({
return update; return update;
}); });
if (isTemporary) {
return;
}
queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => { queryClient.setQueryData<ConversationData>([QueryKeys.allConversations], (convoData) => {
if (!convoData) { if (!convoData) {
return convoData; return convoData;
@ -357,7 +360,12 @@ export default function useEventHandlers({
const finalHandler = useCallback( const finalHandler = useCallback(
(data: TFinalResData, submission: EventSubmission) => { (data: TFinalResData, submission: EventSubmission) => {
const { requestMessage, responseMessage, conversation, runMessages } = data; const { requestMessage, responseMessage, conversation, runMessages } = data;
const { messages, conversation: submissionConvo, isRegenerate = false } = submission; const {
messages,
conversation: submissionConvo,
isRegenerate = false,
isTemporary = false,
} = submission;
setShowStopButton(false); setShowStopButton(false);
setCompleted((prev) => new Set(prev.add(submission.initialResponse.messageId))); setCompleted((prev) => new Set(prev.add(submission.initialResponse.messageId)));
@ -401,6 +409,7 @@ export default function useEventHandlers({
if ( if (
genTitle && genTitle &&
isNewConvo && isNewConvo &&
!isTemporary &&
requestMessage && requestMessage &&
requestMessage.parentMessageId === Constants.NO_PARENT requestMessage.parentMessageId === Constants.NO_PARENT
) { ) {

View file

@ -38,6 +38,7 @@ const useNewConvo = (index = 0) => {
const clearAllConversations = store.useClearConvoState(); const clearAllConversations = store.useClearConvoState();
const defaultPreset = useRecoilValue(store.defaultPreset); const defaultPreset = useRecoilValue(store.defaultPreset);
const { setConversation } = store.useCreateConversationAtom(index); const { setConversation } = store.useCreateConversationAtom(index);
const [isTemporary, setIsTemporary] = useRecoilState(store.isTemporary);
const [files, setFiles] = useRecoilState(store.filesByIndex(index)); const [files, setFiles] = useRecoilState(store.filesByIndex(index));
const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`); const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`);
const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index)); const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index));
@ -195,6 +196,9 @@ const useNewConvo = (index = 0) => {
keepAddedConvos?: boolean; keepAddedConvos?: boolean;
} = {}) { } = {}) {
pauseGlobalAudio(); pauseGlobalAudio();
if (isTemporary) {
setIsTemporary(false);
}
const templateConvoId = _template.conversationId ?? ''; const templateConvoId = _template.conversationId ?? '';
const paramEndpoint = const paramEndpoint =

View file

@ -14,13 +14,22 @@ import { getDefaultModelSpec, getModelSpecIconURL } from '~/utils';
import { ToolCallsMapProvider } from '~/Providers'; import { ToolCallsMapProvider } from '~/Providers';
import ChatView from '~/components/Chat/ChatView'; import ChatView from '~/components/Chat/ChatView';
import useAuthRedirect from './useAuthRedirect'; import useAuthRedirect from './useAuthRedirect';
import temporaryStore from '~/store/temporary';
import { Spinner } from '~/components/svg'; import { Spinner } from '~/components/svg';
import { useRecoilCallback } from 'recoil';
import store from '~/store'; import store from '~/store';
export default function ChatRoute() { export default function ChatRoute() {
useHealthCheck(); useHealthCheck();
const { data: startupConfig } = useGetStartupConfig(); const { data: startupConfig } = useGetStartupConfig();
const { isAuthenticated, user } = useAuthRedirect(); const { isAuthenticated, user } = useAuthRedirect();
const setIsTemporary = useRecoilCallback(
({ set }) =>
(value: boolean) => {
set(temporaryStore.isTemporary, value);
},
[],
);
useAppStartup({ startupConfig, user }); useAppStartup({ startupConfig, user });
const index = 0; const index = 0;
@ -141,6 +150,14 @@ export default function ChatRoute() {
return null; return null;
} }
const isTemporaryChat = conversation && conversation.expiredAt ? true : false;
if (conversationId !== Constants.NEW_CONVO && !isTemporaryChat) {
setIsTemporary(false);
} else if (isTemporaryChat) {
setIsTemporary(isTemporaryChat);
}
return ( return (
<ToolCallsMapProvider conversationId={conversation.conversationId ?? ''}> <ToolCallsMapProvider conversationId={conversation.conversationId ?? ''}>
<ChatView index={index} /> <ChatView index={index} />

View file

@ -11,6 +11,7 @@ import prompts from './prompts';
import lang from './language'; import lang from './language';
import settings from './settings'; import settings from './settings';
import misc from './misc'; import misc from './misc';
import isTemporary from './temporary';
export default { export default {
...artifacts, ...artifacts,
...families, ...families,
@ -25,4 +26,5 @@ export default {
...lang, ...lang,
...settings, ...settings,
...misc, ...misc,
...isTemporary,
}; };

View file

@ -0,0 +1,10 @@
import { atom } from 'recoil';
const isTemporary = atom<boolean>({
key: 'isTemporary',
default: false,
});
export default {
isTemporary,
};

View file

@ -455,6 +455,7 @@ export const intefaceSchema = z
presets: z.boolean().optional(), presets: z.boolean().optional(),
prompts: z.boolean().optional(), prompts: z.boolean().optional(),
agents: z.boolean().optional(), agents: z.boolean().optional(),
temporaryChat: z.boolean().optional(),
}) })
.default({ .default({
endpointsMenu: true, endpointsMenu: true,
@ -466,6 +467,7 @@ export const intefaceSchema = z
bookmarks: true, bookmarks: true,
prompts: true, prompts: true,
agents: true, agents: true,
temporaryChat: true,
}); });
export type TInterfaceConfig = z.infer<typeof intefaceSchema>; export type TInterfaceConfig = z.infer<typeof intefaceSchema>;

View file

@ -3,7 +3,8 @@ import { EndpointURLs } from './config';
import * as s from './schemas'; import * as s from './schemas';
export default function createPayload(submission: t.TSubmission) { export default function createPayload(submission: t.TSubmission) {
const { conversation, userMessage, endpointOption, isEdited, isContinued } = submission; const { conversation, userMessage, endpointOption, isEdited, isContinued, isTemporary } =
submission;
const { conversationId } = s.tConvoUpdateSchema.parse(conversation); const { conversationId } = s.tConvoUpdateSchema.parse(conversation);
const { endpoint, endpointType } = endpointOption as { const { endpoint, endpointType } = endpointOption as {
endpoint: s.EModelEndpoint; endpoint: s.EModelEndpoint;
@ -23,6 +24,7 @@ export default function createPayload(submission: t.TSubmission) {
...endpointOption, ...endpointOption,
isContinued: !!(isEdited && isContinued), isContinued: !!(isEdited && isContinued),
conversationId, conversationId,
isTemporary,
}; };
return { server, payload }; return { server, payload };

View file

@ -590,6 +590,8 @@ export const tConversationSchema = z.object({
greeting: z.string().optional(), greeting: z.string().optional(),
spec: z.string().nullable().optional(), spec: z.string().nullable().optional(),
iconURL: z.string().nullable().optional(), iconURL: z.string().nullable().optional(),
/* temporary chat */
expiredAt: z.string().nullable().optional(),
/** @deprecated */ /** @deprecated */
resendImages: z.boolean().optional(), resendImages: z.boolean().optional(),
/** @deprecated */ /** @deprecated */

View file

@ -44,6 +44,7 @@ export type TPayload = Partial<TMessage> &
isContinued: boolean; isContinued: boolean;
conversationId: string | null; conversationId: string | null;
messages?: TMessages; messages?: TMessages;
isTemporary: boolean;
}; };
export type TSubmission = { export type TSubmission = {
@ -53,6 +54,7 @@ export type TSubmission = {
userMessage: TMessage; userMessage: TMessage;
isEdited?: boolean; isEdited?: boolean;
isContinued?: boolean; isContinued?: boolean;
isTemporary: boolean;
messages: TMessage[]; messages: TMessage[];
isRegenerate?: boolean; isRegenerate?: boolean;
conversationId?: string; conversationId?: string;