diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 6da870ea7..85bc995ae 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -37,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 @@ -124,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: diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py index 00c66a2c7..367db3ff7 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 f86697f44..c6d8b38d8 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 a0772223f..f45c67ac2 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 9adf2801b..86e1a9e58 100644 --- a/backend/apps/web/routers/utils.py +++ b/backend/apps/web/routers/utils.py @@ -9,7 +9,7 @@ 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, DATA_DIR, UPLOAD_DIR from constants import ERROR_MESSAGES @@ -165,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/utils/misc.py b/backend/utils/misc.py index 5635c57ac..385a2c415 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/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts index 8734a5885..5f16f83f5 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 000000000..ed4d4e029 --- /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 604a56893..96d7d2e17 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 bcfb19163..5f252b3df 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/Settings/Account.svelte b/src/lib/components/chat/Settings/Account.svelte new file mode 100644 index 000000000..e4ae634c7 --- /dev/null +++ b/src/lib/components/chat/Settings/Account.svelte @@ -0,0 +1,179 @@ + + +
+
+ { + 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 000000000..fe5253e54 --- /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 f04e9e5f0..e348c807a 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.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; @@ -126,6 +128,7 @@ let authContent = ''; // Account + let profileImageUrl = ''; let currentPassword = ''; let newPassword = ''; let newPasswordConfirm = ''; @@ -559,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') { @@ -616,7 +594,6 @@ responseAutoCopy = settings.responseAutoCopy ?? false; titleAutoGenerateModel = settings.titleAutoGenerateModel ?? ''; gravatarEmail = settings.gravatarEmail ?? ''; - speakVoice = settings.speakVoice ?? ''; const getVoicesLoop = setInterval(async () => { @@ -631,12 +608,6 @@ 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 ''; }); @@ -2040,184 +2011,12 @@ {/if} - {:else if selectedTab === 'auth'} -
{ - console.log('auth save'); - saveSettings({ - authHeader: authEnabled ? `${authType} ${authContent}` : undefined - }); + {:else if selectedTab === 'account'} + { show = false; }} - > -
-
-
-
Authorization Header
- - -
-
- - {#if authEnabled} -
- -
-
- - -
- -
-
-
- Toggle between 'Basic' - and 'Bearer' by - clicking on the label next to the input. -
-
- -
- -
-
Preview Authorization Header
-