📁 fix: Consistent Export Filenames Across Formats (#14708)

* 📁 fix: Consistent Export Filenames Across Formats

Conversation exports produced different filenames per format: txt/md/csv go
through export-from-json, whose default formatter replaces only the first
whitespace run (non-global regex), while png/json downloads keep spaces.
Normalize the filename once before export so every format yields
Word1_Word2_Word3.ext; the preset JSON export shared the same defect.

* 🧹 chore: fix import order via sort-imports
This commit is contained in:
Oliver Faust 2026-08-10 16:36:41 +02:00 committed by GitHub
parent 87a8b9aa12
commit 26bcbb713c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 45 additions and 4 deletions

View file

@ -11,6 +11,7 @@ import {
} from '@librechat/client';
import type { TConversation } from 'librechat-data-provider';
import { useLocalize, useExportConversation } from '~/hooks';
import { normalizeExportFilename } from '~/utils';
const TYPE_OPTIONS = [
{ value: 'screenshot', label: 'screenshot (.png)' },
@ -72,7 +73,7 @@ export default function ExportModal({
const { exportConversation } = useExportConversation({
conversation,
filename: filenamify(filename),
filename: normalizeExportFilename(filenamify(filename)),
type,
includeOptions,
exportBranches,

View file

@ -1,18 +1,23 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import filenamify from 'filenamify';
import exportFromJSON from 'export-from-json';
import { useToastContext } from '@librechat/client';
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil';
import { useCreatePresetMutation, useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider';
import {
normalizeExportFilename,
removeUnavailableTools,
getConvoSwitchLogic,
cleanupPreset,
} from '~/utils';
import {
useUpdatePresetMutation,
useDeletePresetMutation,
useGetPresetsQuery,
} from '~/data-provider';
import { cleanupPreset, removeUnavailableTools, getConvoSwitchLogic } from '~/utils';
import useGetConversation from '~/hooks/Conversations/useGetConversation';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useAuthContext } from '~/hooks/AuthContext';
@ -264,7 +269,7 @@ export default function usePresets(index = 0) {
if (!preset) {
return;
}
const fileName = filenamify(preset.title || 'preset');
const fileName = normalizeExportFilename(filenamify(preset.title || 'preset'));
exportFromJSON({
data: cleanupPreset({ preset }),
fileName,

View file

@ -0,0 +1,26 @@
import { normalizeExportFilename } from './files';
describe('normalizeExportFilename', () => {
it('replaces every whitespace run with a single underscore', () => {
expect(normalizeExportFilename('Word1 Word2 Word3')).toBe('Word1_Word2_Word3');
});
it('collapses consecutive whitespace into one underscore', () => {
expect(normalizeExportFilename('Word1 Word2\tWord3')).toBe('Word1_Word2_Word3');
});
it('leaves filenames without whitespace unchanged', () => {
expect(normalizeExportFilename('single-word_name')).toBe('single-word_name');
});
it('handles an empty string', () => {
expect(normalizeExportFilename('')).toBe('');
});
it('never leaves whitespace, so downstream whitespace-based formatters are no-ops', () => {
const inputs = ['Word1 Word2 Word3', ' leading', 'trailing ', 'tab\there', 'a b c'];
for (const input of inputs) {
expect(normalizeExportFilename(input)).not.toMatch(/\s/);
}
});
});

View file

@ -439,3 +439,12 @@ export function sortPagesByRelevance(
}
return [...pages].sort((a, b) => (pageRelevance[b] || 0) - (pageRelevance[a] || 0));
}
/**
* Collapses whitespace runs to underscores so export filenames come out
* identical across all export formats: `export-from-json`'s default formatter
* only replaces the first whitespace run, while direct downloads replace none.
*/
export function normalizeExportFilename(filename: string): string {
return filename.replace(/\s+/g, '_');
}