diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 6e4f5c09..0d6cc732 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -119,7 +119,11 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)): loader = WebBaseLoader(form_data.url) data = loader.load() store_data_in_vector_db(data, form_data.collection_name) - return {"status": True, "collection_name": form_data.collection_name} + return { + "status": True, + "collection_name": form_data.collection_name, + "filename": form_data.url, + } except Exception as e: print(e) raise HTTPException( @@ -176,7 +180,11 @@ def store_doc( result = store_data_in_vector_db(data, collection_name) if result: - return {"status": True, "collection_name": collection_name} + return { + "status": True, + "collection_name": collection_name, + "filename": filename, + } else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index 0030c25e..89616064 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -1,7 +1,16 @@ from fastapi import FastAPI, Depends from fastapi.routing import APIRoute from fastapi.middleware.cors import CORSMiddleware -from apps.web.routers import auths, users, chats, modelfiles, prompts, configs, utils +from apps.web.routers import ( + auths, + users, + chats, + documents, + modelfiles, + prompts, + configs, + utils, +) from config import WEBUI_VERSION, WEBUI_AUTH app = FastAPI() @@ -22,9 +31,8 @@ app.add_middleware( app.include_router(auths.router, prefix="/auths", tags=["auths"]) app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(chats.router, prefix="/chats", tags=["chats"]) -app.include_router(modelfiles.router, - prefix="/modelfiles", - tags=["modelfiles"]) +app.include_router(documents.router, prefix="/documents", tags=["documents"]) +app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"]) app.include_router(prompts.router, prefix="/prompts", tags=["prompts"]) app.include_router(configs.router, prefix="/configs", tags=["configs"]) diff --git a/backend/apps/web/models/documents.py b/backend/apps/web/models/documents.py new file mode 100644 index 00000000..0196c38b --- /dev/null +++ b/backend/apps/web/models/documents.py @@ -0,0 +1,124 @@ +from pydantic import BaseModel +from peewee import * +from playhouse.shortcuts import model_to_dict +from typing import List, Union, Optional +import time + +from utils.utils import decode_token +from utils.misc import get_gravatar_url + +from apps.web.internal.db import DB + +import json + +#################### +# Documents DB Schema +#################### + + +class Document(Model): + collection_name = CharField(unique=True) + name = CharField(unique=True) + title = CharField() + filename = CharField() + content = TextField(null=True) + user_id = CharField() + timestamp = DateField() + + class Meta: + database = DB + + +class DocumentModel(BaseModel): + collection_name: str + name: str + title: str + filename: str + content: Optional[str] = None + user_id: str + timestamp: int # timestamp in epoch + + +#################### +# Forms +#################### + + +class DocumentUpdateForm(BaseModel): + name: str + title: str + + +class DocumentForm(DocumentUpdateForm): + collection_name: str + filename: str + content: Optional[str] = None + + +class DocumentsTable: + def __init__(self, db): + self.db = db + self.db.create_tables([Document]) + + def insert_new_doc( + self, user_id: str, form_data: DocumentForm + ) -> Optional[DocumentModel]: + document = DocumentModel( + **{ + **form_data.model_dump(), + "user_id": user_id, + "timestamp": int(time.time()), + } + ) + + try: + result = Document.create(**document.model_dump()) + if result: + return document + else: + return None + except: + return None + + def get_doc_by_name(self, name: str) -> Optional[DocumentModel]: + try: + document = Document.get(Document.name == name) + return DocumentModel(**model_to_dict(document)) + except: + return None + + def get_docs(self) -> List[DocumentModel]: + return [ + DocumentModel(**model_to_dict(doc)) + for doc in Document.select() + # .limit(limit).offset(skip) + ] + + def update_doc_by_name( + self, name: str, form_data: DocumentUpdateForm + ) -> Optional[DocumentModel]: + try: + query = Document.update( + title=form_data.title, + name=form_data.name, + timestamp=int(time.time()), + ).where(Document.name == name) + query.execute() + + doc = Document.get(Document.name == form_data.name) + return DocumentModel(**model_to_dict(doc)) + except Exception as e: + print(e) + return None + + def delete_doc_by_name(self, name: str) -> bool: + try: + query = Document.delete().where((Document.name == name)) + query.execute() # Remove the rows, return number of rows removed. + + return True + except: + return False + + +Documents = DocumentsTable(DB) diff --git a/backend/apps/web/routers/documents.py b/backend/apps/web/routers/documents.py new file mode 100644 index 00000000..c64ee8f0 --- /dev/null +++ b/backend/apps/web/routers/documents.py @@ -0,0 +1,119 @@ +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import json + +from apps.web.models.documents import ( + Documents, + DocumentForm, + DocumentUpdateForm, + DocumentModel, +) + +from utils.utils import get_current_user +from constants import ERROR_MESSAGES + +router = APIRouter() + +############################ +# GetDocuments +############################ + + +@router.get("/", response_model=List[DocumentModel]) +async def get_documents(user=Depends(get_current_user)): + return Documents.get_docs() + + +############################ +# CreateNewDoc +############################ + + +@router.post("/create", response_model=Optional[DocumentModel]) +async def create_new_doc(form_data: DocumentForm, user=Depends(get_current_user)): + if user.role != "admin": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + doc = Documents.get_doc_by_name(form_data.name) + if doc == None: + doc = Documents.insert_new_doc(user.id, form_data) + + if doc: + return doc + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FILE_EXISTS, + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NAME_TAG_TAKEN, + ) + + +############################ +# GetDocByName +############################ + + +@router.get("/name/{name}", response_model=Optional[DocumentModel]) +async def get_doc_by_name(name: str, user=Depends(get_current_user)): + doc = Documents.get_doc_by_name(name) + + if doc: + return doc + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateDocByName +############################ + + +@router.post("/name/{name}/update", response_model=Optional[DocumentModel]) +async def update_doc_by_name( + name: str, form_data: DocumentUpdateForm, user=Depends(get_current_user) +): + if user.role != "admin": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + doc = Documents.update_doc_by_name(name, form_data) + if doc: + return doc + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NAME_TAG_TAKEN, + ) + + +############################ +# DeleteDocByName +############################ + + +@router.delete("/name/{name}/delete", response_model=bool) +async def delete_doc_by_name(name: str, user=Depends(get_current_user)): + if user.role != "admin": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = Documents.delete_doc_by_name(name) + return result diff --git a/backend/config.py b/backend/config.py index 03718a06..2a96d018 100644 --- a/backend/config.py +++ b/backend/config.py @@ -58,7 +58,7 @@ if OPENAI_API_BASE_URL == "": # WEBUI_VERSION #################################### -WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.50") +WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.61") #################################### # WEBUI_AUTH (Required for security) diff --git a/backend/constants.py b/backend/constants.py index 0f7a46a0..c9bfaec5 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -18,6 +18,9 @@ class ERROR_MESSAGES(str, Enum): "Uh-oh! This username is already registered. Please choose another username." ) COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string." + FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file." + + NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string." INVALID_TOKEN = ( "Your session has expired or the token is invalid. Please sign in again." ) diff --git a/src/lib/apis/documents/index.ts b/src/lib/apis/documents/index.ts new file mode 100644 index 00000000..fb208ea4 --- /dev/null +++ b/src/lib/apis/documents/index.ts @@ -0,0 +1,177 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewDoc = async ( + token: string, + collection_name: string, + filename: string, + name: string, + title: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + collection_name: collection_name, + filename: filename, + name: name, + title: title + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDocs = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDocByName = async (token: string, name: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type DocUpdateForm = { + name: string; + title: string; +}; + +export const updateDocByName = async (token: string, name: string, form: DocUpdateForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: form.name, + title: form.title + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteDocByName = async (token: string, name: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/name/${name}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/AddFilesPlaceholder.svelte b/src/lib/components/AddFilesPlaceholder.svelte new file mode 100644 index 00000000..7cc51c6f --- /dev/null +++ b/src/lib/components/AddFilesPlaceholder.svelte @@ -0,0 +1,6 @@ +
📄
+
Add Files
+ +
+ Drop any files here to add to the conversation +
diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 54ccc8f4..34f4de18 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -7,6 +7,9 @@ import Prompts from './MessageInput/PromptCommands.svelte'; import Suggestions from './MessageInput/Suggestions.svelte'; import { uploadDocToVectorDB } from '$lib/apis/rag'; + import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte'; + import { SUPPORTED_FILE_TYPE } from '$lib/constants'; + import Documents from './MessageInput/Documents.svelte'; export let submitPrompt: Function; export let stopResponse: Function; @@ -16,6 +19,7 @@ let filesInputElement; let promptsElement; + let documentsElement; let inputFiles; let dragged = false; @@ -115,12 +119,16 @@ onMount(() => { const dropZone = document.querySelector('body'); - dropZone?.addEventListener('dragover', (e) => { + const onDragOver = (e) => { e.preventDefault(); dragged = true; - }); + }; - dropZone.addEventListener('drop', async (e) => { + const onDragLeave = () => { + dragged = false; + }; + + const onDrop = async (e) => { e.preventDefault(); console.log(e); @@ -143,14 +151,7 @@ const file = inputFiles[0]; if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) { reader.readAsDataURL(file); - } else if ( - [ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'text/plain', - 'text/csv' - ].includes(file['type']) - ) { + } else if (SUPPORTED_FILE_TYPE.includes(file['type'])) { uploadDoc(file); } else { toast.error(`Unsupported File Type '${file['type']}'.`); @@ -161,11 +162,17 @@ } dragged = false; - }); + }; - dropZone?.addEventListener('dragleave', () => { - dragged = false; - }); + dropZone?.addEventListener('dragover', onDragOver); + dropZone?.addEventListener('drop', onDrop); + dropZone?.addEventListener('dragleave', onDragLeave); + + return () => { + dropZone?.removeEventListener('dragover', onDragOver); + dropZone?.removeEventListener('drop', onDrop); + dropZone?.removeEventListener('dragleave', onDragLeave); + }; }); @@ -179,12 +186,7 @@
-
🗂️
-
Add Files
- -
- Drop any files/images here to add to the conversation -
+
@@ -224,6 +226,22 @@
{#if prompt.charAt(0) === '/'} + {:else if prompt.charAt(0) === '#'} + { + console.log(e); + files = [ + ...files, + { + type: 'doc', + ...e.detail, + upload_status: true + } + ]; + }} + /> {:else if messages.length == 0 && suggestionPrompts.length !== 0} {/if} @@ -256,14 +274,7 @@ const file = inputFiles[0]; if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) { reader.readAsDataURL(file); - } else if ( - [ - 'application/pdf', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'text/plain', - 'text/csv' - ].includes(file['type']) - ) { + } else if (SUPPORTED_FILE_TYPE.includes(file['type'])) { uploadDoc(file); filesInputElement.value = ''; } else { @@ -448,8 +459,10 @@ editButton?.click(); } - if (prompt.charAt(0) === '/' && e.key === 'ArrowUp') { - promptsElement.selectUp(); + if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') { + e.preventDefault(); + + (promptsElement || documentsElement).selectUp(); const commandOptionButton = [ ...document.getElementsByClassName('selected-command-option-button') @@ -457,8 +470,10 @@ commandOptionButton.scrollIntoView({ block: 'center' }); } - if (prompt.charAt(0) === '/' && e.key === 'ArrowDown') { - promptsElement.selectDown(); + if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') { + e.preventDefault(); + + (promptsElement || documentsElement).selectDown(); const commandOptionButton = [ ...document.getElementsByClassName('selected-command-option-button') @@ -466,7 +481,7 @@ commandOptionButton.scrollIntoView({ block: 'center' }); } - if (prompt.charAt(0) === '/' && e.key === 'Enter') { + if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Enter') { e.preventDefault(); const commandOptionButton = [ @@ -476,7 +491,7 @@ commandOptionButton?.click(); } - if (prompt.charAt(0) === '/' && e.key === 'Tab') { + if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Tab') { e.preventDefault(); const commandOptionButton = [ diff --git a/src/lib/components/chat/MessageInput/Documents.svelte b/src/lib/components/chat/MessageInput/Documents.svelte new file mode 100644 index 00000000..bcfb1916 --- /dev/null +++ b/src/lib/components/chat/MessageInput/Documents.svelte @@ -0,0 +1,78 @@ + + +{#if filteredDocs.length > 0} +
+
+
+
#
+
+ +
+
+ {#each filteredDocs as doc, docIdx} + + {/each} +
+
+
+
+{/if} diff --git a/src/lib/components/documents/EditDocModal.svelte b/src/lib/components/documents/EditDocModal.svelte new file mode 100644 index 00000000..35ea1539 --- /dev/null +++ b/src/lib/components/documents/EditDocModal.svelte @@ -0,0 +1,151 @@ + + + +
+
+
Edit Doc
+ +
+
+ +
+
+
{ + submitHandler(); + }} + > +
+
+
Name Tag
+ +
+
+ # +
+ +
+ + +
+ +
+
Title
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+ + diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 7ea08f98..927d87d6 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -68,7 +68,7 @@
{#if $user?.role === 'admin'} -
+
@@ -136,7 +136,7 @@
-
+
+ +
+ +
{/if}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a43104ad..260e675e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -11,6 +11,13 @@ export const WEB_UI_VERSION = 'v1.0.0-alpha-static'; export const REQUIRED_OLLAMA_VERSION = '0.1.16'; +export const SUPPORTED_FILE_TYPE = [ + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'text/plain', + 'text/csv' +]; + // Source: https://kit.svelte.dev/docs/modules#$env-static-public // This feature, akin to $env/static/private, exclusively incorporates environment variables // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_). diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 55c83b25..c7d8f5e6 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -11,8 +11,23 @@ export const chatId = writable(''); export const chats = writable([]); export const models = writable([]); + export const modelfiles = writable([]); export const prompts = writable([]); +export const documents = writable([ + { + collection_name: 'collection_name', + filename: 'filename', + name: 'name', + title: 'title' + }, + { + collection_name: 'collection_name1', + filename: 'filename1', + name: 'name1', + title: 'title1' + } +]); export const settings = writable({}); export const showSettings = writable(false); diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 46bc8f04..129a1d12 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -128,6 +128,37 @@ export const findWordIndices = (text) => { return matches; }; +export const removeFirstHashWord = (inputString) => { + // Split the string into an array of words + const words = inputString.split(' '); + + // Find the index of the first word that starts with # + const index = words.findIndex((word) => word.startsWith('#')); + + // Remove the first word with # + if (index !== -1) { + words.splice(index, 1); + } + + // Join the remaining words back into a string + const resultString = words.join(' '); + + return resultString; +}; + +export const transformFileName = (fileName) => { + // Convert to lowercase + const lowerCaseFileName = fileName.toLowerCase(); + + // Remove special characters using regular expression + const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, ''); + + // Replace spaces with dashes + const finalFileName = sanitizedFileName.replace(/\s+/g, '-'); + + return finalFileName; +}; + export const calculateSHA256 = async (file) => { // Create a FileReader to read the file asynchronously const reader = new FileReader(); diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index c264592e..39ae0eea 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -13,13 +13,22 @@ import { getOpenAIModels } from '$lib/apis/openai'; - import { user, showSettings, settings, models, modelfiles, prompts } from '$lib/stores'; + import { + user, + showSettings, + settings, + models, + modelfiles, + prompts, + documents + } from '$lib/stores'; import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte'; import { checkVersion } from '$lib/utils'; import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte'; + import { getDocs } from '$lib/apis/documents'; let ollamaVersion = ''; let loaded = false; @@ -93,11 +102,10 @@ console.log(); await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); + await modelfiles.set(await getModelfiles(localStorage.token)); - await prompts.set(await getPrompts(localStorage.token)); - - console.log($modelfiles); + await documents.set(await getDocs(localStorage.token)); modelfiles.subscribe(async () => { // should fetch models diff --git a/src/routes/(app)/documents/+page.svelte b/src/routes/(app)/documents/+page.svelte new file mode 100644 index 00000000..d79056e2 --- /dev/null +++ b/src/routes/(app)/documents/+page.svelte @@ -0,0 +1,446 @@ + + +{#key selectedDoc} + +{/key} + +
+
+
+
+
My Documents
+
+ +
+
+
+ + + +
+ +
+ +
+ +
+
+ + { + if (inputFiles && inputFiles.length > 0) { + const file = inputFiles[0]; + if (SUPPORTED_FILE_TYPE.includes(file['type'])) { + uploadDoc(file); + } else { + toast.error(`Unsupported File Type '${file['type']}'.`); + } + + inputFiles = null; + e.target.value = ''; + } else { + toast.error(`File not found.`); + } + }} + /> + +
+
+
+
Add Files
+ +
+ Drop any files here to add to my documents +
+
+
+
+ + {#each $documents.filter((p) => query === '' || p.name.includes(query)) as doc} +
+
+
+
+
+ {#if doc} + + + + + {:else} + + {/if} +
+
+
#{doc.name} ({doc.filename})
+
+ {doc.title} +
+
+
+
+
+ + + + + +
+
+ {/each} + {#if $documents.length != 0} +
+ +
+
+ { + console.log(importFiles); + + const reader = new FileReader(); + reader.onload = async (event) => { + const savedDocs = JSON.parse(event.target.result); + console.log(savedDocs); + + for (const doc of savedDocs) { + await createNewDoc( + localStorage.token, + doc.collection_name, + doc.filename, + doc.name, + doc.title + ).catch((error) => { + toast.error(error); + return null; + }); + } + + await documents.set(await getDocs(localStorage.token)); + }; + + reader.readAsText(importFiles[0]); + }} + /> + + + + + + +
+
+ {/if} + +
+
+ + + +
+ +
+ Tip: Use '#' in the prompt input to swiftly load and select your documents. +
+
+
+
+