diff --git a/Dockerfile b/Dockerfile index 37f556dc..3d4e380a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,9 +5,10 @@ FROM node:alpine as build WORKDIR /app # wget embedding model weight from alpine (does not exist from slim-buster) -RUN wget "https://chroma-onnx-models.s3.amazonaws.com/all-MiniLM-L6-v2/onnx.tar.gz" +RUN wget "https://chroma-onnx-models.s3.amazonaws.com/all-MiniLM-L6-v2/onnx.tar.gz" -O - | \ + tar -xzf - -C /app -COPY package.json package-lock.json ./ +COPY package.json package-lock.json ./ RUN npm ci COPY . . @@ -34,20 +35,17 @@ COPY ./backend/requirements.txt ./requirements.txt RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir RUN pip3 install -r requirements.txt --no-cache-dir -# Install pandoc +# Install pandoc and netcat # RUN python -c "import pypandoc; pypandoc.download_pandoc()" RUN apt-get update \ - && apt-get install -y pandoc \ + && apt-get install -y pandoc netcat-openbsd \ && rm -rf /var/lib/apt/lists/* # RUN python -c "from sentence_transformers import SentenceTransformer; model = SentenceTransformer('all-MiniLM-L6-v2')" # copy embedding weight from build RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 -COPY --from=build /app/onnx.tar.gz /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 - -RUN cd /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 &&\ - tar -xzf onnx.tar.gz +COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx # copy built frontend files COPY --from=build /app/build /app/build @@ -55,4 +53,4 @@ COPY --from=build /app/build /app/build # copy backend files COPY ./backend . -CMD [ "bash", "start.sh"] +CMD [ "bash", "start.sh"] \ No newline at end of file diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index c5276399..85bc995a 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -24,6 +24,7 @@ from langchain_community.document_loaders import ( UnstructuredMarkdownLoader, UnstructuredXMLLoader, UnstructuredRSTLoader, + UnstructuredExcelLoader, ) from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma @@ -36,7 +37,7 @@ from typing import Optional import uuid import time -from utils.misc import calculate_sha256 +from utils.misc import calculate_sha256, calculate_sha256_string from utils.utils import get_current_user from config import UPLOAD_DIR, EMBED_MODEL, CHROMA_CLIENT, CHUNK_SIZE, CHUNK_OVERLAP from constants import ERROR_MESSAGES @@ -123,10 +124,15 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)): try: loader = WebBaseLoader(form_data.url) data = loader.load() - store_data_in_vector_db(data, form_data.collection_name) + + collection_name = form_data.collection_name + if collection_name == "": + collection_name = calculate_sha256_string(form_data.url)[:63] + + store_data_in_vector_db(data, collection_name) return { "status": True, - "collection_name": form_data.collection_name, + "collection_name": collection_name, "filename": form_data.url, } except Exception as e: @@ -137,6 +143,87 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)): ) +def get_loader(file, file_path): + file_ext = file.filename.split(".")[-1].lower() + known_type = True + + known_source_ext = [ + "go", + "py", + "java", + "sh", + "bat", + "ps1", + "cmd", + "js", + "ts", + "css", + "cpp", + "hpp", + "h", + "c", + "cs", + "sql", + "log", + "ini", + "pl", + "pm", + "r", + "dart", + "dockerfile", + "env", + "php", + "hs", + "hsc", + "lua", + "nginxconf", + "conf", + "m", + "mm", + "plsql", + "perl", + "rb", + "rs", + "db2", + "scala", + "bash", + "swift", + "vue", + "svelte", + ] + + if file_ext == "pdf": + loader = PyPDFLoader(file_path) + elif file_ext == "csv": + loader = CSVLoader(file_path) + elif file_ext == "rst": + loader = UnstructuredRSTLoader(file_path, mode="elements") + elif file_ext == "xml": + loader = UnstructuredXMLLoader(file_path) + elif file_ext == "md": + loader = UnstructuredMarkdownLoader(file_path) + elif file.content_type == "application/epub+zip": + loader = UnstructuredEPubLoader(file_path) + elif ( + file.content_type + == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + or file_ext in ["doc", "docx"] + ): + loader = Docx2txtLoader(file_path) + elif file.content_type in [ + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ] or file_ext in ["xls", "xlsx"]: + loader = UnstructuredExcelLoader(file_path) + elif file_ext in known_source_ext or file.content_type.find("text/") >= 0: + loader = TextLoader(file_path) + else: + loader = TextLoader(file_path) + known_type = False + + return loader, known_type + + @app.post("/doc") def store_doc( collection_name: Optional[str] = Form(None), @@ -146,21 +233,6 @@ def store_doc( # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm" print(file.content_type) - - text_xml=["xml"] - octet_markdown=["md"] - known_source_ext=[ - "go", "py", "java", "sh", "bat", "ps1", "cmd", "js", "ts", - "css", "cpp", "hpp","h", "c", "cs", "sql", "log", "ini", - "pl", "pm", "r", "dart", "dockerfile", "env", "php", "hs", - "hsc", "lua", "nginxconf", "conf", "m", "mm", "plsql", "perl", - "rb", "rs", "db2", "scala", "bash", "swift", "vue", "svelte" - ] - docx_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document" - known_doc_ext=["doc","docx"] - file_ext=file.filename.split(".")[-1].lower() - known_type=True - try: filename = file.filename file_path = f"{UPLOAD_DIR}/{filename}" @@ -174,27 +246,7 @@ def store_doc( collection_name = calculate_sha256(f)[:63] f.close() - if file_ext=="pdf": - loader = PyPDFLoader(file_path) - elif (file.content_type ==docx_type or file_ext in known_doc_ext): - loader = Docx2txtLoader(file_path) - elif file_ext=="csv": - loader = CSVLoader(file_path) - elif file_ext=="rst": - loader = UnstructuredRSTLoader(file_path, mode="elements") - elif file_ext in text_xml: - loader=UnstructuredXMLLoader(file_path) - elif file_ext in known_source_ext or file.content_type.find("text/")>=0: - loader = TextLoader(file_path) - elif file_ext in octet_markdown: - loader = UnstructuredMarkdownLoader(file_path) - elif file.content_type == "application/epub+zip": - loader = UnstructuredEPubLoader(file_path) - else: - loader = TextLoader(file_path) - known_type=False - - + loader, known_type = get_loader(file, file_path) data = loader.load() result = store_data_in_vector_db(data, collection_name) @@ -203,7 +255,7 @@ def store_doc( "status": True, "collection_name": collection_name, "filename": filename, - "known_type":known_type, + "known_type": known_type, } else: raise HTTPException( diff --git a/backend/apps/web/internal/db.py b/backend/apps/web/internal/db.py index 3d639f3c..1f8c3bf7 100644 --- a/backend/apps/web/internal/db.py +++ b/backend/apps/web/internal/db.py @@ -1,4 +1,6 @@ from peewee import * +from config import DATA_DIR -DB = SqliteDatabase("./data/ollama.db") + +DB = SqliteDatabase(f"{DATA_DIR}/ollama.db") DB.connect() diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py index 00c66a2c..367db3ff 100644 --- a/backend/apps/web/models/auths.py +++ b/backend/apps/web/models/auths.py @@ -63,6 +63,15 @@ class SigninForm(BaseModel): password: str +class ProfileImageUrlForm(BaseModel): + profile_image_url: str + + +class UpdateProfileForm(BaseModel): + profile_image_url: str + name: str + + class UpdatePasswordForm(BaseModel): password: str new_password: str diff --git a/backend/apps/web/models/users.py b/backend/apps/web/models/users.py index f86697f4..c6d8b38d 100644 --- a/backend/apps/web/models/users.py +++ b/backend/apps/web/models/users.py @@ -65,7 +65,7 @@ class UsersTable: "name": name, "email": email, "role": role, - "profile_image_url": get_gravatar_url(email), + "profile_image_url": "/user.png", "timestamp": int(time.time()), } ) @@ -108,6 +108,20 @@ class UsersTable: except: return None + def update_user_profile_image_url_by_id( + self, id: str, profile_image_url: str + ) -> Optional[UserModel]: + try: + query = User.update(profile_image_url=profile_image_url).where( + User.id == id + ) + query.execute() + + user = User.get(User.id == id) + return UserModel(**model_to_dict(user)) + except: + return None + def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]: try: query = User.update(**updated).where(User.id == id) diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index a0772223..f45c67ac 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -11,6 +11,7 @@ import uuid from apps.web.models.auths import ( SigninForm, SignupForm, + UpdateProfileForm, UpdatePasswordForm, UserResponse, SigninResponse, @@ -40,14 +41,37 @@ async def get_session_user(user=Depends(get_current_user)): } +############################ +# Update Profile +############################ + + +@router.post("/update/profile", response_model=UserResponse) +async def update_profile( + form_data: UpdateProfileForm, session_user=Depends(get_current_user) +): + if session_user: + user = Users.update_user_by_id( + session_user.id, + {"profile_image_url": form_data.profile_image_url, "name": form_data.name}, + ) + if user: + return user + else: + raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT()) + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + ############################ # Update Password ############################ @router.post("/update/password", response_model=bool) -async def update_password(form_data: UpdatePasswordForm, - session_user=Depends(get_current_user)): +async def update_password( + form_data: UpdatePasswordForm, session_user=Depends(get_current_user) +): if session_user: user = Auths.authenticate_user(session_user.email, form_data.password) @@ -93,18 +117,19 @@ async def signin(form_data: SigninForm): async def signup(request: Request, form_data: SignupForm): if not request.app.state.ENABLE_SIGNUP: raise HTTPException(400, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - + if not validate_email_format(form_data.email.lower()): raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) - + if Users.get_user_by_email(form_data.email.lower()): raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) - + try: role = "admin" if Users.get_num_users() == 0 else "pending" hashed = get_password_hash(form_data.password) - user = Auths.insert_new_auth(form_data.email.lower(), - hashed, form_data.name, role) + user = Auths.insert_new_auth( + form_data.email.lower(), hashed, form_data.name, role + ) if user: token = create_token(data={"email": user.email}) @@ -120,11 +145,10 @@ async def signup(request: Request, form_data: SignupForm): "profile_image_url": user.profile_image_url, } else: - raise HTTPException( - 500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) except Exception as err: - raise HTTPException(500, - detail=ERROR_MESSAGES.DEFAULT(err)) + raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) + ############################ # ToggleSignUp diff --git a/backend/apps/web/routers/utils.py b/backend/apps/web/routers/utils.py index b2ca409a..86e1a9e5 100644 --- a/backend/apps/web/routers/utils.py +++ b/backend/apps/web/routers/utils.py @@ -9,9 +9,9 @@ import os import aiohttp import json -from utils.misc import calculate_sha256 +from utils.misc import calculate_sha256, get_gravatar_url -from config import OLLAMA_API_BASE_URL +from config import OLLAMA_API_BASE_URL, DATA_DIR, UPLOAD_DIR from constants import ERROR_MESSAGES @@ -96,8 +96,7 @@ async def download( file_name = parse_huggingface_url(url) if file_name: - os.makedirs("./uploads", exist_ok=True) - file_path = os.path.join("./uploads", f"{file_name}") + file_path = f"{UPLOAD_DIR}/{file_name}" return StreamingResponse( download_file_stream(url, file_path, file_name), @@ -109,8 +108,7 @@ async def download( @router.post("/upload") def upload(file: UploadFile = File(...)): - os.makedirs("./data/uploads", exist_ok=True) - file_path = os.path.join("./data/uploads", file.filename) + file_path = f"{UPLOAD_DIR}/{file.filename}" # Save file in chunks with open(file_path, "wb+") as f: @@ -167,3 +165,10 @@ def upload(file: UploadFile = File(...)): yield f"data: {json.dumps(res)}\n\n" return StreamingResponse(file_process_stream(), media_type="text/event-stream") + + +@router.get("/gravatar") +async def get_gravatar( + email: str, +): + return get_gravatar_url(email) diff --git a/backend/config.py b/backend/config.py index 8f37eb41..05733f88 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,29 +1,17 @@ -from dotenv import load_dotenv, find_dotenv import os - - import chromadb from chromadb import Settings - - from secrets import token_bytes from base64 import b64encode - from constants import ERROR_MESSAGES - - from pathlib import Path -load_dotenv(find_dotenv("../.env")) +try: + from dotenv import load_dotenv, find_dotenv - -#################################### -# File Upload -#################################### - - -UPLOAD_DIR = "./data/uploads" -Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True) + load_dotenv(find_dotenv("../.env")) +except ImportError: + print("dotenv not installed, skipping...") #################################### @@ -32,6 +20,21 @@ Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True) ENV = os.environ.get("ENV", "dev") + +#################################### +# DATA/FRONTEND BUILD DIR +#################################### + +DATA_DIR = str(Path(os.getenv("DATA_DIR", "./data")).resolve()) +FRONTEND_BUILD_DIR = str(Path(os.getenv("FRONTEND_BUILD_DIR", "../build"))) + +#################################### +# File Upload DIR +#################################### + +UPLOAD_DIR = f"{DATA_DIR}/uploads" +Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True) + #################################### # OLLAMA_API_BASE_URL #################################### @@ -107,7 +110,7 @@ if WEBUI_AUTH and WEBUI_JWT_SECRET_KEY == "": # RAG #################################### -CHROMA_DATA_PATH = "./data/vector_db" +CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" EMBED_MODEL = "all-MiniLM-L6-v2" CHROMA_CLIENT = chromadb.PersistentClient( path=CHROMA_DATA_PATH, settings=Settings(allow_reset=True) diff --git a/backend/main.py b/backend/main.py index e4d4bdb5..f7a82b66 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,7 +14,7 @@ from apps.openai.main import app as openai_app from apps.web.main import app as webui_app from apps.rag.main import app as rag_app -from config import ENV +from config import ENV, FRONTEND_BUILD_DIR class SPAStaticFiles(StaticFiles): @@ -58,4 +58,8 @@ app.mount("/openai/api", openai_app) app.mount("/rag/api/v1", rag_app) -app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files") +app.mount( + "/", + SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True), + name="spa-static-files", +) diff --git a/backend/requirements.txt b/backend/requirements.txt index c28fcb68..68cba254 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -25,6 +25,10 @@ docx2txt unstructured markdown pypandoc +pandas +openpyxl +pyxlsb +xlrd PyJWT pyjwt[crypto] diff --git a/backend/start.sh b/backend/start.sh index e6d09364..09a791fc 100755 --- a/backend/start.sh +++ b/backend/start.sh @@ -4,4 +4,4 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) cd "$SCRIPT_DIR" || exit PORT="${PORT:-8080}" -uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*' +exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*' diff --git a/backend/utils/misc.py b/backend/utils/misc.py index 5635c57a..385a2c41 100644 --- a/backend/utils/misc.py +++ b/backend/utils/misc.py @@ -24,6 +24,16 @@ def calculate_sha256(file): return sha256.hexdigest() +def calculate_sha256_string(string): + # Create a new SHA-256 hash object + sha256_hash = hashlib.sha256() + # Update the hash object with the bytes of the input string + sha256_hash.update(string.encode("utf-8")) + # Get the hexadecimal representation of the hash + hashed_string = sha256_hash.hexdigest() + return hashed_string + + def validate_email_format(email: str) -> bool: if not re.match(r"[^@]+@[^@]+\.[^@]+", email): return False diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 00000000..6ceafb16 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy +Our primary goal is to ensure the protection and confidentiality of sensitive data stored by users on ollama-webui. +## Supported Versions + + +| Version | Supported | +| ------- | ------------------ | +| main | :white_check_mark: | +| others | :x: | + + +## Reporting a Vulnerability + +If you discover a security issue within our system, please notify us immediately via a pull request or contact us on discord. + +## Product Security +We regularly audit our internal processes and system's architecture for vulnerabilities using a combination of automated and manual testing techniques. + +We are planning on implementing SAST and SCA scans in our project soon. + diff --git a/run-compose.sh b/run-compose.sh index 0557bce9..7b0f8d2b 100755 --- a/run-compose.sh +++ b/run-compose.sh @@ -11,8 +11,8 @@ TICK='\u2713' # Detect GPU driver get_gpu_driver() { - # Detect NVIDIA GPUs - if lspci | grep -i nvidia >/dev/null; then + # Detect NVIDIA GPUs using lspci or nvidia-smi + if lspci | grep -i nvidia >/dev/null || nvidia-smi >/dev/null 2>&1; then echo "nvidia" return fi @@ -181,6 +181,9 @@ else DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.data.yaml" export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable fi + if [[ -n $webui_port ]]; then + export OLLAMA_WEBUI_PORT=$webui_port # Set OLLAMA_WEBUI_PORT environment variable + fi DEFAULT_COMPOSE_COMMAND+=" up -d" DEFAULT_COMPOSE_COMMAND+=" --remove-orphans" DEFAULT_COMPOSE_COMMAND+=" --force-recreate" diff --git a/run-ollama-docker.sh b/run-ollama-docker.sh index c8ce166a..c2a025be 100644 --- a/run-ollama-docker.sh +++ b/run-ollama-docker.sh @@ -10,10 +10,10 @@ docker pull ollama/ollama:latest docker_args="-d -v ollama:/root/.ollama -p $host_port:$container_port --name ollama ollama/ollama" -if [ "$use_gpu" == "y" ]; then - docker_args+=" --gpus=all" +if [ "$use_gpu" = "y" ]; then + docker_args="--gpus=all $docker_args" fi -docker run "$docker_args" +docker run $docker_args -docker image prune -f +docker image prune -f \ No newline at end of file diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts index 8734a588..5f16f83f 100644 --- a/src/lib/apis/auths/index.ts +++ b/src/lib/apis/auths/index.ts @@ -89,6 +89,37 @@ export const userSignUp = async (name: string, email: string, password: string) return res; }; +export const updateUserProfile = async (token: string, name: string, profileImageUrl: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/update/profile`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + name: name, + profile_image_url: profileImageUrl + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const updateUserPassword = async (token: string, password: string, newPassword: string) => { let error = null; diff --git a/src/lib/apis/utils/index.ts b/src/lib/apis/utils/index.ts new file mode 100644 index 00000000..ed4d4e02 --- /dev/null +++ b/src/lib/apis/utils/index.ts @@ -0,0 +1,23 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getGravatarUrl = async (email: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + return res; +}; diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 604a5689..96d7d2e1 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -6,7 +6,7 @@ import Prompts from './MessageInput/PromptCommands.svelte'; import Suggestions from './MessageInput/Suggestions.svelte'; - import { uploadDocToVectorDB } from '$lib/apis/rag'; + import { uploadDocToVectorDB, uploadWebToVectorDB } from '$lib/apis/rag'; import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte'; import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants'; import Documents from './MessageInput/Documents.svelte'; @@ -137,6 +137,33 @@ } }; + const uploadWeb = async (url) => { + console.log(url); + + const doc = { + type: 'doc', + name: url, + collection_name: '', + upload_status: false, + error: '' + }; + + try { + files = [...files, doc]; + const res = await uploadWebToVectorDB(localStorage.token, '', url); + + if (res) { + doc.upload_status = true; + doc.collection_name = res.collection_name; + files = files; + } + } catch (e) { + // Remove the failed doc from the files array + files = files.filter((f) => f.name !== url); + toast.error(e); + } + }; + onMount(() => { const dropZone = document.querySelector('body'); @@ -258,6 +285,10 @@ { + console.log(e); + uploadWeb(e.detail); + }} on:select={(e) => { console.log(e); files = [ diff --git a/src/lib/components/chat/MessageInput/Documents.svelte b/src/lib/components/chat/MessageInput/Documents.svelte index bcfb1916..5f252b3d 100644 --- a/src/lib/components/chat/MessageInput/Documents.svelte +++ b/src/lib/components/chat/MessageInput/Documents.svelte @@ -2,8 +2,9 @@ import { createEventDispatcher } from 'svelte'; import { documents } from '$lib/stores'; - import { removeFirstHashWord } from '$lib/utils'; + import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils'; import { tick } from 'svelte'; + import toast from 'svelte-french-toast'; export let prompt = ''; @@ -37,9 +38,20 @@ chatInputElement?.focus(); await tick(); }; + + const confirmSelectWeb = async (url) => { + dispatch('url', url); + + prompt = removeFirstHashWord(prompt); + const chatInputElement = document.getElementById('chat-textarea'); + + await tick(); + chatInputElement?.focus(); + await tick(); + }; -{#if filteredDocs.length > 0} +{#if filteredDocs.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
@@ -55,6 +67,7 @@ : ''}" type="button" on:click={() => { + console.log(doc); confirmSelect(doc); }} on:mousemove={() => { @@ -71,6 +84,29 @@
{/each} + + {#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')} + + {/if}
diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index ef88207f..915928b0 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -1,6 +1,7 @@ + +
+
+ { + const files = e?.target?.files ?? []; + let reader = new FileReader(); + reader.onload = (event) => { + let originalImageUrl = `${event.target.result}`; + + const img = new Image(); + img.src = originalImageUrl; + + img.onload = function () { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // Calculate the aspect ratio of the image + const aspectRatio = img.width / img.height; + + // Calculate the new width and height to fit within 100x100 + let newWidth, newHeight; + if (aspectRatio > 1) { + newWidth = 100 * aspectRatio; + newHeight = 100; + } else { + newWidth = 100; + newHeight = 100 / aspectRatio; + } + + // Set the canvas size + canvas.width = 100; + canvas.height = 100; + + // Calculate the position to center the image + const offsetX = (100 - newWidth) / 2; + const offsetY = (100 - newHeight) / 2; + + // Draw the image on the canvas + ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight); + + // Get the base64 representation of the compressed image + const compressedSrc = canvas.toDataURL('image/jpeg'); + + // Display the compressed image + profileImageUrl = compressedSrc; + + e.target.files = null; + }; + }; + + if ( + files.length > 0 && + ['image/gif', 'image/jpeg', 'image/png'].includes(files[0]['type']) + ) { + reader.readAsDataURL(files[0]); + } + }} + /> + +
Profile
+ +
+
+
+ +
+ +
+ +
+
+
Name
+ +
+ +
+
+
+
+ +
+ +
+ +
+ +
+
diff --git a/src/lib/components/chat/Settings/Account/UpdatePassword.svelte b/src/lib/components/chat/Settings/Account/UpdatePassword.svelte new file mode 100644 index 00000000..fe5253e5 --- /dev/null +++ b/src/lib/components/chat/Settings/Account/UpdatePassword.svelte @@ -0,0 +1,106 @@ + + +
{ + updatePasswordHandler(); + }} +> +
+
Change Password
+ +
+ + {#if show} +
+
+
Current Password
+ +
+ +
+
+ +
+
New Password
+ +
+ +
+
+ +
+
Confirm Password
+ +
+ +
+
+
+ +
+ +
+ {/if} +
diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 36ac2a62..e348c807 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -20,7 +20,7 @@ import { createNewChat, deleteAllChats, getAllChats, getChatList } from '$lib/apis/chats'; import { WEB_UI_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; - import { config, models, settings, user, chats } from '$lib/stores'; + import { config, models, voices, settings, user, chats } from '$lib/stores'; import { splitStream, getGravatarURL, getImportOrigin, convertOpenAIChats } from '$lib/utils'; import Advanced from './Settings/Advanced.svelte'; @@ -36,6 +36,8 @@ import { resetVectorDB } from '$lib/apis/rag'; import { setDefaultPromptSuggestions } from '$lib/apis/configs'; import { getBackendConfig } from '$lib/apis'; + import UpdatePassword from './Settings/Account/UpdatePassword.svelte'; + import Account from './Settings/Account.svelte'; export let show = false; @@ -112,6 +114,9 @@ let gravatarEmail = ''; let titleAutoGenerateModel = ''; + // Voice + let speakVoice = ''; + // Chats let saveChatHistory = true; let importFiles; @@ -123,6 +128,7 @@ let authContent = ''; // Account + let profileImageUrl = ''; let currentPassword = ''; let newPassword = ''; let newPasswordConfirm = ''; @@ -556,31 +562,6 @@ return models; }; - const updatePasswordHandler = async () => { - if (newPassword === newPasswordConfirm) { - const res = await updateUserPassword(localStorage.token, currentPassword, newPassword).catch( - (error) => { - toast.error(error); - return null; - } - ); - - if (res) { - toast.success('Successfully updated.'); - } - - currentPassword = ''; - newPassword = ''; - newPasswordConfirm = ''; - } else { - toast.error( - `The passwords you entered don't quite match. Please double-check and try again.` - ); - newPassword = ''; - newPasswordConfirm = ''; - } - }; - onMount(async () => { console.log('settings', $user.role === 'admin'); if ($user.role === 'admin') { @@ -613,15 +594,20 @@ responseAutoCopy = settings.responseAutoCopy ?? false; titleAutoGenerateModel = settings.titleAutoGenerateModel ?? ''; gravatarEmail = settings.gravatarEmail ?? ''; + speakVoice = settings.speakVoice ?? ''; + + const getVoicesLoop = setInterval(async () => { + const _voices = await speechSynthesis.getVoices(); + await voices.set(_voices); + + // do your loop + if (_voices.length > 0) { + clearInterval(getVoicesLoop); + } + }, 100); saveChatHistory = settings.saveChatHistory ?? true; - authEnabled = settings.authHeader !== undefined ? true : false; - if (authEnabled) { - authType = settings.authHeader.split(' ')[0]; - authContent = settings.authHeader.split(' ')[1]; - } - ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => { return ''; }); @@ -901,7 +887,7 @@ toggleTheme(); }} > - + -->
@@ -1602,6 +1588,9 @@
{ + saveSettings({ + speakVoice: speakVoice !== '' ? speakVoice : undefined + }); show = false; }} > @@ -1683,7 +1672,7 @@ bind:value={titleAutoGenerateModel} placeholder="Select a model" > - + {#each $models.filter((m) => m.size != null) as model}
-