Browse Source

Merge branch 'ollama-webui:main' into main

Gregory 1 year ago
parent
commit
4f5d06143c

+ 8 - 3
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:

+ 9 - 0
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

+ 15 - 1
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)

+ 35 - 11
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

+ 8 - 1
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)

+ 10 - 0
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

+ 31 - 0
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;
 

+ 23 - 0
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;
+};

+ 32 - 1
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 @@
 					<Documents
 						bind:this={documentsElement}
 						bind:prompt
+						on:url={(e) => {
+							console.log(e);
+							uploadWeb(e.detail);
+						}}
 						on:select={(e) => {
 							console.log(e);
 							files = [

+ 38 - 2
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();
+	};
 </script>
 
-{#if filteredDocs.length > 0}
+{#if filteredDocs.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
 	<div class="md:px-2 mb-3 text-left w-full">
 		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
 			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
@@ -55,6 +67,7 @@
 								: ''}"
 							type="button"
 							on:click={() => {
+								console.log(doc);
 								confirmSelect(doc);
 							}}
 							on:mousemove={() => {
@@ -71,6 +84,29 @@
 							</div>
 						</button>
 					{/each}
+
+					{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
+						<button
+							class="px-3 py-1.5 rounded-lg w-full text-left bg-gray-100 selected-command-option-button"
+							type="button"
+							on:click={() => {
+								const url = prompt.split(' ')?.at(0)?.substring(1);
+								if (isValidHttpUrl(url)) {
+									confirmSelectWeb(url);
+								} else {
+									toast.error(
+										'Oops! Looks like the URL is invalid. Please double-check and try again.'
+									);
+								}
+							}}
+						>
+							<div class=" font-medium text-black line-clamp-1">
+								{prompt.split(' ')?.at(0)?.substring(1)}
+							</div>
+
+							<div class=" text-xs text-gray-600 line-clamp-1">Web</div>
+						</button>
+					{/if}
 				</div>
 			</div>
 		</div>

+ 179 - 0
src/lib/components/chat/Settings/Account.svelte

@@ -0,0 +1,179 @@
+<script lang="ts">
+	import toast from 'svelte-french-toast';
+	import { onMount } from 'svelte';
+
+	import { user } from '$lib/stores';
+	import { updateUserProfile } from '$lib/apis/auths';
+
+	import UpdatePassword from './Account/UpdatePassword.svelte';
+	import { getGravatarUrl } from '$lib/apis/utils';
+
+	export let saveHandler: Function;
+
+	let profileImageUrl = '';
+	let name = '';
+
+	const submitHandler = async () => {
+		const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
+			(error) => {
+				toast.error(error);
+			}
+		);
+
+		if (updatedUser) {
+			await user.set(updatedUser);
+			return true;
+		}
+		return false;
+	};
+
+	onMount(() => {
+		name = $user.name;
+		profileImageUrl = $user.profile_image_url;
+	});
+</script>
+
+<div class="flex flex-col h-full justify-between text-sm">
+	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
+		<input
+			id="profile-image-input"
+			type="file"
+			hidden
+			accept="image/*"
+			on:change={(e) => {
+				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]);
+				}
+			}}
+		/>
+
+		<div class=" mb-2.5 text-sm font-medium">Profile</div>
+
+		<div class="flex space-x-5">
+			<div class="flex flex-col">
+				<div class="self-center">
+					<button
+						class="relative rounded-full dark:bg-gray-700"
+						type="button"
+						on:click={() => {
+							document.getElementById('profile-image-input')?.click();
+						}}
+					>
+						<img
+							src={profileImageUrl !== '' ? profileImageUrl : '/user.png'}
+							alt="profile"
+							class=" rounded-full w-16 h-16 object-cover"
+						/>
+
+						<div
+							class="absolute flex justify-center rounded-full bottom-0 left-0 right-0 top-0 h-full w-full overflow-hidden bg-gray-700 bg-fixed opacity-0 transition duration-300 ease-in-out hover:opacity-50"
+						>
+							<div class="my-auto text-gray-100">
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									viewBox="0 0 20 20"
+									fill="currentColor"
+									class="w-5 h-5"
+								>
+									<path
+										d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
+									/>
+								</svg>
+							</div>
+						</div>
+					</button>
+				</div>
+				<button
+					class=" text-xs text-gray-600"
+					on:click={async () => {
+						const url = await getGravatarUrl($user.email);
+
+						profileImageUrl = url;
+					}}>Use Gravatar</button
+				>
+			</div>
+
+			<div class="flex-1">
+				<div class="flex flex-col w-full">
+					<div class=" mb-1 text-xs text-gray-500">Name</div>
+
+					<div class="flex-1">
+						<input
+							class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+							type="text"
+							bind:value={name}
+							required
+						/>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<hr class=" dark:border-gray-700 my-4" />
+		<UpdatePassword />
+	</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"
+			on:click={async () => {
+				const res = await submitHandler();
+
+				if (res) {
+					saveHandler();
+				}
+			}}
+		>
+			Save
+		</button>
+	</div>
+</div>

+ 106 - 0
src/lib/components/chat/Settings/Account/UpdatePassword.svelte

@@ -0,0 +1,106 @@
+<script lang="ts">
+	import toast from 'svelte-french-toast';
+	import { updateUserPassword } from '$lib/apis/auths';
+
+	let show = false;
+	let currentPassword = '';
+	let newPassword = '';
+	let newPasswordConfirm = '';
+
+	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 = '';
+		}
+	};
+</script>
+
+<form
+	class="flex flex-col text-sm"
+	on:submit|preventDefault={() => {
+		updatePasswordHandler();
+	}}
+>
+	<div class="flex justify-between mb-2.5 items-center text-sm">
+		<div class="  font-medium">Change Password</div>
+		<button
+			class=" text-xs font-medium text-gray-500"
+			type="button"
+			on:click={() => {
+				show = !show;
+			}}>{show ? 'Hide' : 'Show'}</button
+		>
+	</div>
+
+	{#if show}
+		<div class=" space-y-1.5">
+			<div class="flex flex-col w-full">
+				<div class=" mb-1 text-xs text-gray-500">Current Password</div>
+
+				<div class="flex-1">
+					<input
+						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+						type="password"
+						bind:value={currentPassword}
+						autocomplete="current-password"
+						required
+					/>
+				</div>
+			</div>
+
+			<div class="flex flex-col w-full">
+				<div class=" mb-1 text-xs text-gray-500">New Password</div>
+
+				<div class="flex-1">
+					<input
+						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+						type="password"
+						bind:value={newPassword}
+						autocomplete="new-password"
+						required
+					/>
+				</div>
+			</div>
+
+			<div class="flex flex-col w-full">
+				<div class=" mb-1 text-xs text-gray-500">Confirm Password</div>
+
+				<div class="flex-1">
+					<input
+						class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+						type="password"
+						bind:value={newPasswordConfirm}
+						autocomplete="off"
+						required
+					/>
+				</div>
+			</div>
+		</div>
+
+		<div class="mt-3 flex justify-end">
+			<button
+				class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
+			>
+				Update password
+			</button>
+		</div>
+	{/if}
+</form>

+ 7 - 208
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}
 						</div>
 					</div>
-				{:else if selectedTab === 'auth'}
-					<form
-						class="flex flex-col h-full justify-between space-y-3 text-sm"
-						on:submit|preventDefault={() => {
-							console.log('auth save');
-							saveSettings({
-								authHeader: authEnabled ? `${authType} ${authContent}` : undefined
-							});
-							show = false;
-						}}
-					>
-						<div class=" space-y-3">
-							<div>
-								<div class=" py-1 flex w-full justify-between">
-									<div class=" self-center text-sm font-medium">Authorization Header</div>
-
-									<button
-										class="p-1 px-3 text-xs flex rounded transition"
-										type="button"
-										on:click={() => {
-											toggleAuthHeader();
-										}}
-									>
-										{#if authEnabled === true}
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												viewBox="0 0 24 24"
-												fill="currentColor"
-												class="w-4 h-4"
-											>
-												<path
-													fill-rule="evenodd"
-													d="M12 1.5a5.25 5.25 0 00-5.25 5.25v3a3 3 0 00-3 3v6.75a3 3 0 003 3h10.5a3 3 0 003-3v-6.75a3 3 0 00-3-3v-3c0-2.9-2.35-5.25-5.25-5.25zm3.75 8.25v-3a3.75 3.75 0 10-7.5 0v3h7.5z"
-													clip-rule="evenodd"
-												/>
-											</svg>
-
-											<span class="ml-2 self-center"> On </span>
-										{:else}
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												viewBox="0 0 24 24"
-												fill="currentColor"
-												class="w-4 h-4"
-											>
-												<path
-													d="M18 1.5c2.9 0 5.25 2.35 5.25 5.25v3.75a.75.75 0 01-1.5 0V6.75a3.75 3.75 0 10-7.5 0v3a3 3 0 013 3v6.75a3 3 0 01-3 3H3.75a3 3 0 01-3-3v-6.75a3 3 0 013-3h9v-3c0-2.9 2.35-5.25 5.25-5.25z"
-												/>
-											</svg>
-
-											<span class="ml-2 self-center">Off</span>
-										{/if}
-									</button>
-								</div>
-							</div>
-
-							{#if authEnabled}
-								<hr class=" dark:border-gray-700" />
-
-								<div class="mt-2">
-									<div class=" py-1 flex w-full space-x-2">
-										<button
-											class=" py-1 font-semibold flex rounded transition"
-											on:click={() => {
-												authType = authType === 'Basic' ? 'Bearer' : 'Basic';
-											}}
-											type="button"
-										>
-											{#if authType === 'Basic'}
-												<span class="self-center mr-2">Basic</span>
-											{:else if authType === 'Bearer'}
-												<span class="self-center mr-2">Bearer</span>
-											{/if}
-										</button>
-
-										<div class="flex-1">
-											<input
-												class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
-												placeholder="Enter Authorization Header Content"
-												bind:value={authContent}
-											/>
-										</div>
-									</div>
-									<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
-										Toggle between <span class=" text-gray-500 dark:text-gray-300 font-medium"
-											>'Basic'</span
-										>
-										and <span class=" text-gray-500 dark:text-gray-300 font-medium">'Bearer'</span> by
-										clicking on the label next to the input.
-									</div>
-								</div>
-
-								<hr class=" dark:border-gray-700" />
-
-								<div>
-									<div class=" mb-2.5 text-sm font-medium">Preview Authorization Header</div>
-									<textarea
-										value={JSON.stringify({
-											Authorization: `${authType} ${authContent}`
-										})}
-										class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
-										rows="2"
-										disabled
-									/>
-								</div>
-							{/if}
-						</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>
 				{:else if selectedTab === 'account'}
-					<form
-						class="flex flex-col h-full text-sm"
-						on:submit|preventDefault={() => {
-							updatePasswordHandler();
+					<Account
+						saveHandler={() => {
+							show = false;
 						}}
-					>
-						<div class=" mb-2.5 font-medium">Change Password</div>
-
-						<div class=" space-y-1.5">
-							<div class="flex flex-col w-full">
-								<div class=" mb-1 text-xs text-gray-500">Current Password</div>
-
-								<div class="flex-1">
-									<input
-										class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
-										type="password"
-										bind:value={currentPassword}
-										autocomplete="current-password"
-										required
-									/>
-								</div>
-							</div>
-
-							<div class="flex flex-col w-full">
-								<div class=" mb-1 text-xs text-gray-500">New Password</div>
-
-								<div class="flex-1">
-									<input
-										class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
-										type="password"
-										bind:value={newPassword}
-										autocomplete="new-password"
-										required
-									/>
-								</div>
-							</div>
-
-							<div class="flex flex-col w-full">
-								<div class=" mb-1 text-xs text-gray-500">Confirm Password</div>
-
-								<div class="flex-1">
-									<input
-										class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
-										type="password"
-										bind:value={newPasswordConfirm}
-										autocomplete="off"
-										required
-									/>
-								</div>
-							</div>
-						</div>
-
-						<div class="mt-3 flex justify-end">
-							<button
-								class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
-							>
-								Update password
-							</button>
-						</div>
-					</form>
+					/>
 				{:else if selectedTab === 'about'}
 					<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
 						<div class=" space-y-3">

+ 52 - 31
src/lib/utils/index.ts

@@ -212,8 +212,12 @@ const convertOpenAIMessages = (convo) => {
 		const message = mapping[message_id];
 		currentId = message_id;
 		try {
-				if (messages.length == 0 && (message['message'] == null || 
-				(message['message']['content']['parts']?.[0] == '' && message['message']['content']['text'] == null))) {
+			if (
+				messages.length == 0 &&
+				(message['message'] == null ||
+					(message['message']['content']['parts']?.[0] == '' &&
+						message['message']['content']['text'] == null))
+			) {
 				// Skip chat messages with no content
 				continue;
 			} else {
@@ -222,7 +226,10 @@ const convertOpenAIMessages = (convo) => {
 					parentId: lastId,
 					childrenIds: message['children'] || [],
 					role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user',
-					content: message['message']?.['content']?.['parts']?.[0] ||  message['message']?.['content']?.['text'] || '',
+					content:
+						message['message']?.['content']?.['parts']?.[0] ||
+						message['message']?.['content']?.['text'] ||
+						'',
 					model: 'gpt-3.5-turbo',
 					done: true,
 					context: null
@@ -231,7 +238,7 @@ const convertOpenAIMessages = (convo) => {
 				lastId = currentId;
 			}
 		} catch (error) {
-			console.log("Error with", message, "\nError:", error);
+			console.log('Error with', message, '\nError:', error);
 		}
 	}
 
@@ -256,31 +263,31 @@ const validateChat = (chat) => {
 	// Because ChatGPT sometimes has features we can't use like DALL-E or migh have corrupted messages, need to validate
 	const messages = chat.messages;
 
-    // Check if messages array is empty
-    if (messages.length === 0) {
-        return false;
-    }
-
-    // Last message's children should be an empty array
-    const lastMessage = messages[messages.length - 1];
-    if (lastMessage.childrenIds.length !== 0) {
-        return false;
-    }
-
-    // First message's parent should be null
-    const firstMessage = messages[0];
-    if (firstMessage.parentId !== null) {
-        return false;
-    }
-
-    // Every message's content should be a string
-    for (let message of messages) {
-        if (typeof message.content !== 'string') {
-            return false;
-        }
-    }
-
-    return true;
+	// Check if messages array is empty
+	if (messages.length === 0) {
+		return false;
+	}
+
+	// Last message's children should be an empty array
+	const lastMessage = messages[messages.length - 1];
+	if (lastMessage.childrenIds.length !== 0) {
+		return false;
+	}
+
+	// First message's parent should be null
+	const firstMessage = messages[0];
+	if (firstMessage.parentId !== null) {
+		return false;
+	}
+
+	// Every message's content should be a string
+	for (let message of messages) {
+		if (typeof message.content !== 'string') {
+			return false;
+		}
+	}
+
+	return true;
 };
 
 export const convertOpenAIChats = (_chats) => {
@@ -298,8 +305,22 @@ export const convertOpenAIChats = (_chats) => {
 				chat: chat,
 				timestamp: convo['timestamp']
 			});
-		} else { failed ++}
+		} else {
+			failed++;
+		}
 	}
-	console.log(failed, "Conversations could not be imported");
+	console.log(failed, 'Conversations could not be imported');
 	return chats;
 };
+
+export const isValidHttpUrl = (string) => {
+	let url;
+
+	try {
+		url = new URL(string);
+	} catch (_) {
+		return false;
+	}
+
+	return url.protocol === 'http:' || url.protocol === 'https:';
+};