mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
💬 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:
parent
5f9543f6fc
commit
8c404ae056
24 changed files with 185 additions and 13 deletions
|
|
@ -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(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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, {
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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} />}
|
||||||
|
|
|
||||||
|
|
@ -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" />;
|
||||||
|
|
|
||||||
|
|
@ -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 />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
client/src/components/Input/ModelSelect/TemporaryChat.tsx
Normal file
61
client/src/components/Input/ModelSelect/TemporaryChat.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
10
client/src/store/temporary.ts
Normal file
10
client/src/store/temporary.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
const isTemporary = atom<boolean>({
|
||||||
|
key: 'isTemporary',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isTemporary,
|
||||||
|
};
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue