forked from open-webui/open-webui
feat: rag folder scan support
This commit is contained in:
parent
9f869f6573
commit
e07001e5f6
9 changed files with 350 additions and 12 deletions
|
@ -10,6 +10,8 @@ from fastapi import (
|
||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import os, shutil
|
import os, shutil
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
# from chromadb.utils import embedding_functions
|
# from chromadb.utils import embedding_functions
|
||||||
|
@ -28,19 +30,39 @@ from langchain_community.document_loaders import (
|
||||||
UnstructuredExcelLoader,
|
UnstructuredExcelLoader,
|
||||||
)
|
)
|
||||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
from langchain_community.vectorstores import Chroma
|
|
||||||
from langchain.chains import RetrievalQA
|
from langchain.chains import RetrievalQA
|
||||||
|
from langchain_community.vectorstores import Chroma
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import mimetypes
|
||||||
import uuid
|
import uuid
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from utils.misc import calculate_sha256, calculate_sha256_string
|
|
||||||
|
from apps.web.models.documents import (
|
||||||
|
Documents,
|
||||||
|
DocumentForm,
|
||||||
|
DocumentResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
from utils.misc import (
|
||||||
|
calculate_sha256,
|
||||||
|
calculate_sha256_string,
|
||||||
|
sanitize_filename,
|
||||||
|
extract_folders_after_data_docs,
|
||||||
|
)
|
||||||
from utils.utils import get_current_user, get_admin_user
|
from utils.utils import get_current_user, get_admin_user
|
||||||
from config import UPLOAD_DIR, EMBED_MODEL, CHROMA_CLIENT, CHUNK_SIZE, CHUNK_OVERLAP
|
from config import (
|
||||||
|
UPLOAD_DIR,
|
||||||
|
DOCS_DIR,
|
||||||
|
EMBED_MODEL,
|
||||||
|
CHROMA_CLIENT,
|
||||||
|
CHUNK_SIZE,
|
||||||
|
CHUNK_OVERLAP,
|
||||||
|
)
|
||||||
from constants import ERROR_MESSAGES
|
from constants import ERROR_MESSAGES
|
||||||
|
|
||||||
# EMBEDDING_FUNC = embedding_functions.SentenceTransformerEmbeddingFunction(
|
# EMBEDDING_FUNC = embedding_functions.SentenceTransformerEmbeddingFunction(
|
||||||
|
@ -220,8 +242,8 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_loader(file, file_path):
|
def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||||
file_ext = file.filename.split(".")[-1].lower()
|
file_ext = filename.split(".")[-1].lower()
|
||||||
known_type = True
|
known_type = True
|
||||||
|
|
||||||
known_source_ext = [
|
known_source_ext = [
|
||||||
|
@ -279,20 +301,20 @@ def get_loader(file, file_path):
|
||||||
loader = UnstructuredXMLLoader(file_path)
|
loader = UnstructuredXMLLoader(file_path)
|
||||||
elif file_ext == "md":
|
elif file_ext == "md":
|
||||||
loader = UnstructuredMarkdownLoader(file_path)
|
loader = UnstructuredMarkdownLoader(file_path)
|
||||||
elif file.content_type == "application/epub+zip":
|
elif file_content_type == "application/epub+zip":
|
||||||
loader = UnstructuredEPubLoader(file_path)
|
loader = UnstructuredEPubLoader(file_path)
|
||||||
elif (
|
elif (
|
||||||
file.content_type
|
file_content_type
|
||||||
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
or file_ext in ["doc", "docx"]
|
or file_ext in ["doc", "docx"]
|
||||||
):
|
):
|
||||||
loader = Docx2txtLoader(file_path)
|
loader = Docx2txtLoader(file_path)
|
||||||
elif file.content_type in [
|
elif file_content_type in [
|
||||||
"application/vnd.ms-excel",
|
"application/vnd.ms-excel",
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
] or file_ext in ["xls", "xlsx"]:
|
] or file_ext in ["xls", "xlsx"]:
|
||||||
loader = UnstructuredExcelLoader(file_path)
|
loader = UnstructuredExcelLoader(file_path)
|
||||||
elif file_ext in known_source_ext or file.content_type.find("text/") >= 0:
|
elif file_ext in known_source_ext or file_content_type.find("text/") >= 0:
|
||||||
loader = TextLoader(file_path)
|
loader = TextLoader(file_path)
|
||||||
else:
|
else:
|
||||||
loader = TextLoader(file_path)
|
loader = TextLoader(file_path)
|
||||||
|
@ -323,7 +345,7 @@ def store_doc(
|
||||||
collection_name = calculate_sha256(f)[:63]
|
collection_name = calculate_sha256(f)[:63]
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
loader, known_type = get_loader(file, file_path)
|
loader, known_type = get_loader(file.filename, file.content_type, file_path)
|
||||||
data = loader.load()
|
data = loader.load()
|
||||||
result = store_data_in_vector_db(data, collection_name)
|
result = store_data_in_vector_db(data, collection_name)
|
||||||
|
|
||||||
|
@ -353,6 +375,61 @@ def store_doc(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/scan")
|
||||||
|
def scan_docs_dir(user=Depends(get_admin_user)):
|
||||||
|
try:
|
||||||
|
for path in Path(DOCS_DIR).rglob("./**/*"):
|
||||||
|
if path.is_file() and not path.name.startswith("."):
|
||||||
|
tags = extract_folders_after_data_docs(path)
|
||||||
|
filename = path.name
|
||||||
|
file_content_type = mimetypes.guess_type(path)
|
||||||
|
|
||||||
|
f = open(path, "rb")
|
||||||
|
collection_name = calculate_sha256(f)[:63]
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
loader, known_type = get_loader(filename, file_content_type, str(path))
|
||||||
|
data = loader.load()
|
||||||
|
|
||||||
|
result = store_data_in_vector_db(data, collection_name)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
sanitized_filename = sanitize_filename(filename)
|
||||||
|
doc = Documents.get_doc_by_name(sanitized_filename)
|
||||||
|
|
||||||
|
if doc == None:
|
||||||
|
doc = Documents.insert_new_doc(
|
||||||
|
user.id,
|
||||||
|
DocumentForm(
|
||||||
|
**{
|
||||||
|
"name": sanitized_filename,
|
||||||
|
"title": filename,
|
||||||
|
"collection_name": collection_name,
|
||||||
|
"filename": filename,
|
||||||
|
"content": (
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"tags": list(
|
||||||
|
map(
|
||||||
|
lambda name: {"name": name},
|
||||||
|
tags,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if len(tags)
|
||||||
|
else "{}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@app.get("/reset/db")
|
@app.get("/reset/db")
|
||||||
def reset_vector_db(user=Depends(get_admin_user)):
|
def reset_vector_db(user=Depends(get_admin_user)):
|
||||||
CHROMA_CLIENT.reset()
|
CHROMA_CLIENT.reset()
|
||||||
|
|
|
@ -96,6 +96,10 @@ async def get_doc_by_name(name: str, user=Depends(get_current_user)):
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
class TagItem(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
class TagDocumentForm(BaseModel):
|
class TagDocumentForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
tags: List[dict]
|
tags: List[dict]
|
||||||
|
|
|
@ -43,6 +43,14 @@ Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
|
||||||
CACHE_DIR = f"{DATA_DIR}/cache"
|
CACHE_DIR = f"{DATA_DIR}/cache"
|
||||||
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# Docs DIR
|
||||||
|
####################################
|
||||||
|
|
||||||
|
DOCS_DIR = f"{DATA_DIR}/docs"
|
||||||
|
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# OLLAMA_API_BASE_URL
|
# OLLAMA_API_BASE_URL
|
||||||
####################################
|
####################################
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from pathlib import Path
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -38,3 +39,40 @@ def validate_email_format(email: str) -> bool:
|
||||||
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_filename(file_name):
|
||||||
|
# Convert to lowercase
|
||||||
|
lower_case_file_name = file_name.lower()
|
||||||
|
|
||||||
|
# Remove special characters using regular expression
|
||||||
|
sanitized_file_name = re.sub(r"[^\w\s]", "", lower_case_file_name)
|
||||||
|
|
||||||
|
# Replace spaces with dashes
|
||||||
|
final_file_name = re.sub(r"\s+", "-", sanitized_file_name)
|
||||||
|
|
||||||
|
return final_file_name
|
||||||
|
|
||||||
|
|
||||||
|
def extract_folders_after_data_docs(path):
|
||||||
|
# Convert the path to a Path object if it's not already
|
||||||
|
path = Path(path)
|
||||||
|
|
||||||
|
# Extract parts of the path
|
||||||
|
parts = path.parts
|
||||||
|
|
||||||
|
# Find the index of '/data/docs' in the path
|
||||||
|
try:
|
||||||
|
index_data_docs = parts.index("data") + 1
|
||||||
|
index_docs = parts.index("docs", index_data_docs) + 1
|
||||||
|
except ValueError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Exclude the filename and accumulate folder names
|
||||||
|
tags = []
|
||||||
|
|
||||||
|
folders = parts[index_docs:-1]
|
||||||
|
for idx, part in enumerate(folders):
|
||||||
|
tags.append("/".join(folders[: idx + 1]))
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
|
@ -138,6 +138,32 @@ export const queryCollection = async (
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const scanDocs = async (token: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${RAG_API_BASE_URL}/scan`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const resetVectorDB = async (token: string) => {
|
export const resetVectorDB = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
|
@ -366,7 +366,7 @@
|
||||||
|
|
||||||
{#if message.done}
|
{#if message.done}
|
||||||
<div
|
<div
|
||||||
class=" flex justify-start space-x-1 -mt-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
||||||
>
|
>
|
||||||
{#if siblings.length > 1}
|
{#if siblings.length > 1}
|
||||||
<div class="flex self-center min-w-fit">
|
<div class="flex self-center min-w-fit">
|
||||||
|
|
68
src/lib/components/documents/Settings/General.svelte
Normal file
68
src/lib/components/documents/Settings/General.svelte
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getDocs } from '$lib/apis/documents';
|
||||||
|
import { scanDocs } from '$lib/apis/rag';
|
||||||
|
import { documents } from '$lib/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let saveHandler: Function;
|
||||||
|
const scanHandler = async () => {
|
||||||
|
const res = await scanDocs(localStorage.token);
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
await documents.set(await getDocs(localStorage.token));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
|
on:submit|preventDefault={() => {
|
||||||
|
// console.log('submit');
|
||||||
|
saveHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||||
|
<div>
|
||||||
|
<div class=" mb-2 text-sm font-medium">General Settings</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">Scan for documents from '/data/docs'</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded flex flex-row space-x-1 items-center"
|
||||||
|
on:click={() => {
|
||||||
|
scanHandler();
|
||||||
|
console.log('check');
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div class="self-center">Scan</div>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-3 h-3"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
<button
|
||||||
|
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div> -->
|
||||||
|
</form>
|
86
src/lib/components/documents/SettingsModal.svelte
Normal file
86
src/lib/components/documents/SettingsModal.svelte
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<script>
|
||||||
|
import Modal from '../common/Modal.svelte';
|
||||||
|
import General from './Settings/General.svelte';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
|
||||||
|
let selectedTab = 'general';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:show>
|
||||||
|
<div>
|
||||||
|
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||||
|
<div class=" text-lg font-medium self-center">Document Settings</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<hr class=" dark:border-gray-800" />
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
|
||||||
|
<div
|
||||||
|
class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||||
|
'general'
|
||||||
|
? 'bg-gray-200 dark:bg-gray-700'
|
||||||
|
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||||
|
on:click={() => {
|
||||||
|
selectedTab = 'general';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" self-center mr-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" self-center">General</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 md:min-h-[380px]">
|
||||||
|
{#if selectedTab === 'general'}
|
||||||
|
<General
|
||||||
|
saveHandler={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<!-- <General
|
||||||
|
saveHandler={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
/> -->
|
||||||
|
<!-- {:else if selectedTab === 'users'}
|
||||||
|
<Users
|
||||||
|
saveHandler={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
/> -->
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
|
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
|
||||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||||
|
import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
|
||||||
let importFiles = '';
|
let importFiles = '';
|
||||||
|
|
||||||
let inputFiles = '';
|
let inputFiles = '';
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
|
|
||||||
let tags = [];
|
let tags = [];
|
||||||
|
|
||||||
|
let showSettingsModal = false;
|
||||||
let showEditDocModal = false;
|
let showEditDocModal = false;
|
||||||
let selectedDoc;
|
let selectedDoc;
|
||||||
let selectedTag = '';
|
let selectedTag = '';
|
||||||
|
@ -179,11 +181,38 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingsModal bind:show={showSettingsModal} />
|
||||||
|
|
||||||
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
|
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
|
||||||
<div class=" py-2.5 flex flex-col justify-between w-full overflow-y-auto">
|
<div class=" py-2.5 flex flex-col justify-between w-full overflow-y-auto">
|
||||||
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
|
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
|
||||||
<div class="mb-6 flex justify-between items-center">
|
<div class="mb-6 flex justify-between items-center">
|
||||||
<div class=" text-2xl font-semibold self-center">My Documents</div>
|
<div class=" text-2xl font-semibold self-center">My Documents</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="flex items-center space-x-1 border border-gray-200 dark:border-gray-600 px-3 py-1 rounded-lg"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showSettingsModal = !showSettingsModal;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class=" text-xs">Document Settings</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full space-x-2">
|
<div class=" flex w-full space-x-2">
|
||||||
|
@ -419,6 +448,8 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" my-2.5" />
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if $documents.length > 0}
|
{#if $documents.length > 0}
|
||||||
|
|
Loading…
Reference in a new issue