diff --git a/api/server/routes/__tests__/share.spec.js b/api/server/routes/__tests__/share.spec.js index c9d6cb0037..1d20d224af 100644 --- a/api/server/routes/__tests__/share.spec.js +++ b/api/server/routes/__tests__/share.spec.js @@ -35,27 +35,29 @@ jest.mock('@librechat/data-schemas', () => ({ SYSTEM_TENANT_ID: '__SYSTEM__', })); -jest.mock('librechat-data-provider', () => ({ - PermissionTypes: { - SHARED_LINKS: 'SHARED_LINKS', - }, - Permissions: { - CREATE: 'CREATE', - SHARE_PUBLIC: 'SHARE_PUBLIC', - }, - RetentionMode: { - ALL: 'all', - TEMPORARY: 'temporary', - }, - FileSources: { - local: 'local', - s3: 's3', - cloudfront: 'cloudfront', - azure_blob: 'azure_blob', - firebase: 'firebase', - text: 'text', - }, -})); +jest.mock('librechat-data-provider', () => { + const RetentionMode = { ALL: 'all', TEMPORARY: 'temporary', EPHEMERAL: 'ephemeral' }; + return { + PermissionTypes: { + SHARED_LINKS: 'SHARED_LINKS', + }, + Permissions: { + CREATE: 'CREATE', + SHARE_PUBLIC: 'SHARE_PUBLIC', + }, + RetentionMode, + isAllDataRetention: (mode) => mode === RetentionMode.ALL || mode === RetentionMode.EPHEMERAL, + isForcedTemporaryRetention: (mode) => mode === RetentionMode.EPHEMERAL, + FileSources: { + local: 'local', + s3: 's3', + cloudfront: 'cloudfront', + azure_blob: 'azure_blob', + firebase: 'firebase', + text: 'text', + }, + }; +}); jest.mock('mongoose', () => ({ models: { diff --git a/api/server/utils/import/importBatchBuilder.js b/api/server/utils/import/importBatchBuilder.js index b1856737cd..c382888b82 100644 --- a/api/server/utils/import/importBatchBuilder.js +++ b/api/server/utils/import/importBatchBuilder.js @@ -7,8 +7,9 @@ const { const { EModelEndpoint, Constants, - RetentionMode, openAISettings, + isAllDataRetention, + isForcedTemporaryRetention, } = require('librechat-data-provider'); const { bulkIncrementTagCounts, bulkSaveConvos, bulkSaveMessages } = require('~/models'); const { FALLBACK_MODEL_BY_ENDPOINT } = require('./defaults'); @@ -45,19 +46,20 @@ class ImportBatchBuilder { return this.retentionFields; } - if (this.interfaceConfig?.retentionMode !== RetentionMode.ALL) { + if (!isAllDataRetention(this.interfaceConfig?.retentionMode)) { this.retentionFields = {}; return this.retentionFields; } + const isTemporary = isForcedTemporaryRetention(this.interfaceConfig?.retentionMode); try { this.retentionFields = { - isTemporary: false, + isTemporary, expiredAt: createTempChatExpirationDate(this.interfaceConfig), }; } catch (error) { logger.error('[ImportBatchBuilder] Error creating import expiration date:', error); - this.retentionFields = { isTemporary: false, expiredAt: createFallbackRetentionDate() }; + this.retentionFields = { isTemporary, expiredAt: createFallbackRetentionDate() }; } return this.retentionFields; } diff --git a/api/server/utils/import/importers.spec.js b/api/server/utils/import/importers.spec.js index a9bd679f55..46e87889e0 100644 --- a/api/server/utils/import/importers.spec.js +++ b/api/server/utils/import/importers.spec.js @@ -1145,6 +1145,23 @@ describe('importLibreChatConvo', () => { expect(result.conversation.expiredAt).toBeInstanceOf(Date); expect(result.conversation.expiredAt).toBe(message.expiredAt); }); + + it('marks imported conversations and messages temporary under ephemeral retention', () => { + const requestUserId = 'user-123'; + const builder = new ImportBatchBuilder(requestUserId, { + retentionMode: RetentionMode.EPHEMERAL, + temporaryChatRetention: 24, + }); + builder.startConversation(EModelEndpoint.openAI); + const message = builder.addUserMessage('Ephemeral import'); + const result = builder.finishConversation('Imported ephemeral chat'); + + expect(message.isTemporary).toBe(true); + expect(message.expiredAt).toBeInstanceOf(Date); + expect(result.conversation.isTemporary).toBe(true); + expect(result.conversation.expiredAt).toBeInstanceOf(Date); + expect(result.conversation.expiredAt).toBe(message.expiredAt); + }); }); }); diff --git a/client/src/components/Chat/TemporaryChat.tsx b/client/src/components/Chat/TemporaryChat.tsx index 39d42462bc..9a083c65e3 100644 --- a/client/src/components/Chat/TemporaryChat.tsx +++ b/client/src/components/Chat/TemporaryChat.tsx @@ -3,24 +3,32 @@ import { useRecoilValue } from 'recoil'; import { TooltipAnchor } from '@librechat/client'; import { MessageCircleDashed } from 'lucide-react'; import { useRecoilState, useRecoilCallback } from 'recoil'; +import { isForcedTemporaryRetention } from 'librechat-data-provider'; import { useShortcutAriaKey, useShortcutHint } from '~/hooks/useKeyboardShortcuts'; +import { useGetStartupConfig } from '~/data-provider'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; import store from '~/store'; export function TemporaryChat() { const localize = useLocalize(); + const { data: startupConfig } = useGetStartupConfig(); const [isTemporary, setIsTemporary] = useRecoilState(store.isTemporary); const conversation = useRecoilValue(store.conversationByIndex(0)); const isSubmitting = useRecoilValue(store.isSubmittingFamily(0)); const tooltipDescription = useShortcutHint('toggleTemporaryChat', localize('com_ui_temporary')); const ariaKey = useShortcutAriaKey('toggleTemporaryChat'); + const isEnforced = isForcedTemporaryRetention(startupConfig?.interface?.retentionMode); + const handleBadgeToggle = useRecoilCallback( () => () => { + if (isEnforced) { + return; + } setIsTemporary(!isTemporary); }, - [isTemporary], + [isTemporary, isEnforced], ); if ( @@ -30,21 +38,26 @@ export function TemporaryChat() { return null; } + const isActive = isEnforced || isTemporary; + const label = isEnforced ? localize('com_ui_temporary_enforced') : localize('com_ui_temporary'); + return (