]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
webui: use date in more human readable exported filename (#19939)
authorWoof Dog <redacted>
Mon, 16 Mar 2026 10:18:13 +0000 (10:18 +0000)
committerGitHub <redacted>
Mon, 16 Mar 2026 10:18:13 +0000 (11:18 +0100)
* webui: use date in exported filename

Move conversation naming and export to utils

update index.html.gz

* webui: move literals to message export constants file

* webui: move export naming and download back to the conversation store

* chore: update webui build output

* webui: add comments to some constants

* chore: update webui build output

tools/server/public/index.html.gz
tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsImportExportTab.svelte
tools/server/webui/src/lib/constants/index.ts
tools/server/webui/src/lib/constants/message-export.ts [new file with mode: 0644]
tools/server/webui/src/lib/stores/conversations.svelte.ts
tools/server/webui/src/lib/utils/conversation-utils.ts

index 493058aa015d15926eef9f15b49ab76090b19e6a..98e2e49431946d1ba182b0b8a619b8089f189ae1 100644 (file)
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
index 68839438f6dd9862e99395df5aee67db05fca228..537c839f5853ef0f7010722daa9110d0100098c5 100644 (file)
@@ -3,6 +3,7 @@
        import { Button } from '$lib/components/ui/button';
        import { DialogConversationSelection, DialogConfirmation } from '$lib/components/app';
        import { createMessageCountMap } from '$lib/utils';
+       import { ISO_DATE_TIME_SEPARATOR } from '$lib/constants';
        import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
        import { toast } from 'svelte-sonner';
 
                                })
                        );
 
-                       const blob = new Blob([JSON.stringify(allData, null, 2)], {
-                               type: 'application/json'
-                       });
-                       const url = URL.createObjectURL(blob);
-                       const a = document.createElement('a');
-
-                       a.href = url;
-                       a.download = `conversations_${new Date().toISOString().split('T')[0]}.json`;
-                       document.body.appendChild(a);
-                       a.click();
-                       document.body.removeChild(a);
-                       URL.revokeObjectURL(url);
+                       conversationsStore.downloadConversationFile(
+                               allData,
+                               `${new Date().toISOString().split(ISO_DATE_TIME_SEPARATOR)[0]}_conversations.json`
+                       );
 
                        exportedConversations = selectedConversations;
                        showExportSummary = true;
index 41c117df543b997d70f59702a561b9486344db43..f3593c03b1291646ef1c48201bd688f21c03db7c 100644 (file)
@@ -24,6 +24,7 @@ export * from './max-bundle-size';
 export * from './mcp';
 export * from './mcp-form';
 export * from './mcp-resource';
+export * from './message-export';
 export * from './model-id';
 export * from './precision';
 export * from './processing-info';
diff --git a/tools/server/webui/src/lib/constants/message-export.ts b/tools/server/webui/src/lib/constants/message-export.ts
new file mode 100644 (file)
index 0000000..79fa36f
--- /dev/null
@@ -0,0 +1,20 @@
+// Conversation filename constants
+
+// Length of the trimmed conversation ID in the filename
+export const EXPORT_CONV_ID_TRIM_LENGTH = 8;
+// Maximum length of the sanitized conversation name snippet
+export const EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH = 20;
+// Characters to keep in the ISO timestamp. 19 keeps 2026-01-01T00:00:00
+export const ISO_TIMESTAMP_SLICE_LENGTH = 19;
+
+// Replacements for making the conversation title filename-friendly
+export const NON_ALPHANUMERIC_REGEX = /[^a-z0-9]/gi;
+export const EXPORT_CONV_NONALNUM_REPLACEMENT = '_';
+export const MULTIPLE_UNDERSCORE_REGEX = /_+/g;
+
+// Replacements to the ISO date for use in the export filename
+export const ISO_DATE_TIME_SEPARATOR = 'T';
+export const ISO_DATE_TIME_SEPARATOR_REPLACEMENT = '_';
+
+export const ISO_TIME_SEPARATOR = ':';
+export const ISO_TIME_SEPARATOR_REPLACEMENT = '-';
index ec1daa90d9612df43d9dcc638a081553e17495b9..39f206479faa51aeca6b37f82a5a7e13eb59d2d9 100644 (file)
@@ -26,6 +26,18 @@ import { config } from '$lib/stores/settings.svelte';
 import { filterByLeafNodeId, findLeafNode } from '$lib/utils';
 import type { McpServerOverride } from '$lib/types/database';
 import { MessageRole } from '$lib/enums';
+import {
+       ISO_DATE_TIME_SEPARATOR,
+       ISO_DATE_TIME_SEPARATOR_REPLACEMENT,
+       ISO_TIMESTAMP_SLICE_LENGTH,
+       EXPORT_CONV_ID_TRIM_LENGTH,
+       EXPORT_CONV_NONALNUM_REPLACEMENT,
+       EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH,
+       ISO_TIME_SEPARATOR,
+       ISO_TIME_SEPARATOR_REPLACEMENT,
+       NON_ALPHANUMERIC_REGEX,
+       MULTIPLE_UNDERSCORE_REGEX
+} from '$lib/constants';
 
 class ConversationsStore {
        /**
@@ -620,56 +632,83 @@ class ConversationsStore {
         */
 
        /**
-        * Downloads a conversation as JSON file.
-        * @param convId - The conversation ID to download
+        * Generates a sanitized filename for a conversation export
+        * @param conversation - The conversation metadata
+        * @param msgs - Optional array of messages belonging to the conversation
+        * @returns The generated filename string
         */
-       async downloadConversation(convId: string): Promise<void> {
-               let conversation: DatabaseConversation | null;
-               let messages: DatabaseMessage[];
+       generateConversationFilename(
+               conversation: { id?: string; name?: string },
+               msgs?: DatabaseMessage[]
+       ): string {
+               const conversationName = (conversation.name ?? '').trim().toLowerCase();
 
-               if (this.activeConversation?.id === convId) {
-                       conversation = this.activeConversation;
-                       messages = this.activeMessages;
-               } else {
-                       conversation = await DatabaseService.getConversation(convId);
-                       if (!conversation) return;
-                       messages = await DatabaseService.getConversationMessages(convId);
-               }
+               const sanitizedName = conversationName
+                       .replace(NON_ALPHANUMERIC_REGEX, EXPORT_CONV_NONALNUM_REPLACEMENT)
+                       .replace(MULTIPLE_UNDERSCORE_REGEX, '_')
+                       .substring(0, EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH);
+
+               // If we have messages, use the timestamp of the newest message
+               const referenceDate = msgs?.length
+                       ? new Date(Math.max(...msgs.map((m) => m.timestamp)))
+                       : new Date();
 
-               this.triggerDownload({ conv: conversation, messages });
+               const iso = referenceDate.toISOString().slice(0, ISO_TIMESTAMP_SLICE_LENGTH);
+               const formattedDate = iso
+                       .replace(ISO_DATE_TIME_SEPARATOR, ISO_DATE_TIME_SEPARATOR_REPLACEMENT)
+                       .replaceAll(ISO_TIME_SEPARATOR, ISO_TIME_SEPARATOR_REPLACEMENT);
+               const trimmedConvId = conversation.id?.slice(0, EXPORT_CONV_ID_TRIM_LENGTH) ?? '';
+               return `${formattedDate}_conv_${trimmedConvId}_${sanitizedName}.json`;
        }
 
        /**
-        * Exports all conversations with their messages as a JSON file
-        * @returns The list of exported conversations
+        * Triggers a browser download of the provided exported conversation data
+        * @param data - The exported conversation payload (either a single conversation or array of them)
+        * @param filename - Filename; if omitted, a deterministic name is generated
         */
-       async exportAllConversations(): Promise<DatabaseConversation[]> {
-               const allConversations = await DatabaseService.getAllConversations();
+       downloadConversationFile(data: ExportedConversations, filename?: string): void {
+               // Choose the first conversation or message
+               const conversation =
+                       'conv' in data ? data.conv : Array.isArray(data) ? data[0]?.conv : undefined;
+               const msgs =
+                       'messages' in data ? data.messages : Array.isArray(data) ? data[0]?.messages : undefined;
 
-               if (allConversations.length === 0) {
-                       throw new Error('No conversations to export');
+               if (!conversation) {
+                       console.error('Invalid data: missing conversation');
+                       return;
                }
 
-               const allData = await Promise.all(
-                       allConversations.map(async (conv) => {
-                               const messages = await DatabaseService.getConversationMessages(conv.id);
-                               return { conv, messages };
-                       })
-               );
+               const downloadFilename = filename ?? this.generateConversationFilename(conversation, msgs);
 
-               const blob = new Blob([JSON.stringify(allData, null, 2)], { type: 'application/json' });
+               const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
-               a.download = `all_conversations_${new Date().toISOString().split('T')[0]}.json`;
+               a.download = downloadFilename;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
+       }
+
+       /**
+        * Downloads a conversation as JSON file.
+        * @param convId - The conversation ID to download
+        */
+       async downloadConversation(convId: string): Promise<void> {
+               let conversation: DatabaseConversation | null;
+               let messages: DatabaseMessage[];
 
-               toast.success(`All conversations (${allConversations.length}) prepared for download`);
+               if (this.activeConversation?.id === convId) {
+                       conversation = this.activeConversation;
+                       messages = this.activeMessages;
+               } else {
+                       conversation = await DatabaseService.getConversation(convId);
+                       if (!conversation) return;
+                       messages = await DatabaseService.getConversationMessages(convId);
+               }
 
-               return allConversations;
+               this.downloadConversationFile({ conv: conversation, messages });
        }
 
        /**
@@ -743,37 +782,6 @@ class ConversationsStore {
                await this.loadConversations();
                return result;
        }
-
-       /**
-        * Triggers file download in browser
-        */
-       private triggerDownload(data: ExportedConversations, filename?: string): void {
-               const conversation =
-                       'conv' in data ? data.conv : Array.isArray(data) ? data[0]?.conv : undefined;
-
-               if (!conversation) {
-                       console.error('Invalid data: missing conversation');
-                       return;
-               }
-
-               const conversationName = conversation.name?.trim() || '';
-               const truncatedSuffix = conversationName
-                       .toLowerCase()
-                       .replace(/[^a-z0-9]/gi, '_')
-                       .replace(/_+/g, '_')
-                       .substring(0, 20);
-               const downloadFilename = filename || `conversation_${conversation.id}_${truncatedSuffix}.json`;
-
-               const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
-               const url = URL.createObjectURL(blob);
-               const a = document.createElement('a');
-               a.href = url;
-               a.download = downloadFilename;
-               document.body.appendChild(a);
-               a.click();
-               document.body.removeChild(a);
-               URL.revokeObjectURL(url);
-       }
 }
 
 export const conversationsStore = new ConversationsStore();
index aee244a08055ec2960a1d3aaa6fd746f72e6e69f..2c3d838999b27966b0880046ec6c98e316169a54 100644 (file)
@@ -1,6 +1,7 @@
 /**
  * Utility functions for conversation data manipulation
  */
+import type { DatabaseMessage } from '$lib/types';
 
 /**
  * Creates a map of conversation IDs to their message counts from exported conversation data