From 8fd7aa1d4b7c1db7bda900044d955acc8baea89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D0=B8=D0=B9=20=D0=95=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D0=B5=D0=B5=D0=B2?= Date: Tue, 15 Sep 2026 13:17:48 +0600 Subject: [PATCH 1/7] feat: add select knowledge to message --- i18n/mobile/chat/de.json | 17 ++- i18n/mobile/chat/en.json | 17 ++- i18n/mobile/chat/es.json | 17 ++- i18n/mobile/chat/fr.json | 17 ++- i18n/mobile/chat/ja.json | 17 ++- i18n/mobile/chat/pt.json | 17 ++- i18n/mobile/chat/ru.json | 17 ++- i18n/mobile/chat/zh.json | 17 ++- .../.babelrc | 0 .../eslint.config.cjs | 0 .../attach-knowledge-sheet/project.json | 9 ++ .../src/index.ts | 0 .../src/lib/component.tsx | 127 ++++++++++++++++++ .../src/lib/components/index.ts | 2 + .../knowledge-base-row/component.tsx | 28 ++++ .../components/knowledge-base-row}/index.ts | 0 .../knowledge-file-row/component.tsx | 21 +++ .../components/knowledge-file-row}/index.ts | 0 .../attach-knowledge-sheet/src/lib/index.ts | 1 + .../tsconfig.json | 0 .../tsconfig.lib.json | 0 .../features/attached-chat-items/.babelrc | 12 ++ .../README.md | 4 +- .../attached-chat-items/eslint.config.cjs | 12 ++ .../project.json | 6 +- .../features/attached-chat-items/src/index.ts | 1 + .../attached-chat-items/src/lib/component.tsx | 78 +++++++++++ .../attached-chat-items/src/lib/index.ts | 1 + .../attached-chat-items/tsconfig.json | 17 +++ .../attached-chat-items/tsconfig.lib.json | 19 +++ .../attached-file-item/src/lib/component.tsx | 49 +++---- .../components/file-info-modal/component.tsx | 18 ++- .../attached-file-item/src/lib/utils/index.ts | 1 - .../src/lib/utils/prepare-file-info.ts | 4 +- .../src/lib/utils/resolve-file-type.ts | 26 ---- .../attached-files-list/src/lib/component.tsx | 55 -------- .../chat/features/chat/src/lib/component.tsx | 22 +-- .../lib/components/user-message/component.tsx | 6 +- .../create-chat/src/lib/component.tsx | 14 +- .../form-chat-input/src/lib/component.tsx | 42 ++++-- .../attachments-menu-sheet/component.tsx | 71 +++++++++- .../src/use-create-new-chat.ts | 12 +- .../use-send-message/src/use-send-message.ts | 12 +- .../src/lib/component.tsx | 76 +++-------- .../upsert-folder-sheet/src/lib/component.tsx | 11 +- .../src/lib/components/index.ts | 1 - .../src/use-attached-files.ts | 38 ++++-- .../ui-kit/src/attached-item}/component.tsx | 26 ++-- .../ui/ui-kit/src/attached-item/index.ts | 1 + libs/mobile/shared/ui/ui-kit/src/index.ts | 2 + .../component.tsx | 104 ++++++++++++++ .../src/searchable-list-bottom-sheet/index.ts | 1 + .../api/src/lib/chats/models/message.ts | 4 +- .../lib/chats/utils/create-message-pair.ts | 14 +- .../utils/prepare-create-chat-payload.ts | 8 +- .../utils/prepare-send-message-payload.ts | 8 +- .../data-access/api/src/lib/files/api.ts | 24 +++- .../data-access/api/src/lib/files/config.ts | 1 + .../data-access/api/src/lib/files/service.ts | 6 +- .../data-access/api/src/lib/knowledge/api.ts | 16 ++- .../api/src/lib/knowledge/config.ts | 1 + .../api/src/lib/knowledge/models/index.ts | 1 + .../models/knowledge-file-list-response.ts | 15 +++ .../api/src/lib/knowledge/service.ts | 12 +- .../files/attached-knowledge-collection.ts | 23 ++++ .../src/models/files/attached-list-item.ts | 13 ++ .../common/src/models/files/index.ts | 2 + tsconfig.base.json | 7 +- 68 files changed, 910 insertions(+), 311 deletions(-) rename libs/mobile/chat/features/{attached-files-list => attach-knowledge-sheet}/.babelrc (100%) rename libs/mobile/chat/features/{attached-files-list => attach-knowledge-sheet}/eslint.config.cjs (100%) create mode 100644 libs/mobile/chat/features/attach-knowledge-sheet/project.json rename libs/mobile/chat/features/{attached-files-list => attach-knowledge-sheet}/src/index.ts (100%) create mode 100644 libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx create mode 100644 libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/index.ts create mode 100644 libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-base-row/component.tsx rename libs/mobile/chat/features/{attached-files-list/src/lib => attach-knowledge-sheet/src/lib/components/knowledge-base-row}/index.ts (100%) create mode 100644 libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-file-row/component.tsx rename libs/mobile/{folder/features/upsert-folder-sheet/src/lib/components/attached-knowledge => chat/features/attach-knowledge-sheet/src/lib/components/knowledge-file-row}/index.ts (100%) create mode 100644 libs/mobile/chat/features/attach-knowledge-sheet/src/lib/index.ts rename libs/mobile/chat/features/{attached-files-list => attach-knowledge-sheet}/tsconfig.json (100%) rename libs/mobile/chat/features/{attached-files-list => attach-knowledge-sheet}/tsconfig.lib.json (100%) create mode 100644 libs/mobile/chat/features/attached-chat-items/.babelrc rename libs/mobile/chat/features/{attached-files-list => attached-chat-items}/README.md (54%) create mode 100644 libs/mobile/chat/features/attached-chat-items/eslint.config.cjs rename libs/mobile/chat/features/{attached-files-list => attached-chat-items}/project.json (60%) create mode 100644 libs/mobile/chat/features/attached-chat-items/src/index.ts create mode 100644 libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx create mode 100644 libs/mobile/chat/features/attached-chat-items/src/lib/index.ts create mode 100644 libs/mobile/chat/features/attached-chat-items/tsconfig.json create mode 100644 libs/mobile/chat/features/attached-chat-items/tsconfig.lib.json delete mode 100644 libs/mobile/chat/features/attached-file-item/src/lib/utils/resolve-file-type.ts delete mode 100644 libs/mobile/chat/features/attached-files-list/src/lib/component.tsx delete mode 100644 libs/mobile/folder/features/upsert-folder-sheet/src/lib/components/index.ts rename libs/mobile/{folder/features/upsert-folder-sheet/src/lib/components/attached-knowledge => shared/ui/ui-kit/src/attached-item}/component.tsx (71%) create mode 100644 libs/mobile/shared/ui/ui-kit/src/attached-item/index.ts create mode 100644 libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx create mode 100644 libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/index.ts create mode 100644 libs/shared/data-access/api/src/lib/knowledge/models/knowledge-file-list-response.ts create mode 100644 libs/shared/data-access/common/src/models/files/attached-knowledge-collection.ts create mode 100644 libs/shared/data-access/common/src/models/files/attached-list-item.ts diff --git a/i18n/mobile/chat/de.json b/i18n/mobile/chat/de.json index 040f99f8..589eacb6 100644 --- a/i18n/mobile/chat/de.json +++ b/i18n/mobile/chat/de.json @@ -30,7 +30,8 @@ "TEXT_CAPTURE": "Aufnehmen", "TEXT_CHOOSE_FROM_GALLERY": "Aus Galerie wählen", "TEXT_UPLOAD_FILES": "Dateien hochladen", - "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Sie können keine leere Datei hochladen." + "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Sie können keine leere Datei hochladen.", + "TEXT_ATTACH_KNOWLEDGE": "Wissen anhängen" }, "TOOL_PERMISSIONS_POPUP": { "TEXT_FULL_ACCESS": "Vollzugriff", @@ -91,9 +92,6 @@ } }, "ATTACHED_FILE_ITEM": { - "TEXT_FILE": "Datei", - "TEXT_DOCUMENT": "Dokument", - "TEXT_COLLECTION": "Sammlung", "FILE_INFO_MODAL": { "TEXT_FORMATTING_DISCLAIMER": "Die Formatierung kann von der Quelle abweichen.", "TEXT_NO_PREVIEW_AVAILABLE": "Keine Vorschau verfügbar", @@ -189,6 +187,17 @@ "BUTTON_SUBMIT": "Senden", "BUTTON_DENY": "Überspringen" } + }, + "ATTACH_KNOWLEDGE_SHEET": { + "TEXT_TITLE": "Wissen", + "TEXT_SEARCH_KNOWLEDGE": "Wissen suchen", + "TEXT_NO_KNOWLEDGE": "Es ist kein Wissen vorhanden.", + "TEXT_SEARCH_FILES": "Dateien durchsuchen", + "TEXT_NO_FILES": "Keine Dateien in dieser Wissensdatenbank." + }, + "ATTACHED_CHAT_ITEMS": { + "TEXT_COLLECTION": "Sammlung", + "TEXT_FILE": "Datei" } } } diff --git a/i18n/mobile/chat/en.json b/i18n/mobile/chat/en.json index 2aefda5f..de316751 100644 --- a/i18n/mobile/chat/en.json +++ b/i18n/mobile/chat/en.json @@ -30,7 +30,8 @@ "TEXT_CAPTURE": "Capture", "TEXT_CHOOSE_FROM_GALLERY": "Choose from gallery", "TEXT_UPLOAD_FILES": "Upload files", - "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "You cannot upload an empty file." + "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "You cannot upload an empty file.", + "TEXT_ATTACH_KNOWLEDGE": "Attach Knowledge" }, "TOOL_PERMISSIONS_POPUP": { "TEXT_FULL_ACCESS": "Full access", @@ -91,9 +92,6 @@ } }, "ATTACHED_FILE_ITEM": { - "TEXT_FILE": "File", - "TEXT_DOCUMENT": "Document", - "TEXT_COLLECTION": "Collection", "FILE_INFO_MODAL": { "TEXT_FORMATTING_DISCLAIMER": "Formatting may be inconsistent from source.", "TEXT_NO_PREVIEW_AVAILABLE": "No preview available", @@ -189,6 +187,17 @@ "BUTTON_SUBMIT": "Send", "BUTTON_DENY": "Skip" } + }, + "ATTACH_KNOWLEDGE_SHEET": { + "TEXT_TITLE": "Knowledge", + "TEXT_SEARCH_KNOWLEDGE": "Search knowledge", + "TEXT_NO_KNOWLEDGE": "There is no knowledge.", + "TEXT_SEARCH_FILES": "Search files", + "TEXT_NO_FILES": "No files in this knowledge base." + }, + "ATTACHED_CHAT_ITEMS": { + "TEXT_COLLECTION": "Collection", + "TEXT_FILE": "File" } } } diff --git a/i18n/mobile/chat/es.json b/i18n/mobile/chat/es.json index 6de8cad0..42a4829d 100644 --- a/i18n/mobile/chat/es.json +++ b/i18n/mobile/chat/es.json @@ -30,7 +30,8 @@ "TEXT_CAPTURE": "Capturar", "TEXT_CHOOSE_FROM_GALLERY": "Elegir de la galería", "TEXT_UPLOAD_FILES": "Subir archivos", - "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "No puedes subir un archivo vacío." + "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "No puedes subir un archivo vacío.", + "TEXT_ATTACH_KNOWLEDGE": "Adjuntar conocimiento" }, "TOOL_PERMISSIONS_POPUP": { "TEXT_FULL_ACCESS": "Acceso completo", @@ -91,9 +92,6 @@ } }, "ATTACHED_FILE_ITEM": { - "TEXT_FILE": "Archivo", - "TEXT_DOCUMENT": "Documento", - "TEXT_COLLECTION": "Colección", "FILE_INFO_MODAL": { "TEXT_FORMATTING_DISCLAIMER": "El formato puede ser inconsistente con respecto al origen.", "TEXT_NO_PREVIEW_AVAILABLE": "Vista previa no disponible", @@ -189,6 +187,17 @@ "BUTTON_SUBMIT": "Enviar", "BUTTON_DENY": "Omitir" } + }, + "ATTACH_KNOWLEDGE_SHEET": { + "TEXT_TITLE": "Conocimiento", + "TEXT_SEARCH_KNOWLEDGE": "Buscar conocimiento", + "TEXT_NO_KNOWLEDGE": "No hay conocimiento.", + "TEXT_SEARCH_FILES": "Buscar archivos", + "TEXT_NO_FILES": "No hay archivos en esta base de conocimiento." + }, + "ATTACHED_CHAT_ITEMS": { + "TEXT_COLLECTION": "Colección", + "TEXT_FILE": "Archivo" } } } diff --git a/i18n/mobile/chat/fr.json b/i18n/mobile/chat/fr.json index 704012ea..cae2d011 100644 --- a/i18n/mobile/chat/fr.json +++ b/i18n/mobile/chat/fr.json @@ -30,7 +30,8 @@ "TEXT_CAPTURE": "Capturer", "TEXT_CHOOSE_FROM_GALLERY": "Choisir dans la galerie", "TEXT_UPLOAD_FILES": "Téléverser des fichiers", - "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Vous ne pouvez pas téléverser un fichier vide." + "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Vous ne pouvez pas téléverser un fichier vide.", + "TEXT_ATTACH_KNOWLEDGE": "Joindre une base de connaissances" }, "TOOL_PERMISSIONS_POPUP": { "TEXT_FULL_ACCESS": "Accès complet", @@ -91,9 +92,6 @@ } }, "ATTACHED_FILE_ITEM": { - "TEXT_FILE": "Fichier", - "TEXT_DOCUMENT": "Document", - "TEXT_COLLECTION": "Collection", "FILE_INFO_MODAL": { "TEXT_FORMATTING_DISCLAIMER": "La mise en forme peut différer de la source.", "TEXT_NO_PREVIEW_AVAILABLE": "Aucun aperçu disponible", @@ -189,6 +187,17 @@ "BUTTON_SUBMIT": "Envoyer", "BUTTON_DENY": "Ignorer" } + }, + "ATTACH_KNOWLEDGE_SHEET": { + "TEXT_TITLE": "Connaissances", + "TEXT_SEARCH_KNOWLEDGE": "Rechercher des connaissances", + "TEXT_NO_KNOWLEDGE": "Aucune connaissance.", + "TEXT_SEARCH_FILES": "Rechercher des fichiers", + "TEXT_NO_FILES": "Aucun fichier dans cette base de connaissances." + }, + "ATTACHED_CHAT_ITEMS": { + "TEXT_COLLECTION": "Collection", + "TEXT_FILE": "Fichier" } } } diff --git a/i18n/mobile/chat/ja.json b/i18n/mobile/chat/ja.json index b3a298ef..7ea51177 100644 --- a/i18n/mobile/chat/ja.json +++ b/i18n/mobile/chat/ja.json @@ -30,7 +30,8 @@ "TEXT_CAPTURE": "撮影", "TEXT_CHOOSE_FROM_GALLERY": "ギャラリーから選択", "TEXT_UPLOAD_FILES": "ファイルをアップロード", - "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "空のファイルはアップロードできません。" + "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "空のファイルはアップロードできません。", + "TEXT_ATTACH_KNOWLEDGE": "ナレッジを添付" }, "TOOL_PERMISSIONS_POPUP": { "TEXT_FULL_ACCESS": "フルアクセス", @@ -91,9 +92,6 @@ } }, "ATTACHED_FILE_ITEM": { - "TEXT_FILE": "ファイル", - "TEXT_DOCUMENT": "ドキュメント", - "TEXT_COLLECTION": "コレクション", "FILE_INFO_MODAL": { "TEXT_FORMATTING_DISCLAIMER": "書式は元のソースと一致しない場合があります。", "TEXT_NO_PREVIEW_AVAILABLE": "プレビューはありません", @@ -188,6 +186,17 @@ "BUTTON_SUBMIT": "送信", "BUTTON_DENY": "スキップ" } + }, + "ATTACH_KNOWLEDGE_SHEET": { + "TEXT_TITLE": "ナレッジ", + "TEXT_SEARCH_KNOWLEDGE": "ナレッジを検索", + "TEXT_NO_KNOWLEDGE": "ナレッジがありません。", + "TEXT_SEARCH_FILES": "ファイルを検索", + "TEXT_NO_FILES": "このナレッジベースにファイルはありません。" + }, + "ATTACHED_CHAT_ITEMS": { + "TEXT_COLLECTION": "コレクション", + "TEXT_FILE": "ファイル" } } } diff --git a/i18n/mobile/chat/pt.json b/i18n/mobile/chat/pt.json index e46ba959..1ee9453d 100644 --- a/i18n/mobile/chat/pt.json +++ b/i18n/mobile/chat/pt.json @@ -30,7 +30,8 @@ "TEXT_CAPTURE": "Capturar", "TEXT_CHOOSE_FROM_GALLERY": "Escolher da galeria", "TEXT_UPLOAD_FILES": "Enviar arquivos", - "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Não é possível enviar um arquivo vazio." + "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Não é possível enviar um arquivo vazio.", + "TEXT_ATTACH_KNOWLEDGE": "Anexar conhecimento" }, "TOOL_PERMISSIONS_POPUP": { "TEXT_FULL_ACCESS": "Acesso total", @@ -91,9 +92,6 @@ } }, "ATTACHED_FILE_ITEM": { - "TEXT_FILE": "Arquivo", - "TEXT_DOCUMENT": "Documento", - "TEXT_COLLECTION": "Coleção", "FILE_INFO_MODAL": { "TEXT_FORMATTING_DISCLAIMER": "A formatação pode ser inconsistente em relação à origem.", "TEXT_NO_PREVIEW_AVAILABLE": "Pré-visualização indisponível", @@ -189,6 +187,17 @@ "BUTTON_SUBMIT": "Enviar", "BUTTON_DENY": "Ignorar" } + }, + "ATTACH_KNOWLEDGE_SHEET": { + "TEXT_TITLE": "Conhecimento", + "TEXT_SEARCH_KNOWLEDGE": "Pesquisar conhecimento", + "TEXT_NO_KNOWLEDGE": "Não há conhecimento.", + "TEXT_SEARCH_FILES": "Pesquisar arquivos", + "TEXT_NO_FILES": "Não há arquivos nesta base de conhecimento." + }, + "ATTACHED_CHAT_ITEMS": { + "TEXT_COLLECTION": "Coleção", + "TEXT_FILE": "Arquivo" } } } diff --git a/i18n/mobile/chat/ru.json b/i18n/mobile/chat/ru.json index c0d19619..1b481724 100644 --- a/i18n/mobile/chat/ru.json +++ b/i18n/mobile/chat/ru.json @@ -30,7 +30,8 @@ "TEXT_CAPTURE": "Сделать снимок", "TEXT_CHOOSE_FROM_GALLERY": "Выбрать из галереи", "TEXT_UPLOAD_FILES": "Загрузить файлы", - "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Нельзя загрузить пустой файл." + "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Нельзя загрузить пустой файл.", + "TEXT_ATTACH_KNOWLEDGE": "Прикрепить базу знаний" }, "TOOL_PERMISSIONS_POPUP": { "TEXT_FULL_ACCESS": "Полный доступ", @@ -91,9 +92,6 @@ } }, "ATTACHED_FILE_ITEM": { - "TEXT_FILE": "Файл", - "TEXT_DOCUMENT": "Документ", - "TEXT_COLLECTION": "Коллекция", "FILE_INFO_MODAL": { "TEXT_FORMATTING_DISCLAIMER": "Форматирование может отличаться от исходного.", "TEXT_NO_PREVIEW_AVAILABLE": "Предпросмотр недоступен", @@ -190,6 +188,17 @@ "BUTTON_SUBMIT": "Отправить", "BUTTON_DENY": "Пропустить" } + }, + "ATTACH_KNOWLEDGE_SHEET": { + "TEXT_TITLE": "База знаний", + "TEXT_SEARCH_KNOWLEDGE": "Поиск по базе знаний", + "TEXT_NO_KNOWLEDGE": "База знаний пуста.", + "TEXT_SEARCH_FILES": "Поиск файлов", + "TEXT_NO_FILES": "В этой базе знаний нет файлов." + }, + "ATTACHED_CHAT_ITEMS": { + "TEXT_COLLECTION": "Коллекция", + "TEXT_FILE": "Файл" } } } diff --git a/i18n/mobile/chat/zh.json b/i18n/mobile/chat/zh.json index 5d357220..7da672b2 100644 --- a/i18n/mobile/chat/zh.json +++ b/i18n/mobile/chat/zh.json @@ -30,7 +30,8 @@ "TEXT_CAPTURE": "拍摄", "TEXT_CHOOSE_FROM_GALLERY": "从相册选择", "TEXT_UPLOAD_FILES": "上传文件", - "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "无法上传空文件。" + "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "无法上传空文件。", + "TEXT_ATTACH_KNOWLEDGE": "附加知识库" }, "TOOL_PERMISSIONS_POPUP": { "TEXT_FULL_ACCESS": "完全访问", @@ -91,9 +92,6 @@ } }, "ATTACHED_FILE_ITEM": { - "TEXT_FILE": "文件", - "TEXT_DOCUMENT": "文档", - "TEXT_COLLECTION": "集合", "FILE_INFO_MODAL": { "TEXT_FORMATTING_DISCLAIMER": "格式可能与源文件不一致。", "TEXT_NO_PREVIEW_AVAILABLE": "暂无预览", @@ -188,6 +186,17 @@ "BUTTON_SUBMIT": "发送", "BUTTON_DENY": "跳过" } + }, + "ATTACH_KNOWLEDGE_SHEET": { + "TEXT_TITLE": "知识库", + "TEXT_SEARCH_KNOWLEDGE": "搜索知识库", + "TEXT_NO_KNOWLEDGE": "暂无知识库。", + "TEXT_SEARCH_FILES": "搜索文件", + "TEXT_NO_FILES": "此知识库中没有文件。" + }, + "ATTACHED_CHAT_ITEMS": { + "TEXT_COLLECTION": "集合", + "TEXT_FILE": "文件" } } } diff --git a/libs/mobile/chat/features/attached-files-list/.babelrc b/libs/mobile/chat/features/attach-knowledge-sheet/.babelrc similarity index 100% rename from libs/mobile/chat/features/attached-files-list/.babelrc rename to libs/mobile/chat/features/attach-knowledge-sheet/.babelrc diff --git a/libs/mobile/chat/features/attached-files-list/eslint.config.cjs b/libs/mobile/chat/features/attach-knowledge-sheet/eslint.config.cjs similarity index 100% rename from libs/mobile/chat/features/attached-files-list/eslint.config.cjs rename to libs/mobile/chat/features/attach-knowledge-sheet/eslint.config.cjs diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/project.json b/libs/mobile/chat/features/attach-knowledge-sheet/project.json new file mode 100644 index 00000000..cb39d65f --- /dev/null +++ b/libs/mobile/chat/features/attach-knowledge-sheet/project.json @@ -0,0 +1,9 @@ +{ + "name": "mobile/chat/features/attach-knowledge-sheet", + "$schema": "../../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/mobile/chat/features/attach-knowledge-sheet/src", + "projectType": "library", + "tags": ["app:mobile", "scope:chat", "type:features"], + "// targets": "to see all targets run: nx show project mobile/chat/features/attach-knowledge-sheet --web", + "targets": {} +} diff --git a/libs/mobile/chat/features/attached-files-list/src/index.ts b/libs/mobile/chat/features/attach-knowledge-sheet/src/index.ts similarity index 100% rename from libs/mobile/chat/features/attached-files-list/src/index.ts rename to libs/mobile/chat/features/attach-knowledge-sheet/src/index.ts diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx new file mode 100644 index 00000000..f1f2a009 --- /dev/null +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx @@ -0,0 +1,127 @@ +import { BottomSheetModal } from '@gorhom/bottom-sheet'; +import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; +import { ForwardedRef, ReactElement, useImperativeHandle, useRef, useState } from 'react'; +import { SearchableListBottomSheet } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { Knowledge, knowledgeApi } from '@open-webui-react-native/shared/data-access/api'; +import { FileData } from '@open-webui-react-native/shared/data-access/common'; +import { useDebouncedQuery } from '@open-webui-react-native/shared/utils/use-debounced-query'; +import { KnowledgeBaseRow, KnowledgeFileRow } from './components'; + +export type AttachKnowledgeSheetMethods = { + present: () => void; +}; + +export type AttachKnowledgeSheetRef = ForwardedRef; + +export interface AttachKnowledgeSheetProps { + ref?: AttachKnowledgeSheetRef; + isCollectionAttached: (id: string) => boolean; + isFileAttached: (id: string) => boolean; + onSelectCollection: (knowledge: Knowledge) => void; + onSelectFile: (knowledge: Knowledge, file: FileData) => void; +} + +export function AttachKnowledgeSheet({ + ref, + isCollectionAttached, + isFileAttached, + onSelectCollection, + onSelectFile, +}: AttachKnowledgeSheetProps): ReactElement { + const translate = useTranslation('CHAT.ATTACH_KNOWLEDGE_SHEET'); + const sheetRef = useRef(null); + + const [openedKnowledge, setOpenedKnowledge] = useState(null); + + const { query, setQuery } = useDebouncedQuery(); + + const { data: knowledgeList, isLoading: isKnowledgeLoading } = knowledgeApi.useGetKnowledge(); + const { data: knowledgeFiles, isLoading: isFilesLoading } = knowledgeApi.useGetKnowledgeFiles(openedKnowledge?.id, 1); + + const closeModal = (): void => { + sheetRef.current?.close(); + setOpenedKnowledge(null); + setQuery(''); + }; + + const openModal = (): void => sheetRef.current?.present(); + + useImperativeHandle(ref, () => ({ present: openModal }), []); + + const handleGoBack = (): void => { + if (openedKnowledge) { + setOpenedKnowledge(null); + setQuery(''); + + return; + } + + closeModal(); + }; + + const handleShowFilesPress = (knowledge: Knowledge): void => { + setQuery(''); + setOpenedKnowledge(knowledge); + }; + + const handleSelectCollection = (knowledge: Knowledge): void => { + onSelectCollection(knowledge); + closeModal(); + }; + + const handleSelectFile = (knowledge: Knowledge, file: FileData): void => { + onSelectFile(knowledge, file); + closeModal(); + }; + + if (openedKnowledge) { + const files = (knowledgeFiles?.items ?? []).filter((file) => new RegExp(query, 'i').test(file.meta.name)); + + return ( + item.id} + renderItem={({ item }) => ( + handleSelectFile(openedKnowledge, item)} + /> + )} + /> + ); + } + + const filteredKnowledge = (knowledgeList ?? []).filter((item) => new RegExp(query, 'i').test(item.name)); + + return ( + item.id} + renderItem={({ item }) => ( + handleSelectCollection(item)} + onShowFilesPress={() => handleShowFilesPress(item)} + /> + )} + /> + ); +} diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/index.ts b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/index.ts new file mode 100644 index 00000000..6699bf07 --- /dev/null +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/index.ts @@ -0,0 +1,2 @@ +export * from './knowledge-base-row'; +export * from './knowledge-file-row'; diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-base-row/component.tsx b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-base-row/component.tsx new file mode 100644 index 00000000..434615f1 --- /dev/null +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-base-row/component.tsx @@ -0,0 +1,28 @@ +import { ReactElement } from 'react'; +import { AppPressable, AppText, Icon, IconButton, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { Knowledge } from '@open-webui-react-native/shared/data-access/api'; + +export interface KnowledgeBaseRowProps { + item: Knowledge; + isSelected: boolean; + onPress: () => void; + onShowFilesPress: () => void; +} + +export function KnowledgeBaseRow({ item, isSelected, onPress, onShowFilesPress }: KnowledgeBaseRowProps): ReactElement { + return ( + + + + {item.name} + {item.description} + + {isSelected && } + + + ); +} diff --git a/libs/mobile/chat/features/attached-files-list/src/lib/index.ts b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-base-row/index.ts similarity index 100% rename from libs/mobile/chat/features/attached-files-list/src/lib/index.ts rename to libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-base-row/index.ts diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-file-row/component.tsx b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-file-row/component.tsx new file mode 100644 index 00000000..6b94f5d4 --- /dev/null +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-file-row/component.tsx @@ -0,0 +1,21 @@ +import { ReactElement } from 'react'; +import { AppPressable, AppText, Icon, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { FileData } from '@open-webui-react-native/shared/data-access/common'; + +export interface KnowledgeFileRowProps { + item: FileData; + isSelected: boolean; + onPress: () => void; +} + +export function KnowledgeFileRow({ item, isSelected, onPress }: KnowledgeFileRowProps): ReactElement { + return ( + + + + {item.meta.name} + + {isSelected && } + + ); +} diff --git a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/components/attached-knowledge/index.ts b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-file-row/index.ts similarity index 100% rename from libs/mobile/folder/features/upsert-folder-sheet/src/lib/components/attached-knowledge/index.ts rename to libs/mobile/chat/features/attach-knowledge-sheet/src/lib/components/knowledge-file-row/index.ts diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/index.ts b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/index.ts new file mode 100644 index 00000000..bb824842 --- /dev/null +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/libs/mobile/chat/features/attached-files-list/tsconfig.json b/libs/mobile/chat/features/attach-knowledge-sheet/tsconfig.json similarity index 100% rename from libs/mobile/chat/features/attached-files-list/tsconfig.json rename to libs/mobile/chat/features/attach-knowledge-sheet/tsconfig.json diff --git a/libs/mobile/chat/features/attached-files-list/tsconfig.lib.json b/libs/mobile/chat/features/attach-knowledge-sheet/tsconfig.lib.json similarity index 100% rename from libs/mobile/chat/features/attached-files-list/tsconfig.lib.json rename to libs/mobile/chat/features/attach-knowledge-sheet/tsconfig.lib.json diff --git a/libs/mobile/chat/features/attached-chat-items/.babelrc b/libs/mobile/chat/features/attached-chat-items/.babelrc new file mode 100644 index 00000000..1ea870ea --- /dev/null +++ b/libs/mobile/chat/features/attached-chat-items/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/mobile/chat/features/attached-files-list/README.md b/libs/mobile/chat/features/attached-chat-items/README.md similarity index 54% rename from libs/mobile/chat/features/attached-files-list/README.md rename to libs/mobile/chat/features/attached-chat-items/README.md index 83220517..16c754fa 100644 --- a/libs/mobile/chat/features/attached-files-list/README.md +++ b/libs/mobile/chat/features/attached-chat-items/README.md @@ -1,7 +1,7 @@ -# mobile/chat/features/attached-files-list +# mobile/chat/features/attached-chat-items This library was generated with [Nx](https://nx.dev). ## Running unit tests -Run `nx test mobile/chat/features/attached-files-list` to execute the unit tests via [Jest](https://jestjs.io). +Run `nx test mobile/chat/features/attached-chat-items` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/mobile/chat/features/attached-chat-items/eslint.config.cjs b/libs/mobile/chat/features/attached-chat-items/eslint.config.cjs new file mode 100644 index 00000000..6f47c1a7 --- /dev/null +++ b/libs/mobile/chat/features/attached-chat-items/eslint.config.cjs @@ -0,0 +1,12 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/react'], + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/libs/mobile/chat/features/attached-files-list/project.json b/libs/mobile/chat/features/attached-chat-items/project.json similarity index 60% rename from libs/mobile/chat/features/attached-files-list/project.json rename to libs/mobile/chat/features/attached-chat-items/project.json index 78bd98f5..d7ecc6ae 100644 --- a/libs/mobile/chat/features/attached-files-list/project.json +++ b/libs/mobile/chat/features/attached-chat-items/project.json @@ -1,9 +1,9 @@ { - "name": "mobile/chat/features/attached-files-list", + "name": "mobile/chat/features/attached-chat-items", "$schema": "../../../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/mobile/chat/features/attached-files-list/src", + "sourceRoot": "libs/mobile/chat/features/attached-chat-items/src", "projectType": "library", "tags": ["app:mobile", "scope:chat", "type:features"], - "// targets": "to see all targets run: nx show project mobile/chat/features/attached-files-list --web", + "// targets": "to see all targets run: nx show project mobile/chat/features/attached-chat-items --web", "targets": {} } diff --git a/libs/mobile/chat/features/attached-chat-items/src/index.ts b/libs/mobile/chat/features/attached-chat-items/src/index.ts new file mode 100644 index 00000000..f41a696f --- /dev/null +++ b/libs/mobile/chat/features/attached-chat-items/src/index.ts @@ -0,0 +1 @@ +export * from './lib'; diff --git a/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx b/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx new file mode 100644 index 00000000..cc4b0a35 --- /dev/null +++ b/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx @@ -0,0 +1,78 @@ +import { Observable } from '@legendapp/state'; +import { useSelector } from '@legendapp/state/react'; +import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; +import { ReactElement } from 'react'; +import { AttachedFileItem, formatFileSize } from '@open-webui-react-native/mobile/chat/features/attached-file-item'; +import { AttachedImageItem } from '@open-webui-react-native/mobile/chat/features/attached-image-item'; +import { AttachedItem, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { AttachedListItem, ImageData } from '@open-webui-react-native/shared/data-access/common'; + +interface AttachedChatItemsProps { + onDeleteItemPress: (id: string) => void; + attachedItems: Observable>; + onImagePress: (index: number) => void; + onDeleteImagePress: (uri: string) => void; + attachedImages: Observable>; +} + +export function AttachedChatItems({ + onDeleteItemPress, + attachedItems, + onImagePress, + onDeleteImagePress, + attachedImages, +}: AttachedChatItemsProps): ReactElement | null { + const translate = useTranslation('CHAT.ATTACHED_CHAT_ITEMS'); + const items = useSelector(attachedItems); + const images = useSelector(attachedImages); + + if (items.length === 0 && images.length === 0) { + return null; + } + + return ( + + {items.flatMap((item) => { + if (!item) { + return []; + } + + if (item.kind === 'collection') { + return [ + onDeleteItemPress(item.collection.id)} + />, + ]; + } + + return [ + , + ]; + })} + + {images.flatMap((image, index) => + image + ? [ + onImagePress(index)} + onDeleteImagePress={onDeleteImagePress} + image={image} + />, + ] + : [], + )} + + + ); +} diff --git a/libs/mobile/chat/features/attached-chat-items/src/lib/index.ts b/libs/mobile/chat/features/attached-chat-items/src/lib/index.ts new file mode 100644 index 00000000..bb824842 --- /dev/null +++ b/libs/mobile/chat/features/attached-chat-items/src/lib/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/libs/mobile/chat/features/attached-chat-items/tsconfig.json b/libs/mobile/chat/features/attached-chat-items/tsconfig.json new file mode 100644 index 00000000..ec74bfc3 --- /dev/null +++ b/libs/mobile/chat/features/attached-chat-items/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../../../../tsconfig.base.json" +} diff --git a/libs/mobile/chat/features/attached-chat-items/tsconfig.lib.json b/libs/mobile/chat/features/attached-chat-items/tsconfig.lib.json new file mode 100644 index 00000000..27f2b917 --- /dev/null +++ b/libs/mobile/chat/features/attached-chat-items/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/mobile/chat/features/attached-file-item/src/lib/component.tsx b/libs/mobile/chat/features/attached-file-item/src/lib/component.tsx index 4d9de00a..0f301b12 100644 --- a/libs/mobile/chat/features/attached-file-item/src/lib/component.tsx +++ b/libs/mobile/chat/features/attached-file-item/src/lib/component.tsx @@ -1,25 +1,21 @@ import { Fragment, ReactElement, useRef } from 'react'; -import { cn } from '@open-webui-react-native/mobile/shared/ui/styles'; -import { - AppModalHandle, - AppPressable, - AppPressableProps, - AppText, - Icon, - IconButton, - View, -} from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { AppModalHandle, AppPressableProps, AttachedItem } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { FileData } from '@open-webui-react-native/shared/data-access/common'; import { FileInfoModal } from './components'; -import { formatFileSize, resolveFileType } from './utils'; interface AttachedFileItemProps { file: FileData; + subtitle: string; onDeleteFilePress?: (id: string) => void; className?: AppPressableProps['className']; } -export function AttachedFileItem({ file, onDeleteFilePress, className }: AttachedFileItemProps): ReactElement { +export function AttachedFileItem({ + file, + subtitle, + onDeleteFilePress, + className, +}: AttachedFileItemProps): ReactElement { const modalRef = useRef(null); const handleFilePress = (): void => { @@ -28,29 +24,14 @@ export function AttachedFileItem({ file, onDeleteFilePress, className }: Attache return ( - - - - - - {file.filename} - {resolveFileType(file)} - - - {formatFileSize(file.meta.size)} - - {onDeleteFilePress && ( - onDeleteFilePress?.(file.id)} - className='absolute active:opacity-1 active:bg-background-secondary bg-background-primary border border-text-secondary p-0 rounded-full items-center justify-center w-[20] h-[20] top-[-6] right-[-6]' - iconProps={{ className: 'color-text-primary', width: 12 }} - /> - )} - + onDeletePress={onDeleteFilePress ? () => onDeleteFilePress(file.id) : undefined} + /> ); diff --git a/libs/mobile/chat/features/attached-file-item/src/lib/components/file-info-modal/component.tsx b/libs/mobile/chat/features/attached-file-item/src/lib/components/file-info-modal/component.tsx index 4999dcc9..ad28c9a8 100644 --- a/libs/mobile/chat/features/attached-file-item/src/lib/components/file-info-modal/component.tsx +++ b/libs/mobile/chat/features/attached-file-item/src/lib/components/file-info-modal/component.tsx @@ -1,5 +1,5 @@ import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; -import { ReactElement } from 'react'; +import { ReactElement, useState } from 'react'; import { AppModal, AppModalProps, @@ -8,6 +8,7 @@ import { Icon, View, } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { filesApi } from '@open-webui-react-native/shared/data-access/api'; import { FileData } from '@open-webui-react-native/shared/data-access/common'; import { prepareFileInfo } from '../../utils'; @@ -18,11 +19,20 @@ interface FileInfoModalProps extends AppModalProps { export const FileInfoModal = ({ file, ...modalProps }: FileInfoModalProps): ReactElement => { const translate = useTranslation('CHAT.ATTACHED_FILE_ITEM.FILE_INFO_MODAL'); + // NOTE: A file's extracted text isn't always already in memory (e.g. a file picked from a + // knowledge base only ever carries its metadata), so it's fetched on demand once the modal + // is actually shown, same as the web app does. + const [shouldFetchContent, setShouldFetchContent] = useState(false); + + const { data: fetchedContent } = filesApi.useGetFileContent(shouldFetchContent ? file.id : undefined); + + const content = file.data?.content || fetchedContent?.content; + return ( - + setShouldFetchContent(true)}> {file.filename} - {prepareFileInfo(file)} + {prepareFileInfo(file, content)} {translate('TEXT_FORMATTING_DISCLAIMER')} - {file.data.content || translate('TEXT_NO_PREVIEW_AVAILABLE')} + {content || translate('TEXT_NO_PREVIEW_AVAILABLE')} diff --git a/libs/mobile/chat/features/attached-file-item/src/lib/utils/index.ts b/libs/mobile/chat/features/attached-file-item/src/lib/utils/index.ts index 017e5011..c1bf95e9 100644 --- a/libs/mobile/chat/features/attached-file-item/src/lib/utils/index.ts +++ b/libs/mobile/chat/features/attached-file-item/src/lib/utils/index.ts @@ -1,3 +1,2 @@ -export * from './resolve-file-type'; export * from './format-file-size'; export * from './prepare-file-info'; diff --git a/libs/mobile/chat/features/attached-file-item/src/lib/utils/prepare-file-info.ts b/libs/mobile/chat/features/attached-file-item/src/lib/utils/prepare-file-info.ts index 325f7748..fe6b57d8 100644 --- a/libs/mobile/chat/features/attached-file-item/src/lib/utils/prepare-file-info.ts +++ b/libs/mobile/chat/features/attached-file-item/src/lib/utils/prepare-file-info.ts @@ -3,6 +3,6 @@ import { FileData } from '@open-webui-react-native/shared/data-access/common'; import { getLineCount } from '@open-webui-react-native/shared/utils/strings'; import { formatFileSize } from './format-file-size'; -export const prepareFileInfo = (file: FileData): string => { - return `${formatFileSize(file.meta.size)} • ${i18n.t('CHAT.ATTACHED_FILE_ITEM.FILE_INFO_MODAL.TEXT_EXTRACTED_LINES', { count: getLineCount(file.data.content) })}`; +export const prepareFileInfo = (file: FileData, content?: string): string => { + return `${formatFileSize(file.meta.size)} • ${i18n.t('CHAT.ATTACHED_FILE_ITEM.FILE_INFO_MODAL.TEXT_EXTRACTED_LINES', { count: getLineCount(content ?? '') })}`; }; diff --git a/libs/mobile/chat/features/attached-file-item/src/lib/utils/resolve-file-type.ts b/libs/mobile/chat/features/attached-file-item/src/lib/utils/resolve-file-type.ts deleted file mode 100644 index 10e584c7..00000000 --- a/libs/mobile/chat/features/attached-file-item/src/lib/utils/resolve-file-type.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { i18n } from '@ronas-it/react-native-common-modules/i18n'; -import { last, split, toLower } from 'lodash-es'; -import { FileData } from '@open-webui-react-native/shared/data-access/common'; - -export function resolveFileType(file: FileData): string { - const contentType = toLower(file.meta.contentType || ''); - const ext = toLower(last(split(file.filename, '.')) || ''); - - const documentExts = ['pdf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx']; - const collectionExts = ['zip', 'rar', '7z', 'tar', 'gz']; - - if ( - contentType.startsWith('application/pdf') || - contentType.startsWith('application/msword') || - contentType.includes('spreadsheet') || - documentExts.includes(ext) - ) { - return i18n.t('CHAT.ATTACHED_FILE_ITEM.TEXT_DOCUMENT'); - } - - if (contentType.includes('zip') || contentType.includes('tar') || collectionExts.includes(ext)) { - return i18n.t('CHAT.ATTACHED_FILE_ITEM.TEXT_COLLECTION'); - } - - return i18n.t('CHAT.ATTACHED_FILE_ITEM.TEXT_FILE'); -} diff --git a/libs/mobile/chat/features/attached-files-list/src/lib/component.tsx b/libs/mobile/chat/features/attached-files-list/src/lib/component.tsx deleted file mode 100644 index f79cf03a..00000000 --- a/libs/mobile/chat/features/attached-files-list/src/lib/component.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Observable } from '@legendapp/state'; -import { useSelector } from '@legendapp/state/react'; -import { ReactElement } from 'react'; -import { AttachedFileItem } from '@open-webui-react-native/mobile/chat/features/attached-file-item'; -import { AttachedImageItem } from '@open-webui-react-native/mobile/chat/features/attached-image-item'; -import { View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; -import { FileData, ImageData } from '@open-webui-react-native/shared/data-access/common'; - -interface AttachedFilesListProps { - onDeleteFilePress: (id: string) => void; - attachedFiles: Observable>; - onImagePress: (index: number) => void; - onDeleteImagePress: (uri: string) => void; - attachedImages: Observable>; -} - -export function AttachedFilesList({ - onDeleteFilePress, - attachedFiles, - onImagePress, - onDeleteImagePress, - attachedImages, -}: AttachedFilesListProps): ReactElement | null { - const files = useSelector(attachedFiles); - const images = useSelector(attachedImages); - - if (files.length === 0 && images.length === 0) { - return null; - } - - return ( - - {files.flatMap((file) => - file ? [] : [], - )} - - {images.flatMap((image, index) => - image - ? [ - onImagePress(index)} - onDeleteImagePress={onDeleteImagePress} - image={image} - />, - ] - : [], - )} - - - ); -} diff --git a/libs/mobile/chat/features/chat/src/lib/component.tsx b/libs/mobile/chat/features/chat/src/lib/component.tsx index 268015df..e9b50481 100644 --- a/libs/mobile/chat/features/chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/component.tsx @@ -25,7 +25,7 @@ import { isTemporaryChatId, usersApi, } from '@open-webui-react-native/shared/data-access/api'; -import { FileData, ImageData, Role } from '@open-webui-react-native/shared/data-access/common'; +import { AttachedListItem, 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 { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; @@ -59,17 +59,19 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C const [queuedMessage, setQueuedMessage] = useState<{ inputValue: string; options: Array; - attachedFiles: Array; + attachedItems: Array; attachedImages: Array; } | null>(null); const { - attachedFiles, + attachedItems, attachedImages, handleImageUploaded, handleDeleteImage, handleFileUploaded, - handleDeleteFile, + handleKnowledgeCollectionAttached, + handleKnowledgeFileAttached, + handleDeleteItem, resetAttachments, } = useAttachedFiles(); @@ -218,12 +220,12 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C setQueuedMessage({ inputValue, options, - attachedFiles: attachedFiles.get(), + attachedItems: attachedItems.get(), attachedImages: attachedImages.get(), }); ToastService.show(translate('TEXT_MESSAGE_QUEUED')); } else { - sendMessage(inputValue, selectedModelId, options, attachedFiles.get(), attachedImages.get()); + sendMessage(inputValue, selectedModelId, options, attachedItems.get(), attachedImages.get()); } reset(); @@ -264,7 +266,7 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C queuedMessage.inputValue, selectedModelId, queuedMessage.options, - queuedMessage.attachedFiles, + queuedMessage.attachedItems, queuedMessage.attachedImages, ); setQueuedMessage(null); @@ -341,12 +343,14 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C name='inputValue' onSubmit={onSubmit} isLoading={isSending || !isSocketConnected || isComposerBlockedByGeneration} - attachedFiles={attachedFiles} + attachedItems={attachedItems} onFileUploaded={handleFileUploaded} - onDeleteFilePress={handleDeleteFile} + onDeleteItemPress={handleDeleteItem} attachedImages={attachedImages} onImageUploaded={handleImageUploaded} onDeleteImagePress={handleDeleteImage} + onKnowledgeCollectionSelected={handleKnowledgeCollectionAttached} + onKnowledgeFileSelected={handleKnowledgeFileAttached} modelId={selectedModelId} isResponseGenerating={isResponseGenerating} isMessageQueueEnabled={isMessageQueueEnabled} 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 a0e38246..8e3b42d2 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 @@ -1,6 +1,6 @@ import { ReactElement, useMemo } from 'react'; import { LayoutChangeEvent } from 'react-native'; -import { AttachedFileItem } from '@open-webui-react-native/mobile/chat/features/attached-file-item'; +import { AttachedFileItem, formatFileSize } from '@open-webui-react-native/mobile/chat/features/attached-file-item'; import { MessageVersionControls } from '@open-webui-react-native/mobile/chat/features/message-version-controls'; import { UseSiblingMessagesReturn } from '@open-webui-react-native/mobile/chat/features/use-manage-messages-siblings'; import { @@ -86,7 +86,9 @@ function ChatUserMessageComponent({ + subtitle={formatFileSize(file.file.meta.size)} + className='max-w-[70%] self-end' + /> ))} { @@ -100,12 +102,14 @@ export function CreateChat({ onSubmit={onSubmit} isLoading={isCreating || !isSocketConnected} isSuggestionShown={true} - attachedFiles={attachedFiles} + attachedItems={attachedItems} onFileUploaded={handleFileUploaded} - onDeleteFilePress={handleDeleteFile} + onDeleteItemPress={handleDeleteItem} attachedImages={attachedImages} onImageUploaded={handleImageUploaded} onDeleteImagePress={handleDeleteImage} + onKnowledgeCollectionSelected={handleKnowledgeCollectionAttached} + onKnowledgeFileSelected={handleKnowledgeFileAttached} onChatCreated={onChatCreated} modelId={modelId} /> 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 a0912aa0..8585fc4f 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 @@ -4,7 +4,7 @@ import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; import { xor } from 'lodash-es'; import { ReactElement, useMemo, useState } from 'react'; import { Control, FieldValues, Path, useController } from 'react-hook-form'; -import { AttachedFilesList } from '@open-webui-react-native/mobile/chat/features/attached-files-list'; +import { AttachedChatItems } from '@open-webui-react-native/mobile/chat/features/attached-chat-items'; import { SoundWaveRecorder } from '@open-webui-react-native/mobile/chat/features/sound-wave-recorder'; import { SuggestionsList } from '@open-webui-react-native/mobile/chat/features/suggestions-list'; import { useVoiceModeModal } from '@open-webui-react-native/mobile/chat/features/voice-mode-modal'; @@ -25,7 +25,13 @@ import { toolsSelectionState$, usersApi, } from '@open-webui-react-native/shared/data-access/api'; -import { AttachedImage, FileData, ImageData } from '@open-webui-react-native/shared/data-access/common'; +import { + AttachedImage, + AttachedKnowledgeCollection, + AttachedListItem, + FileData, + ImageData, +} from '@open-webui-react-native/shared/data-access/common'; import { withOfflineGuard } from '@open-webui-react-native/shared/features/network'; import { FeatureID, isFeatureEnabled } from '@open-webui-react-native/shared/utils/feature-flag'; import { toDataUrl } from '@open-webui-react-native/shared/utils/files'; @@ -42,12 +48,14 @@ interface FormChatInputProps extends AppInputProps { name: Path; control: Control; onSubmit: (options: Array) => void; - attachedFiles: Observable>; + attachedItems: Observable>; onFileUploaded: (file: FileData) => void; - onDeleteFilePress: (id: string) => void; + onDeleteItemPress: (id: string) => void; attachedImages: Observable>; onImageUploaded: (image: ImageData) => void; onDeleteImagePress: (fileName: string) => void; + onKnowledgeCollectionSelected: (collection: AttachedKnowledgeCollection) => void; + onKnowledgeFileSelected: (file: FileData) => void; chat?: ChatResponse; modelId?: string; onChatCreated?: (id: string) => void; @@ -66,12 +74,14 @@ export function FormChatInput({ name, control, onSubmit, - attachedFiles, + attachedItems, onFileUploaded, - onDeleteFilePress, + onDeleteItemPress, attachedImages, onImageUploaded, onDeleteImagePress, + onKnowledgeCollectionSelected, + onKnowledgeFileSelected, chat, modelId, onChatCreated, @@ -92,7 +102,7 @@ export function FormChatInput({ const { field } = useController({ control, name }); - const files = useSelector(attachedFiles); + const items = useSelector(attachedItems); const images = useSelector(attachedImages); const [isMicrophonePreparing, setIsMicrophonePreparing] = useState(false); @@ -121,7 +131,13 @@ export function FormChatInput({ const { handleImagePress, selectedImageIndex, isPreviewVisible, handleCloseImagePress } = useImagePreview(); const { present: openVoiceModeModal } = useVoiceModeModal(); - const isInputEmpty = !field.value?.trim() && files.length === 0 && images.length === 0; + const isInputEmpty = !field.value?.trim() && items.length === 0 && images.length === 0; + + const isKnowledgeCollectionAttached = (id: string): boolean => + items.some((item) => item?.kind === 'collection' && item.collection.id === id); + + const isKnowledgeFileAttached = (id: string): boolean => + items.some((item) => item?.kind === 'file' && item.isFromKnowledge && item.file.id === id); const imagesForPreview = images.flatMap((image, index) => image @@ -192,9 +208,9 @@ export function FormChatInput({ scrollEnabled textClassName='text-md-sm sm:text-md' accessoryTop={ - ({ onFileUploaded={onFileUploaded} disabled={isLoading} onImageUploaded={onImageUploaded} + isKnowledgeCollectionAttached={isKnowledgeCollectionAttached} + isKnowledgeFileAttached={isKnowledgeFileAttached} + onKnowledgeCollectionSelected={onKnowledgeCollectionSelected} + onKnowledgeFileSelected={onKnowledgeFileSelected} /> {config?.features.enableImageGeneration && ( void; onImageUploaded?: (image: ImageData) => void; + isKnowledgeCollectionAttached: (id: string) => boolean; + isKnowledgeFileAttached: (id: string) => boolean; + onKnowledgeCollectionSelected: (collection: AttachedKnowledgeCollection) => void; + onKnowledgeFileSelected: (file: FileData) => void; } export function AttachmentsMenuSheet({ disabled, onFileUploaded, onImageUploaded, + isKnowledgeCollectionAttached, + isKnowledgeFileAttached, + onKnowledgeCollectionSelected, + onKnowledgeFileSelected, }: AttachmentsMenuSheetProps): ReactElement { const translate = useTranslation('CHAT.FORM_CHAT_INPUT.ATTACHMENTS_ACTIONS_POPUP'); const modalRef = useRef(null); + const attachKnowledgeSheetRef = useRef(null); const { mutate: uploadFile, isPending: isFileUploading, @@ -73,6 +91,29 @@ export function AttachmentsMenuSheet({ uploadFile(getDocumentFormData(file)); }; + const handleAttachKnowledgePress = (): void => { + closeModal(); + attachKnowledgeSheetRef.current?.present(); + }; + + const handleSelectKnowledgeCollection = (knowledge: Knowledge): void => { + onKnowledgeCollectionSelected( + new AttachedKnowledgeCollection({ + id: knowledge.id, + type: FileType.COLLECTION, + name: knowledge.name, + description: knowledge.description, + status: 'processed', + }), + ); + }; + + // NOTE: the file already carries everything needed (id, meta, collectionName) straight from + // GET /knowledge/{id}/files — no need to reconstruct it, unlike the collection case below. + const handleSelectKnowledgeFile = (_knowledge: Knowledge, file: FileData): void => { + onKnowledgeFileSelected(file); + }; + const actions: Array = [ { title: translate('TEXT_CAPTURE'), @@ -90,6 +131,11 @@ export function AttachmentsMenuSheet({ onPress: handlePickFile, isLoading: isFileUploading, }, + { + title: translate('TEXT_ATTACH_KNOWLEDGE'), + iconName: 'database', + onPress: handleAttachKnowledgePress, + }, ]; const renderTrigger = ({ onPress }: { onPress: () => void }): ReactElement => ( @@ -107,8 +153,19 @@ export function AttachmentsMenuSheet({ } }, [isFileUploaded]); - return ; + return ( + + + + + ); } 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 96860975..1ba0c38c 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 @@ -13,7 +13,7 @@ import { toolsSelectionState$, usersApi, } from '@open-webui-react-native/shared/data-access/api'; -import { FileData, ImageData } from '@open-webui-react-native/shared/data-access/common'; +import { AttachedListItem, ImageData } from '@open-webui-react-native/shared/data-access/common'; import { queryClient } from '@open-webui-react-native/shared/data-access/query-client'; import { socketService } from '@open-webui-react-native/shared/data-access/websocket'; @@ -34,11 +34,17 @@ export function useCreateNewChat({ onSuccess }: UseCreateNewChatArgs): typeof re prompt: string, model: string, generationOptions?: Array, - attachedFiles?: Array, + attachedItems?: Array, attachedImages?: Array, folderId?: string, ): void => { - const payload = prepareCreateChatPayload({ prompt, model, attachedFiles, attachedImages, folderId }); + const payload = prepareCreateChatPayload({ + prompt, + model, + attachedItems, + attachedImages, + folderId, + }); // NOTE: The tools were picked on the create-chat screen, before a chat id existed, so the // selection is parked under a placeholder key. Move it onto the real id — otherwise the chat 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 96108bc6..b7211f45 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 @@ -9,7 +9,7 @@ import { prepareCompleteChatPayload, prepareSendMessagePayload, } from '@open-webui-react-native/shared/data-access/api'; -import { FileData, ImageData } from '@open-webui-react-native/shared/data-access/common'; +import { AttachedListItem, ImageData } from '@open-webui-react-native/shared/data-access/common'; import { socketService } from '@open-webui-react-native/shared/data-access/websocket'; interface UseSendMessageArgs { @@ -32,14 +32,20 @@ export function useSendMessage({ chatData }: UseSendMessageArgs): typeof result prompt: string, model: string, generationOptions?: Array, - attachedFiles?: Array, + attachedItems?: Array, attachedImages?: Array, ): void => { if (!chatData) { return; } - const payload = prepareSendMessagePayload({ prompt, chatData, model, attachedFiles, attachedImages }); + const payload = prepareSendMessagePayload({ + prompt, + chatData, + model, + attachedItems, + attachedImages, + }); const triggerCompletion = (data: EntityPartial): void => { const completePayload = prepareCompleteChatPayload({ diff --git a/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx b/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx index 4e88c571..4675ce37 100644 --- a/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx +++ b/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx @@ -1,19 +1,7 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet'; import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; import { ForwardedRef, ReactElement, useImperativeHandle, useRef, useState } from 'react'; -import { Keyboard } from 'react-native'; -import { - AppBottomSheet, - AppBottomSheetKeyboardAwareScrollView, - AppBottomSheetPropsType, - AppFlashList, - AppSafeAreaView, - AppSpinner, - ListEmptyComponent, - SearchInput, - SheetHeader, - View, -} from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { SearchableListBottomSheet } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { Knowledge, knowledgeApi } from '@open-webui-react-native/shared/data-access/api'; import { useDebouncedQuery } from '@open-webui-react-native/shared/utils/use-debounced-query'; import { KnowledgeRow } from './components'; @@ -24,12 +12,12 @@ export type SelectKnowledgeSheetMethods = { export type SelectKnowledgeSheetRef = ForwardedRef; -export type SelectKnowledgeSheetProps = Partial> & { +export type SelectKnowledgeSheetProps = { onConfirm: (selectedKnowledge: Array) => void; ref?: SelectKnowledgeSheetRef; }; -export function SelectKnowledgeSheet({ onConfirm, ref, ...props }: SelectKnowledgeSheetProps): ReactElement { +export function SelectKnowledgeSheet({ onConfirm, ref }: SelectKnowledgeSheetProps): ReactElement { const translate = useTranslation('FOLDER.SELECT_KNOWLEDGE_SHEET'); const sheetRef = useRef(null); @@ -59,11 +47,6 @@ export function SelectKnowledgeSheet({ onConfirm, ref, ...props }: SelectKnowled }; }, []); - const onCancelPress = (): void => { - setQuery(''); - Keyboard.dismiss(); - }; - const renderItem = ({ item }: { item: Knowledge }): ReactElement => { const isSelected = selectedKnowledge.some((knowledge) => knowledge.id === item.id); @@ -79,48 +62,19 @@ export function SelectKnowledgeSheet({ onConfirm, ref, ...props }: SelectKnowled }; return ( - - - - {isLoading ? ( - - - - ) : ( - - - - } - /> - - - )} - - } + title={translate('TEXT_SELECT_KNOWLEDGE')} + onGoBack={closeModal} + onConfirmPress={handleConfirm} + searchQuery={query} + onSearchQueryChange={setQuery} + searchPlaceholder={translate('TEXT_SEARCH_KNOWLEDGE')} + isLoading={isLoading} + emptyDescription={translate('TEXT_NO_KNOWLEDGE')} + data={filteredData} + renderItem={renderItem} + keyExtractor={(item) => item.id} /> ); } diff --git a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx b/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx index 1a68db44..f43e3752 100644 --- a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx +++ b/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx @@ -22,6 +22,7 @@ import { AppBottomSheetPropsType, AppBottomSheetKeyboardAwareScrollView, AppSpinner, + AttachedItem, } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { FormValues } from '@open-webui-react-native/mobile/shared/utils/form'; import { @@ -34,7 +35,6 @@ import { AttachedFile, FileData, FileType } from '@open-webui-react-native/share import { FeatureID, isFeatureEnabled } from '@open-webui-react-native/shared/utils/feature-flag'; import { getDocumentFormData } from '@open-webui-react-native/shared/utils/files'; import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; -import { AttachedKnowledge } from './components'; import { UpsertFolderFormSchema } from './forms'; export type UpsertFolderSheetMethods = { @@ -68,7 +68,7 @@ export function UpsertFolderSheet({ ref, ...props }: UpsertFolderSheetProps): Re const { data: folder, isLoading: isFolderLoading } = foldersApi.useGetFolder(folderId as string, { enabled: !!folderId, }); - const { attachedFiles, handleFileUploaded, handleDeleteFile, resetAttachments } = useAttachedFiles(); + const { attachedItems, handleFileUploaded, handleDeleteItem, resetAttachments } = useAttachedFiles(); const { control, handleSubmit, reset } = useForm({ defaultValues: new UpsertFolderFormSchema(), @@ -90,7 +90,7 @@ export function UpsertFolderSheet({ ref, ...props }: UpsertFolderSheetProps): Re } }, [folder]); - const files = useSelector(attachedFiles).flatMap((file) => (file ? [file] : [])); + const files = useSelector(attachedItems).flatMap((item) => (item?.kind === 'file' ? [item.file] : [])); const closeModal = (): void => sheetRef.current?.close(); @@ -181,8 +181,7 @@ export function UpsertFolderSheet({ ref, ...props }: UpsertFolderSheetProps): Re : translate('TEXT_COLLECTION'); return ( - isFile - ? handleDeleteFile(item.id) + ? handleDeleteItem(item.id) : setSelectedKnowledge((prev) => prev.filter((knowledge) => knowledge.id !== item.id)) } /> diff --git a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/components/index.ts b/libs/mobile/folder/features/upsert-folder-sheet/src/lib/components/index.ts deleted file mode 100644 index cfcd1b67..00000000 --- a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './attached-knowledge'; diff --git a/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts b/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts index d9f52179..9a30e39a 100644 --- a/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts +++ b/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts @@ -2,18 +2,38 @@ import { useObservable } from '@legendapp/state/react'; import { fileSystemService } from '@open-webui-react-native/mobile/shared/data-access/file-system-service'; import { ImageMimeType } from '@open-webui-react-native/mobile/shared/data-access/image-picker-service'; import { compressImage } from '@open-webui-react-native/mobile/shared/utils/compressor'; -import { FileData, ImageData } from '@open-webui-react-native/shared/data-access/common'; +import { + AttachedKnowledgeCollection, + AttachedListItem, + FileData, + getAttachedListItemId, + ImageData, +} from '@open-webui-react-native/shared/data-access/common'; export function useAttachedFiles(): typeof result { - const attachedFiles = useObservable>([]); + const attachedItems = useObservable>([]); const attachedImages = useObservable>([]); + const pushItem = (item: AttachedListItem): void => { + attachedItems.set((prev) => + prev.some((attached) => getAttachedListItemId(attached) === getAttachedListItemId(item)) ? prev : [...prev, item], + ); + }; + const handleFileUploaded = (file: FileData): void => { - attachedFiles.set((prev) => [...prev, file]); + pushItem({ kind: 'file', file, isFromKnowledge: false }); + }; + + const handleKnowledgeFileAttached = (file: FileData): void => { + pushItem({ kind: 'file', file, isFromKnowledge: true }); + }; + + const handleKnowledgeCollectionAttached = (collection: AttachedKnowledgeCollection): void => { + pushItem({ kind: 'collection', collection }); }; - const handleDeleteFile = (id: string): void => { - attachedFiles.set((prev) => prev.filter((file) => file.id !== id)); + const handleDeleteItem = (id: string): void => { + attachedItems.set((prev) => prev.filter((item) => getAttachedListItemId(item) !== id)); }; const handleImageUploaded = async (image: ImageData): Promise => { @@ -43,14 +63,16 @@ export function useAttachedFiles(): typeof result { }; const resetAttachments = (): void => { - attachedFiles.set([]); + attachedItems.set([]); attachedImages.set([]); }; const result = { - attachedFiles, + attachedItems, handleFileUploaded, - handleDeleteFile, + handleKnowledgeFileAttached, + handleKnowledgeCollectionAttached, + handleDeleteItem, attachedImages, handleImageUploaded, handleDeleteImage, diff --git a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/components/attached-knowledge/component.tsx b/libs/mobile/shared/ui/ui-kit/src/attached-item/component.tsx similarity index 71% rename from libs/mobile/folder/features/upsert-folder-sheet/src/lib/components/attached-knowledge/component.tsx rename to libs/mobile/shared/ui/ui-kit/src/attached-item/component.tsx index 971fe168..ee41d291 100644 --- a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/components/attached-knowledge/component.tsx +++ b/libs/mobile/shared/ui/ui-kit/src/attached-item/component.tsx @@ -1,32 +1,26 @@ import { ReactElement } from 'react'; import { cn } from '@open-webui-react-native/mobile/shared/ui/styles'; -import { - AppPressable, - AppPressableProps, - AppText, - Icon, - IconButton, - IconName, - View, -} from '@open-webui-react-native/mobile/shared/ui/ui-kit'; +import { Icon, IconName } from '../icon'; +import { IconButton } from '../icon-button'; +import { AppPressable, AppPressableProps } from '../pressable'; +import { AppText } from '../text'; +import { View } from '../view'; -interface AttachedKnowledgeProps extends AppPressableProps { - index: number; +interface AttachedItemProps extends AppPressableProps { title: string; subTitle: string; iconName: IconName; - onDeletePress?: (id: number) => void; + onDeletePress?: () => void; } -export function AttachedKnowledge({ - index, +export function AttachedItem({ title, subTitle, iconName, className, onDeletePress, ...restProps -}: AttachedKnowledgeProps): ReactElement { +}: AttachedItemProps): ReactElement { return ( onDeletePress(index)} + onPress={onDeletePress} className='rounded-full bg-background-primary w-24 h-24 p-0 items-center justify-center' iconProps={{ className: 'color-text-primary', width: 8 }} /> diff --git a/libs/mobile/shared/ui/ui-kit/src/attached-item/index.ts b/libs/mobile/shared/ui/ui-kit/src/attached-item/index.ts new file mode 100644 index 00000000..bb824842 --- /dev/null +++ b/libs/mobile/shared/ui/ui-kit/src/attached-item/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/libs/mobile/shared/ui/ui-kit/src/index.ts b/libs/mobile/shared/ui/ui-kit/src/index.ts index 1206a37e..cd43bc19 100644 --- a/libs/mobile/shared/ui/ui-kit/src/index.ts +++ b/libs/mobile/shared/ui/ui-kit/src/index.ts @@ -44,3 +44,5 @@ export * from './keyboard-sticky-view'; export * from './pressable-search-input'; export * from './full-screen-search-modal'; export * from './gesture-pressable-icon-button'; +export * from './attached-item'; +export * from './searchable-list-bottom-sheet'; diff --git a/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx new file mode 100644 index 00000000..995d95e8 --- /dev/null +++ b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx @@ -0,0 +1,104 @@ +import { BottomSheetModal } from '@gorhom/bottom-sheet'; +import { ReactElement } from 'react'; +import { Keyboard } from 'react-native'; +import { AppBottomSheet, AppBottomSheetPropsType } from '../bottom-sheet'; +import { AppFlashList, AppFlashListProps } from '../flash-list'; +import { AppBottomSheetKeyboardAwareScrollView } from '../keyboard-aware-scroll-view'; +import { ListEmptyComponent } from '../list-empty-component'; +import { AppSafeAreaView } from '../safe-area-view'; +import { SearchInput } from '../search-input'; +import { SheetHeader, SheetHeaderProps } from '../sheet-header'; +import { AppSpinner } from '../spinner'; +import { View } from '../view'; + +export type SearchableListBottomSheetRef = React.RefObject; + +export type SearchableListBottomSheetProps = Partial> & { + ref?: SearchableListBottomSheetRef; + title: SheetHeaderProps['title']; + accessoryLeft?: SheetHeaderProps['accessoryLeft']; + accessoryRight?: SheetHeaderProps['accessoryRight']; + onGoBack: () => void; + onConfirmPress?: SheetHeaderProps['onConfirmPress']; + confirmButtonProps?: SheetHeaderProps['confirmButtonProps']; + searchQuery: string; + onSearchQueryChange: (query: string) => void; + searchPlaceholder?: string; + isLoading?: boolean; + emptyDescription: string; + data: Array; + renderItem: AppFlashListProps['renderItem']; + keyExtractor?: AppFlashListProps['keyExtractor']; +}; + +export function SearchableListBottomSheet({ + ref, + title, + accessoryLeft, + accessoryRight, + onGoBack, + onConfirmPress, + confirmButtonProps, + searchQuery, + onSearchQueryChange, + searchPlaceholder, + isLoading, + emptyDescription, + data, + renderItem, + keyExtractor, + ...restProps +}: SearchableListBottomSheetProps): ReactElement { + const handleCancelSearch = (): void => { + onSearchQueryChange(''); + Keyboard.dismiss(); + }; + + return ( + + + + {isLoading ? ( + + + + ) : ( + + + } + /> + + + )} + + } + /> + ); +} diff --git a/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/index.ts b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/index.ts new file mode 100644 index 00000000..bb824842 --- /dev/null +++ b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/index.ts @@ -0,0 +1 @@ +export * from './component'; diff --git a/libs/shared/data-access/api/src/lib/chats/models/message.ts b/libs/shared/data-access/api/src/lib/chats/models/message.ts index f5824aba..4834a9c8 100644 --- a/libs/shared/data-access/api/src/lib/chats/models/message.ts +++ b/libs/shared/data-access/api/src/lib/chats/models/message.ts @@ -3,6 +3,7 @@ import { Expose, Type } from 'class-transformer'; import { AttachedFile, AttachedImage, + AttachedKnowledgeCollection, FileType, MessageSource, Role, @@ -64,11 +65,12 @@ export class Message extends BaseEntity { subTypes: [ { value: AttachedFile, name: FileType.FILE }, { value: AttachedImage, name: FileType.IMAGE }, + { value: AttachedKnowledgeCollection, name: FileType.COLLECTION }, ], }, keepDiscriminatorProperty: true, }) - public files?: Array; + public files?: Array; @Expose() @Type(() => MessageSource) diff --git a/libs/shared/data-access/api/src/lib/chats/utils/create-message-pair.ts b/libs/shared/data-access/api/src/lib/chats/utils/create-message-pair.ts index 5315824c..80291a68 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/create-message-pair.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/create-message-pair.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs'; import uuid from 'react-native-uuid'; -import { FileData, ImageData, Role } from '@open-webui-react-native/shared/data-access/common'; +import { AttachedListItem, ImageData, Role } from '@open-webui-react-native/shared/data-access/common'; import { prepareAttachedFiles, prepareAttachedImages } from '../../files'; import { Message } from '../models'; @@ -8,7 +8,7 @@ export interface CreateMessagePairArgs { prompt: string; model: string; currentMessageId?: string; - attachedFiles?: Array; + attachedItems?: Array; attachedImages?: Array; } @@ -16,7 +16,7 @@ export function createMessagePair({ prompt, model, currentMessageId, - attachedFiles, + attachedItems, attachedImages, }: CreateMessagePairArgs): typeof result { const userMessageId = uuid.v4(); @@ -25,9 +25,15 @@ export function createMessagePair({ const timestampSec = Math.floor(now.unix()); const timestampMs = now.valueOf(); + const attachedFiles = (attachedItems ?? []).flatMap((item) => (item.kind === 'file' ? [item.file] : [])); + const attachedCollections = (attachedItems ?? []).flatMap((item) => + item.kind === 'collection' ? [item.collection] : [], + ); + const files = [ - ...(attachedFiles ? prepareAttachedFiles(attachedFiles) : []), + ...prepareAttachedFiles(attachedFiles), ...(attachedImages ? prepareAttachedImages(attachedImages) : []), + ...attachedCollections, ]; const userMessage = new Message({ diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-create-chat-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-create-chat-payload.ts index 3aa639ad..897ac869 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/prepare-create-chat-payload.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/prepare-create-chat-payload.ts @@ -1,5 +1,5 @@ import { i18n } from '@ronas-it/react-native-common-modules/i18n'; -import { FileData, ImageData } from '@open-webui-react-native/shared/data-access/common'; +import { AttachedListItem, ImageData } from '@open-webui-react-native/shared/data-access/common'; import { Chat, CreateNewChatRequest } from '../models'; import { buildMessagesHistory } from './build-messages-history'; import { createMessagePair } from './create-message-pair'; @@ -7,7 +7,7 @@ import { createMessagePair } from './create-message-pair'; export interface PrepareCreateChatPayloadArgs { prompt: string; model: string; - attachedFiles?: Array; + attachedItems?: Array; attachedImages?: Array; folderId?: string; } @@ -15,14 +15,14 @@ export interface PrepareCreateChatPayloadArgs { export function prepareCreateChatPayload({ prompt, model, - attachedFiles, + attachedItems, attachedImages, folderId, }: PrepareCreateChatPayloadArgs): CreateNewChatRequest { const { userMessage, assistantMessage, assistantMessageId, timestampMs } = createMessagePair({ prompt, model, - attachedFiles, + attachedItems, attachedImages, }); diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-send-message-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-send-message-payload.ts index b06a33ec..25ac4706 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/prepare-send-message-payload.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/prepare-send-message-payload.ts @@ -1,5 +1,5 @@ import { EntityPartial } from '@ronas-it/rtkq-entity-api'; -import { FileData, ImageData } from '@open-webui-react-native/shared/data-access/common'; +import { AttachedListItem, ImageData } from '@open-webui-react-native/shared/data-access/common'; import { Chat, ChatResponse } from '../models'; import { buildMessagesHistory } from './build-messages-history'; import { createMessagePair } from './create-message-pair'; @@ -9,7 +9,7 @@ export interface PrepareSendMessagePayloadArgs { prompt: string; chatData: ChatResponse; model: string; - attachedFiles?: Array; + attachedItems?: Array; attachedImages?: Array; } @@ -17,14 +17,14 @@ export function prepareSendMessagePayload({ prompt, chatData, model, - attachedFiles, + attachedItems, attachedImages, }: PrepareSendMessagePayloadArgs): EntityPartial { const { userMessage, assistantMessage, assistantMessageId } = createMessagePair({ prompt, model, currentMessageId: chatData.chat.history.currentId, - attachedFiles, + attachedItems, attachedImages, }); diff --git a/libs/shared/data-access/api/src/lib/files/api.ts b/libs/shared/data-access/api/src/lib/files/api.ts index c701a629..ce1113b0 100644 --- a/libs/shared/data-access/api/src/lib/files/api.ts +++ b/libs/shared/data-access/api/src/lib/files/api.ts @@ -1,7 +1,14 @@ -import { useMutation, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; +import { + useMutation, + UseMutationOptions, + UseMutationResult, + useQuery, + UseQueryOptions, + UseQueryResult, +} from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { ApiErrorData } from '@open-webui-react-native/shared/data-access/api-client'; -import { FileData } from '@open-webui-react-native/shared/data-access/common'; +import { FileData, FileDataContent } from '@open-webui-react-native/shared/data-access/common'; import { filesApiConfig } from './config'; import { filesService } from './service'; @@ -15,6 +22,19 @@ function useUploadFile( }); } +function useGetFileContent( + id?: string, + props?: UseQueryOptions>, +): UseQueryResult> { + return useQuery>({ + queryFn: () => filesService.getFileContent(id!), + queryKey: filesApiConfig.getFileContentQueryKey(id!), + enabled: !!id, + ...props, + }); +} + export const filesApi = { useUploadFile, + useGetFileContent, }; diff --git a/libs/shared/data-access/api/src/lib/files/config.ts b/libs/shared/data-access/api/src/lib/files/config.ts index 86576f6f..f005e423 100644 --- a/libs/shared/data-access/api/src/lib/files/config.ts +++ b/libs/shared/data-access/api/src/lib/files/config.ts @@ -2,4 +2,5 @@ export const filesApiConfig = { route: 'v1/files/', filesStorageRoute: '/api/v1/files/', uploadFileQueryKey: ['files', 'upload'], + getFileContentQueryKey: (id: string): Array => ['files', 'get-content', id], }; diff --git a/libs/shared/data-access/api/src/lib/files/service.ts b/libs/shared/data-access/api/src/lib/files/service.ts index 969b67a7..675bc510 100644 --- a/libs/shared/data-access/api/src/lib/files/service.ts +++ b/libs/shared/data-access/api/src/lib/files/service.ts @@ -1,6 +1,6 @@ import { plainToInstance } from 'class-transformer'; import { getApiService } from '@open-webui-react-native/shared/data-access/api-client'; -import { FileData } from '@open-webui-react-native/shared/data-access/common'; +import { FileData, FileDataContent } from '@open-webui-react-native/shared/data-access/common'; import { filesApiConfig } from './config'; class FilesService { @@ -9,6 +9,10 @@ class FilesService { return plainToInstance(FileData, response); } + + public async getFileContent(id: string): Promise { + return getApiService().get(`${filesApiConfig.route}${id}/data/content`); + } } export const filesService = new FilesService(); diff --git a/libs/shared/data-access/api/src/lib/knowledge/api.ts b/libs/shared/data-access/api/src/lib/knowledge/api.ts index 1fce46e6..3ae7a58a 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/api.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/api.ts @@ -2,7 +2,7 @@ import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query import { AxiosError } from 'axios'; import { ApiErrorData } from '@open-webui-react-native/shared/data-access/api-client'; import { knowledgeApiConfig } from './config'; -import { Knowledge } from './models'; +import { Knowledge, KnowledgeFileListResponse } from './models'; import { knowledgeService } from './service'; function useGetKnowledge( @@ -15,6 +15,20 @@ function useGetKnowledge( }); } +function useGetKnowledgeFiles( + id?: string, + page = 1, + props?: UseQueryOptions>, +): UseQueryResult> { + return useQuery>({ + queryFn: () => knowledgeService.getKnowledgeFiles(id!, page), + queryKey: knowledgeApiConfig.getKnowledgeFilesQueryKey(id!, page), + enabled: !!id, + ...props, + }); +} + export const knowledgeApi = { useGetKnowledge, + useGetKnowledgeFiles, }; diff --git a/libs/shared/data-access/api/src/lib/knowledge/config.ts b/libs/shared/data-access/api/src/lib/knowledge/config.ts index c96b4650..3eacebbe 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/config.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/config.ts @@ -1,4 +1,5 @@ export const knowledgeApiConfig = { route: 'v1/knowledge', getKnowledgeQueryKey: ['knowledge', 'get'], + getKnowledgeFilesQueryKey: (id: string, page: number): Array => ['knowledge', 'get-files', id, `${page}`], }; diff --git a/libs/shared/data-access/api/src/lib/knowledge/models/index.ts b/libs/shared/data-access/api/src/lib/knowledge/models/index.ts index 4e09ac6f..80de324c 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/models/index.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/models/index.ts @@ -1,5 +1,6 @@ export * from './knowledge'; export * from './knowledge-file'; +export * from './knowledge-file-list-response'; export * from './knowledge-data'; export * from './knowledge-access-control'; export * from './knowledge-access-control-groups'; diff --git a/libs/shared/data-access/api/src/lib/knowledge/models/knowledge-file-list-response.ts b/libs/shared/data-access/api/src/lib/knowledge/models/knowledge-file-list-response.ts new file mode 100644 index 00000000..4fa0c0ee --- /dev/null +++ b/libs/shared/data-access/api/src/lib/knowledge/models/knowledge-file-list-response.ts @@ -0,0 +1,15 @@ +import { Expose, Type } from 'class-transformer'; +import { FileData } from '@open-webui-react-native/shared/data-access/common'; + +export class KnowledgeFileListResponse { + @Expose() + @Type(() => FileData) + public items: Array; + + @Expose() + public total: number; + + constructor(response: Partial = {}) { + Object.assign(this, response); + } +} diff --git a/libs/shared/data-access/api/src/lib/knowledge/service.ts b/libs/shared/data-access/api/src/lib/knowledge/service.ts index 1af26738..ccc2c324 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/service.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/service.ts @@ -2,7 +2,7 @@ import { plainToInstance } from 'class-transformer'; import { getApiService } from '@open-webui-react-native/shared/data-access/api-client'; import { EntityPromiseService } from '@open-webui-react-native/shared/data-access/base-entity'; import { knowledgeApiConfig } from './config'; -import { Knowledge, KnowledgeResponse } from './models'; +import { Knowledge, KnowledgeFileListResponse, KnowledgeResponse } from './models'; class KnowledgeService extends EntityPromiseService { constructor() { @@ -26,6 +26,16 @@ class KnowledgeService extends EntityPromiseService { }), ); } + + public async getKnowledgeFiles(id: string, page: number): Promise { + const response = await getApiService().get(`${knowledgeApiConfig.route}/${id}/files`, { + page, + }); + + return plainToInstance(KnowledgeFileListResponse, response, { + excludeExtraneousValues: true, + }); + } } export const knowledgeService = new KnowledgeService(); diff --git a/libs/shared/data-access/common/src/models/files/attached-knowledge-collection.ts b/libs/shared/data-access/common/src/models/files/attached-knowledge-collection.ts new file mode 100644 index 00000000..9874144d --- /dev/null +++ b/libs/shared/data-access/common/src/models/files/attached-knowledge-collection.ts @@ -0,0 +1,23 @@ +import { Expose } from 'class-transformer'; +import { FileType } from '../../enums'; + +export class AttachedKnowledgeCollection { + @Expose() + public id: string; + + @Expose() + public type: FileType.COLLECTION; + + @Expose() + public name: string; + + @Expose() + public description: string; + + @Expose() + public status: string; + + constructor(attachedKnowledgeCollection: Partial) { + Object.assign(this, attachedKnowledgeCollection); + } +} diff --git a/libs/shared/data-access/common/src/models/files/attached-list-item.ts b/libs/shared/data-access/common/src/models/files/attached-list-item.ts new file mode 100644 index 00000000..b8de8ff2 --- /dev/null +++ b/libs/shared/data-access/common/src/models/files/attached-list-item.ts @@ -0,0 +1,13 @@ +import { AttachedKnowledgeCollection } from './attached-knowledge-collection'; +import { FileData } from './file-data'; + +// NOTE: a single message can attach a mix of freshly-uploaded files, files picked from a +// knowledge base, and whole knowledge-base collections — this discriminated union is the one +// list all three live in, client-side, before being split apart into the outgoing wire shape. +export type AttachedListItem = + | { kind: 'file'; file: FileData; isFromKnowledge: boolean } + | { kind: 'collection'; collection: AttachedKnowledgeCollection }; + +export function getAttachedListItemId(item: AttachedListItem): string { + return item.kind === 'file' ? item.file.id : item.collection.id; +} diff --git a/libs/shared/data-access/common/src/models/files/index.ts b/libs/shared/data-access/common/src/models/files/index.ts index d2375178..d7fd1858 100644 --- a/libs/shared/data-access/common/src/models/files/index.ts +++ b/libs/shared/data-access/common/src/models/files/index.ts @@ -1,4 +1,6 @@ export * from './attached-file'; +export * from './attached-knowledge-collection'; +export * from './attached-list-item'; export * from './file-data'; export * from './file-meta'; export * from './attached-image'; diff --git a/tsconfig.base.json b/tsconfig.base.json index 24470c31..815f2c8c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -53,11 +53,14 @@ "@open-webui-react-native/mobile/chat/features/archived-chats-list": [ "libs/mobile/chat/features/archived-chats-list/src/index.ts" ], + "@open-webui-react-native/mobile/chat/features/attached-chat-items": [ + "libs/mobile/chat/features/attached-chat-items/src/index.ts" + ], "@open-webui-react-native/mobile/chat/features/attached-file-item": [ "libs/mobile/chat/features/attached-file-item/src/index.ts" ], - "@open-webui-react-native/mobile/chat/features/attached-files-list": [ - "libs/mobile/chat/features/attached-files-list/src/index.ts" + "@open-webui-react-native/mobile/chat/features/attach-knowledge-sheet": [ + "libs/mobile/chat/features/attach-knowledge-sheet/src/index.ts" ], "@open-webui-react-native/mobile/chat/features/attached-image-item": [ "libs/mobile/chat/features/attached-image-item/src/index.ts" From 6fd6b298495fef1dd1cbf7e57fc3eafa36d6cc4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D0=B8=D0=B9=20=D0=95=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D0=B5=D0=B5=D0=B2?= Date: Tue, 15 Sep 2026 15:49:30 +0600 Subject: [PATCH 2/7] feat: add attached knowledges to message item --- .../src/lib/component.tsx | 12 ++++- .../lib/components/user-message/component.tsx | 46 +++++++++++++------ .../component.tsx | 13 ++++++ .../data-access/api/src/lib/knowledge/api.ts | 28 ++++++----- .../api/src/lib/knowledge/config.ts | 4 +- 5 files changed, 74 insertions(+), 29 deletions(-) diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx index f1f2a009..d9026e69 100644 --- a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx @@ -36,7 +36,13 @@ export function AttachKnowledgeSheet({ const { query, setQuery } = useDebouncedQuery(); const { data: knowledgeList, isLoading: isKnowledgeLoading } = knowledgeApi.useGetKnowledge(); - const { data: knowledgeFiles, isLoading: isFilesLoading } = knowledgeApi.useGetKnowledgeFiles(openedKnowledge?.id, 1); + const { + data: knowledgeFiles, + isLoading: isFilesLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = knowledgeApi.useGetKnowledgeFiles(openedKnowledge?.id); const closeModal = (): void => { sheetRef.current?.close(); @@ -75,7 +81,7 @@ export function AttachKnowledgeSheet({ }; if (openedKnowledge) { - const files = (knowledgeFiles?.items ?? []).filter((file) => new RegExp(query, 'i').test(file.meta.name)); + const files = (knowledgeFiles ?? []).filter((file) => new RegExp(query, 'i').test(file.meta.name)); return ( item.id} + onEndReached={() => hasNextPage && fetchNextPage()} + isFetchingNextPage={isFetchingNextPage} renderItem={({ item }) => ( (files?.filter((file) => file.type === FileType.FILE) as Array) ?? [], - [files], - ); - const attachedImages = useMemo( () => (files ?? []).filter((file) => file.type === FileType.IMAGE).map((file, index) => ({ ...file, index })), [files], @@ -82,14 +79,33 @@ function ChatUserMessageComponent({ {formatDateTime(timestamp, 'chat-relative-time')} - {attachedFiles.map((file, index) => ( - - ))} + {(files ?? []).flatMap((file, index) => { + if (file.type === FileType.FILE) { + return [ + , + ]; + } + + if (file.type === FileType.COLLECTION) { + return [ + , + ]; + } + + return []; + })} = Partial; renderItem: AppFlashListProps['renderItem']; keyExtractor?: AppFlashListProps['keyExtractor']; + onEndReached?: () => void; + isFetchingNextPage?: boolean; }; export function SearchableListBottomSheet({ @@ -47,6 +49,8 @@ export function SearchableListBottomSheet({ data, renderItem, keyExtractor, + onEndReached, + isFetchingNextPage, ...restProps }: SearchableListBottomSheetProps): ReactElement { const handleCancelSearch = (): void => { @@ -92,7 +96,16 @@ export function SearchableListBottomSheet({ renderItem={renderItem} keyExtractor={keyExtractor} className='pb-16' + onEndReached={onEndReached} + onEndReachedThreshold={0.5} ListEmptyComponent={} + ListFooterComponent={ + isFetchingNextPage ? ( + + + + ) : null + } /> diff --git a/libs/shared/data-access/api/src/lib/knowledge/api.ts b/libs/shared/data-access/api/src/lib/knowledge/api.ts index 3ae7a58a..99b4a569 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/api.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/api.ts @@ -1,8 +1,15 @@ -import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import { + useInfiniteQuery, + UseInfiniteQueryResult, + useQuery, + UseQueryOptions, + UseQueryResult, +} from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { ApiErrorData } from '@open-webui-react-native/shared/data-access/api-client'; +import { FileData, getNextPageParam } from '@open-webui-react-native/shared/data-access/common'; import { knowledgeApiConfig } from './config'; -import { Knowledge, KnowledgeFileListResponse } from './models'; +import { Knowledge } from './models'; import { knowledgeService } from './service'; function useGetKnowledge( @@ -15,16 +22,15 @@ function useGetKnowledge( }); } -function useGetKnowledgeFiles( - id?: string, - page = 1, - props?: UseQueryOptions>, -): UseQueryResult> { - return useQuery>({ - queryFn: () => knowledgeService.getKnowledgeFiles(id!, page), - queryKey: knowledgeApiConfig.getKnowledgeFilesQueryKey(id!, page), +function useGetKnowledgeFiles(id?: string): UseInfiniteQueryResult, AxiosError> { + return useInfiniteQuery({ + queryFn: ({ pageParam }) => knowledgeService.getKnowledgeFiles(id!, pageParam).then((response) => response.items), + queryKey: knowledgeApiConfig.getKnowledgeFilesQueryKey(id!), + initialPageParam: 1, + getNextPageParam: (lastPage, result, lastPageParam) => + getNextPageParam({ lastPage, result, lastPageParam, itemsPerPage: knowledgeApiConfig.filesPerPage }), + select: (data) => data.pages.flat(), enabled: !!id, - ...props, }); } diff --git a/libs/shared/data-access/api/src/lib/knowledge/config.ts b/libs/shared/data-access/api/src/lib/knowledge/config.ts index 3eacebbe..2060c5b0 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/config.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/config.ts @@ -1,5 +1,7 @@ export const knowledgeApiConfig = { route: 'v1/knowledge', getKnowledgeQueryKey: ['knowledge', 'get'], - getKnowledgeFilesQueryKey: (id: string, page: number): Array => ['knowledge', 'get-files', id, `${page}`], + getKnowledgeFilesQueryKey: (id: string): Array => ['knowledge', 'get-files', id], + // NOTE: matches the backend's default page size (PAGE_ITEM_COUNT) for GET /knowledge/{id}/files + filesPerPage: 30, }; From 4ab4213a08f3a33480218c9a6139076b1823cb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D0=B8=D0=B9=20=D0=95=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D0=B5=D0=B5=D0=B2?= Date: Tue, 15 Sep 2026 16:22:29 +0600 Subject: [PATCH 3/7] refactor: refactor attached items list --- .../attached-chat-items/src/lib/component.tsx | 52 ++++++++----------- .../lib/components/user-message/component.tsx | 16 +++--- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx b/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx index cc4b0a35..d7102aa7 100644 --- a/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx +++ b/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx @@ -32,45 +32,39 @@ export function AttachedChatItems({ return ( - {items.flatMap((item) => { + {items.map((item) => { if (!item) { - return []; + return null; } - if (item.kind === 'collection') { - return [ - onDeleteItemPress(item.collection.id)} - />, - ]; - } - - return [ + return item.kind === 'collection' ? ( + onDeleteItemPress(item.collection.id)} + /> + ) : ( , - ]; + /> + ); })} - {images.flatMap((image, index) => - image - ? [ - onImagePress(index)} - onDeleteImagePress={onDeleteImagePress} - image={image} - />, - ] - : [], + {images.map((image, index) => + image ? ( + onImagePress(index)} + onDeleteImagePress={onDeleteImagePress} + image={image} + /> + ) : null, )} 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 00102cb2..0b65ef5b 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 @@ -79,20 +79,20 @@ function ChatUserMessageComponent({ {formatDateTime(timestamp, 'chat-relative-time')} - {(files ?? []).flatMap((file, index) => { + {(files ?? []).map((file, index) => { if (file.type === FileType.FILE) { - return [ + return ( , - ]; + /> + ); } if (file.type === FileType.COLLECTION) { - return [ + return ( , - ]; + /> + ); } - return []; + return null; })} Date: Tue, 15 Sep 2026 16:46:29 +0600 Subject: [PATCH 4/7] refactor: refactor search list sheet --- .../src/lib/component.tsx | 82 +++++++++++-------- .../src/lib/component.tsx | 55 +++++++------ .../component.tsx | 79 +++++++++--------- 3 files changed, 115 insertions(+), 101 deletions(-) diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx index d9026e69..98ff3578 100644 --- a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx @@ -4,7 +4,6 @@ import { ForwardedRef, ReactElement, useImperativeHandle, useRef, useState } fro import { SearchableListBottomSheet } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { Knowledge, knowledgeApi } from '@open-webui-react-native/shared/data-access/api'; import { FileData } from '@open-webui-react-native/shared/data-access/common'; -import { useDebouncedQuery } from '@open-webui-react-native/shared/utils/use-debounced-query'; import { KnowledgeBaseRow, KnowledgeFileRow } from './components'; export type AttachKnowledgeSheetMethods = { @@ -21,6 +20,13 @@ export interface AttachKnowledgeSheetProps { onSelectFile: (knowledge: Knowledge, file: FileData) => void; } +const matchesKnowledgeQuery = (knowledge: Knowledge, query: string): boolean => + new RegExp(query, 'i').test(knowledge.name); + +const matchesFileQuery = (file: FileData, query: string): boolean => new RegExp(query, 'i').test(file.meta.name); + +const extractId = (item: { id: string }): string => item.id; + export function AttachKnowledgeSheet({ ref, isCollectionAttached, @@ -33,8 +39,6 @@ export function AttachKnowledgeSheet({ const [openedKnowledge, setOpenedKnowledge] = useState(null); - const { query, setQuery } = useDebouncedQuery(); - const { data: knowledgeList, isLoading: isKnowledgeLoading } = knowledgeApi.useGetKnowledge(); const { data: knowledgeFiles, @@ -47,7 +51,6 @@ export function AttachKnowledgeSheet({ const closeModal = (): void => { sheetRef.current?.close(); setOpenedKnowledge(null); - setQuery(''); }; const openModal = (): void => sheetRef.current?.present(); @@ -57,7 +60,6 @@ export function AttachKnowledgeSheet({ const handleGoBack = (): void => { if (openedKnowledge) { setOpenedKnowledge(null); - setQuery(''); return; } @@ -65,10 +67,7 @@ export function AttachKnowledgeSheet({ closeModal(); }; - const handleShowFilesPress = (knowledge: Knowledge): void => { - setQuery(''); - setOpenedKnowledge(knowledge); - }; + const handleShowFiles = (knowledge: Knowledge): void => setOpenedKnowledge(knowledge); const handleSelectCollection = (knowledge: Knowledge): void => { onSelectCollection(knowledge); @@ -80,56 +79,67 @@ export function AttachKnowledgeSheet({ closeModal(); }; + const handleFetchNextPage = (): void => { + if (hasNextPage) { + fetchNextPage(); + } + }; + if (openedKnowledge) { - const files = (knowledgeFiles ?? []).filter((file) => new RegExp(query, 'i').test(file.meta.name)); + const renderFileItem = ({ item }: { item: FileData }): ReactElement => { + const handlePress = (): void => handleSelectFile(openedKnowledge, item); + + return ; + }; return ( item.id} - onEndReached={() => hasNextPage && fetchNextPage()} - isFetchingNextPage={isFetchingNextPage} - renderItem={({ item }) => ( - handleSelectFile(openedKnowledge, item)} - /> - )} + data={knowledgeFiles ?? []} + searchPredicate={matchesFileQuery} + keyExtractor={extractId} + pagination={{ onEndReached: handleFetchNextPage, isFetchingNextPage }} + renderItem={renderFileItem} /> ); } - const filteredKnowledge = (knowledgeList ?? []).filter((item) => new RegExp(query, 'i').test(item.name)); + const renderKnowledgeItem = ({ item }: { item: Knowledge }): ReactElement => { + const handlePress = (): void => handleSelectCollection(item); + const handleShowFilesPress = (): void => handleShowFiles(item); + + return ( + + ); + }; return ( item.id} - renderItem={({ item }) => ( - handleSelectCollection(item)} - onShowFilesPress={() => handleShowFilesPress(item)} - /> - )} + data={knowledgeList ?? []} + searchPredicate={matchesKnowledgeQuery} + keyExtractor={extractId} + renderItem={renderKnowledgeItem} /> ); } diff --git a/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx b/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx index 4675ce37..7eee29a3 100644 --- a/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx +++ b/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx @@ -3,7 +3,6 @@ import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; import { ForwardedRef, ReactElement, useImperativeHandle, useRef, useState } from 'react'; import { SearchableListBottomSheet } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { Knowledge, knowledgeApi } from '@open-webui-react-native/shared/data-access/api'; -import { useDebouncedQuery } from '@open-webui-react-native/shared/utils/use-debounced-query'; import { KnowledgeRow } from './components'; export type SelectKnowledgeSheetMethods = { @@ -17,18 +16,19 @@ export type SelectKnowledgeSheetProps = { ref?: SelectKnowledgeSheetRef; }; +const matchesKnowledgeQuery = (knowledge: Knowledge, query: string): boolean => + new RegExp(query, 'i').test(knowledge.name); + +const extractKnowledgeId = (knowledge: Knowledge): string => knowledge.id; + export function SelectKnowledgeSheet({ onConfirm, ref }: SelectKnowledgeSheetProps): ReactElement { const translate = useTranslation('FOLDER.SELECT_KNOWLEDGE_SHEET'); const sheetRef = useRef(null); const [selectedKnowledge, setSelectedKnowledge] = useState>([]); - const { query, setQuery } = useDebouncedQuery(); - const { data: knowledge, isLoading } = knowledgeApi.useGetKnowledge(); - const filteredData = (knowledge ?? []).filter((item) => new RegExp(query, 'i').test(item.name)); - const closeModal = (): void => sheetRef.current?.close(); const openModal = (): void => sheetRef.current?.present(); @@ -38,27 +38,29 @@ export function SelectKnowledgeSheet({ onConfirm, ref }: SelectKnowledgeSheetPro closeModal(); }; - useImperativeHandle(ref, () => { - return { - present: (selectedKnowledge: Array) => { - setSelectedKnowledge(selectedKnowledge); - openModal(); - }, - }; - }, []); + const handlePresent = (initialSelectedKnowledge: Array): void => { + setSelectedKnowledge(initialSelectedKnowledge); + openModal(); + }; + + useImperativeHandle(ref, () => ({ present: handlePresent }), []); + + const toggleKnowledgeSelection = (item: Knowledge): void => { + setSelectedKnowledge((prev) => + prev.some((knowledge) => knowledge.id === item.id) + ? prev.filter((knowledge) => knowledge.id !== item.id) + : [...prev, item], + ); + }; const renderItem = ({ item }: { item: Knowledge }): ReactElement => { const isSelected = selectedKnowledge.some((knowledge) => knowledge.id === item.id); + const handlePress = (): void => toggleKnowledgeSelection(item); - return ( - - setSelectedKnowledge((prev) => (isSelected ? [...prev.filter((i) => i.id !== item.id)] : [...prev, item])) - } - isSelected={isSelected} - /> - ); + return ; }; return ( @@ -66,15 +68,14 @@ export function SelectKnowledgeSheet({ onConfirm, ref }: SelectKnowledgeSheetPro ref={sheetRef} title={translate('TEXT_SELECT_KNOWLEDGE')} onGoBack={closeModal} - onConfirmPress={handleConfirm} - searchQuery={query} - onSearchQueryChange={setQuery} + headerProps={{ onConfirmPress: handleConfirm }} searchPlaceholder={translate('TEXT_SEARCH_KNOWLEDGE')} isLoading={isLoading} emptyDescription={translate('TEXT_NO_KNOWLEDGE')} - data={filteredData} + data={knowledge ?? []} + searchPredicate={matchesKnowledgeQuery} renderItem={renderItem} - keyExtractor={(item) => item.id} + keyExtractor={extractKnowledgeId} /> ); } diff --git a/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx index 18a613f6..d8d0de9d 100644 --- a/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx +++ b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx @@ -1,6 +1,7 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet'; -import { ReactElement } from 'react'; +import { ReactElement, useEffect } from 'react'; import { Keyboard } from 'react-native'; +import { useDebouncedQuery } from '@open-webui-react-native/shared/utils/use-debounced-query'; import { AppBottomSheet, AppBottomSheetPropsType } from '../bottom-sheet'; import { AppFlashList, AppFlashListProps } from '../flash-list'; import { AppBottomSheetKeyboardAwareScrollView } from '../keyboard-aware-scroll-view'; @@ -13,51 +14,57 @@ import { View } from '../view'; export type SearchableListBottomSheetRef = React.RefObject; -export type SearchableListBottomSheetProps = Partial> & { - ref?: SearchableListBottomSheetRef; - title: SheetHeaderProps['title']; - accessoryLeft?: SheetHeaderProps['accessoryLeft']; - accessoryRight?: SheetHeaderProps['accessoryRight']; - onGoBack: () => void; - onConfirmPress?: SheetHeaderProps['onConfirmPress']; - confirmButtonProps?: SheetHeaderProps['confirmButtonProps']; - searchQuery: string; - onSearchQueryChange: (query: string) => void; - searchPlaceholder?: string; - isLoading?: boolean; - emptyDescription: string; - data: Array; - renderItem: AppFlashListProps['renderItem']; - keyExtractor?: AppFlashListProps['keyExtractor']; - onEndReached?: () => void; +export interface SearchableListBottomSheetPagination { + onEndReached: () => void; isFetchingNextPage?: boolean; -}; +} + +export type SearchableListBottomSheetProps = Partial> & + Pick, 'data' | 'renderItem' | 'keyExtractor'> & { + ref?: SearchableListBottomSheetRef; + title: SheetHeaderProps['title']; + onGoBack: () => void; + headerProps?: Pick; + searchPlaceholder?: string; + isLoading?: boolean; + emptyDescription: string; + searchPredicate: (item: TItem, query: string) => boolean; + pagination?: SearchableListBottomSheetPagination; + // NOTE: clears the search field whenever this value changes, without remounting the sheet + // itself (a `key` would also tear down and recreate the underlying BottomSheetModal, closing it). + searchResetKey?: string | number; + }; export function SearchableListBottomSheet({ ref, title, - accessoryLeft, - accessoryRight, onGoBack, - onConfirmPress, - confirmButtonProps, - searchQuery, - onSearchQueryChange, + headerProps, searchPlaceholder, isLoading, emptyDescription, data, + searchPredicate, renderItem, keyExtractor, - onEndReached, - isFetchingNextPage, + pagination, + searchResetKey, ...restProps }: SearchableListBottomSheetProps): ReactElement { + const { query, setQuery } = useDebouncedQuery(); + + useEffect(() => { + setQuery(''); + + }, [searchResetKey]); + const handleCancelSearch = (): void => { - onSearchQueryChange(''); + setQuery(''); Keyboard.dismiss(); }; + const filteredData = (data ?? []).filter((item) => searchPredicate(item, query)); + return ( ({ + {...headerProps} /> ({ } ListFooterComponent={ - isFetchingNextPage ? ( + pagination?.isFetchingNextPage ? ( From 213839e912fe81786f90411a0e6e0fad326d1ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D0=B8=D0=B9=20=D0=95=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D0=B5=D0=B5=D0=B2?= Date: Tue, 15 Sep 2026 18:31:11 +0600 Subject: [PATCH 5/7] fix: fixes and adjustments --- .../src/lib/component.tsx | 58 +++++++++------ .../src/lib/component.tsx | 25 +++++-- .../component.tsx | 71 ++++++++----------- .../api/src/lib/chats/models/chat.ts | 19 ++++- .../lib/chats/models/complete-chat-request.ts | 19 ++++- .../lib/chats/utils/get-completion-files.ts | 18 +++++ .../lib/chats/utils/handle-completed-chat.ts | 11 ++- .../api/src/lib/chats/utils/index.ts | 1 + .../utils/prepare-complete-chat-payload.ts | 12 ++-- .../data-access/api/src/lib/knowledge/api.ts | 50 +++++++------ .../api/src/lib/knowledge/config.ts | 9 +-- .../api/src/lib/knowledge/service.ts | 18 +++-- 12 files changed, 189 insertions(+), 122 deletions(-) create mode 100644 libs/shared/data-access/api/src/lib/chats/utils/get-completion-files.ts diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx index 98ff3578..edb2945c 100644 --- a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx @@ -4,6 +4,7 @@ import { ForwardedRef, ReactElement, useImperativeHandle, useRef, useState } fro import { SearchableListBottomSheet } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { Knowledge, knowledgeApi } from '@open-webui-react-native/shared/data-access/api'; import { FileData } from '@open-webui-react-native/shared/data-access/common'; +import { useDebouncedQuery } from '@open-webui-react-native/shared/utils/use-debounced-query'; import { KnowledgeBaseRow, KnowledgeFileRow } from './components'; export type AttachKnowledgeSheetMethods = { @@ -20,11 +21,6 @@ export interface AttachKnowledgeSheetProps { onSelectFile: (knowledge: Knowledge, file: FileData) => void; } -const matchesKnowledgeQuery = (knowledge: Knowledge, query: string): boolean => - new RegExp(query, 'i').test(knowledge.name); - -const matchesFileQuery = (file: FileData, query: string): boolean => new RegExp(query, 'i').test(file.meta.name); - const extractId = (item: { id: string }): string => item.id; export function AttachKnowledgeSheet({ @@ -36,30 +32,42 @@ export function AttachKnowledgeSheet({ }: AttachKnowledgeSheetProps): ReactElement { const translate = useTranslation('CHAT.ATTACH_KNOWLEDGE_SHEET'); const sheetRef = useRef(null); + const { query, setQuery, debouncedQuery } = useDebouncedQuery({ delay: 300 }); const [openedKnowledge, setOpenedKnowledge] = useState(null); - const { data: knowledgeList, isLoading: isKnowledgeLoading } = knowledgeApi.useGetKnowledge(); + const { + data: knowledgeList, + isLoading: isKnowledgeLoading, + fetchNextPage: fetchNextKnowledgePage, + hasNextPage: hasNextKnowledgePage, + isFetchingNextPage: isFetchingNextKnowledgePage, + } = knowledgeApi.useSearchKnowledge(debouncedQuery); const { data: knowledgeFiles, isLoading: isFilesLoading, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = knowledgeApi.useGetKnowledgeFiles(openedKnowledge?.id); + fetchNextPage: fetchNextFilesPage, + hasNextPage: hasNextFilesPage, + isFetchingNextPage: isFetchingNextFilesPage, + } = knowledgeApi.useGetKnowledgeFiles(openedKnowledge?.id, debouncedQuery); const closeModal = (): void => { sheetRef.current?.close(); setOpenedKnowledge(null); + setQuery(''); }; - const openModal = (): void => sheetRef.current?.present(); + const openModal = (): void => { + setQuery(''); + sheetRef.current?.present(); + }; useImperativeHandle(ref, () => ({ present: openModal }), []); const handleGoBack = (): void => { if (openedKnowledge) { setOpenedKnowledge(null); + setQuery(''); return; } @@ -67,7 +75,10 @@ export function AttachKnowledgeSheet({ closeModal(); }; - const handleShowFiles = (knowledge: Knowledge): void => setOpenedKnowledge(knowledge); + const handleShowFiles = (knowledge: Knowledge): void => { + setQuery(''); + setOpenedKnowledge(knowledge); + }; const handleSelectCollection = (knowledge: Knowledge): void => { onSelectCollection(knowledge); @@ -79,9 +90,15 @@ export function AttachKnowledgeSheet({ closeModal(); }; - const handleFetchNextPage = (): void => { - if (hasNextPage) { - fetchNextPage(); + const handleFetchNextKnowledgePage = (): void => { + if (hasNextKnowledgePage) { + fetchNextKnowledgePage(); + } + }; + + const handleFetchNextFilesPage = (): void => { + if (hasNextFilesPage) { + fetchNextFilesPage(); } }; @@ -100,14 +117,14 @@ export function AttachKnowledgeSheet({ ref={sheetRef} title={openedKnowledge.name} onGoBack={handleGoBack} + query={query} + onQueryChange={setQuery} searchPlaceholder={translate('TEXT_SEARCH_FILES')} - searchResetKey={openedKnowledge.id} isLoading={isFilesLoading} emptyDescription={translate('TEXT_NO_FILES')} data={knowledgeFiles ?? []} - searchPredicate={matchesFileQuery} keyExtractor={extractId} - pagination={{ onEndReached: handleFetchNextPage, isFetchingNextPage }} + pagination={{ onEndReached: handleFetchNextFilesPage, isFetchingNextPage: isFetchingNextFilesPage }} renderItem={renderFileItem} /> ); @@ -132,13 +149,14 @@ export function AttachKnowledgeSheet({ ref={sheetRef} title={translate('TEXT_TITLE')} onGoBack={closeModal} + query={query} + onQueryChange={setQuery} searchPlaceholder={translate('TEXT_SEARCH_KNOWLEDGE')} - searchResetKey='root' isLoading={isKnowledgeLoading} emptyDescription={translate('TEXT_NO_KNOWLEDGE')} data={knowledgeList ?? []} - searchPredicate={matchesKnowledgeQuery} keyExtractor={extractId} + pagination={{ onEndReached: handleFetchNextKnowledgePage, isFetchingNextPage: isFetchingNextKnowledgePage }} renderItem={renderKnowledgeItem} /> ); diff --git a/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx b/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx index 7eee29a3..26319fb2 100644 --- a/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx +++ b/libs/mobile/folder/features/select-knowledge-sheet/src/lib/component.tsx @@ -3,6 +3,7 @@ import { useTranslation } from '@ronas-it/react-native-common-modules/i18n'; import { ForwardedRef, ReactElement, useImperativeHandle, useRef, useState } from 'react'; import { SearchableListBottomSheet } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { Knowledge, knowledgeApi } from '@open-webui-react-native/shared/data-access/api'; +import { useDebouncedQuery } from '@open-webui-react-native/shared/utils/use-debounced-query'; import { KnowledgeRow } from './components'; export type SelectKnowledgeSheetMethods = { @@ -16,18 +17,22 @@ export type SelectKnowledgeSheetProps = { ref?: SelectKnowledgeSheetRef; }; -const matchesKnowledgeQuery = (knowledge: Knowledge, query: string): boolean => - new RegExp(query, 'i').test(knowledge.name); - const extractKnowledgeId = (knowledge: Knowledge): string => knowledge.id; export function SelectKnowledgeSheet({ onConfirm, ref }: SelectKnowledgeSheetProps): ReactElement { const translate = useTranslation('FOLDER.SELECT_KNOWLEDGE_SHEET'); const sheetRef = useRef(null); + const { query, setQuery, debouncedQuery } = useDebouncedQuery({ delay: 300 }); const [selectedKnowledge, setSelectedKnowledge] = useState>([]); - const { data: knowledge, isLoading } = knowledgeApi.useGetKnowledge(); + const { + data: knowledge, + isLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = knowledgeApi.useSearchKnowledge(debouncedQuery); const closeModal = (): void => sheetRef.current?.close(); @@ -40,9 +45,16 @@ export function SelectKnowledgeSheet({ onConfirm, ref }: SelectKnowledgeSheetPro const handlePresent = (initialSelectedKnowledge: Array): void => { setSelectedKnowledge(initialSelectedKnowledge); + setQuery(''); openModal(); }; + const handleFetchNextPage = (): void => { + if (hasNextPage) { + fetchNextPage(); + } + }; + useImperativeHandle(ref, () => ({ present: handlePresent }), []); const toggleKnowledgeSelection = (item: Knowledge): void => { @@ -69,13 +81,16 @@ export function SelectKnowledgeSheet({ onConfirm, ref }: SelectKnowledgeSheetPro title={translate('TEXT_SELECT_KNOWLEDGE')} onGoBack={closeModal} headerProps={{ onConfirmPress: handleConfirm }} + query={query} + onQueryChange={setQuery} searchPlaceholder={translate('TEXT_SEARCH_KNOWLEDGE')} isLoading={isLoading} emptyDescription={translate('TEXT_NO_KNOWLEDGE')} data={knowledge ?? []} - searchPredicate={matchesKnowledgeQuery} + extraData={selectedKnowledge} renderItem={renderItem} keyExtractor={extractKnowledgeId} + pagination={{ onEndReached: handleFetchNextPage, isFetchingNextPage }} /> ); } diff --git a/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx index d8d0de9d..f3611678 100644 --- a/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx +++ b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx @@ -1,12 +1,10 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet'; -import { ReactElement, useEffect } from 'react'; +import { ReactElement } from 'react'; import { Keyboard } from 'react-native'; -import { useDebouncedQuery } from '@open-webui-react-native/shared/utils/use-debounced-query'; import { AppBottomSheet, AppBottomSheetPropsType } from '../bottom-sheet'; -import { AppFlashList, AppFlashListProps } from '../flash-list'; -import { AppBottomSheetKeyboardAwareScrollView } from '../keyboard-aware-scroll-view'; +import { AppBottomSheetFlashList } from '../bottom-sheet-flat-list'; +import { AppFlashListProps } from '../flash-list'; import { ListEmptyComponent } from '../list-empty-component'; -import { AppSafeAreaView } from '../safe-area-view'; import { SearchInput } from '../search-input'; import { SheetHeader, SheetHeaderProps } from '../sheet-header'; import { AppSpinner } from '../spinner'; @@ -20,19 +18,17 @@ export interface SearchableListBottomSheetPagination { } export type SearchableListBottomSheetProps = Partial> & - Pick, 'data' | 'renderItem' | 'keyExtractor'> & { + Pick, 'data' | 'renderItem' | 'keyExtractor' | 'extraData'> & { ref?: SearchableListBottomSheetRef; title: SheetHeaderProps['title']; onGoBack: () => void; headerProps?: Pick; + query: string; + onQueryChange: (query: string) => void; searchPlaceholder?: string; isLoading?: boolean; emptyDescription: string; - searchPredicate: (item: TItem, query: string) => boolean; pagination?: SearchableListBottomSheetPagination; - // NOTE: clears the search field whenever this value changes, without remounting the sheet - // itself (a `key` would also tear down and recreate the underlying BottomSheetModal, closing it). - searchResetKey?: string | number; }; export function SearchableListBottomSheet({ @@ -40,31 +36,23 @@ export function SearchableListBottomSheet({ title, onGoBack, headerProps, + query, + onQueryChange, searchPlaceholder, isLoading, emptyDescription, data, - searchPredicate, renderItem, keyExtractor, + extraData, pagination, - searchResetKey, ...restProps }: SearchableListBottomSheetProps): ReactElement { - const { query, setQuery } = useDebouncedQuery(); - - useEffect(() => { - setQuery(''); - - }, [searchResetKey]); - const handleCancelSearch = (): void => { - setQuery(''); + onQueryChange(''); Keyboard.dismiss(); }; - const filteredData = (data ?? []).filter((item) => searchPredicate(item, query)); - return ( ({ {...headerProps} /> ({ ) : ( - - - } - ListFooterComponent={ - pagination?.isFetchingNextPage ? ( - - - - ) : null - } - /> - - + } + ListFooterComponent={ + pagination?.isFetchingNextPage ? ( + + + + ) : null + } + /> )} } diff --git a/libs/shared/data-access/api/src/lib/chats/models/chat.ts b/libs/shared/data-access/api/src/lib/chats/models/chat.ts index d350a397..ee923b73 100644 --- a/libs/shared/data-access/api/src/lib/chats/models/chat.ts +++ b/libs/shared/data-access/api/src/lib/chats/models/chat.ts @@ -1,6 +1,10 @@ import { BaseEntity } from '@ronas-it/rtkq-entity-api'; import { Expose, Type } from 'class-transformer'; -import { AttachedFile } from '@open-webui-react-native/shared/data-access/common'; +import { + AttachedFile, + AttachedKnowledgeCollection, + FileType, +} from '@open-webui-react-native/shared/data-access/common'; import { History } from './history'; import { Message } from './message'; @@ -26,8 +30,17 @@ export class Chat extends BaseEntity { public params?: Record; @Expose() - @Type(() => AttachedFile) - public files?: Array; + @Type(() => AttachedFile, { + discriminator: { + property: 'type', + subTypes: [ + { value: AttachedFile, name: FileType.FILE }, + { value: AttachedKnowledgeCollection, name: FileType.COLLECTION }, + ], + }, + keepDiscriminatorProperty: true, + }) + public files?: Array; @Expose() public tags?: Array; diff --git a/libs/shared/data-access/api/src/lib/chats/models/complete-chat-request.ts b/libs/shared/data-access/api/src/lib/chats/models/complete-chat-request.ts index f3aef9b7..7cd2876e 100644 --- a/libs/shared/data-access/api/src/lib/chats/models/complete-chat-request.ts +++ b/libs/shared/data-access/api/src/lib/chats/models/complete-chat-request.ts @@ -1,5 +1,9 @@ import { Expose, Type } from 'class-transformer'; -import { AttachedFile } from '@open-webui-react-native/shared/data-access/common'; +import { + AttachedFile, + AttachedKnowledgeCollection, + FileType, +} from '@open-webui-react-native/shared/data-access/common'; import { BackgroundTasks } from './background-tasks'; import { ChatMessage } from './chat-message'; import { CompleteChatParams } from './complete-chat-params'; @@ -30,8 +34,17 @@ export class CompleteChatRequest { public params?: CompleteChatParams; @Expose() - @Type(() => AttachedFile) - public files?: Array; + @Type(() => AttachedFile, { + discriminator: { + property: 'type', + subTypes: [ + { value: AttachedFile, name: FileType.FILE }, + { value: AttachedKnowledgeCollection, name: FileType.COLLECTION }, + ], + }, + keepDiscriminatorProperty: true, + }) + public files?: Array; // NOTE: The only channel through which tools reach a completion. The backend resolves every kind // of tool from this array and never falls back to the model's or the user's defaults, so an diff --git a/libs/shared/data-access/api/src/lib/chats/utils/get-completion-files.ts b/libs/shared/data-access/api/src/lib/chats/utils/get-completion-files.ts new file mode 100644 index 00000000..fc9f6620 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/chats/utils/get-completion-files.ts @@ -0,0 +1,18 @@ +import { uniqBy } from 'lodash-es'; +import { + AttachedFile, + AttachedKnowledgeCollection, + FileType, +} from '@open-webui-react-native/shared/data-access/common'; +import { Message } from '../models'; + +export type CompletionFile = AttachedFile | AttachedKnowledgeCollection; + +export function getCompletionFiles(messages: Array): Array { + return uniqBy( + messages + .flatMap((message) => message.files ?? []) + .filter((file): file is CompletionFile => file.type === FileType.FILE || file.type === FileType.COLLECTION), + 'id', + ); +} 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 95d62d11..3f56f9b4 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 @@ -1,10 +1,11 @@ -import { merge, uniqBy } from 'lodash-es'; +import { merge } from 'lodash-es'; import { captureApiError } from '@open-webui-react-native/shared/data-access/api-client'; -import { AttachedFile, FileType, MessageSource } from '@open-webui-react-native/shared/data-access/common'; +import { MessageSource } from '@open-webui-react-native/shared/data-access/common'; import { queryClient } from '@open-webui-react-native/shared/data-access/query-client'; import { chatQueriesKeys } from '../chat-queries-keys'; import { Chat, ChatResponse, History, Message } from '../models'; import { chatService } from '../service'; +import { getCompletionFiles } from './get-completion-files'; import { prepareCompletedChatPayload } from './prepare-completed-chat-payload'; import { isTemporaryChatId } from './temporary-chat-id'; @@ -52,11 +53,7 @@ export const handleCompletedChat = async ( message, ); - // Only files should be included in `files` field - const files = uniqBy( - chat.messages.flatMap((msg) => msg.files ?? []).filter((file): file is AttachedFile => file.type === FileType.FILE), - 'id', - ); + const files = getCompletionFiles(chat.messages); const updateChatPayload = new Chat({ messages: updatedMessages, 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 5453d52c..1dda69c6 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 @@ -1,6 +1,7 @@ export * from './prepare-create-chat-payload'; export * from './prepare-complete-chat-payload'; export * from './prepare-completed-chat-payload'; +export * from './get-completion-files'; export * from './prepare-send-message-payload'; export * from './patch-chat-message-with-completion'; export * from './patch-completed-message'; 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 943e0107..426c322e 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 @@ -1,5 +1,4 @@ -import { uniqBy } from 'lodash-es'; -import { AttachedFile, FileType, Role } from '@open-webui-react-native/shared/data-access/common'; +import { FileType, Role } from '@open-webui-react-native/shared/data-access/common'; import { queryClient } from '@open-webui-react-native/shared/data-access/query-client'; import { modelsApiConfig } from '../../ai-models/config'; import { AIModel } from '../../ai-models/models'; @@ -22,6 +21,7 @@ import { Message, } from '../models'; import { toolApprovalState$, toolsSelectionState$ } from '../state'; +import { getCompletionFiles } from './get-completion-files'; import { resolveDefaultToolIds } from './resolve-default-tool-ids'; export interface PrepareCompleteChatPayloadArgs { @@ -141,11 +141,9 @@ export function prepareCompleteChatPayload({ return [...systemMessage, ...historyMessages]; }; - // Only files should be included in `files` field - const files = uniqBy( - messages.flatMap((msg) => msg.files ?? []).filter((file): file is AttachedFile => file.type === FileType.FILE), - 'id', - ); + // Images go in message content as image_url parts — RAG only wants uploaded files and + // knowledge collections (the backend looks up type:collection by id). + const files = getCompletionFiles(messages); const historyMessagesCount = Object.keys(chatResponse?.chat.history.messages ?? {}).length; const isFirstExchange = historyMessagesCount > 0 && historyMessagesCount <= 2; diff --git a/libs/shared/data-access/api/src/lib/knowledge/api.ts b/libs/shared/data-access/api/src/lib/knowledge/api.ts index 99b4a569..24beb3da 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/api.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/api.ts @@ -1,10 +1,4 @@ -import { - useInfiniteQuery, - UseInfiniteQueryResult, - useQuery, - UseQueryOptions, - UseQueryResult, -} from '@tanstack/react-query'; +import { useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { ApiErrorData } from '@open-webui-react-native/shared/data-access/api-client'; import { FileData, getNextPageParam } from '@open-webui-react-native/shared/data-access/common'; @@ -12,29 +6,45 @@ import { knowledgeApiConfig } from './config'; import { Knowledge } from './models'; import { knowledgeService } from './service'; -function useGetKnowledge( - props?: UseQueryOptions, AxiosError>, -): UseQueryResult, AxiosError> { - return useQuery, AxiosError>({ - queryFn: knowledgeService.getKnowledge, - queryKey: knowledgeApiConfig.getKnowledgeQueryKey, - ...props, +function useSearchKnowledge(query?: string): UseInfiniteQueryResult, AxiosError> { + const searchText = query?.trim() ?? ''; + + return useInfiniteQuery({ + queryFn: async ({ pageParam }) => { + const response = await knowledgeService.searchKnowledge(pageParam, searchText); + + return response.items ?? []; + }, + queryKey: knowledgeApiConfig.getSearchKnowledgeQueryKey(searchText), + initialPageParam: 1, + getNextPageParam: (lastPage, result, lastPageParam) => + getNextPageParam({ lastPage, result, lastPageParam, itemsPerPage: knowledgeApiConfig.pageSize }), + select: (data) => data.pages.flat(), }); } -function useGetKnowledgeFiles(id?: string): UseInfiniteQueryResult, AxiosError> { +function useGetKnowledgeFiles( + id?: string, + query?: string, +): UseInfiniteQueryResult, AxiosError> { + const searchText = query?.trim() ?? ''; + return useInfiniteQuery({ - queryFn: ({ pageParam }) => knowledgeService.getKnowledgeFiles(id!, pageParam).then((response) => response.items), - queryKey: knowledgeApiConfig.getKnowledgeFilesQueryKey(id!), + queryFn: async ({ pageParam }) => { + const response = await knowledgeService.getKnowledgeFiles(id!, pageParam, searchText); + + return response.items ?? []; + }, + queryKey: knowledgeApiConfig.getKnowledgeFilesQueryKey(id ?? '', searchText), initialPageParam: 1, getNextPageParam: (lastPage, result, lastPageParam) => - getNextPageParam({ lastPage, result, lastPageParam, itemsPerPage: knowledgeApiConfig.filesPerPage }), + getNextPageParam({ lastPage, result, lastPageParam, itemsPerPage: knowledgeApiConfig.pageSize }), select: (data) => data.pages.flat(), - enabled: !!id, + enabled: Boolean(id), }); } export const knowledgeApi = { - useGetKnowledge, + useSearchKnowledge, useGetKnowledgeFiles, }; diff --git a/libs/shared/data-access/api/src/lib/knowledge/config.ts b/libs/shared/data-access/api/src/lib/knowledge/config.ts index 2060c5b0..4e96db51 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/config.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/config.ts @@ -1,7 +1,8 @@ export const knowledgeApiConfig = { route: 'v1/knowledge', - getKnowledgeQueryKey: ['knowledge', 'get'], - getKnowledgeFilesQueryKey: (id: string): Array => ['knowledge', 'get-files', id], - // NOTE: matches the backend's default page size (PAGE_ITEM_COUNT) for GET /knowledge/{id}/files - filesPerPage: 30, + getSearchKnowledgeQueryKey: (query: string): Array => ['knowledge', 'search', query], + getKnowledgeFilesQueryKey: (id: string, query: string): Array => ['knowledge', 'get-files', id, query], + // NOTE: matches the backend's default page size (PAGE_ITEM_COUNT) on GET /knowledge/search and + // GET /knowledge/{id}/files + pageSize: 30, }; diff --git a/libs/shared/data-access/api/src/lib/knowledge/service.ts b/libs/shared/data-access/api/src/lib/knowledge/service.ts index ccc2c324..c80d7c67 100644 --- a/libs/shared/data-access/api/src/lib/knowledge/service.ts +++ b/libs/shared/data-access/api/src/lib/knowledge/service.ts @@ -13,23 +13,21 @@ class KnowledgeService extends EntityPromiseService { }); } - public async getKnowledge(): Promise> { - const response = await getApiService().get(`${knowledgeApiConfig.route}/`); + public async searchKnowledge(page: number, query?: string): Promise { + const response = await getApiService().get(`${knowledgeApiConfig.route}/search`, { + page, + ...(query ? { query } : {}), + }); - const transformed = plainToInstance(KnowledgeResponse, response, { + return plainToInstance(KnowledgeResponse, response, { excludeExtraneousValues: true, }); - - return transformed.items.map((item) => - plainToInstance(Knowledge, item, { - excludeExtraneousValues: true, - }), - ); } - public async getKnowledgeFiles(id: string, page: number): Promise { + public async getKnowledgeFiles(id: string, page: number, query?: string): Promise { const response = await getApiService().get(`${knowledgeApiConfig.route}/${id}/files`, { page, + ...(query ? { query } : {}), }); return plainToInstance(KnowledgeFileListResponse, response, { From 2080a3be65a22f0b03afd040bc59c72fcabd5f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D0=B8=D0=B9=20=D0=95=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D0=B5=D0=B5=D0=B2?= Date: Wed, 16 Sep 2026 21:19:30 +0600 Subject: [PATCH 6/7] fix: fix review issues --- i18n/mobile/chat/ro.json | 17 +++++++++++++---- .../src/lib/component.tsx | 9 +++++++-- .../attached-chat-items/src/lib/component.tsx | 4 ++-- .../lib/components/user-message/component.tsx | 4 ++-- .../form-chat-input/src/lib/component.tsx | 5 +++-- .../upsert-folder-sheet/src/lib/component.tsx | 2 +- .../src/use-attached-files.ts | 7 ++++--- .../searchable-list-bottom-sheet/component.tsx | 3 +++ .../src/lib/chats/utils/create-message-pair.ts | 6 +++--- .../src/models/files/attached-list-item.ts | 7 ++++--- 10 files changed, 42 insertions(+), 22 deletions(-) diff --git a/i18n/mobile/chat/ro.json b/i18n/mobile/chat/ro.json index d339cece..3904ee05 100644 --- a/i18n/mobile/chat/ro.json +++ b/i18n/mobile/chat/ro.json @@ -30,7 +30,8 @@ "TEXT_CAPTURE": "Fotografiază", "TEXT_CHOOSE_FROM_GALLERY": "Alege din galerie", "TEXT_UPLOAD_FILES": "Încarcă fișiere", - "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Nu poți încărca un fișier gol." + "TEXT_YOU_CANNOT_UPLOAD_EMPTY_FILE": "Nu poți încărca un fișier gol.", + "TEXT_ATTACH_KNOWLEDGE": "Atașează bază de cunoștințe" }, "TOOL_PERMISSIONS_POPUP": { "TEXT_FULL_ACCESS": "Acces complet", @@ -91,9 +92,6 @@ } }, "ATTACHED_FILE_ITEM": { - "TEXT_FILE": "Fișier", - "TEXT_DOCUMENT": "Document", - "TEXT_COLLECTION": "Colecție", "FILE_INFO_MODAL": { "TEXT_FORMATTING_DISCLAIMER": "Formatarea poate diferi față de sursa originală.", "TEXT_NO_PREVIEW_AVAILABLE": "Previzualizare indisponibilă", @@ -190,6 +188,17 @@ "BUTTON_SUBMIT": "Trimite", "BUTTON_DENY": "Omite" } + }, + "ATTACH_KNOWLEDGE_SHEET": { + "TEXT_TITLE": "Bază de cunoștințe", + "TEXT_SEARCH_KNOWLEDGE": "Caută în baza de cunoștințe", + "TEXT_NO_KNOWLEDGE": "Nu există nicio bază de cunoștințe.", + "TEXT_SEARCH_FILES": "Caută fișiere", + "TEXT_NO_FILES": "Nu există fișiere în această bază de cunoștințe." + }, + "ATTACHED_CHAT_ITEMS": { + "TEXT_COLLECTION": "Colecție", + "TEXT_FILE": "Fișier" } } } diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx index edb2945c..ee210d03 100644 --- a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx @@ -53,8 +53,6 @@ export function AttachKnowledgeSheet({ const closeModal = (): void => { sheetRef.current?.close(); - setOpenedKnowledge(null); - setQuery(''); }; const openModal = (): void => { @@ -64,6 +62,11 @@ export function AttachKnowledgeSheet({ useImperativeHandle(ref, () => ({ present: openModal }), []); + const handleDismiss = (): void => { + setOpenedKnowledge(null); + setQuery(''); + }; + const handleGoBack = (): void => { if (openedKnowledge) { setOpenedKnowledge(null); @@ -117,6 +120,7 @@ export function AttachKnowledgeSheet({ ref={sheetRef} title={openedKnowledge.name} onGoBack={handleGoBack} + onDismiss={handleDismiss} query={query} onQueryChange={setQuery} searchPlaceholder={translate('TEXT_SEARCH_FILES')} @@ -149,6 +153,7 @@ export function AttachKnowledgeSheet({ ref={sheetRef} title={translate('TEXT_TITLE')} onGoBack={closeModal} + onDismiss={handleDismiss} query={query} onQueryChange={setQuery} searchPlaceholder={translate('TEXT_SEARCH_KNOWLEDGE')} diff --git a/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx b/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx index d7102aa7..c8ed6e9e 100644 --- a/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx +++ b/libs/mobile/chat/features/attached-chat-items/src/lib/component.tsx @@ -5,7 +5,7 @@ import { ReactElement } from 'react'; import { AttachedFileItem, formatFileSize } from '@open-webui-react-native/mobile/chat/features/attached-file-item'; import { AttachedImageItem } from '@open-webui-react-native/mobile/chat/features/attached-image-item'; import { AttachedItem, View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; -import { AttachedListItem, ImageData } from '@open-webui-react-native/shared/data-access/common'; +import { AttachedListItem, FileType, ImageData } from '@open-webui-react-native/shared/data-access/common'; interface AttachedChatItemsProps { onDeleteItemPress: (id: string) => void; @@ -37,7 +37,7 @@ export function AttachedChatItems({ return null; } - return item.kind === 'collection' ? ( + return item.kind === FileType.COLLECTION ? ( 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 8585fc4f..43028607 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 @@ -30,6 +30,7 @@ import { AttachedKnowledgeCollection, AttachedListItem, FileData, + FileType, ImageData, } from '@open-webui-react-native/shared/data-access/common'; import { withOfflineGuard } from '@open-webui-react-native/shared/features/network'; @@ -134,10 +135,10 @@ export function FormChatInput({ const isInputEmpty = !field.value?.trim() && items.length === 0 && images.length === 0; const isKnowledgeCollectionAttached = (id: string): boolean => - items.some((item) => item?.kind === 'collection' && item.collection.id === id); + items.some((item) => item?.kind === FileType.COLLECTION && item.collection.id === id); const isKnowledgeFileAttached = (id: string): boolean => - items.some((item) => item?.kind === 'file' && item.isFromKnowledge && item.file.id === id); + items.some((item) => item?.kind === FileType.FILE && item.isFromKnowledge && item.file.id === id); const imagesForPreview = images.flatMap((image, index) => image diff --git a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx b/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx index f43e3752..ab6b91bc 100644 --- a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx +++ b/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx @@ -90,7 +90,7 @@ export function UpsertFolderSheet({ ref, ...props }: UpsertFolderSheetProps): Re } }, [folder]); - const files = useSelector(attachedItems).flatMap((item) => (item?.kind === 'file' ? [item.file] : [])); + const files = useSelector(attachedItems).flatMap((item) => (item?.kind === FileType.FILE ? [item.file] : [])); const closeModal = (): void => sheetRef.current?.close(); diff --git a/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts b/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts index 9a30e39a..384aeef9 100644 --- a/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts +++ b/libs/mobile/shared/features/use-attached-files/src/use-attached-files.ts @@ -6,6 +6,7 @@ import { AttachedKnowledgeCollection, AttachedListItem, FileData, + FileType, getAttachedListItemId, ImageData, } from '@open-webui-react-native/shared/data-access/common'; @@ -21,15 +22,15 @@ export function useAttachedFiles(): typeof result { }; const handleFileUploaded = (file: FileData): void => { - pushItem({ kind: 'file', file, isFromKnowledge: false }); + pushItem({ kind: FileType.FILE, file, isFromKnowledge: false }); }; const handleKnowledgeFileAttached = (file: FileData): void => { - pushItem({ kind: 'file', file, isFromKnowledge: true }); + pushItem({ kind: FileType.FILE, file, isFromKnowledge: true }); }; const handleKnowledgeCollectionAttached = (collection: AttachedKnowledgeCollection): void => { - pushItem({ kind: 'collection', collection }); + pushItem({ kind: FileType.COLLECTION, collection }); }; const handleDeleteItem = (id: string): void => { diff --git a/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx index f3611678..c6814adb 100644 --- a/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx +++ b/libs/mobile/shared/ui/ui-kit/src/searchable-list-bottom-sheet/component.tsx @@ -22,6 +22,7 @@ export type SearchableListBottomSheetProps = Partial void; + onDismiss?: () => void; headerProps?: Pick; query: string; onQueryChange: (query: string) => void; @@ -35,6 +36,7 @@ export function SearchableListBottomSheet({ ref, title, onGoBack, + onDismiss, headerProps, query, onQueryChange, @@ -62,6 +64,7 @@ export function SearchableListBottomSheet({ snapPoints={['100%']} stackBehavior='push' className='px-0' + onDismiss={onDismiss} content={ (item.kind === 'file' ? [item.file] : [])); + const attachedFiles = (attachedItems ?? []).flatMap((item) => (item.kind === FileType.FILE ? [item.file] : [])); const attachedCollections = (attachedItems ?? []).flatMap((item) => - item.kind === 'collection' ? [item.collection] : [], + item.kind === FileType.COLLECTION ? [item.collection] : [], ); const files = [ diff --git a/libs/shared/data-access/common/src/models/files/attached-list-item.ts b/libs/shared/data-access/common/src/models/files/attached-list-item.ts index b8de8ff2..87b155e1 100644 --- a/libs/shared/data-access/common/src/models/files/attached-list-item.ts +++ b/libs/shared/data-access/common/src/models/files/attached-list-item.ts @@ -1,3 +1,4 @@ +import { FileType } from '../../enums'; import { AttachedKnowledgeCollection } from './attached-knowledge-collection'; import { FileData } from './file-data'; @@ -5,9 +6,9 @@ import { FileData } from './file-data'; // knowledge base, and whole knowledge-base collections — this discriminated union is the one // list all three live in, client-side, before being split apart into the outgoing wire shape. export type AttachedListItem = - | { kind: 'file'; file: FileData; isFromKnowledge: boolean } - | { kind: 'collection'; collection: AttachedKnowledgeCollection }; + | { kind: FileType.FILE; file: FileData; isFromKnowledge: boolean } + | { kind: FileType.COLLECTION; collection: AttachedKnowledgeCollection }; export function getAttachedListItemId(item: AttachedListItem): string { - return item.kind === 'file' ? item.file.id : item.collection.id; + return item.kind === FileType.FILE ? item.file.id : item.collection.id; } From 5715d35b3a3ed4d2e1acb4ac6d5aa6a720a548ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D0=B8=D0=BB=D0=B8=D0=B9=20=D0=95=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D0=B5=D0=B5=D0=B2?= Date: Wed, 16 Sep 2026 21:41:00 +0600 Subject: [PATCH 7/7] refactor: refactor attach knowledge sheet --- .../attach-knowledge-sheet/src/lib/component.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx index ee210d03..05af9f8a 100644 --- a/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx +++ b/libs/mobile/chat/features/attach-knowledge-sheet/src/lib/component.tsx @@ -56,21 +56,19 @@ export function AttachKnowledgeSheet({ }; const openModal = (): void => { - setQuery(''); sheetRef.current?.present(); }; useImperativeHandle(ref, () => ({ present: openModal }), []); - const handleDismiss = (): void => { + const resetToKnowledgeList = (): void => { setOpenedKnowledge(null); setQuery(''); }; const handleGoBack = (): void => { if (openedKnowledge) { - setOpenedKnowledge(null); - setQuery(''); + resetToKnowledgeList(); return; } @@ -120,7 +118,7 @@ export function AttachKnowledgeSheet({ ref={sheetRef} title={openedKnowledge.name} onGoBack={handleGoBack} - onDismiss={handleDismiss} + onDismiss={resetToKnowledgeList} query={query} onQueryChange={setQuery} searchPlaceholder={translate('TEXT_SEARCH_FILES')} @@ -153,7 +151,7 @@ export function AttachKnowledgeSheet({ ref={sheetRef} title={translate('TEXT_TITLE')} onGoBack={closeModal} - onDismiss={handleDismiss} + onDismiss={resetToKnowledgeList} query={query} onQueryChange={setQuery} searchPlaceholder={translate('TEXT_SEARCH_KNOWLEDGE')}