From 8101660525aae3456dbe9099d5006eb937f8587a Mon Sep 17 00:00:00 2001 From: Mikhail Leonov Date: Mon, 24 Aug 2026 13:38:59 +0600 Subject: [PATCH 1/5] feat: add functionality to archive, delete, and export all chats with user confirmation dialogs and localization support --- i18n/mobile/app/de.json | 4 + i18n/mobile/app/en.json | 4 + i18n/mobile/app/es.json | 4 + i18n/mobile/app/fr.json | 4 + i18n/mobile/app/ja.json | 4 + i18n/mobile/app/pt.json | 4 + i18n/mobile/app/ru.json | 4 + i18n/mobile/app/zh.json | 4 + i18n/mobile/chat/de.json | 3 +- i18n/mobile/chat/en.json | 3 +- i18n/mobile/chat/es.json | 3 +- i18n/mobile/chat/fr.json | 3 +- i18n/mobile/chat/ja.json | 3 +- i18n/mobile/chat/pt.json | 3 +- i18n/mobile/chat/ru.json | 3 +- i18n/mobile/chat/zh.json | 3 +- .../lib/hooks/use-export-archived-chats.ts | 6 +- .../chat/features/chat/src/lib/component.tsx | 37 ++++- .../lib/components/user-message/component.tsx | 35 +++-- .../src/use-create-new-chat.ts | 2 + .../use-send-message/src/use-send-message.ts | 60 ++++---- .../features/settings/src/lib/component.tsx | 129 ++++++++++++++---- .../features/settings/src/lib/config.ts | 6 + .../features/settings/src/lib/hooks/index.ts | 1 + .../src/lib/hooks/use-export-all-chats.ts | 32 +++++ .../features/settings/src/lib/types/index.ts | 1 - .../src/lib/types/settings-toggles.ts | 7 - .../settings-section-item/component.tsx | 14 +- .../file-system-service/src/service.ts | 12 ++ .../data-access/api/src/lib/chats/api.ts | 36 +++++ .../data-access/api/src/lib/chats/service.ts | 8 ++ .../utils/prepare-complete-chat-payload.ts | 8 +- .../data-access/api/src/lib/folders/config.ts | 9 +- .../api/src/lib/users/models/ui-settings.ts | 17 +++ libs/shared/utils/feature-flag/src/config.ts | 3 + .../feature-flag/src/enums/feature-id.ts | 3 + 36 files changed, 379 insertions(+), 103 deletions(-) create mode 100644 libs/mobile/settings/features/settings/src/lib/config.ts create mode 100644 libs/mobile/settings/features/settings/src/lib/hooks/index.ts create mode 100644 libs/mobile/settings/features/settings/src/lib/hooks/use-export-all-chats.ts delete mode 100644 libs/mobile/settings/features/settings/src/lib/types/settings-toggles.ts diff --git a/i18n/mobile/app/de.json b/i18n/mobile/app/de.json index 39b1fe73..46a2dc4f 100644 --- a/i18n/mobile/app/de.json +++ b/i18n/mobile/app/de.json @@ -23,7 +23,11 @@ "TEXT_ARCHIVED_CHATS": "Archivierte Chats", "TEXT_EXPORT_ALL_CHATS": "Alle Chats exportieren", "TEXT_ARCHIVE_ALL": "Alle archivieren", + "TEXT_ARCHIVE_ALL_CHATS_TITLE": "Alle Chats archivieren", + "TEXT_ARCHIVE_ALL_CHATS_MESSAGE": "Möchten Sie wirklich alle Chats archivieren? Diese Aktion kann nicht rückgängig gemacht werden.", "TEXT_DELETE_ALL": "Alle löschen", + "TEXT_DELETE_ALL_CHATS_TITLE": "Alle Chats löschen", + "TEXT_DELETE_ALL_CHATS_MESSAGE": "Möchten Sie wirklich alle Chats löschen? Diese Aktion kann nicht rückgängig gemacht werden.", "TEXT_ALWAYS_ON_WEB_SEARCH": "Websuche immer aktiv", "TEXT_HAPTIC_FEEDBACK": "Haptisches Feedback", "TEXT_ENABLE_MESSAGE_QUEUE": "Nachrichtenwarteschlange aktivieren", diff --git a/i18n/mobile/app/en.json b/i18n/mobile/app/en.json index e3e95b49..d735724f 100644 --- a/i18n/mobile/app/en.json +++ b/i18n/mobile/app/en.json @@ -23,7 +23,11 @@ "TEXT_ARCHIVED_CHATS": "Archived chats", "TEXT_EXPORT_ALL_CHATS": "Export all chats", "TEXT_ARCHIVE_ALL": "Archive all", + "TEXT_ARCHIVE_ALL_CHATS_TITLE": "Archive All Chats", + "TEXT_ARCHIVE_ALL_CHATS_MESSAGE": "Are you sure you want to archive all chats? This action cannot be undone.", "TEXT_DELETE_ALL": "Delete all", + "TEXT_DELETE_ALL_CHATS_TITLE": "Delete All Chats", + "TEXT_DELETE_ALL_CHATS_MESSAGE": "Are you sure you want to delete all chats? This action cannot be undone.", "TEXT_ALWAYS_ON_WEB_SEARCH": "Always-on web search", "TEXT_HAPTIC_FEEDBACK": "Haptic feedback", "TEXT_ENABLE_MESSAGE_QUEUE": "Enable message queue", diff --git a/i18n/mobile/app/es.json b/i18n/mobile/app/es.json index 84dd8760..7ace55a3 100644 --- a/i18n/mobile/app/es.json +++ b/i18n/mobile/app/es.json @@ -23,7 +23,11 @@ "TEXT_ARCHIVED_CHATS": "Chats archivados", "TEXT_EXPORT_ALL_CHATS": "Exportar todos los chats", "TEXT_ARCHIVE_ALL": "Archivar todo", + "TEXT_ARCHIVE_ALL_CHATS_TITLE": "Archivar todos los chats", + "TEXT_ARCHIVE_ALL_CHATS_MESSAGE": "¿Estás seguro de que quieres archivar todos los chats? Esta acción no se puede deshacer.", "TEXT_DELETE_ALL": "Eliminar todo", + "TEXT_DELETE_ALL_CHATS_TITLE": "Eliminar todos los chats", + "TEXT_DELETE_ALL_CHATS_MESSAGE": "¿Estás seguro de que quieres eliminar todos los chats? Esta acción no se puede deshacer.", "TEXT_ALWAYS_ON_WEB_SEARCH": "Búsqueda web siempre activa", "TEXT_HAPTIC_FEEDBACK": "Respuesta háptica", "TEXT_ENABLE_MESSAGE_QUEUE": "Activar cola de mensajes", diff --git a/i18n/mobile/app/fr.json b/i18n/mobile/app/fr.json index 3757cd79..c4ef4db4 100644 --- a/i18n/mobile/app/fr.json +++ b/i18n/mobile/app/fr.json @@ -23,7 +23,11 @@ "TEXT_ARCHIVED_CHATS": "Discussions archivées", "TEXT_EXPORT_ALL_CHATS": "Exporter toutes les discussions", "TEXT_ARCHIVE_ALL": "Tout archiver", + "TEXT_ARCHIVE_ALL_CHATS_TITLE": "Archiver toutes les discussions", + "TEXT_ARCHIVE_ALL_CHATS_MESSAGE": "Voulez-vous vraiment archiver toutes les discussions ? Cette action est irréversible.", "TEXT_DELETE_ALL": "Tout supprimer", + "TEXT_DELETE_ALL_CHATS_TITLE": "Supprimer toutes les discussions", + "TEXT_DELETE_ALL_CHATS_MESSAGE": "Voulez-vous vraiment supprimer toutes les discussions ? Cette action est irréversible.", "TEXT_ALWAYS_ON_WEB_SEARCH": "Recherche web toujours active", "TEXT_HAPTIC_FEEDBACK": "Retour haptique", "TEXT_ENABLE_MESSAGE_QUEUE": "Activer la file d'attente des messages", diff --git a/i18n/mobile/app/ja.json b/i18n/mobile/app/ja.json index 9426e399..0de99f0a 100644 --- a/i18n/mobile/app/ja.json +++ b/i18n/mobile/app/ja.json @@ -23,7 +23,11 @@ "TEXT_ARCHIVED_CHATS": "アーカイブ済みチャット", "TEXT_EXPORT_ALL_CHATS": "すべてのチャットをエクスポート", "TEXT_ARCHIVE_ALL": "すべてアーカイブ", + "TEXT_ARCHIVE_ALL_CHATS_TITLE": "すべてのチャットをアーカイブ", + "TEXT_ARCHIVE_ALL_CHATS_MESSAGE": "すべてのチャットをアーカイブしてもよろしいですか?この操作は元に戻せません。", "TEXT_DELETE_ALL": "すべて削除", + "TEXT_DELETE_ALL_CHATS_TITLE": "すべてのチャットを削除", + "TEXT_DELETE_ALL_CHATS_MESSAGE": "すべてのチャットを削除してもよろしいですか?この操作は元に戻せません。", "TEXT_ALWAYS_ON_WEB_SEARCH": "ウェブ検索を常に有効にする", "TEXT_HAPTIC_FEEDBACK": "触覚フィードバック", "TEXT_ENABLE_MESSAGE_QUEUE": "メッセージキューを有効にする", diff --git a/i18n/mobile/app/pt.json b/i18n/mobile/app/pt.json index f5ed4dcb..322d923f 100644 --- a/i18n/mobile/app/pt.json +++ b/i18n/mobile/app/pt.json @@ -23,7 +23,11 @@ "TEXT_ARCHIVED_CHATS": "Chats arquivados", "TEXT_EXPORT_ALL_CHATS": "Exportar todos os chats", "TEXT_ARCHIVE_ALL": "Arquivar tudo", + "TEXT_ARCHIVE_ALL_CHATS_TITLE": "Arquivar todos os chats", + "TEXT_ARCHIVE_ALL_CHATS_MESSAGE": "Tem certeza de que deseja arquivar todos os chats? Esta ação não pode ser desfeita.", "TEXT_DELETE_ALL": "Excluir tudo", + "TEXT_DELETE_ALL_CHATS_TITLE": "Excluir todos os chats", + "TEXT_DELETE_ALL_CHATS_MESSAGE": "Tem certeza de que deseja excluir todos os chats? Esta ação não pode ser desfeita.", "TEXT_ALWAYS_ON_WEB_SEARCH": "Busca na web sempre ativa", "TEXT_HAPTIC_FEEDBACK": "Resposta tátil", "TEXT_ENABLE_MESSAGE_QUEUE": "Ativar fila de mensagens", diff --git a/i18n/mobile/app/ru.json b/i18n/mobile/app/ru.json index a539f9bc..7434f5b3 100644 --- a/i18n/mobile/app/ru.json +++ b/i18n/mobile/app/ru.json @@ -23,7 +23,11 @@ "TEXT_ARCHIVED_CHATS": "Архивные чаты", "TEXT_EXPORT_ALL_CHATS": "Экспортировать все чаты", "TEXT_ARCHIVE_ALL": "Архивировать все", + "TEXT_ARCHIVE_ALL_CHATS_TITLE": "Архивировать все чаты", + "TEXT_ARCHIVE_ALL_CHATS_MESSAGE": "Вы уверены, что хотите архивировать все чаты? Это действие нельзя отменить.", "TEXT_DELETE_ALL": "Удалить все", + "TEXT_DELETE_ALL_CHATS_TITLE": "Удалить все чаты", + "TEXT_DELETE_ALL_CHATS_MESSAGE": "Вы уверены, что хотите удалить все чаты? Это действие нельзя отменить.", "TEXT_ALWAYS_ON_WEB_SEARCH": "Всегда искать в интернете", "TEXT_HAPTIC_FEEDBACK": "Тактильная отдача", "TEXT_ENABLE_MESSAGE_QUEUE": "Включить очередь сообщений", diff --git a/i18n/mobile/app/zh.json b/i18n/mobile/app/zh.json index 169b38b3..f34f5a74 100644 --- a/i18n/mobile/app/zh.json +++ b/i18n/mobile/app/zh.json @@ -23,7 +23,11 @@ "TEXT_ARCHIVED_CHATS": "已归档聊天", "TEXT_EXPORT_ALL_CHATS": "导出所有聊天", "TEXT_ARCHIVE_ALL": "全部归档", + "TEXT_ARCHIVE_ALL_CHATS_TITLE": "归档所有聊天", + "TEXT_ARCHIVE_ALL_CHATS_MESSAGE": "确定要归档所有聊天吗?此操作无法撤销。", "TEXT_DELETE_ALL": "全部删除", + "TEXT_DELETE_ALL_CHATS_TITLE": "删除所有聊天", + "TEXT_DELETE_ALL_CHATS_MESSAGE": "确定要删除所有聊天吗?此操作无法撤销。", "TEXT_ALWAYS_ON_WEB_SEARCH": "始终开启联网搜索", "TEXT_HAPTIC_FEEDBACK": "触感反馈", "TEXT_ENABLE_MESSAGE_QUEUE": "启用消息队列", diff --git a/i18n/mobile/chat/de.json b/i18n/mobile/chat/de.json index 58a668a4..d94444a8 100644 --- a/i18n/mobile/chat/de.json +++ b/i18n/mobile/chat/de.json @@ -6,7 +6,8 @@ }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Nachricht senden", - "TEXT_MODEL_NOT_SELECTED": "Kein Modell ausgewählt" + "TEXT_MODEL_NOT_SELECTED": "Kein Modell ausgewählt", + "TEXT_MESSAGE_QUEUED": "Nachricht in Warteschlange" }, "CHAT_MENU_LIST": { "PINNED_CHAT_LIST": { diff --git a/i18n/mobile/chat/en.json b/i18n/mobile/chat/en.json index 98478474..6ff6a718 100644 --- a/i18n/mobile/chat/en.json +++ b/i18n/mobile/chat/en.json @@ -6,7 +6,8 @@ }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Send a message", - "TEXT_MODEL_NOT_SELECTED": "Model not selected" + "TEXT_MODEL_NOT_SELECTED": "Model not selected", + "TEXT_MESSAGE_QUEUED": "Message queued" }, "CHAT_MENU_LIST": { "PINNED_CHAT_LIST": { diff --git a/i18n/mobile/chat/es.json b/i18n/mobile/chat/es.json index 5c4b823e..a2d7e857 100644 --- a/i18n/mobile/chat/es.json +++ b/i18n/mobile/chat/es.json @@ -6,7 +6,8 @@ }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Enviar un mensaje", - "TEXT_MODEL_NOT_SELECTED": "Modelo no seleccionado" + "TEXT_MODEL_NOT_SELECTED": "Modelo no seleccionado", + "TEXT_MESSAGE_QUEUED": "Mensaje en cola" }, "CHAT_MENU_LIST": { "PINNED_CHAT_LIST": { diff --git a/i18n/mobile/chat/fr.json b/i18n/mobile/chat/fr.json index 6c610f3a..f592a874 100644 --- a/i18n/mobile/chat/fr.json +++ b/i18n/mobile/chat/fr.json @@ -6,7 +6,8 @@ }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Envoyer un message", - "TEXT_MODEL_NOT_SELECTED": "Modèle non sélectionné" + "TEXT_MODEL_NOT_SELECTED": "Modèle non sélectionné", + "TEXT_MESSAGE_QUEUED": "Message mis en file d'attente" }, "CHAT_MENU_LIST": { "PINNED_CHAT_LIST": { diff --git a/i18n/mobile/chat/ja.json b/i18n/mobile/chat/ja.json index 87a27b64..d1d9a085 100644 --- a/i18n/mobile/chat/ja.json +++ b/i18n/mobile/chat/ja.json @@ -6,7 +6,8 @@ }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "メッセージを送信", - "TEXT_MODEL_NOT_SELECTED": "モデルが選択されていません" + "TEXT_MODEL_NOT_SELECTED": "モデルが選択されていません", + "TEXT_MESSAGE_QUEUED": "メッセージをキューに追加しました" }, "CHAT_MENU_LIST": { "PINNED_CHAT_LIST": { diff --git a/i18n/mobile/chat/pt.json b/i18n/mobile/chat/pt.json index 0379e46b..76487982 100644 --- a/i18n/mobile/chat/pt.json +++ b/i18n/mobile/chat/pt.json @@ -6,7 +6,8 @@ }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Enviar uma mensagem", - "TEXT_MODEL_NOT_SELECTED": "Modelo não selecionado" + "TEXT_MODEL_NOT_SELECTED": "Modelo não selecionado", + "TEXT_MESSAGE_QUEUED": "Mensagem na fila" }, "CHAT_MENU_LIST": { "PINNED_CHAT_LIST": { diff --git a/i18n/mobile/chat/ru.json b/i18n/mobile/chat/ru.json index 02a69f81..0e0203d6 100644 --- a/i18n/mobile/chat/ru.json +++ b/i18n/mobile/chat/ru.json @@ -6,7 +6,8 @@ }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Отправить сообщение", - "TEXT_MODEL_NOT_SELECTED": "Модель не выбрана" + "TEXT_MODEL_NOT_SELECTED": "Модель не выбрана", + "TEXT_MESSAGE_QUEUED": "Сообщение добавлено в очередь" }, "CHAT_MENU_LIST": { "PINNED_CHAT_LIST": { diff --git a/i18n/mobile/chat/zh.json b/i18n/mobile/chat/zh.json index 8a05814d..e859609c 100644 --- a/i18n/mobile/chat/zh.json +++ b/i18n/mobile/chat/zh.json @@ -6,7 +6,8 @@ }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "发送消息", - "TEXT_MODEL_NOT_SELECTED": "未选择模型" + "TEXT_MODEL_NOT_SELECTED": "未选择模型", + "TEXT_MESSAGE_QUEUED": "消息已加入队列" }, "CHAT_MENU_LIST": { "PINNED_CHAT_LIST": { diff --git a/libs/mobile/chat/features/archived-chats-list/src/lib/hooks/use-export-archived-chats.ts b/libs/mobile/chat/features/archived-chats-list/src/lib/hooks/use-export-archived-chats.ts index 42ef8340..950ee1ec 100644 --- a/libs/mobile/chat/features/archived-chats-list/src/lib/hooks/use-export-archived-chats.ts +++ b/libs/mobile/chat/features/archived-chats-list/src/lib/hooks/use-export-archived-chats.ts @@ -1,4 +1,3 @@ -import * as FileSystem from 'expo-file-system/legacy'; import { useState } from 'react'; import { fileSystemService, @@ -21,10 +20,7 @@ export const useExportArchivedChats = (): UseExportArchivedChatsResult => { setIsExporting(true); try { - const fileUri = `${FileSystem.documentDirectory}${downloadFileName}`; - const downloadedFile = await fileSystemService.downloadFile(exportAllChatsApiURL, fileUri); - - await fileSystemService.shareAsync(downloadedFile.uri, downloadFileName, MimeType.JSON, UtiType.JSON); + await fileSystemService.downloadAndShareFile(exportAllChatsApiURL, downloadFileName, MimeType.JSON, UtiType.JSON); } catch { ToastService.showError(); } finally { diff --git a/libs/mobile/chat/features/chat/src/lib/component.tsx b/libs/mobile/chat/features/chat/src/lib/component.tsx index 432862e2..b8c70fd9 100644 --- a/libs/mobile/chat/features/chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/component.tsx @@ -20,6 +20,7 @@ import { ChatGenerationOption, chatQueriesKeys, createMessagesList, + usersApi, } from '@open-webui-react-native/shared/data-access/api'; import { Role } from '@open-webui-react-native/shared/data-access/common'; import { useSubscribeToQueryCache } from '@open-webui-react-native/shared/data-access/query-client'; @@ -46,11 +47,18 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C const [isInputFocusing, setIsInputFocusing] = useState(false); //NOTE: Needs to avoid ChatBottomButton jumping when auto-scrolling after focus const isSocketConnected = useSelector(webSocketState$.isSocketConnected); + const { data: userSettings } = usersApi.useGetUserSettings(); + // NOTE: Default matches the Open WebUI web app's own fallback (Interface.svelte) + const isMessageQueueEnabled = userSettings?.ui.enableMessageQueue ?? true; const [isMessagesListLoaded, setIsMessagesListLoaded] = useState(false); const [isChatVisible, setIsChatVisible] = useState(false); const [activeInputMode, setActiveInputMode] = useState(null); const [inputRerenderKey, setInputRerenderKey] = useState(0); + const [queuedMessage, setQueuedMessage] = useState<{ + inputValue: string; + options: Array; + } | null>(null); const { attachedFiles, @@ -86,6 +94,10 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C // NOTE: chat.messages is a legacy snapshot the backend no longer maintains, so the current branch is derived from history const messages = useMemo(() => (history ? createMessagesList(history, history.currentId) : []), [history]); const isResponseGenerating = !history?.messages[history.currentId].done; + // NOTE: With message queueing enabled, a generating response no longer force-disables the composer or + // swaps the send button for "stop" — the user trades the visible stop-generation action for being able + // to compose/send the next message immediately (queued via `queuedMessage`). + const isComposerBlockedByGeneration = isResponseGenerating && !isMessageQueueEnabled; const shouldHideContent = isLoading || isRefetching || !isMessagesListLoaded || !selectedModelId; @@ -174,7 +186,13 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C analyticsService.trackEvent(AnalyticsEvent.GENERATE_IMAGE_USED); } - sendMessage(inputValue, selectedModelId, options, attachedFiles.get(), attachedImages.get()); + if (isResponseGenerating && isMessageQueueEnabled) { + setQueuedMessage({ inputValue, options }); + ToastService.show(translate('TEXT_MESSAGE_QUEUED')); + } else { + sendMessage(inputValue, selectedModelId, options, attachedFiles.get(), attachedImages.get()); + } + analyticsService.trackEvent(AnalyticsEvent.MESSAGE_SENT, { modelId: selectedModelId }); reset(); resetAttachments(); @@ -208,6 +226,19 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C } }, [isNewChat, isSuccess, chatId]); + useEffect(() => { + if (!isResponseGenerating && queuedMessage && selectedModelId) { + sendMessage( + queuedMessage.inputValue, + selectedModelId, + queuedMessage.options, + attachedFiles.get(), + attachedImages.get(), + ); + setQueuedMessage(null); + } + }, [isResponseGenerating, queuedMessage, selectedModelId, sendMessage, attachedFiles, attachedImages]); + return ( {shouldHideContent && ( @@ -264,7 +295,7 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C onFocus={handleInputFocus} name='inputValue' onSubmit={onSubmit} - isLoading={isSending || !isSocketConnected || isResponseGenerating} + isLoading={isSending || !isSocketConnected || isComposerBlockedByGeneration} attachedFiles={attachedFiles} onFileUploaded={handleFileUploaded} onDeleteFilePress={handleDeleteFile} @@ -272,7 +303,7 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C onImageUploaded={handleImageUploaded} onDeleteImagePress={handleDeleteImage} modelId={selectedModelId} - isResponseGenerating={isResponseGenerating} + isResponseGenerating={isComposerBlockedByGeneration} inputRerenderKey={inputRerenderKey} chat={chat} /> diff --git a/libs/mobile/chat/features/chat/src/lib/components/user-message/component.tsx b/libs/mobile/chat/features/chat/src/lib/components/user-message/component.tsx index d91bb7b2..d9aa1754 100644 --- a/libs/mobile/chat/features/chat/src/lib/components/user-message/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/components/user-message/component.tsx @@ -10,7 +10,7 @@ import { import { AppMarkdownView } from '@open-webui-react-native/mobile/shared/features/markdown-view'; import { cn, colors, screenWidth, spacings } from '@open-webui-react-native/mobile/shared/ui/styles'; import { AppText, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; -import { Message } from '@open-webui-react-native/shared/data-access/api'; +import { Message, usersApi } from '@open-webui-react-native/shared/data-access/api'; import { AttachedFile, FileType } from '@open-webui-react-native/shared/data-access/common'; import { formatDateTime } from '@open-webui-react-native/shared/utils/date'; import { deepMemo } from '@open-webui-react-native/shared/utils/deep-memo'; @@ -39,6 +39,11 @@ function ChatUserMessageComponent({ }: ChatUserMessageProps): ReactElement { const { files, content: text, timestamp } = message; + const { data: userSettings } = usersApi.useGetUserSettings(); + // NOTE: Defaults match the Open WebUI web app's own fallbacks (Interface.svelte) + const isChatBubbleUIEnabled = userSettings?.ui.chatBubble ?? true; + const isUserMessageMarkdownEnabled = userSettings?.ui.renderMarkdownInUserMessages ?? true; + const attachedFiles = useMemo( () => (files?.filter((file) => file.type === FileType.FILE) as Array) ?? [], [files], @@ -52,6 +57,26 @@ function ChatUserMessageComponent({ const { handleImagePress, handleAllPhotosPress, selectedImageIndex, isPreviewVisible, handleCloseImagePress } = useImagePreview(); + // NOTE: `isChatBubbleUIEnabled === false` flattens the user bubble to plain flowing text (matching the AI + // message style), independently of `isUserMessageMarkdownEnabled` which only controls markdown rendering. + const renderMessageContent = (): ReactElement => { + const content = isUserMessageMarkdownEnabled ? ( + + {text} + + ) : ( + {text} + ); + + if (!isChatBubbleUIEnabled) { + return content; + } + + return ( + {content} + ); + }; + return ( @@ -71,13 +96,7 @@ function ChatUserMessageComponent({ onShowAllImages={handleAllPhotosPress} /> - {!!text && ( - - - {text} - - - )} + {!!text && renderMessageContent()} , folderId?: string, ): void => { + // TODO: Apply usersApi.useGetUserSettings().data?.ui.temporaryChatByDefault here once this client + // supports non-persisted chats (no client-side-only chat mode exists yet, unlike the web app). const payload = prepareCreateChatPayload({ prompt, model, attachedFiles, attachedImages, folderId }); createNewChat(payload, { diff --git a/libs/mobile/chat/features/use-send-message/src/use-send-message.ts b/libs/mobile/chat/features/use-send-message/src/use-send-message.ts index 037fba2f..65cb0f1c 100644 --- a/libs/mobile/chat/features/use-send-message/src/use-send-message.ts +++ b/libs/mobile/chat/features/use-send-message/src/use-send-message.ts @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { ChatGenerationOption, ChatResponse, @@ -24,34 +25,37 @@ export function useSendMessage({ chatData }: UseSendMessageArgs): typeof result }, }); - const sendMessage = ( - prompt: string, - model: string, - generationOptions?: Array, - attachedFiles?: Array, - attachedImages?: Array, - ): void => { - if (!chatData) { - return; - } - - const payload = prepareSendMessagePayload({ prompt, chatData, model, attachedFiles, attachedImages }); - - updateChat(payload, { - onSuccess: (data) => { - const completePayload = prepareCompleteChatPayload({ - chatId: data.id, - messageId: data.chat!.history.currentId, - messages: data.chat!.messages, - sessionId: socketSessionId, - model, - generationOptions, - }); - - completeChat(completePayload); - }, - }); - }; + const sendMessage = useCallback( + ( + prompt: string, + model: string, + generationOptions?: Array, + attachedFiles?: Array, + attachedImages?: Array, + ): void => { + if (!chatData) { + return; + } + + const payload = prepareSendMessagePayload({ prompt, chatData, model, attachedFiles, attachedImages }); + + updateChat(payload, { + onSuccess: (data) => { + const completePayload = prepareCompleteChatPayload({ + chatId: data.id, + messageId: data.chat!.history.currentId, + messages: data.chat!.messages, + sessionId: socketSessionId, + model, + generationOptions, + }); + + completeChat(completePayload); + }, + }); + }, + [chatData, socketSessionId, updateChat, completeChat], + ); const result = { sendMessage, diff --git a/libs/mobile/settings/features/settings/src/lib/component.tsx b/libs/mobile/settings/features/settings/src/lib/component.tsx index c06540f0..104c153f 100644 --- a/libs/mobile/settings/features/settings/src/lib/component.tsx +++ b/libs/mobile/settings/features/settings/src/lib/component.tsx @@ -1,6 +1,6 @@ import { useSelector } from '@legendapp/state/react'; import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; -import { ReactElement, useRef, useState } from 'react'; +import { ReactElement, useRef } from 'react'; import { Alert } from 'react-native'; import { SettingsSection, SettingsSectionOption } from '@open-webui-react-native/mobile/settings/ui/section'; import { useLogout } from '@open-webui-react-native/mobile/shared/features/use-logout'; @@ -9,12 +9,14 @@ import { Avatar, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { navigationConfig } from '@open-webui-react-native/mobile/shared/utils/navigation'; import { authApi, + chatApi, modelsApi, UiSettings, UserSettings, usersApi, } from '@open-webui-react-native/shared/data-access/api'; import { appState$ } from '@open-webui-react-native/shared/data-access/app-state'; +import { alertService } from '@open-webui-react-native/shared/utils/alert-service'; import { availableLanguages, availableMarkdownRenderers, @@ -40,15 +42,7 @@ import { SelectionSheet, SelectionSheetMethods, } from './components'; -import { SettingsToggles } from './types'; - -const initialToggles: SettingsToggles = { - isWebSearchAlwaysOn: true, - isMessageQueueEnabled: true, - isChatBubbleUIEnabled: true, - isTemporaryChatEnabled: true, - isUserMessageMarkdownEnabled: true, -}; +import { useExportAllChats } from './hooks'; // NOTE: Work in progress, not all features are implemented yet. export function Settings(): ReactElement { @@ -64,8 +58,13 @@ export function Settings(): ReactElement { const { data: settings, isError: isSettingsError } = usersApi.useGetUserSettings(); const { mutate: updateUserSettings } = usersApi.useUpdateUserSettings(); const { modelId: defaultModelId, modelName: defaultModelName } = useSetSelectedModel(); - - const [toggles, setToggles] = useState(initialToggles); + const { mutate: archiveAllChats, isPending: isArchivingAllChats } = chatApi.useArchiveAllChats({ + onError: () => ToastService.showError(), + }); + const { mutate: deleteAllChats, isPending: isDeletingAllChats } = chatApi.useDeleteAllChats({ + onError: () => ToastService.showError(), + }); + const { exportAllChats, isExporting: isExportingAllChats } = useExportAllChats(); const profileAvatarSheetRef = useRef(null); const profileNameSheetRef = useRef(null); @@ -76,11 +75,6 @@ export function Settings(): ReactElement { const languageSheetRef = useRef(null); const markdownRendererSheetRef = useRef(null); - const createToggleHandler = - (toggle: keyof SettingsToggles) => - (isEnabled: boolean): void => - setToggles((currentToggles) => ({ ...currentToggles, [toggle]: isEnabled })); - const avatarSource = profile?.profileImageUrl ? { uri: profile.profileImageUrl } : undefined; const languageLabel = availableLanguages.find(({ code }) => code === locale)?.label; const markdownRendererOption = availableMarkdownRenderers.find(({ code }) => code === markdownRenderer); @@ -91,6 +85,14 @@ export function Settings(): ReactElement { : (defaultModelName ?? translate('TEXT_NOT_SET')); const defaultSystemPromptValue = isSettingsError ? translate('TEXT_LOAD_ERROR') : settings?.ui.system; + // NOTE: Defaults match the Open WebUI web app's own fallbacks (Interface.svelte) so a fresh + // account looks the same on mobile and web before the user changes anything. + const isWebSearchAlwaysOnEnabled = settings?.ui.webSearch ?? false; + const isMessageQueueEnabled = settings?.ui.enableMessageQueue ?? true; + const isChatBubbleUIEnabled = settings?.ui.chatBubble ?? true; + const isTemporaryChatByDefaultEnabled = settings?.ui.temporaryChatByDefault ?? false; + const isUserMessageMarkdownEnabled = settings?.ui.renderMarkdownInUserMessages ?? true; + const languageItems = availableLanguages.map(({ code, label }) => ({ value: code, label })); const markdownRendererItems = availableMarkdownRenderers.map(({ code, labelKey }) => ({ value: code, @@ -121,6 +123,16 @@ export function Settings(): ReactElement { appState$.setMarkdownRenderer(value); }; + const createUiSettingToggleHandler = + (key: K) => + (value: UiSettings[K]): void => { + if (!settings) { + return; + } + + updateUserSettings(new UserSettings({ ui: new UiSettings({ ...settings.ui, [key]: value }) })); + }; + const handleArchivedChatsPress = (): void => isFeatureEnabled(FeatureID.ARCHIVE_CHAT) ? navigateOnce(`${navigationConfig.main.chat.index}/${navigationConfig.main.chat.archivedChats}`) @@ -158,6 +170,63 @@ export function Settings(): ReactElement { ); }; + const createChatActionHandler = ({ + featureID, + isPending, + action, + confirm, + }: { + featureID: FeatureID; + isPending: boolean; + action: () => void; + confirm?: { title: string; message: string; confirmButtonStyle?: 'destructive' }; + }): (() => void) => { + const guardedAction = (): void => { + if (!isPending) { + action(); + } + }; + + return (): void => { + if (!isFeatureEnabled(featureID)) { + return ToastService.showFeatureNotImplemented(); + } + + if (confirm) { + alertService.confirm({ ...confirm, onConfirm: guardedAction }); + } else { + guardedAction(); + } + }; + }; + + const handleArchiveAllChatsPress = createChatActionHandler({ + featureID: FeatureID.ARCHIVE_ALL_CHATS, + isPending: isArchivingAllChats, + action: archiveAllChats, + confirm: { + title: translate('TEXT_ARCHIVE_ALL_CHATS_TITLE'), + message: translate('TEXT_ARCHIVE_ALL_CHATS_MESSAGE'), + }, + }); + + const handleDeleteAllChatsPress = createChatActionHandler({ + featureID: FeatureID.DELETE_ALL_CHATS, + isPending: isDeletingAllChats, + action: deleteAllChats, + confirm: { + title: translate('TEXT_DELETE_ALL_CHATS_TITLE'), + message: translate('TEXT_DELETE_ALL_CHATS_MESSAGE'), + confirmButtonStyle: 'destructive', + }, + }); + + const handleExportAllChatsPress = createChatActionHandler({ + featureID: FeatureID.EXPORT_ALL_CHATS, + isPending: isExportingAllChats, + action: exportAllChats, + }); + const generalOptions: Array = [ { label: translate('TEXT_DEFAULT_MODEL'), @@ -219,13 +288,13 @@ export function Settings(): ReactElement { const chatsOptions: Array = [ { label: translate('TEXT_ARCHIVED_CHATS'), iconName: 'archive', onPress: handleArchivedChatsPress }, - { type: 'action', label: translate('TEXT_EXPORT_ALL_CHATS'), onPress: ToastService.showFeatureNotImplemented }, - { type: 'action', label: translate('TEXT_ARCHIVE_ALL'), onPress: ToastService.showFeatureNotImplemented }, + { type: 'action', label: translate('TEXT_EXPORT_ALL_CHATS'), onPress: handleExportAllChatsPress }, + { type: 'action', label: translate('TEXT_ARCHIVE_ALL'), onPress: handleArchiveAllChatsPress }, { type: 'action', label: translate('TEXT_DELETE_ALL'), isDanger: true, - onPress: ToastService.showFeatureNotImplemented, + onPress: handleDeleteAllChatsPress, }, ]; @@ -233,8 +302,8 @@ export function Settings(): ReactElement { { type: 'switch', label: translate('TEXT_ALWAYS_ON_WEB_SEARCH'), - isEnabled: toggles.isWebSearchAlwaysOn, - onValueChange: createToggleHandler('isWebSearchAlwaysOn'), + isEnabled: isWebSearchAlwaysOnEnabled, + onValueChange: createUiSettingToggleHandler('webSearch'), }, { type: 'switch', @@ -245,26 +314,26 @@ export function Settings(): ReactElement { { type: 'switch', label: translate('TEXT_ENABLE_MESSAGE_QUEUE'), - isEnabled: toggles.isMessageQueueEnabled, - onValueChange: createToggleHandler('isMessageQueueEnabled'), + isEnabled: isMessageQueueEnabled, + onValueChange: createUiSettingToggleHandler('enableMessageQueue'), }, { type: 'switch', label: translate('TEXT_CHAT_BUBBLE_UI'), - isEnabled: toggles.isChatBubbleUIEnabled, - onValueChange: createToggleHandler('isChatBubbleUIEnabled'), + isEnabled: isChatBubbleUIEnabled, + onValueChange: createUiSettingToggleHandler('chatBubble'), }, { type: 'switch', label: translate('TEXT_TEMPORARY_CHAT_BY_DEFAULT'), - isEnabled: toggles.isTemporaryChatEnabled, - onValueChange: createToggleHandler('isTemporaryChatEnabled'), + isEnabled: isTemporaryChatByDefaultEnabled, + onValueChange: createUiSettingToggleHandler('temporaryChatByDefault'), }, { type: 'switch', label: translate('TEXT_RENDER_MARKDOWN_IN_USER_MESSAGE'), - isEnabled: toggles.isUserMessageMarkdownEnabled, - onValueChange: createToggleHandler('isUserMessageMarkdownEnabled'), + isEnabled: isUserMessageMarkdownEnabled, + onValueChange: createUiSettingToggleHandler('renderMarkdownInUserMessages'), }, { label: translate('TEXT_MARKDOWN_RENDERER'), diff --git a/libs/mobile/settings/features/settings/src/lib/config.ts b/libs/mobile/settings/features/settings/src/lib/config.ts new file mode 100644 index 00000000..5ac56eb7 --- /dev/null +++ b/libs/mobile/settings/features/settings/src/lib/config.ts @@ -0,0 +1,6 @@ +import { getApiUrl } from '@open-webui-react-native/shared/utils/config'; + +export const settingsApiConfig = { + exportAllChatsApiURL: `${getApiUrl()}/api/v1/chats/all`, + downloadFileName: 'chats_export.json', +}; diff --git a/libs/mobile/settings/features/settings/src/lib/hooks/index.ts b/libs/mobile/settings/features/settings/src/lib/hooks/index.ts new file mode 100644 index 00000000..dfcc3dd1 --- /dev/null +++ b/libs/mobile/settings/features/settings/src/lib/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-export-all-chats'; diff --git a/libs/mobile/settings/features/settings/src/lib/hooks/use-export-all-chats.ts b/libs/mobile/settings/features/settings/src/lib/hooks/use-export-all-chats.ts new file mode 100644 index 00000000..fa627051 --- /dev/null +++ b/libs/mobile/settings/features/settings/src/lib/hooks/use-export-all-chats.ts @@ -0,0 +1,32 @@ +import { useState } from 'react'; +import { + fileSystemService, + MimeType, + UtiType, +} from '@open-webui-react-native/mobile/shared/data-access/file-system-service'; +import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; +import { settingsApiConfig } from '../config'; + +interface UseExportAllChatsResult { + isExporting: boolean; + exportAllChats: () => Promise; +} + +export const useExportAllChats = (): UseExportAllChatsResult => { + const [isExporting, setIsExporting] = useState(false); + const { exportAllChatsApiURL, downloadFileName } = settingsApiConfig; + + const exportAllChats = async (): Promise => { + setIsExporting(true); + + try { + await fileSystemService.downloadAndShareFile(exportAllChatsApiURL, downloadFileName, MimeType.JSON, UtiType.JSON); + } catch { + ToastService.showError(); + } finally { + setIsExporting(false); + } + }; + + return { isExporting, exportAllChats }; +}; diff --git a/libs/mobile/settings/features/settings/src/lib/types/index.ts b/libs/mobile/settings/features/settings/src/lib/types/index.ts index d80f5a49..e3ef313f 100644 --- a/libs/mobile/settings/features/settings/src/lib/types/index.ts +++ b/libs/mobile/settings/features/settings/src/lib/types/index.ts @@ -1,2 +1 @@ -export * from './settings-toggles'; export * from './support-attachment'; diff --git a/libs/mobile/settings/features/settings/src/lib/types/settings-toggles.ts b/libs/mobile/settings/features/settings/src/lib/types/settings-toggles.ts deleted file mode 100644 index eab109f8..00000000 --- a/libs/mobile/settings/features/settings/src/lib/types/settings-toggles.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface SettingsToggles { - isWebSearchAlwaysOn: boolean; - isMessageQueueEnabled: boolean; - isChatBubbleUIEnabled: boolean; - isTemporaryChatEnabled: boolean; - isUserMessageMarkdownEnabled: boolean; -} diff --git a/libs/mobile/settings/ui/section/src/lib/components/settings-section-item/component.tsx b/libs/mobile/settings/ui/section/src/lib/components/settings-section-item/component.tsx index cdf7341b..068acfa9 100644 --- a/libs/mobile/settings/ui/section/src/lib/components/settings-section-item/component.tsx +++ b/libs/mobile/settings/ui/section/src/lib/components/settings-section-item/component.tsx @@ -1,4 +1,4 @@ -import { ReactElement } from 'react'; +import { Fragment, ReactElement } from 'react'; import { cn } from '@open-webui-react-native/mobile/shared/ui/styles'; import { AppPressable, AppSwitch, AppText, Icon, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { SettingsSectionOption } from '../../types'; @@ -10,17 +10,17 @@ interface SettingsSectionItemProps { export function SettingsSectionItem({ option }: SettingsSectionItemProps): ReactElement { const { label, iconName, isDanger } = option; - const title = ( - + const titleContent = ( + {iconName && } - {label} - + {label} + ); if (option.type === 'switch') { return ( - {title} + {titleContent} ); @@ -30,7 +30,7 @@ export function SettingsSectionItem({ option }: SettingsSectionItemProps): React - {title} + {titleContent} {option.type !== 'action' && ( {!!option.value && ( diff --git a/libs/mobile/shared/data-access/file-system-service/src/service.ts b/libs/mobile/shared/data-access/file-system-service/src/service.ts index b009a730..4e5ee77c 100644 --- a/libs/mobile/shared/data-access/file-system-service/src/service.ts +++ b/libs/mobile/shared/data-access/file-system-service/src/service.ts @@ -66,6 +66,18 @@ export class FileSystemService { } } + public async downloadAndShareFile( + uri: string, + fileName: string, + mimeType: MimeType, + utiType: UtiType, + ): Promise { + const fileUri = `${FileSystem.documentDirectory}${fileName}`; + const downloadedFile = await this.downloadFile(uri, fileUri); + + await this.shareAsync(downloadedFile.uri, fileName, mimeType, utiType); + } + private createTemporaryFile(fileName: string, content: string, extension: FileExtension): string { const file = new File(this._cacheDirectory, `${fileName}.${extension}`); file.write(content); diff --git a/libs/shared/data-access/api/src/lib/chats/api.ts b/libs/shared/data-access/api/src/lib/chats/api.ts index 5bd90ce8..6602fc24 100644 --- a/libs/shared/data-access/api/src/lib/chats/api.ts +++ b/libs/shared/data-access/api/src/lib/chats/api.ts @@ -738,6 +738,40 @@ export function useGetAllArchivedChatsJson(): UseQueryResult }); } +function invalidateAllChatListsQueries(): void { + queryClient.invalidateQueries({ queryKey: chatServiceConfig.getChatListQueryKey }); + queryClient.invalidateQueries({ queryKey: chatServiceConfig.getPinnedChatListQueryKey }); + // NOTE: Invalidate all folders chat lists regardless of folderId + queryClient.invalidateQueries({ queryKey: foldersApiConfig.getFolderChatListQueryKeyPrefix, exact: false }); + queryClient.invalidateQueries({ queryKey: foldersApiConfig.getFolderChatsQueryKeyPrefix, exact: false }); + // NOTE: Also invalidates chatServiceConfig.getAllArchivedChatsQueryKey + invalidateArchivedChatListQuery(); +} + +export function useArchiveAllChats( + options?: UseMutationOptions, void>, +): UseMutationResult, void> { + return useMutation({ + mutationFn: () => chatService.archiveAllChats(), + onSuccess: () => { + invalidateAllChatListsQueries(); + }, + ...options, + }); +} + +export function useDeleteAllChats( + options?: UseMutationOptions, void>, +): UseMutationResult, void> { + return useMutation({ + mutationFn: () => chatService.deleteAllChats(), + onSuccess: () => { + invalidateAllChatListsQueries(); + }, + ...options, + }); +} + export const chatApi = { useGet, useUpdate, @@ -757,4 +791,6 @@ export const chatApi = { useGetArchivedChatList, useGetAllArchivedChatsJson, useUpdateChatFolder, + useArchiveAllChats, + useDeleteAllChats, }; diff --git a/libs/shared/data-access/api/src/lib/chats/service.ts b/libs/shared/data-access/api/src/lib/chats/service.ts index d1e1d12b..39753450 100644 --- a/libs/shared/data-access/api/src/lib/chats/service.ts +++ b/libs/shared/data-access/api/src/lib/chats/service.ts @@ -174,6 +174,14 @@ export class ChatService extends EntityPromiseService { }), ); } + + public async archiveAllChats(): Promise { + await getApiService().post(`${chatServiceConfig.versionedRoute}/archive/all`); + } + + public async deleteAllChats(): Promise { + await getApiService().delete(`${chatServiceConfig.versionedRoute}/`); + } } export const chatService = new ChatService(); diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-complete-chat-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-complete-chat-payload.ts index 7cc23b13..d6e7f530 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/prepare-complete-chat-payload.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/prepare-complete-chat-payload.ts @@ -32,12 +32,12 @@ export function prepareCompleteChatPayload({ userMessage, assistantMessageId, }: PrepareCompleteChatPayloadArgs): CompleteChatRequest { + const userSettings = queryClient.getQueryData(usersApiConfig.getUserSettingsQueryKey); + const prepareChatMessages = (): Array => { const chatResponse = queryClient.getQueryData(chatQueriesKeys.get(chatId).queryKey); const chatSystemPrompt = (chatResponse?.chat.params?.system as string | undefined)?.trim(); - const globalSystemPrompt = queryClient - .getQueryData(usersApiConfig.getUserSettingsQueryKey) - ?.ui.system?.trim(); + const globalSystemPrompt = userSettings?.ui.system?.trim(); // The user's default system prompt (Settings > General). Prepended to every completion // request rather than persisted into chat history, since the full history is resent each call. // Chat system prompt overrides the global system prompt. @@ -96,7 +96,7 @@ export function prepareCompleteChatPayload({ features: new Features({ codeInterpreter: generationOptions?.includes(ChatGenerationOption.CODE_INTERPRETER), imageGeneration: generationOptions?.includes(ChatGenerationOption.IMAGE_GENERATION), - webSearch: generationOptions?.includes(ChatGenerationOption.WEB_SEARCH), + webSearch: (userSettings?.ui.webSearch ?? false) || generationOptions?.includes(ChatGenerationOption.WEB_SEARCH), }), stream: true, model, diff --git a/libs/shared/data-access/api/src/lib/folders/config.ts b/libs/shared/data-access/api/src/lib/folders/config.ts index 7419b731..3c89435a 100644 --- a/libs/shared/data-access/api/src/lib/folders/config.ts +++ b/libs/shared/data-access/api/src/lib/folders/config.ts @@ -1,3 +1,6 @@ +const folderChatListQueryKeyPrefix = ['folders', 'chat-list']; +const folderChatsQueryKeyPrefix = ['folders', 'chats']; + export const foldersApiConfig = { route: 'v1/folders', chatsRoute: 'v1/chats', @@ -5,8 +8,10 @@ export const foldersApiConfig = { updateFolderQueryKey: ['folders', 'update'], deleteFolderQueryKey: ['folders', 'delete'], getFoldersQueryKey: ['folders', 'get'], - getFolderChatListQueryKey: (folderId: string): Array => ['folders', 'chat-list', folderId], - getFolderChatsQueryKey: (folderId: string): Array => ['folders', 'chats', folderId], + getFolderChatListQueryKeyPrefix: folderChatListQueryKeyPrefix, + getFolderChatListQueryKey: (folderId: string): Array => [...folderChatListQueryKeyPrefix, folderId], + getFolderChatsQueryKeyPrefix: folderChatsQueryKeyPrefix, + getFolderChatsQueryKey: (folderId: string): Array => [...folderChatsQueryKeyPrefix, folderId], getFolderQueryKey: (folderId: string): Array => ['folders', 'folder', folderId], chatsPerPage: 60, }; diff --git a/libs/shared/data-access/api/src/lib/users/models/ui-settings.ts b/libs/shared/data-access/api/src/lib/users/models/ui-settings.ts index cf9de988..a5927a55 100644 --- a/libs/shared/data-access/api/src/lib/users/models/ui-settings.ts +++ b/libs/shared/data-access/api/src/lib/users/models/ui-settings.ts @@ -10,6 +10,23 @@ export class UiSettings { @Expose() public system?: string; + // NOTE: Keys below match the Open WebUI web app's own `$settings` store keys 1:1 + // (src/lib/components/chat/Settings/Interface.svelte), so settings stay in sync across clients. + @Expose() + public webSearch?: boolean; + + @Expose() + public enableMessageQueue?: boolean; + + @Expose() + public chatBubble?: boolean; + + @Expose() + public temporaryChatByDefault?: boolean; + + @Expose() + public renderMarkdownInUserMessages?: boolean; + constructor(response: Partial) { Object.assign(this, response); } diff --git a/libs/shared/utils/feature-flag/src/config.ts b/libs/shared/utils/feature-flag/src/config.ts index 98d2f1dc..fddea8ad 100644 --- a/libs/shared/utils/feature-flag/src/config.ts +++ b/libs/shared/utils/feature-flag/src/config.ts @@ -12,4 +12,7 @@ export const featureFlagConfig: Record = { [FeatureID.USER_EDIT_MESSAGE]: 'production', [FeatureID.AI_EDIT_MESSAGE]: 'development', [FeatureID.AI_REGENERATE_MESSAGE]: 'development', + [FeatureID.ARCHIVE_ALL_CHATS]: 'development', + [FeatureID.DELETE_ALL_CHATS]: 'development', + [FeatureID.EXPORT_ALL_CHATS]: 'development', }; diff --git a/libs/shared/utils/feature-flag/src/enums/feature-id.ts b/libs/shared/utils/feature-flag/src/enums/feature-id.ts index 1de78ddb..79dd8f3d 100644 --- a/libs/shared/utils/feature-flag/src/enums/feature-id.ts +++ b/libs/shared/utils/feature-flag/src/enums/feature-id.ts @@ -9,4 +9,7 @@ export enum FeatureID { USER_EDIT_MESSAGE = 'user-edit-message', // https://app.clickup.com/t/24336023/PRD-1976 AI_EDIT_MESSAGE = 'ai-edit-message', // https://app.clickup.com/t/24336023/PRD-1976 AI_REGENERATE_MESSAGE = 'ai-regenerate-message', // https://app.clickup.com/t/24336023/PRD-2009 + ARCHIVE_ALL_CHATS = 'archive-all-chats', // https://app.clickup.com/t/24336023/PRD-2695 + DELETE_ALL_CHATS = 'delete-all-chats', // https://app.clickup.com/t/24336023/PRD-2695 + EXPORT_ALL_CHATS = 'export-all-chats', // https://app.clickup.com/t/24336023/PRD-2695 } From ddb180b530264f35a696f301a618d349f463d815 Mon Sep 17 00:00:00 2001 From: Mikhail Leonov Date: Mon, 24 Aug 2026 14:15:49 +0600 Subject: [PATCH 2/5] feat: implement temporary chat functionality with localization support and UI updates --- apps/mobile/app/(main)/chat/[id].tsx | 31 ++++++++++----- i18n/mobile/chat/de.json | 3 +- i18n/mobile/chat/en.json | 3 +- i18n/mobile/chat/es.json | 3 +- i18n/mobile/chat/fr.json | 3 +- i18n/mobile/chat/ja.json | 3 +- i18n/mobile/chat/pt.json | 3 +- i18n/mobile/chat/ru.json | 3 +- i18n/mobile/chat/zh.json | 3 +- .../chat/features/chat/src/lib/component.tsx | 12 +++++- .../create-chat/src/lib/component.tsx | 17 +++++++- .../src/use-create-new-chat.ts | 39 ++++++++++++++++++- .../use-send-message/src/use-send-message.ts | 37 ++++++++++++------ .../lib/chats/utils/handle-completed-chat.ts | 7 ++++ .../api/src/lib/chats/utils/index.ts | 1 + .../src/lib/chats/utils/temporary-chat-id.ts | 10 +++++ 16 files changed, 144 insertions(+), 34 deletions(-) create mode 100644 libs/shared/data-access/api/src/lib/chats/utils/temporary-chat-id.ts diff --git a/apps/mobile/app/(main)/chat/[id].tsx b/apps/mobile/app/(main)/chat/[id].tsx index b42b2390..f1dd5134 100644 --- a/apps/mobile/app/(main)/chat/[id].tsx +++ b/apps/mobile/app/(main)/chat/[id].tsx @@ -15,13 +15,14 @@ import { Icon, FullScreenSearchModal, IconButton, + View, } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { ChatScreenParams, navigationConfig, useInitialNavigation, } from '@open-webui-react-native/mobile/shared/utils/navigation'; -import { chatApi, modelsApi } from '@open-webui-react-native/shared/data-access/api'; +import { chatApi, isTemporaryChatId, modelsApi } from '@open-webui-react-native/shared/data-access/api'; import { appState$ } from '@open-webui-react-native/shared/data-access/app-state'; import { useNavigateOnce } from '@open-webui-react-native/shared/utils/navigation'; import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; @@ -39,9 +40,11 @@ export default function ChatScreen(): ReactElement { const navigateToClonedChat = (id: string): void => navigateOnce(navigationConfig.main.chat.view({ id })); const isOfflineMode = useSelector(appState$.isOfflineMode); + const isTemporaryChat = isTemporaryChatId(id); const { data: models, isLoading: isModelsLoading } = modelsApi.useGetModels(); - const { data: chat, isLoading: isChatLoading } = chatApi.useGet(id); + // NOTE: Temporary chats are never persisted, so there's nothing to fetch — read the client-seeded cache only. + const { data: chat, isLoading: isChatLoading } = chatApi.useGet(id, { enabled: !isTemporaryChat }); const { modelId, modelName, onSelectModel } = useSetSelectedModel(id); const handleGoBackPress = (): void => router.back(); @@ -73,13 +76,23 @@ export default function ChatScreen(): ReactElement { isLoading && !modelId ? ( translate('TEXT_LOADING') ) : ( - + + {isTemporaryChat && ( + + + + {translate('TEXT_TEMPORARY_CHAT')} + + + )} + + ) } onGoBack={handleGoBackPress} diff --git a/i18n/mobile/chat/de.json b/i18n/mobile/chat/de.json index d94444a8..08fe0d30 100644 --- a/i18n/mobile/chat/de.json +++ b/i18n/mobile/chat/de.json @@ -2,7 +2,8 @@ "CHAT": { "CHAT_SCREEN": { "TEXT_LOADING": "Wird geladen...", - "TEXT_SELECT_A_MODEL": "Modell auswählen" + "TEXT_SELECT_A_MODEL": "Modell auswählen", + "TEXT_TEMPORARY_CHAT": "Temporärer Chat" }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Nachricht senden", diff --git a/i18n/mobile/chat/en.json b/i18n/mobile/chat/en.json index 6ff6a718..8fc9b7c9 100644 --- a/i18n/mobile/chat/en.json +++ b/i18n/mobile/chat/en.json @@ -2,7 +2,8 @@ "CHAT": { "CHAT_SCREEN": { "TEXT_LOADING": "Loading...", - "TEXT_SELECT_A_MODEL": "Select a model" + "TEXT_SELECT_A_MODEL": "Select a model", + "TEXT_TEMPORARY_CHAT": "Temporary Chat" }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Send a message", diff --git a/i18n/mobile/chat/es.json b/i18n/mobile/chat/es.json index a2d7e857..7e34b85b 100644 --- a/i18n/mobile/chat/es.json +++ b/i18n/mobile/chat/es.json @@ -2,7 +2,8 @@ "CHAT": { "CHAT_SCREEN": { "TEXT_LOADING": "Cargando...", - "TEXT_SELECT_A_MODEL": "Seleccionar un modelo" + "TEXT_SELECT_A_MODEL": "Seleccionar un modelo", + "TEXT_TEMPORARY_CHAT": "Chat temporal" }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Enviar un mensaje", diff --git a/i18n/mobile/chat/fr.json b/i18n/mobile/chat/fr.json index f592a874..f1e14281 100644 --- a/i18n/mobile/chat/fr.json +++ b/i18n/mobile/chat/fr.json @@ -2,7 +2,8 @@ "CHAT": { "CHAT_SCREEN": { "TEXT_LOADING": "Chargement...", - "TEXT_SELECT_A_MODEL": "Sélectionner un modèle" + "TEXT_SELECT_A_MODEL": "Sélectionner un modèle", + "TEXT_TEMPORARY_CHAT": "Discussion temporaire" }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Envoyer un message", diff --git a/i18n/mobile/chat/ja.json b/i18n/mobile/chat/ja.json index d1d9a085..a2026e23 100644 --- a/i18n/mobile/chat/ja.json +++ b/i18n/mobile/chat/ja.json @@ -2,7 +2,8 @@ "CHAT": { "CHAT_SCREEN": { "TEXT_LOADING": "読み込み中...", - "TEXT_SELECT_A_MODEL": "モデルを選択" + "TEXT_SELECT_A_MODEL": "モデルを選択", + "TEXT_TEMPORARY_CHAT": "一時チャット" }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "メッセージを送信", diff --git a/i18n/mobile/chat/pt.json b/i18n/mobile/chat/pt.json index 76487982..e323e881 100644 --- a/i18n/mobile/chat/pt.json +++ b/i18n/mobile/chat/pt.json @@ -2,7 +2,8 @@ "CHAT": { "CHAT_SCREEN": { "TEXT_LOADING": "Carregando...", - "TEXT_SELECT_A_MODEL": "Selecionar um modelo" + "TEXT_SELECT_A_MODEL": "Selecionar um modelo", + "TEXT_TEMPORARY_CHAT": "Chat temporário" }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Enviar uma mensagem", diff --git a/i18n/mobile/chat/ru.json b/i18n/mobile/chat/ru.json index 0e0203d6..3afa8f2f 100644 --- a/i18n/mobile/chat/ru.json +++ b/i18n/mobile/chat/ru.json @@ -2,7 +2,8 @@ "CHAT": { "CHAT_SCREEN": { "TEXT_LOADING": "Загрузка...", - "TEXT_SELECT_A_MODEL": "Выберите модель" + "TEXT_SELECT_A_MODEL": "Выберите модель", + "TEXT_TEMPORARY_CHAT": "Временный чат" }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "Отправить сообщение", diff --git a/i18n/mobile/chat/zh.json b/i18n/mobile/chat/zh.json index e859609c..9c072e05 100644 --- a/i18n/mobile/chat/zh.json +++ b/i18n/mobile/chat/zh.json @@ -2,7 +2,8 @@ "CHAT": { "CHAT_SCREEN": { "TEXT_LOADING": "加载中...", - "TEXT_SELECT_A_MODEL": "选择模型" + "TEXT_SELECT_A_MODEL": "选择模型", + "TEXT_TEMPORARY_CHAT": "临时聊天" }, "CHAT": { "TEXT_INPUT_PLACEHOLDER": "发送消息", diff --git a/libs/mobile/chat/features/chat/src/lib/component.tsx b/libs/mobile/chat/features/chat/src/lib/component.tsx index b8c70fd9..b95130c6 100644 --- a/libs/mobile/chat/features/chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/component.tsx @@ -20,6 +20,7 @@ import { ChatGenerationOption, chatQueriesKeys, createMessagesList, + isTemporaryChatId, usersApi, } from '@open-webui-react-native/shared/data-access/api'; import { Role } from '@open-webui-react-native/shared/data-access/common'; @@ -70,7 +71,15 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C resetAttachments, } = useAttachedFiles(); - const { data: chat, refetch, isLoading, isRefetching, isSuccess } = chatApi.useGet(chatId); + const isTemporaryChat = isTemporaryChatId(chatId); + // NOTE: Temporary chats are never persisted, so there's nothing to fetch — read the client-seeded cache only. + const { + data: chat, + refetch, + isLoading, + isRefetching, + isSuccess, + } = chatApi.useGet(chatId, { enabled: !isTemporaryChat }); const { sendMessage, isLoading: isSending } = useSendMessage({ chatData: chat }); const { editingMessageId, @@ -110,6 +119,7 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C useAppStateChange({ onChange: (lastStatusChangeTimeStamp) => { if ( + !isTemporaryChat && lastStatusChangeTimeStamp && dayjs().diff(lastStatusChangeTimeStamp, 'seconds') > webSocketConfig.pingTimeout ) { diff --git a/libs/mobile/chat/features/create-chat/src/lib/component.tsx b/libs/mobile/chat/features/create-chat/src/lib/component.tsx index 5e162aff..36844391 100644 --- a/libs/mobile/chat/features/create-chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/create-chat/src/lib/component.tsx @@ -7,9 +7,9 @@ import { FormChatInput, FormChatInputSchema } from '@open-webui-react-native/mob import { useCreateNewChat } from '@open-webui-react-native/mobile/chat/features/use-create-new-chat'; import { useAttachedFiles } from '@open-webui-react-native/mobile/shared/features/use-attached-files'; import { useSetSelectedModel } from '@open-webui-react-native/mobile/shared/features/use-set-selected-model'; -import { View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { AppText, Icon, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { FormValues } from '@open-webui-react-native/mobile/shared/utils/form'; -import { ChatGenerationOption } from '@open-webui-react-native/shared/data-access/api'; +import { ChatGenerationOption, usersApi } from '@open-webui-react-native/shared/data-access/api'; import { webSocketState$ } from '@open-webui-react-native/shared/data-access/websocket'; import { AnalyticsEvent, analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; @@ -27,9 +27,14 @@ export function CreateChat({ createFolderModalComponent, }: CreateChatProps): ReactElement { const translate = useTranslation('CHAT.CREATE_CHAT'); + const translateChatScreen = useTranslation('CHAT.CHAT_SCREEN'); const { folderId: existedFolderId } = useLocalSearchParams(); const isSocketConnected = useSelector(webSocketState$.isSocketConnected); + const { data: userSettings } = usersApi.useGetUserSettings(); + // NOTE: Default matches the Open WebUI web app's own fallback (Interface.svelte) + const isTemporaryChat = userSettings?.ui.temporaryChatByDefault ?? false; + const { attachedFiles, attachedImages, @@ -87,6 +92,14 @@ export function CreateChat({ modalComponent={createFolderModalComponent} disabled={!!existedFolderId} /> + {isTemporaryChat && ( + + + + {translateChatScreen('TEXT_TEMPORARY_CHAT')} + + + )} , folderId?: string, ): void => { - // TODO: Apply usersApi.useGetUserSettings().data?.ui.temporaryChatByDefault here once this client - // supports non-persisted chats (no client-side-only chat mode exists yet, unlike the web app). const payload = prepareCreateChatPayload({ prompt, model, attachedFiles, attachedImages, folderId }); + // NOTE: Temporary chats are never persisted (no POST /chats/new, no chat-list entry) — they only + // exist client-side for this session, matching the Open WebUI web app's "Temporary Chat" behavior. + if (userSettings?.ui.temporaryChatByDefault) { + const id = createTemporaryChatId(); + const chatResponse = new ChatResponse({ + id, + title: payload.chat.title, + chat: new Chat({ ...payload.chat, id }), + createdAt: dayjs(), + updatedAt: dayjs(), + archived: false, + pinned: false, + meta: {}, + }); + + queryClient.setQueryData(chatQueriesKeys.get(id).queryKey, chatResponse); + onSuccess?.(id); + + completeChat( + prepareCompleteChatPayload({ + chatId: id, + messageId: chatResponse.chat.history.currentId, + messages: chatResponse.chat.messages, + sessionId: socketSessionId, + model, + generationOptions, + }), + ); + + return; + } + createNewChat(payload, { onSuccess: (data) => { // NOTE: Seed get-chat cache so socket streaming and VoiceMode can track the assistant reply diff --git a/libs/mobile/chat/features/use-send-message/src/use-send-message.ts b/libs/mobile/chat/features/use-send-message/src/use-send-message.ts index 65cb0f1c..96108bc6 100644 --- a/libs/mobile/chat/features/use-send-message/src/use-send-message.ts +++ b/libs/mobile/chat/features/use-send-message/src/use-send-message.ts @@ -1,8 +1,10 @@ +import { EntityPartial } from '@ronas-it/rtkq-entity-api'; import { useCallback } from 'react'; import { ChatGenerationOption, ChatResponse, chatApi, + isTemporaryChatId, patchChatQueryData, prepareCompleteChatPayload, prepareSendMessagePayload, @@ -39,19 +41,30 @@ export function useSendMessage({ chatData }: UseSendMessageArgs): typeof result const payload = prepareSendMessagePayload({ prompt, chatData, model, attachedFiles, attachedImages }); - updateChat(payload, { - onSuccess: (data) => { - const completePayload = prepareCompleteChatPayload({ - chatId: data.id, - messageId: data.chat!.history.currentId, - messages: data.chat!.messages, - sessionId: socketSessionId, - model, - generationOptions, - }); + const triggerCompletion = (data: EntityPartial): void => { + const completePayload = prepareCompleteChatPayload({ + chatId: data.id!, + messageId: data.chat!.history.currentId, + messages: data.chat!.messages, + sessionId: socketSessionId, + model, + generationOptions, + }); + + completeChat(completePayload); + }; - completeChat(completePayload); - }, + // NOTE: Temporary chats are never persisted (no POST /chats/{id}) — apply the same optimistic + // cache patch `useUpdate`'s onMutate would have done, then trigger completion directly. + if (isTemporaryChatId(payload.id)) { + patchChatQueryData(payload.id!, payload); + triggerCompletion(payload); + + return; + } + + updateChat(payload, { + onSuccess: triggerCompletion, }); }, [chatData, socketSessionId, updateChat, completeChat], diff --git a/libs/shared/data-access/api/src/lib/chats/utils/handle-completed-chat.ts b/libs/shared/data-access/api/src/lib/chats/utils/handle-completed-chat.ts index 33cecb36..2ada02b0 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/handle-completed-chat.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/handle-completed-chat.ts @@ -5,6 +5,7 @@ import { chatQueriesKeys } from '../chat-queries-keys'; import { Chat, ChatResponse, History, Message } from '../models'; import { chatService } from '../service'; import { prepareCompletedChatPayload } from './prepare-completed-chat-payload'; +import { isTemporaryChatId } from './temporary-chat-id'; export const handleCompletedChat = async ( message: string, @@ -13,6 +14,12 @@ export const handleCompletedChat = async ( sources?: Array, output?: Message['output'], ): Promise => { + // NOTE: Temporary chats are never persisted — the completed message is already reflected in the + // cache by patchCompletedMessage (see handleChatCompletionEvent), so there's nothing left to do. + if (isTemporaryChatId(chatId)) { + return; + } + const chatData = queryClient.getQueryData(chatQueriesKeys.get(chatId).queryKey); if (!chatData) { diff --git a/libs/shared/data-access/api/src/lib/chats/utils/index.ts b/libs/shared/data-access/api/src/lib/chats/utils/index.ts index 8cb90130..faac6da1 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/index.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/index.ts @@ -18,3 +18,4 @@ export * from './prepare-update-message-in-chat-payload'; export * from './prepare-update-message-to-send-payload'; export * from './prepare-edit-assistant-message-payload'; export * from './prepare-copy-edited-message-payload'; +export * from './temporary-chat-id'; diff --git a/libs/shared/data-access/api/src/lib/chats/utils/temporary-chat-id.ts b/libs/shared/data-access/api/src/lib/chats/utils/temporary-chat-id.ts new file mode 100644 index 00000000..89111923 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/chats/utils/temporary-chat-id.ts @@ -0,0 +1,10 @@ +import uuid from 'react-native-uuid'; + +// NOTE: Prefix mirrors the Open WebUI web app's own `temporary:${socketId}` convention +// (src/lib/utils/chatId.ts) — a synthetic id used only for socket/streaming correlation, +// never round-tripped through a persisted chats-table row. +const TEMPORARY_CHAT_ID_PREFIX = 'temporary:'; + +export const createTemporaryChatId = (): string => `${TEMPORARY_CHAT_ID_PREFIX}${uuid.v4()}`; + +export const isTemporaryChatId = (id?: string | null): boolean => !!id?.startsWith(TEMPORARY_CHAT_ID_PREFIX); From 0db41ece269ab52e99f0cd6de9d32df4c0e83cec Mon Sep 17 00:00:00 2001 From: Mikhail Leonov Date: Mon, 24 Aug 2026 14:36:29 +0600 Subject: [PATCH 3/5] feat: enhance user settings update functionality with optimistic UI updates and error handling --- .../data-access/api/src/lib/users/api.ts | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/libs/shared/data-access/api/src/lib/users/api.ts b/libs/shared/data-access/api/src/lib/users/api.ts index e86380e9..17464221 100644 --- a/libs/shared/data-access/api/src/lib/users/api.ts +++ b/libs/shared/data-access/api/src/lib/users/api.ts @@ -21,11 +21,31 @@ function useGetUserSettings(): UseQueryResult, UserSettings>, -): UseMutationResult, UserSettings> { - return useMutation, UserSettings>({ + props?: UseMutationOptions, UserSettings, UpdateUserSettingsContext>, +): UseMutationResult, UserSettings, UpdateUserSettingsContext> { + return useMutation, UserSettings, UpdateUserSettingsContext>({ mutationFn: usersService.updateUserSettings, + // NOTE: Applied optimistically so toggles (e.g. Settings > Chats switches) flip instantly instead of + // waiting for the round trip — without this the switch briefly snaps back before the response lands. + onMutate: async (settings) => { + await queryClient.cancelQueries({ queryKey: usersApiConfig.getUserSettingsQueryKey }); + + const previousSettings = queryClient.getQueryData(usersApiConfig.getUserSettingsQueryKey); + + queryClient.setQueryData(usersApiConfig.getUserSettingsQueryKey, settings); + + return { previousSettings }; + }, + onError: (_error, _settings, context) => { + if (context?.previousSettings) { + queryClient.setQueryData(usersApiConfig.getUserSettingsQueryKey, context.previousSettings); + } + }, onSuccess: (settings) => { queryClient.setQueryData(usersApiConfig.getUserSettingsQueryKey, settings); }, From 1248f0979656b8984435b91c5eb0f5c8188027d2 Mon Sep 17 00:00:00 2001 From: Mikhail Leonov Date: Mon, 24 Aug 2026 15:53:15 +0600 Subject: [PATCH 4/5] feat: enhance chat functionality by implementing temporary chat handling, message queuing, and improved attachment management --- apps/mobile/app/(main)/chat/[id].tsx | 5 +- .../chat/features/chat/src/lib/component.tsx | 28 +++++++---- .../form-chat-input/src/lib/component.tsx | 3 ++ .../chat-input-bottom-row/component.tsx | 33 ++++++++----- .../use-edit-message/src/use-edit-message.ts | 48 ++++++++++++------- .../voice-mode-modal/src/lib/component.tsx | 7 ++- .../features/settings/src/lib/component.tsx | 14 ++++-- .../use-set-selected-model/src/hook.ts | 16 +++++-- .../data-access/api/src/lib/chats/api.ts | 8 ++++ .../data-access/api/src/lib/users/index.ts | 1 + 10 files changed, 116 insertions(+), 47 deletions(-) diff --git a/apps/mobile/app/(main)/chat/[id].tsx b/apps/mobile/app/(main)/chat/[id].tsx index f1dd5134..365ebccd 100644 --- a/apps/mobile/app/(main)/chat/[id].tsx +++ b/apps/mobile/app/(main)/chat/[id].tsx @@ -100,8 +100,11 @@ export default function ChatScreen(): ReactElement { { - if (!chat) return; + if (!chat || isTemporaryChat) return; chatActionsSheetRef.current?.present(chat); }} /> diff --git a/libs/mobile/chat/features/chat/src/lib/component.tsx b/libs/mobile/chat/features/chat/src/lib/component.tsx index b95130c6..2d01eda4 100644 --- a/libs/mobile/chat/features/chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/component.tsx @@ -23,7 +23,7 @@ import { isTemporaryChatId, usersApi, } from '@open-webui-react-native/shared/data-access/api'; -import { Role } from '@open-webui-react-native/shared/data-access/common'; +import { FileData, ImageData, Role } from '@open-webui-react-native/shared/data-access/common'; import { useSubscribeToQueryCache } from '@open-webui-react-native/shared/data-access/query-client'; import { webSocketConfig, webSocketState$ } from '@open-webui-react-native/shared/data-access/websocket'; import { AnalyticsEvent, analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; @@ -59,6 +59,8 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C const [queuedMessage, setQueuedMessage] = useState<{ inputValue: string; options: Array; + attachedFiles: Array; + attachedImages: Array; } | null>(null); const { @@ -103,9 +105,9 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C // NOTE: chat.messages is a legacy snapshot the backend no longer maintains, so the current branch is derived from history const messages = useMemo(() => (history ? createMessagesList(history, history.currentId) : []), [history]); const isResponseGenerating = !history?.messages[history.currentId].done; - // NOTE: With message queueing enabled, a generating response no longer force-disables the composer or - // swaps the send button for "stop" — the user trades the visible stop-generation action for being able - // to compose/send the next message immediately (queued via `queuedMessage`). + // NOTE: With message queueing enabled, a generating response no longer force-disables the text input — + // the user can keep typing and submit the next message immediately (queued via `queuedMessage`). The + // Stop button (isResponseGenerating prop below) still renders regardless, alongside the send button. const isComposerBlockedByGeneration = isResponseGenerating && !isMessageQueueEnabled; const shouldHideContent = isLoading || isRefetching || !isMessagesListLoaded || !selectedModelId; @@ -197,7 +199,14 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C } if (isResponseGenerating && isMessageQueueEnabled) { - setQueuedMessage({ inputValue, options }); + // NOTE: Snapshot attachments now — they're cleared by resetAttachments() below and must not + // be re-read live when this queued message is flushed later. + setQueuedMessage({ + inputValue, + options, + attachedFiles: attachedFiles.get(), + attachedImages: attachedImages.get(), + }); ToastService.show(translate('TEXT_MESSAGE_QUEUED')); } else { sendMessage(inputValue, selectedModelId, options, attachedFiles.get(), attachedImages.get()); @@ -242,12 +251,12 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C queuedMessage.inputValue, selectedModelId, queuedMessage.options, - attachedFiles.get(), - attachedImages.get(), + queuedMessage.attachedFiles, + queuedMessage.attachedImages, ); setQueuedMessage(null); } - }, [isResponseGenerating, queuedMessage, selectedModelId, sendMessage, attachedFiles, attachedImages]); + }, [isResponseGenerating, queuedMessage, selectedModelId, sendMessage]); return ( @@ -313,7 +322,8 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C onImageUploaded={handleImageUploaded} onDeleteImagePress={handleDeleteImage} modelId={selectedModelId} - isResponseGenerating={isComposerBlockedByGeneration} + isResponseGenerating={isResponseGenerating} + isMessageQueueEnabled={isMessageQueueEnabled} inputRerenderKey={inputRerenderKey} chat={chat} /> diff --git a/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx b/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx index e020ffbd..9b0cf2ce 100644 --- a/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx +++ b/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx @@ -44,6 +44,7 @@ interface FormChatInputProps extends AppInputProps { isLoading?: boolean; isSuggestionShown?: boolean; isResponseGenerating?: boolean; + isMessageQueueEnabled?: boolean; inputRerenderKey?: number; } @@ -67,6 +68,7 @@ export function FormChatInput({ isLoading, isSuggestionShown, isResponseGenerating, + isMessageQueueEnabled, inputRerenderKey, ...restProps }: FormChatInputProps): ReactElement { @@ -180,6 +182,7 @@ export function FormChatInput({ isVoiceModeAvailable={isFeatureEnabled(FeatureID.VOICE_MODE) && isInputEmpty} onStopGenerationPress={onStopGenerationPress} isResponseGenerating={isResponseGenerating} + isMessageQueueEnabled={isMessageQueueEnabled} isLoading={isLoading || isMicrophonePreparing}> diff --git a/libs/mobile/chat/features/form-chat-input/src/lib/components/chat-input-bottom-row/component.tsx b/libs/mobile/chat/features/form-chat-input/src/lib/components/chat-input-bottom-row/component.tsx index feb7ef5c..a8bef653 100644 --- a/libs/mobile/chat/features/form-chat-input/src/lib/components/chat-input-bottom-row/component.tsx +++ b/libs/mobile/chat/features/form-chat-input/src/lib/components/chat-input-bottom-row/component.tsx @@ -6,6 +6,9 @@ export interface ChatInputBottomRowProps extends PropsWithChildren { onVoiceModePress: () => void; onStopGenerationPress: () => void; isResponseGenerating?: boolean; + // NOTE: When queueing is enabled, the send button must stay reachable alongside Stop, so the + // next message can still be composed/queued while a response is generating. + isMessageQueueEnabled?: boolean; isVoiceModeAvailable?: boolean; isSubmitDisabled?: boolean; isLoading?: boolean; @@ -19,26 +22,30 @@ export function ChatInputBottomRow({ children, isSubmitDisabled, isResponseGenerating, + isMessageQueueEnabled, onStopGenerationPress, }: ChatInputBottomRowProps): ReactElement { + const showSendButton = !isResponseGenerating || isMessageQueueEnabled; + return ( {children} - {isResponseGenerating ? ( - + {isResponseGenerating && - ) : ( - - )} + onPress={onStopGenerationPress} />} + {showSendButton && ( + + )} + ); } diff --git a/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts b/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts index 63bf7191..6236eb5e 100644 --- a/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts +++ b/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts @@ -4,6 +4,8 @@ import { FormValues } from '@open-webui-react-native/mobile/shared/utils/form'; import { chatApi, ChatResponse, + isTemporaryChatId, + patchChatQueryData, prepareCompleteChatPayload, prepareUpdateMessageToSendPayload, prepareEditAssistantMessagePayload, @@ -63,7 +65,14 @@ export const useEditMessage = ({ chat, modelId }: UseEditMessageProps): typeof r ? prepareCopyEditedMessagePayload(chat, editingMessageId, message) : prepareUpdateMessageInChatPayload(chat, editingMessageId, message); - await updateChat(preparedChat); + // NOTE: Temporary chats are never persisted — apply the same cache patch onSuccess would have + // done, skipping the PATCH /chats/{id} call that would otherwise fail against a synthetic id. + if (isTemporaryChatId(chat.id)) { + patchChatQueryData(chat.id, preparedChat); + } else { + await updateChat(preparedChat); + } + cancelEditing(); }; @@ -78,21 +87,28 @@ export const useEditMessage = ({ chat, modelId }: UseEditMessageProps): typeof r ? prepareEditAssistantMessagePayload(chat, editingMessageId, message) : prepareUpdateMessageToSendPayload(chat, message, modelId, editedMessage.parentId); - await updateChat(preparedChat, { - onSuccess: (data) => { - if (editedMessage.role === Role.ASSISTANT) return; - - const completePayload = prepareCompleteChatPayload({ - chatId: data.id, - messageId: data.chat!.history.currentId, - messages: data.chat!.messages, - sessionId: socketSessionId, - model: modelId, - }); - - completeChat(completePayload); - }, - }); + const triggerCompletion = (data: ChatResponse): void => { + if (editedMessage.role === Role.ASSISTANT) return; + + const completePayload = prepareCompleteChatPayload({ + chatId: data.id, + messageId: data.chat!.history.currentId, + messages: data.chat!.messages, + sessionId: socketSessionId, + model: modelId, + }); + + completeChat(completePayload); + }; + + // NOTE: Temporary chats are never persisted — apply the same cache patch onSuccess would have + // done, skipping the PATCH /chats/{id} call that would otherwise fail against a synthetic id. + if (isTemporaryChatId(chat.id)) { + patchChatQueryData(chat.id, preparedChat); + triggerCompletion(preparedChat); + } else { + await updateChat(preparedChat, { onSuccess: triggerCompletion }); + } cancelEditing(); }; diff --git a/libs/mobile/chat/features/voice-mode-modal/src/lib/component.tsx b/libs/mobile/chat/features/voice-mode-modal/src/lib/component.tsx index ac4672d0..0f619114 100644 --- a/libs/mobile/chat/features/voice-mode-modal/src/lib/component.tsx +++ b/libs/mobile/chat/features/voice-mode-modal/src/lib/component.tsx @@ -8,7 +8,7 @@ import { speechStreamingService } from '@open-webui-react-native/mobile/shared/d import { useDictateMode } from '@open-webui-react-native/mobile/shared/features/use-dictate-mode'; import { colors, useColorScheme } from '@open-webui-react-native/mobile/shared/ui/styles'; import { AppSafeAreaView, AppText, AppToast, IconButton, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; -import { chatApi } from '@open-webui-react-native/shared/data-access/api'; +import { chatApi, isTemporaryChatId } from '@open-webui-react-native/shared/data-access/api'; import { ImageData as ChatImageData } from '@open-webui-react-native/shared/data-access/common'; import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; import { CameraPreview, CameraPreviewMethods, Loader, SpeechListener } from './components'; @@ -65,7 +65,10 @@ export function VoiceModeModal({ onChatCreated, ref, ...props }: VoiceModeModalP } }; - const { data: chat, isLoading } = chatApi.useGet(chatId as string, { enabled: !!chatId }); + // NOTE: Temporary chats are never persisted, so there's nothing to fetch — read the client-seeded cache only. + const { data: chat, isLoading } = chatApi.useGet(chatId as string, { + enabled: !!chatId && !isTemporaryChatId(chatId), + }); const { sendMessage, isLoading: isSending } = useSendMessage({ chatData: chat }); const { startChatCreation, isLoading: isCreating } = useCreateNewChat({ onSuccess: handleChatCreated }); diff --git a/libs/mobile/settings/features/settings/src/lib/component.tsx b/libs/mobile/settings/features/settings/src/lib/component.tsx index 104c153f..8206dc22 100644 --- a/libs/mobile/settings/features/settings/src/lib/component.tsx +++ b/libs/mobile/settings/features/settings/src/lib/component.tsx @@ -14,8 +14,10 @@ import { UiSettings, UserSettings, usersApi, + usersApiConfig, } from '@open-webui-react-native/shared/data-access/api'; import { appState$ } from '@open-webui-react-native/shared/data-access/app-state'; +import { queryClient } from '@open-webui-react-native/shared/data-access/query-client'; import { alertService } from '@open-webui-react-native/shared/utils/alert-service'; import { availableLanguages, @@ -56,7 +58,9 @@ export function Settings(): ReactElement { const { data: profile } = authApi.useGetProfile(); const { isError: isModelsError } = modelsApi.useGetModels(); const { data: settings, isError: isSettingsError } = usersApi.useGetUserSettings(); - const { mutate: updateUserSettings } = usersApi.useUpdateUserSettings(); + const { mutate: updateUserSettings } = usersApi.useUpdateUserSettings({ + onError: () => ToastService.showError(), + }); const { modelId: defaultModelId, modelName: defaultModelName } = useSetSelectedModel(); const { mutate: archiveAllChats, isPending: isArchivingAllChats } = chatApi.useArchiveAllChats({ onError: () => ToastService.showError(), @@ -126,11 +130,15 @@ export function Settings(): ReactElement { const createUiSettingToggleHandler = (key: K) => (value: UiSettings[K]): void => { - if (!settings) { + // NOTE: Read the latest cached settings (not the `settings` render closure) so two rapid + // toggle taps before a re-render don't clobber each other with a stale `ui` snapshot. + const latestSettings = queryClient.getQueryData(usersApiConfig.getUserSettingsQueryKey); + + if (!latestSettings) { return; } - updateUserSettings(new UserSettings({ ui: new UiSettings({ ...settings.ui, [key]: value }) })); + updateUserSettings(new UserSettings({ ui: new UiSettings({ ...latestSettings.ui, [key]: value }) })); }; const handleArchivedChatsPress = (): void => diff --git a/libs/mobile/shared/features/use-set-selected-model/src/hook.ts b/libs/mobile/shared/features/use-set-selected-model/src/hook.ts index 1c70feec..54914412 100644 --- a/libs/mobile/shared/features/use-set-selected-model/src/hook.ts +++ b/libs/mobile/shared/features/use-set-selected-model/src/hook.ts @@ -1,5 +1,11 @@ import { useEffect, useMemo, useState } from 'react'; -import { appConfigurationApi, chatApi, modelsApi, usersApi } from '@open-webui-react-native/shared/data-access/api'; +import { + appConfigurationApi, + chatApi, + isTemporaryChatId, + modelsApi, + usersApi, +} from '@open-webui-react-native/shared/data-access/api'; export interface UseSetSelectedModelResult { isLoading: boolean; @@ -18,13 +24,17 @@ export const useSetSelectedModel = (chatId?: string): UseSetSelectedModelResult isLoading: isConfigLoading, } = appConfigurationApi.useGetAppConfiguration(); const { data: models, isSuccess: isModelsSuccess, isLoading: isModelsLoading } = modelsApi.useGetModels(); + const isTemporaryChat = isTemporaryChatId(chatId); const { data: chat, isSuccess: isChatSuccess, isLoading: isChatLoading, - } = chatApi.useGet(chatId as string, { enabled: !!chatId }); + } = chatApi.useGet(chatId as string, { enabled: !!chatId && !isTemporaryChat }); - const isDataReceived = isSettingsSuccess && isConfigSuccess && isModelsSuccess && (!chatId || isChatSuccess); + // NOTE: Temporary chats are never persisted, so their useGet query is disabled above (it can't + // succeed) — the seeded cache value it still returns is enough, don't wait on isChatSuccess for it. + const isDataReceived = + isSettingsSuccess && isConfigSuccess && isModelsSuccess && (!chatId || isTemporaryChat || isChatSuccess); const isLoading = isSettingsLoading || isConfigLoading || isModelsLoading || isChatLoading; diff --git a/libs/shared/data-access/api/src/lib/chats/api.ts b/libs/shared/data-access/api/src/lib/chats/api.ts index 6602fc24..0c9a282d 100644 --- a/libs/shared/data-access/api/src/lib/chats/api.ts +++ b/libs/shared/data-access/api/src/lib/chats/api.ts @@ -746,6 +746,14 @@ function invalidateAllChatListsQueries(): void { queryClient.invalidateQueries({ queryKey: foldersApiConfig.getFolderChatsQueryKeyPrefix, exact: false }); // NOTE: Also invalidates chatServiceConfig.getAllArchivedChatsQueryKey invalidateArchivedChatListQuery(); + + // NOTE: Bulk archive/delete doesn't tell us which chat ids were affected — clear every individually + // cached chat (mirroring useDelete's single-chat cache clear) so a screen still holding one of them + // notices via useSubscribeToQueryCache('removed') instead of rendering stale, now-gone data. + queryClient + .getQueryCache() + .findAll({ queryKey: chatQueriesKeys.get._def }) + .forEach((query) => queryClient.setQueryData(query.queryKey, undefined)); } export function useArchiveAllChats( diff --git a/libs/shared/data-access/api/src/lib/users/index.ts b/libs/shared/data-access/api/src/lib/users/index.ts index b25e08b0..bd92859f 100644 --- a/libs/shared/data-access/api/src/lib/users/index.ts +++ b/libs/shared/data-access/api/src/lib/users/index.ts @@ -1,3 +1,4 @@ export * from './service'; export * from './api'; export * from './models'; +export * from './config'; From 767d5d6557df7dc523e2daa69357adba34c4ac67 Mon Sep 17 00:00:00 2001 From: Mikhail Leonov Date: Tue, 25 Aug 2026 17:51:21 +0600 Subject: [PATCH 5/5] fix: minor fix --- libs/mobile/chat/features/chat/src/lib/component.tsx | 1 - .../src/lib/components/user-message/component.tsx | 1 - .../chat/features/create-chat/src/lib/component.tsx | 1 - .../settings/features/settings/src/lib/component.tsx | 11 ----------- libs/shared/data-access/api/src/lib/users/api.ts | 2 -- .../api/src/lib/users/models/ui-settings.ts | 2 -- libs/shared/utils/feature-flag/src/config.ts | 3 --- .../shared/utils/feature-flag/src/enums/feature-id.ts | 3 --- 8 files changed, 24 deletions(-) diff --git a/libs/mobile/chat/features/chat/src/lib/component.tsx b/libs/mobile/chat/features/chat/src/lib/component.tsx index ca7497ff..89321bdc 100644 --- a/libs/mobile/chat/features/chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/component.tsx @@ -49,7 +49,6 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C const isSocketConnected = useSelector(webSocketState$.isSocketConnected); const { data: userSettings } = usersApi.useGetUserSettings(); - // NOTE: Default matches the Open WebUI web app's own fallback (Interface.svelte) const isMessageQueueEnabled = userSettings?.ui.enableMessageQueue ?? true; const [isMessagesListLoaded, setIsMessagesListLoaded] = useState(false); diff --git a/libs/mobile/chat/features/chat/src/lib/components/user-message/component.tsx b/libs/mobile/chat/features/chat/src/lib/components/user-message/component.tsx index d9aa1754..a0e38246 100644 --- a/libs/mobile/chat/features/chat/src/lib/components/user-message/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/components/user-message/component.tsx @@ -40,7 +40,6 @@ function ChatUserMessageComponent({ const { files, content: text, timestamp } = message; const { data: userSettings } = usersApi.useGetUserSettings(); - // NOTE: Defaults match the Open WebUI web app's own fallbacks (Interface.svelte) const isChatBubbleUIEnabled = userSettings?.ui.chatBubble ?? true; const isUserMessageMarkdownEnabled = userSettings?.ui.renderMarkdownInUserMessages ?? true; diff --git a/libs/mobile/chat/features/create-chat/src/lib/component.tsx b/libs/mobile/chat/features/create-chat/src/lib/component.tsx index 36844391..530c441c 100644 --- a/libs/mobile/chat/features/create-chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/create-chat/src/lib/component.tsx @@ -32,7 +32,6 @@ export function CreateChat({ const isSocketConnected = useSelector(webSocketState$.isSocketConnected); const { data: userSettings } = usersApi.useGetUserSettings(); - // NOTE: Default matches the Open WebUI web app's own fallback (Interface.svelte) const isTemporaryChat = userSettings?.ui.temporaryChatByDefault ?? false; const { diff --git a/libs/mobile/settings/features/settings/src/lib/component.tsx b/libs/mobile/settings/features/settings/src/lib/component.tsx index 32f12dee..9c82c149 100644 --- a/libs/mobile/settings/features/settings/src/lib/component.tsx +++ b/libs/mobile/settings/features/settings/src/lib/component.tsx @@ -94,8 +94,6 @@ export function Settings(): ReactElement { const defaultSystemPromptValue = isSettingsError ? translate('TEXT_LOAD_ERROR') : settings?.ui.system; const profileNameValue = isProfileError ? translate('TEXT_LOAD_ERROR') : profile?.name; - // NOTE: Defaults match the Open WebUI web app's own fallbacks (Interface.svelte) so a fresh - // account looks the same on mobile and web before the user changes anything. const isWebSearchAlwaysOnEnabled = settings?.ui.webSearch ?? false; const isMessageQueueEnabled = settings?.ui.enableMessageQueue ?? true; const isChatBubbleUIEnabled = settings?.ui.chatBubble ?? true; @@ -190,12 +188,10 @@ export function Settings(): ReactElement { }; const createChatActionHandler = ({ - featureID, isPending, action, confirm, }: { - featureID: FeatureID; isPending: boolean; action: () => void; confirm?: { title: string; message: string; confirmButtonStyle?: 'destructive' }; @@ -207,10 +203,6 @@ export function Settings(): ReactElement { }; return (): void => { - if (!isFeatureEnabled(featureID)) { - return ToastService.showFeatureNotImplemented(); - } - if (confirm) { alertService.confirm({ ...confirm, onConfirm: guardedAction }); } else { @@ -220,7 +212,6 @@ export function Settings(): ReactElement { }; const handleArchiveAllChatsPress = createChatActionHandler({ - featureID: FeatureID.ARCHIVE_ALL_CHATS, isPending: isArchivingAllChats, action: archiveAllChats, confirm: { @@ -230,7 +221,6 @@ export function Settings(): ReactElement { }); const handleDeleteAllChatsPress = createChatActionHandler({ - featureID: FeatureID.DELETE_ALL_CHATS, isPending: isDeletingAllChats, action: deleteAllChats, confirm: { @@ -241,7 +231,6 @@ export function Settings(): ReactElement { }); const handleExportAllChatsPress = createChatActionHandler({ - featureID: FeatureID.EXPORT_ALL_CHATS, isPending: isExportingAllChats, action: exportAllChats, }); diff --git a/libs/shared/data-access/api/src/lib/users/api.ts b/libs/shared/data-access/api/src/lib/users/api.ts index 17464221..a42788d0 100644 --- a/libs/shared/data-access/api/src/lib/users/api.ts +++ b/libs/shared/data-access/api/src/lib/users/api.ts @@ -30,8 +30,6 @@ function useUpdateUserSettings( ): UseMutationResult, UserSettings, UpdateUserSettingsContext> { return useMutation, UserSettings, UpdateUserSettingsContext>({ mutationFn: usersService.updateUserSettings, - // NOTE: Applied optimistically so toggles (e.g. Settings > Chats switches) flip instantly instead of - // waiting for the round trip — without this the switch briefly snaps back before the response lands. onMutate: async (settings) => { await queryClient.cancelQueries({ queryKey: usersApiConfig.getUserSettingsQueryKey }); diff --git a/libs/shared/data-access/api/src/lib/users/models/ui-settings.ts b/libs/shared/data-access/api/src/lib/users/models/ui-settings.ts index a5927a55..c02ad610 100644 --- a/libs/shared/data-access/api/src/lib/users/models/ui-settings.ts +++ b/libs/shared/data-access/api/src/lib/users/models/ui-settings.ts @@ -10,8 +10,6 @@ export class UiSettings { @Expose() public system?: string; - // NOTE: Keys below match the Open WebUI web app's own `$settings` store keys 1:1 - // (src/lib/components/chat/Settings/Interface.svelte), so settings stay in sync across clients. @Expose() public webSearch?: boolean; diff --git a/libs/shared/utils/feature-flag/src/config.ts b/libs/shared/utils/feature-flag/src/config.ts index fddea8ad..98d2f1dc 100644 --- a/libs/shared/utils/feature-flag/src/config.ts +++ b/libs/shared/utils/feature-flag/src/config.ts @@ -12,7 +12,4 @@ export const featureFlagConfig: Record = { [FeatureID.USER_EDIT_MESSAGE]: 'production', [FeatureID.AI_EDIT_MESSAGE]: 'development', [FeatureID.AI_REGENERATE_MESSAGE]: 'development', - [FeatureID.ARCHIVE_ALL_CHATS]: 'development', - [FeatureID.DELETE_ALL_CHATS]: 'development', - [FeatureID.EXPORT_ALL_CHATS]: 'development', }; diff --git a/libs/shared/utils/feature-flag/src/enums/feature-id.ts b/libs/shared/utils/feature-flag/src/enums/feature-id.ts index 79dd8f3d..1de78ddb 100644 --- a/libs/shared/utils/feature-flag/src/enums/feature-id.ts +++ b/libs/shared/utils/feature-flag/src/enums/feature-id.ts @@ -9,7 +9,4 @@ export enum FeatureID { USER_EDIT_MESSAGE = 'user-edit-message', // https://app.clickup.com/t/24336023/PRD-1976 AI_EDIT_MESSAGE = 'ai-edit-message', // https://app.clickup.com/t/24336023/PRD-1976 AI_REGENERATE_MESSAGE = 'ai-regenerate-message', // https://app.clickup.com/t/24336023/PRD-2009 - ARCHIVE_ALL_CHATS = 'archive-all-chats', // https://app.clickup.com/t/24336023/PRD-2695 - DELETE_ALL_CHATS = 'delete-all-chats', // https://app.clickup.com/t/24336023/PRD-2695 - EXPORT_ALL_CHATS = 'export-all-chats', // https://app.clickup.com/t/24336023/PRD-2695 }