diff --git a/apps/mobile/app/(main)/chat/[id].tsx b/apps/mobile/app/(main)/chat/[id].tsx
index 8c85bcb2..165b8a75 100644
--- a/apps/mobile/app/(main)/chat/[id].tsx
+++ b/apps/mobile/app/(main)/chat/[id].tsx
@@ -15,13 +15,19 @@ 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, getSelectableModels, modelsApi } from '@open-webui-react-native/shared/data-access/api';
+import {
+ chatApi,
+ getSelectableModels,
+ 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 +45,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 selectableModels = useMemo(() => getSelectableModels(models), [models]);
@@ -75,13 +83,23 @@ export default function ChatScreen(): ReactElement {
isLoading && !modelId ? (
translate('TEXT_LOADING')
) : (
-
+
+ {isTemporaryChat && (
+
+
+
+ {translate('TEXT_TEMPORARY_CHAT')}
+
+
+ )}
+
+
)
}
onGoBack={handleGoBackPress}
@@ -89,8 +107,11 @@ export default function ChatScreen(): ReactElement {
{
- if (!chat) return;
+ if (!chat || isTemporaryChat) return;
chatActionsSheetRef.current?.present(chat);
}}
/>
diff --git a/i18n/mobile/app/de.json b/i18n/mobile/app/de.json
index 418dbd3c..6c4deba3 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 6d4e8d15..26f15887 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 6b1ec2c3..607011b3 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 53f0588a..da40c4de 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 3a26d3f5..1742e31e 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 e044b0a5..1a3f673c 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 f4cbc9a0..49d04999 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 c9e70742..5aeb143b 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..08fe0d30 100644
--- a/i18n/mobile/chat/de.json
+++ b/i18n/mobile/chat/de.json
@@ -2,11 +2,13 @@
"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",
- "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..8fc9b7c9 100644
--- a/i18n/mobile/chat/en.json
+++ b/i18n/mobile/chat/en.json
@@ -2,11 +2,13 @@
"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",
- "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..7e34b85b 100644
--- a/i18n/mobile/chat/es.json
+++ b/i18n/mobile/chat/es.json
@@ -2,11 +2,13 @@
"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",
- "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..f1e14281 100644
--- a/i18n/mobile/chat/fr.json
+++ b/i18n/mobile/chat/fr.json
@@ -2,11 +2,13 @@
"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",
- "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..a2026e23 100644
--- a/i18n/mobile/chat/ja.json
+++ b/i18n/mobile/chat/ja.json
@@ -2,11 +2,13 @@
"CHAT": {
"CHAT_SCREEN": {
"TEXT_LOADING": "読み込み中...",
- "TEXT_SELECT_A_MODEL": "モデルを選択"
+ "TEXT_SELECT_A_MODEL": "モデルを選択",
+ "TEXT_TEMPORARY_CHAT": "一時チャット"
},
"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..e323e881 100644
--- a/i18n/mobile/chat/pt.json
+++ b/i18n/mobile/chat/pt.json
@@ -2,11 +2,13 @@
"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",
- "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..3afa8f2f 100644
--- a/i18n/mobile/chat/ru.json
+++ b/i18n/mobile/chat/ru.json
@@ -2,11 +2,13 @@
"CHAT": {
"CHAT_SCREEN": {
"TEXT_LOADING": "Загрузка...",
- "TEXT_SELECT_A_MODEL": "Выберите модель"
+ "TEXT_SELECT_A_MODEL": "Выберите модель",
+ "TEXT_TEMPORARY_CHAT": "Временный чат"
},
"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..9c072e05 100644
--- a/i18n/mobile/chat/zh.json
+++ b/i18n/mobile/chat/zh.json
@@ -2,11 +2,13 @@
"CHAT": {
"CHAT_SCREEN": {
"TEXT_LOADING": "加载中...",
- "TEXT_SELECT_A_MODEL": "选择模型"
+ "TEXT_SELECT_A_MODEL": "选择模型",
+ "TEXT_TEMPORARY_CHAT": "临时聊天"
},
"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 823cfbdf..89321bdc 100644
--- a/libs/mobile/chat/features/chat/src/lib/component.tsx
+++ b/libs/mobile/chat/features/chat/src/lib/component.tsx
@@ -20,8 +20,10 @@ 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';
+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';
@@ -46,11 +48,19 @@ 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();
+ 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;
+ attachedFiles: Array;
+ attachedImages: Array;
+ } | null>(null);
const {
attachedFiles,
@@ -62,7 +72,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,
@@ -88,6 +106,10 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C
const currentMessage = history?.messages[history.currentId];
const isResponseGenerating = !currentMessage?.done;
const isAssistantMessage = currentMessage?.role === Role.ASSISTANT;
+ // 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 firstMessageGeneratedRef = useRef(false);
@@ -102,6 +124,7 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C
useAppStateChange({
onChange: (lastStatusChangeTimeStamp) => {
if (
+ !isTemporaryChat &&
lastStatusChangeTimeStamp &&
dayjs().diff(lastStatusChangeTimeStamp, 'seconds') > webSocketConfig.pingTimeout
) {
@@ -178,7 +201,20 @@ 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) {
+ // 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());
+ }
+
analyticsService.trackEvent(AnalyticsEvent.MESSAGE_SENT, { modelId: selectedModelId });
reset();
resetAttachments();
@@ -212,6 +248,19 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C
}
}, [isNewChat, isSuccess, chatId]);
+ useEffect(() => {
+ if (!isResponseGenerating && queuedMessage && selectedModelId) {
+ sendMessage(
+ queuedMessage.inputValue,
+ selectedModelId,
+ queuedMessage.options,
+ queuedMessage.attachedFiles,
+ queuedMessage.attachedImages,
+ );
+ setQueuedMessage(null);
+ }
+ }, [isResponseGenerating, queuedMessage, selectedModelId, sendMessage]);
+
useEffect(() => {
if (isNewChat && !firstMessageGeneratedRef.current && isAssistantMessage && currentMessage.done) {
firstMessageGeneratedRef.current = true;
@@ -275,7 +324,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}
@@ -284,6 +333,7 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C
onDeleteImagePress={handleDeleteImage}
modelId={selectedModelId}
isResponseGenerating={isResponseGenerating}
+ isMessageQueueEnabled={isMessageQueueEnabled}
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..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
@@ -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,10 @@ function ChatUserMessageComponent({
}: ChatUserMessageProps): ReactElement {
const { files, content: text, timestamp } = message;
+ const { data: userSettings } = usersApi.useGetUserSettings();
+ 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 +56,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 +95,7 @@ function ChatUserMessageComponent({
onShowAllImages={handleAllPhotosPress}
/>
- {!!text && (
-
-
- {text}
-
-
- )}
+ {!!text && renderMessageContent()}
+ {isTemporaryChat && (
+
+
+
+ {translateChatScreen('TEXT_TEMPORARY_CHAT')}
+
+
+ )}
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-create-new-chat/src/use-create-new-chat.ts b/libs/mobile/chat/features/use-create-new-chat/src/use-create-new-chat.ts
index 3e6ed5a4..3ab0d9e4 100644
--- a/libs/mobile/chat/features/use-create-new-chat/src/use-create-new-chat.ts
+++ b/libs/mobile/chat/features/use-create-new-chat/src/use-create-new-chat.ts
@@ -1,12 +1,15 @@
import dayjs from 'dayjs';
import {
+ Chat,
chatApi,
ChatGenerationOption,
chatQueriesKeys,
ChatResponse,
+ createTemporaryChatId,
patchChatList,
prepareCompleteChatPayload,
prepareCreateChatPayload,
+ usersApi,
} from '@open-webui-react-native/shared/data-access/api';
import { FileData, ImageData } from '@open-webui-react-native/shared/data-access/common';
import { queryClient } from '@open-webui-react-native/shared/data-access/query-client';
@@ -23,6 +26,8 @@ export function useCreateNewChat({ onSuccess }: UseCreateNewChatArgs): typeof re
const { mutate: createNewChat, isPending: isChatCreating } = chatApi.useCreateNewChat();
+ const { data: userSettings } = usersApi.useGetUserSettings();
+
const startChatCreation = (
prompt: string,
model: string,
@@ -33,6 +38,38 @@ export function useCreateNewChat({ onSuccess }: UseCreateNewChatArgs): typeof re
): void => {
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-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/use-send-message/src/use-send-message.ts b/libs/mobile/chat/features/use-send-message/src/use-send-message.ts
index 037fba2f..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,7 +1,10 @@
+import { EntityPartial } from '@ronas-it/rtkq-entity-api';
+import { useCallback } from 'react';
import {
ChatGenerationOption,
ChatResponse,
chatApi,
+ isTemporaryChatId,
patchChatQueryData,
prepareCompleteChatPayload,
prepareSendMessagePayload,
@@ -24,23 +27,23 @@ 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 sendMessage = useCallback(
+ (
+ prompt: string,
+ model: string,
+ generationOptions?: Array,
+ attachedFiles?: Array,
+ attachedImages?: Array,
+ ): void => {
+ if (!chatData) {
+ return;
+ }
+
+ const payload = prepareSendMessagePayload({ prompt, chatData, model, attachedFiles, attachedImages });
+
+ const triggerCompletion = (data: EntityPartial): void => {
const completePayload = prepareCompleteChatPayload({
- chatId: data.id,
+ chatId: data.id!,
messageId: data.chat!.history.currentId,
messages: data.chat!.messages,
sessionId: socketSessionId,
@@ -49,9 +52,23 @@ export function useSendMessage({ chatData }: UseSendMessageArgs): typeof result
});
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],
+ );
const result = {
sendMessage,
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 ea008a04..9c82c149 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 {
ContactSupportSheet,
@@ -13,12 +13,16 @@ 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,
+ 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,
availableMarkdownRenderers,
@@ -44,15 +48,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 {
@@ -66,10 +62,17 @@ export function Settings(): ReactElement {
const { data: profile, isError: isProfileError } = 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 [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);
@@ -80,11 +83,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);
@@ -96,6 +94,12 @@ export function Settings(): ReactElement {
const defaultSystemPromptValue = isSettingsError ? translate('TEXT_LOAD_ERROR') : settings?.ui.system;
const profileNameValue = isProfileError ? translate('TEXT_LOAD_ERROR') : profile?.name;
+ 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,
@@ -126,6 +130,20 @@ export function Settings(): ReactElement {
appState$.setMarkdownRenderer(value);
};
+ const createUiSettingToggleHandler =
+ (key: K) =>
+ (value: UiSettings[K]): void => {
+ // 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({ ...latestSettings.ui, [key]: value }) }));
+ };
+
const handleArchivedChatsPress = (): void =>
isFeatureEnabled(FeatureID.ARCHIVE_CHAT)
? navigateOnce(`${navigationConfig.main.chat.index}/${navigationConfig.main.chat.archivedChats}`)
@@ -169,6 +187,54 @@ export function Settings(): ReactElement {
);
};
+ const createChatActionHandler = ({
+ isPending,
+ action,
+ confirm,
+ }: {
+ isPending: boolean;
+ action: () => void;
+ confirm?: { title: string; message: string; confirmButtonStyle?: 'destructive' };
+ }): (() => void) => {
+ const guardedAction = (): void => {
+ if (!isPending) {
+ action();
+ }
+ };
+
+ return (): void => {
+ if (confirm) {
+ alertService.confirm({ ...confirm, onConfirm: guardedAction });
+ } else {
+ guardedAction();
+ }
+ };
+ };
+
+ const handleArchiveAllChatsPress = createChatActionHandler({
+ isPending: isArchivingAllChats,
+ action: archiveAllChats,
+ confirm: {
+ title: translate('TEXT_ARCHIVE_ALL_CHATS_TITLE'),
+ message: translate('TEXT_ARCHIVE_ALL_CHATS_MESSAGE'),
+ },
+ });
+
+ const handleDeleteAllChatsPress = createChatActionHandler({
+ isPending: isDeletingAllChats,
+ action: deleteAllChats,
+ confirm: {
+ title: translate('TEXT_DELETE_ALL_CHATS_TITLE'),
+ message: translate('TEXT_DELETE_ALL_CHATS_MESSAGE'),
+ confirmButtonStyle: 'destructive',
+ },
+ });
+
+ const handleExportAllChatsPress = createChatActionHandler({
+ isPending: isExportingAllChats,
+ action: exportAllChats,
+ });
+
const generalOptions: Array = [
{
label: translate('TEXT_DEFAULT_MODEL'),
@@ -234,13 +300,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,
},
];
@@ -248,8 +314,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',
@@ -260,26 +326,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/mobile/shared/features/use-set-selected-model/src/hook.ts b/libs/mobile/shared/features/use-set-selected-model/src/hook.ts
index 5980d0af..d5d07f3c 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
@@ -3,6 +3,7 @@ import {
appConfigurationApi,
chatApi,
isModelSelectable,
+ isTemporaryChatId,
modelsApi,
usersApi,
} from '@open-webui-react-native/shared/data-access/api';
@@ -24,13 +25,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 5bd90ce8..0c9a282d 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,48 @@ 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();
+
+ // 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(
+ 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 +799,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/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/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/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);
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/api.ts b/libs/shared/data-access/api/src/lib/users/api.ts
index e86380e9..a42788d0 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,29 @@ function useGetUserSettings(): UseQueryResult, UserSettings>,
-): UseMutationResult, UserSettings> {
- return useMutation, UserSettings>({
+ props?: UseMutationOptions, UserSettings, UpdateUserSettingsContext>,
+): UseMutationResult, UserSettings, UpdateUserSettingsContext> {
+ return useMutation, UserSettings, UpdateUserSettingsContext>({
mutationFn: usersService.updateUserSettings,
+ 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);
},
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';
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..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,6 +10,21 @@ export class UiSettings {
@Expose()
public system?: string;
+ @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);
}