浏览代码

feat: multi-user support w/ RBAC

Timothy J. Baek 1 年之前
父节点
当前提交
921eef03b3

+ 20 - 2
backend/apps/ollama/main.py

@@ -1,4 +1,4 @@
-from flask import Flask, request, Response
+from flask import Flask, request, Response, jsonify
 from flask_cors import CORS
 from flask_cors import CORS
 
 
 
 
@@ -6,7 +6,10 @@ import requests
 import json
 import json
 
 
 
 
-from config import OLLAMA_API_BASE_URL
+from apps.web.models.users import Users
+from constants import ERROR_MESSAGES
+from utils import extract_token_from_auth_header
+from config import OLLAMA_API_BASE_URL, OLLAMA_WEBUI_AUTH
 
 
 app = Flask(__name__)
 app = Flask(__name__)
 CORS(
 CORS(
@@ -28,6 +31,21 @@ def proxy(path):
     data = request.get_data()
     data = request.get_data()
     headers = dict(request.headers)
     headers = dict(request.headers)
 
 
+    if OLLAMA_WEBUI_AUTH:
+        if "Authorization" in headers:
+            token = extract_token_from_auth_header(headers["Authorization"])
+            user = Users.get_user_by_token(token)
+            if user:
+                print(user)
+                pass
+            else:
+                return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
+        else:
+            return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
+
+    else:
+        pass
+
     # Make a request to the target server
     # Make a request to the target server
     target_response = requests.request(
     target_response = requests.request(
         method=request.method,
         method=request.method,

+ 25 - 0
backend/apps/web/main.py

@@ -0,0 +1,25 @@
+from fastapi import FastAPI, Request, Depends, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+
+from apps.web.routers import auths
+from config import OLLAMA_WEBUI_VERSION, OLLAMA_WEBUI_AUTH
+
+app = FastAPI()
+
+origins = ["*"]
+
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=origins,
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+
+app.include_router(auths.router, prefix="/auths", tags=["auths"])
+
+
+@app.get("/")
+async def get_status():
+    return {"status": True, "version": OLLAMA_WEBUI_VERSION, "auth": OLLAMA_WEBUI_AUTH}

+ 102 - 0
backend/apps/web/models/auths.py

@@ -0,0 +1,102 @@
+from pydantic import BaseModel
+from typing import List, Union, Optional
+import time
+import uuid
+
+
+from apps.web.models.users import UserModel, Users
+from utils import (
+    verify_password,
+    get_password_hash,
+    bearer_scheme,
+    create_token,
+)
+
+import config
+
+DB = config.DB
+
+####################
+# DB MODEL
+####################
+
+
+class AuthModel(BaseModel):
+    id: str
+    email: str
+    password: str
+    active: bool = True
+
+
+####################
+# Forms
+####################
+
+
+class Token(BaseModel):
+    token: str
+    token_type: str
+
+
+class UserResponse(BaseModel):
+    id: str
+    email: str
+    name: str
+    role: str
+
+
+class SigninResponse(Token, UserResponse):
+    pass
+
+
+class SigninForm(BaseModel):
+    email: str
+    password: str
+
+
+class SignupForm(BaseModel):
+    name: str
+    email: str
+    password: str
+
+
+class AuthsTable:
+    def __init__(self, db):
+        self.db = db
+        self.table = db.auths
+
+    def insert_new_auth(
+        self, email: str, password: str, name: str, role: str = "user"
+    ) -> Optional[UserModel]:
+        print("insert_new_auth")
+
+        id = str(uuid.uuid4())
+
+        auth = AuthModel(
+            **{"id": id, "email": email, "password": password, "active": True}
+        )
+        result = self.table.insert_one(auth.model_dump())
+        user = Users.insert_new_user(id, name, email, role)
+
+        print(result, user)
+        if result and user:
+            return user
+        else:
+            return None
+
+    def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
+        print("authenticate_user")
+
+        auth = self.table.find_one({"email": email, "active": True})
+
+        if auth:
+            if verify_password(password, auth["password"]):
+                user = self.db.users.find_one({"id": auth["id"]})
+                return UserModel(**user)
+            else:
+                return None
+        else:
+            return None
+
+
+Auths = AuthsTable(DB)

+ 76 - 0
backend/apps/web/models/users.py

@@ -0,0 +1,76 @@
+from pydantic import BaseModel
+from typing import List, Union, Optional
+from pymongo import ReturnDocument
+import time
+
+from utils import decode_token
+from config import DB
+
+####################
+# User DB Schema
+####################
+
+
+class UserModel(BaseModel):
+    id: str
+    name: str
+    email: str
+    role: str = "user"
+    created_at: int  # timestamp in epoch
+
+
+####################
+# Forms
+####################
+
+
+class UsersTable:
+    def __init__(self, db):
+        self.db = db
+        self.table = db.users
+
+    def insert_new_user(
+        self, id: str, name: str, email: str, role: str = "user"
+    ) -> Optional[UserModel]:
+        user = UserModel(
+            **{
+                "id": id,
+                "name": name,
+                "email": email,
+                "role": role,
+                "created_at": int(time.time()),
+            }
+        )
+        result = self.table.insert_one(user.model_dump())
+
+        if result:
+            return user
+        else:
+            return None
+
+    def get_user_by_email(self, email: str) -> Optional[UserModel]:
+        user = self.table.find_one({"email": email}, {"_id": False})
+
+        if user:
+            return UserModel(**user)
+        else:
+            return None
+
+    def get_user_by_token(self, token: str) -> Optional[UserModel]:
+        data = decode_token(token)
+
+        if data != None and "email" in data:
+            return self.get_user_by_email(data["email"])
+        else:
+            return None
+
+    def get_users(self, skip: int = 0, limit: int = 50) -> Optional[UserModel]:
+        return [
+            UserModel(**user)
+            for user in list(self.table.find({}, {"_id": False}))
+            .skip(skip)
+            .limit(limit)
+        ]
+
+
+Users = UsersTable(DB)

+ 107 - 0
backend/apps/web/routers/auths.py

@@ -0,0 +1,107 @@
+from fastapi import Response
+from fastapi import Depends, FastAPI, HTTPException, status
+from datetime import datetime, timedelta
+from typing import List, Union
+
+from fastapi import APIRouter
+from pydantic import BaseModel
+import time
+import uuid
+
+from constants import ERROR_MESSAGES
+from utils import (
+    get_password_hash,
+    bearer_scheme,
+    create_token,
+)
+
+from apps.web.models.auths import (
+    SigninForm,
+    SignupForm,
+    UserResponse,
+    SigninResponse,
+    Auths,
+)
+from apps.web.models.users import Users
+import config
+
+router = APIRouter()
+
+DB = config.DB
+
+
+############################
+# GetSessionUser
+############################
+
+
+@router.get("/", response_model=UserResponse)
+async def get_session_user(cred=Depends(bearer_scheme)):
+    token = cred.credentials
+    user = Users.get_user_by_token(token)
+    if user:
+        return {
+            "id": user.id,
+            "email": user.email,
+            "name": user.name,
+            "role": user.role,
+        }
+    else:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+        )
+
+
+############################
+# SignIn
+############################
+
+
+@router.post("/signin", response_model=SigninResponse)
+async def signin(form_data: SigninForm):
+    user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
+    if user:
+        token = create_token(data={"email": user.email})
+
+        return {
+            "token": token,
+            "token_type": "Bearer",
+            "id": user.id,
+            "email": user.email,
+            "name": user.name,
+            "role": user.role,
+        }
+    else:
+        raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT())
+
+
+############################
+# SignUp
+############################
+
+
+@router.post("/signup", response_model=SigninResponse)
+async def signup(form_data: SignupForm):
+    if not Users.get_user_by_email(form_data.email.lower()):
+        try:
+            hashed = get_password_hash(form_data.password)
+            user = Auths.insert_new_auth(form_data.email, hashed, form_data.name)
+
+            if user:
+                token = create_token(data={"email": user.email})
+                # response.set_cookie(key='token', value=token, httponly=True)
+
+                return {
+                    "token": token,
+                    "token_type": "Bearer",
+                    "id": user.id,
+                    "email": user.email,
+                    "name": user.name,
+                    "role": user.role,
+                }
+            else:
+                raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
+        except Exception as err:
+            raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
+    else:
+        raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT())

+ 52 - 2
backend/config.py

@@ -1,11 +1,22 @@
-import sys
-import os
 from dotenv import load_dotenv, find_dotenv
 from dotenv import load_dotenv, find_dotenv
+from pymongo import MongoClient
+
+from secrets import token_bytes
+from base64 import b64encode
+import os
 
 
 load_dotenv(find_dotenv())
 load_dotenv(find_dotenv())
 
 
+####################################
+# ENV (dev,test,prod)
+####################################
+
 ENV = os.environ.get("ENV", "dev")
 ENV = os.environ.get("ENV", "dev")
 
 
+####################################
+# OLLAMA_API_BASE_URL
+####################################
+
 OLLAMA_API_BASE_URL = os.environ.get(
 OLLAMA_API_BASE_URL = os.environ.get(
     "OLLAMA_API_BASE_URL", "http://localhost:11434/api"
     "OLLAMA_API_BASE_URL", "http://localhost:11434/api"
 )
 )
@@ -13,3 +24,42 @@ OLLAMA_API_BASE_URL = os.environ.get(
 if ENV == "prod":
 if ENV == "prod":
     if OLLAMA_API_BASE_URL == "/ollama/api":
     if OLLAMA_API_BASE_URL == "/ollama/api":
         OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api"
         OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api"
+
+####################################
+# OLLAMA_WEBUI_VERSION
+####################################
+
+OLLAMA_WEBUI_VERSION = os.environ.get("OLLAMA_WEBUI_VERSION", "v1.0.0-alpha.9")
+
+####################################
+# OLLAMA_WEBUI_AUTH
+####################################
+
+OLLAMA_WEBUI_AUTH = (
+    True if os.environ.get("OLLAMA_WEBUI_AUTH", "TRUE") == "TRUE" else False
+)
+
+
+if OLLAMA_WEBUI_AUTH:
+    ####################################
+    # OLLAMA_WEBUI_DB
+    ####################################
+
+    OLLAMA_WEBUI_DB_URL = os.environ.get(
+        "OLLAMA_WEBUI_DB_URL", "mongodb://root:root@localhost:27017/"
+    )
+
+    DB_CLIENT = MongoClient(f"{OLLAMA_WEBUI_DB_URL}?authSource=admin")
+    DB = DB_CLIENT["ollama-webui"]
+
+    ####################################
+    # OLLAMA_WEBUI_JWT_SECRET_KEY
+    ####################################
+
+    OLLAMA_WEBUI_JWT_SECRET_KEY = os.environ.get(
+        "OLLAMA_WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t"
+    )
+
+    if ENV == "prod":
+        if OLLAMA_WEBUI_JWT_SECRET_KEY == "":
+            OLLAMA_WEBUI_JWT_SECRET_KEY = str(b64encode(token_bytes(32)).decode())

+ 13 - 0
backend/constants.py

@@ -0,0 +1,13 @@
+from enum import Enum
+
+
+class MESSAGES(str, Enum):
+    DEFAULT = lambda msg="": f"{msg if msg else ''}"
+
+
+class ERROR_MESSAGES(str, Enum):
+    DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
+    UNAUTHORIZED = "401 Unauthorized"
+    NOT_FOUND = "We could not find what you're looking for :/"
+    USER_NOT_FOUND = "We could not find what you're looking for :/"
+    MALICIOUS = "Unusual activities detected, please try again in a few minutes."

+ 5 - 6
backend/main.py

@@ -1,16 +1,14 @@
-import time
-import sys
-
 from fastapi import FastAPI, Request
 from fastapi import FastAPI, Request
 from fastapi.staticfiles import StaticFiles
 from fastapi.staticfiles import StaticFiles
-
 from fastapi import HTTPException
 from fastapi import HTTPException
-from starlette.exceptions import HTTPException as StarletteHTTPException
-
 from fastapi.middleware.wsgi import WSGIMiddleware
 from fastapi.middleware.wsgi import WSGIMiddleware
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
+from starlette.exceptions import HTTPException as StarletteHTTPException
 
 
 from apps.ollama.main import app as ollama_app
 from apps.ollama.main import app as ollama_app
+from apps.web.main import app as webui_app
+
+import time
 
 
 
 
 class SPAStaticFiles(StaticFiles):
 class SPAStaticFiles(StaticFiles):
@@ -47,5 +45,6 @@ async def check_url(request: Request, call_next):
     return response
     return response
 
 
 
 
+app.mount("/api/v1", webui_app)
 app.mount("/ollama/api", WSGIMiddleware(ollama_app))
 app.mount("/ollama/api", WSGIMiddleware(ollama_app))
 app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files")
 app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files")

+ 68 - 0
backend/utils.py

@@ -0,0 +1,68 @@
+from fastapi.security import HTTPBasicCredentials, HTTPBearer
+from pydantic import BaseModel
+from typing import Union, Optional
+
+from passlib.context import CryptContext
+from datetime import datetime, timedelta
+import requests
+import jwt
+
+import config
+
+JWT_SECRET_KEY = config.OLLAMA_WEBUI_JWT_SECRET_KEY
+ALGORITHM = "HS256"
+
+##############
+# Auth Utils
+##############
+
+bearer_scheme = HTTPBearer()
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+
+def verify_password(plain_password, hashed_password):
+    return (
+        pwd_context.verify(plain_password, hashed_password) if hashed_password else None
+    )
+
+
+def get_password_hash(password):
+    return pwd_context.hash(password)
+
+
+def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str:
+    payload = data.copy()
+
+    if expires_delta:
+        expire = datetime.utcnow() + expires_delta
+        payload.update({"exp": expire})
+
+    encoded_jwt = jwt.encode(payload, JWT_SECRET_KEY, algorithm=ALGORITHM)
+    return encoded_jwt
+
+
+def decode_token(token: str) -> Optional[dict]:
+    try:
+        decoded = jwt.decode(token, JWT_SECRET_KEY, options={"verify_signature": False})
+        return decoded
+    except Exception as e:
+        return None
+
+
+def extract_token_from_auth_header(auth_header: str):
+    return auth_header[len("Bearer ") :]
+
+
+def verify_token(request):
+    try:
+        bearer = request.headers["authorization"]
+        if bearer:
+            token = bearer[len("Bearer ") :]
+            decoded = jwt.decode(
+                token, JWT_SECRET_KEY, options={"verify_signature": False}
+            )
+            return decoded
+        else:
+            return None
+    except Exception as e:
+        return None

+ 11 - 0
compose.yaml

@@ -22,6 +22,15 @@ services:
     restart: unless-stopped
     restart: unless-stopped
     image: ollama/ollama:latest
     image: ollama/ollama:latest
 
 
+  ollama-webui-db:
+    image: mongo
+    container_name: ollama-webui-db
+    restart: always
+    # Make sure to change the username/password!
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: root
+      MONGO_INITDB_ROOT_PASSWORD: example
+
   ollama-webui:
   ollama-webui:
     build:
     build:
       context: .
       context: .
@@ -32,10 +41,12 @@ services:
     container_name: ollama-webui
     container_name: ollama-webui
     depends_on:
     depends_on:
       - ollama
       - ollama
+      - ollama-webui-db
     ports:
     ports:
       - 3000:8080
       - 3000:8080
     environment:
     environment:
       - "OLLAMA_API_BASE_URL=http://ollama:11434/api"
       - "OLLAMA_API_BASE_URL=http://ollama:11434/api"
+      - "OLLAMA_WEBUI_DB_URL=mongodb://root:example@ollama-webui-db:27017/"
     extra_hosts:
     extra_hosts:
       - host.docker.internal:host-gateway
       - host.docker.internal:host-gateway
     restart: unless-stopped
     restart: unless-stopped

+ 6 - 1
src/app.css

@@ -4,8 +4,13 @@
 	font-display: swap;
 	font-display: swap;
 }
 }
 
 
+@font-face {
+	font-family: 'Mona Sans';
+	src: url('/assets/fonts/Mona-Sans.woff2');
+	font-display: swap;
+}
+
 html {
 html {
-	@apply bg-gray-800;
 	word-break: break-word;
 	word-break: break-word;
 }
 }
 
 

+ 7 - 4
src/lib/components/chat/SettingsModal.svelte

@@ -2,9 +2,10 @@
 	import sha256 from 'js-sha256';
 	import sha256 from 'js-sha256';
 	import Modal from '../common/Modal.svelte';
 	import Modal from '../common/Modal.svelte';
 
 
-	import { WEB_UI_VERSION, API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants';
+	import { WEB_UI_VERSION, OLLAMA_API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants';
 	import toast from 'svelte-french-toast';
 	import toast from 'svelte-french-toast';
 	import { onMount } from 'svelte';
 	import { onMount } from 'svelte';
+	import { config, user } from '$lib/stores';
 
 
 	export let show = false;
 	export let show = false;
 	export let saveSettings: Function;
 	export let saveSettings: Function;
@@ -119,7 +120,8 @@
 		const res = await fetch(`${API_BASE_URL}/pull`, {
 		const res = await fetch(`${API_BASE_URL}/pull`, {
 			method: 'POST',
 			method: 'POST',
 			headers: {
 			headers: {
-				'Content-Type': 'text/event-stream'
+				'Content-Type': 'text/event-stream',
+				...($user && { Authorization: `Bearer ${localStorage.token}` })
 			},
 			},
 			body: JSON.stringify({
 			body: JSON.stringify({
 				name: modelTag
 				name: modelTag
@@ -175,7 +177,8 @@
 		const res = await fetch(`${API_BASE_URL}/delete`, {
 		const res = await fetch(`${API_BASE_URL}/delete`, {
 			method: 'DELETE',
 			method: 'DELETE',
 			headers: {
 			headers: {
-				'Content-Type': 'text/event-stream'
+				'Content-Type': 'text/event-stream',
+				...($user && { Authorization: `Bearer ${localStorage.token}` })
 			},
 			},
 			body: JSON.stringify({
 			body: JSON.stringify({
 				name: deleteModelTag
 				name: deleteModelTag
@@ -992,7 +995,7 @@
 								<div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div>
 								<div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div>
 								<div class="flex w-full">
 								<div class="flex w-full">
 									<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
 									<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
-										{WEB_UI_VERSION}
+										{$config && $config.version ? $config.version : WEB_UI_VERSION}
 									</div>
 									</div>
 								</div>
 								</div>
 							</div>
 							</div>

+ 126 - 30
src/lib/components/layout/Navbar.svelte

@@ -1,4 +1,6 @@
 <script lang="ts">
 <script lang="ts">
+	import { goto } from '$app/navigation';
+	import { user } from '$lib/stores';
 	import { onMount } from 'svelte';
 	import { onMount } from 'svelte';
 
 
 	let show = false;
 	let show = false;
@@ -22,9 +24,9 @@
 	let chatTitleEditIdx = null;
 	let chatTitleEditIdx = null;
 	let chatTitle = '';
 	let chatTitle = '';
 
 
-	let _chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
+	let showDropdown = false;
 
 
-	onMount(() => {});
+	let _chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
 
 
 	$: if (chats) {
 	$: if (chats) {
 		_chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
 		_chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
@@ -98,6 +100,7 @@
 			<button
 			<button
 				class="flex-grow flex justify-between rounded-md px-3 py-1.5 my-2 hover:bg-gray-900 transition"
 				class="flex-grow flex justify-between rounded-md px-3 py-1.5 my-2 hover:bg-gray-900 transition"
 				on:click={() => {
 				on:click={() => {
+					// goto('/');
 					createNewChat();
 					createNewChat();
 				}}
 				}}
 			>
 			>
@@ -163,6 +166,7 @@
 							? 'bg-gray-900'
 							? 'bg-gray-900'
 							: ''} transition whitespace-nowrap text-ellipsis"
 							: ''} transition whitespace-nowrap text-ellipsis"
 						on:click={() => {
 						on:click={() => {
+							// goto(`/c/${chat.id}`);
 							if (chat.id !== chatTitleEditIdx) {
 							if (chat.id !== chatTitleEditIdx) {
 								chatTitleEditIdx = null;
 								chatTitleEditIdx = null;
 								chatTitle = '';
 								chatTitle = '';
@@ -380,35 +384,127 @@
 					</div>
 					</div>
 					<div class=" self-center">Clear conversations</div>
 					<div class=" self-center">Clear conversations</div>
 				</button>
 				</button>
-				<button
-					class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
-					on:click={() => {
-						openSettings();
-					}}
-				>
-					<div class=" self-center mr-3">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							fill="none"
-							viewBox="0 0 24 24"
-							stroke-width="1.5"
-							stroke="currentColor"
-							class="w-5 h-5"
+
+				{#if $user !== undefined}
+					<button
+						class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
+						on:focus={() => {
+							showDropdown = true;
+						}}
+						on:focusout={() => {
+							setTimeout(() => {
+								showDropdown = false;
+							}, 100);
+						}}
+					>
+						<div class=" self-center mr-3">
+							<img src="/user.png" class=" max-w-[30px] object-cover rounded-full" />
+						</div>
+						<div class=" self-center font-semibold">{$user.name}</div>
+					</button>
+
+					{#if showDropdown}
+						<div
+							id="dropdownDots"
+							class="absolute z-10 bottom-[4.5rem] rounded-lg shadow w-[240px] bg-gray-900"
 						>
 						>
-							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
-							/>
-							<path
-								stroke-linecap="round"
-								stroke-linejoin="round"
-								d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
-							/>
-						</svg>
-					</div>
-					<div class=" self-center font-medium">Settings</div>
-				</button>
+							<div class="py-2 w-full">
+								<button
+									class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
+									on:click={() => {
+										openSettings();
+									}}
+								>
+									<div class=" self-center mr-3">
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke-width="1.5"
+											stroke="currentColor"
+											class="w-5 h-5"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
+											/>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
+											/>
+										</svg>
+									</div>
+									<div class=" self-center font-medium">Settings</div>
+								</button>
+							</div>
+
+							<hr class=" dark:border-gray-700 m-0 p-0" />
+
+							<div class="py-2 w-full">
+								<button
+									class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
+									on:click={() => {
+										localStorage.removeItem('token');
+										location.href = '/';
+									}}
+								>
+									<div class=" self-center mr-3">
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 20 20"
+											fill="currentColor"
+											class="w-5 h-5"
+										>
+											<path
+												fill-rule="evenodd"
+												d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z"
+												clip-rule="evenodd"
+											/>
+											<path
+												fill-rule="evenodd"
+												d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z"
+												clip-rule="evenodd"
+											/>
+										</svg>
+									</div>
+									<div class=" self-center font-medium">Sign Out</div>
+								</button>
+							</div>
+						</div>
+					{/if}
+				{:else}
+					<button
+						class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
+						on:click={() => {
+							openSettings();
+						}}
+					>
+						<div class=" self-center mr-3">
+							<svg
+								xmlns="http://www.w3.org/2000/svg"
+								fill="none"
+								viewBox="0 0 24 24"
+								stroke-width="1.5"
+								stroke="currentColor"
+								class="w-5 h-5"
+							>
+								<path
+									stroke-linecap="round"
+									stroke-linejoin="round"
+									d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
+								/>
+								<path
+									stroke-linecap="round"
+									stroke-linejoin="round"
+									d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
+								/>
+							</svg>
+						</div>
+						<div class=" self-center font-medium">Settings</div>
+					</button>
+				{/if}
 			</div>
 			</div>
 		</div>
 		</div>
 	</div>
 	</div>

+ 6 - 4
src/lib/constants.ts

@@ -1,14 +1,16 @@
-import { browser } from '$app/environment';
+import { dev, browser } from '$app/environment';
 import { PUBLIC_API_BASE_URL } from '$env/static/public';
 import { PUBLIC_API_BASE_URL } from '$env/static/public';
 
 
-export const API_BASE_URL =
+export const OLLAMA_API_BASE_URL =
 	PUBLIC_API_BASE_URL === ''
 	PUBLIC_API_BASE_URL === ''
 		? browser
 		? browser
-			? `http://${location.hostname}:11434/api`
+			? `http://${location.hostname}:8080/ollama/api`
 			: `http://localhost:11434/api`
 			: `http://localhost:11434/api`
 		: PUBLIC_API_BASE_URL;
 		: PUBLIC_API_BASE_URL;
 
 
-export const WEB_UI_VERSION = 'v1.0.0-alpha.8';
+export const WEBUI_API_BASE_URL = dev ? `http://${location.hostname}:8080/api/v1` : `/api/v1`;
+
+export const WEB_UI_VERSION = 'v1.0.0-alpha-static';
 
 
 // Source: https://kit.svelte.dev/docs/modules#$env-static-public
 // Source: https://kit.svelte.dev/docs/modules#$env-static-public
 // This feature, akin to $env/static/private, exclusively incorporates environment variables
 // This feature, akin to $env/static/private, exclusively incorporates environment variables

+ 4 - 0
src/lib/stores/index.ts

@@ -0,0 +1,4 @@
+import { writable } from 'svelte/store';
+
+export const config = writable(undefined);
+export const user = writable(undefined);

+ 12 - 0
src/routes/(app)/+layout.svelte

@@ -0,0 +1,12 @@
+<script>
+	import { config, user } from '$lib/stores';
+	import { goto } from '$app/navigation';
+
+	if ($config && $config.auth && $user === undefined) {
+		goto('/auth');
+	}
+</script>
+
+{#if $config !== undefined}
+	<slot />
+{/if}

+ 23 - 14
src/routes/+page.svelte → src/routes/(app)/+page.svelte

@@ -10,17 +10,17 @@
 	import 'katex/dist/katex.min.css';
 	import 'katex/dist/katex.min.css';
 	import toast from 'svelte-french-toast';
 	import toast from 'svelte-french-toast';
 
 
-	import { API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants';
+	import { OLLAMA_API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants';
 	import { onMount, tick } from 'svelte';
 	import { onMount, tick } from 'svelte';
 
 
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
 	import Suggestions from '$lib/components/chat/Suggestions.svelte';
 	import Suggestions from '$lib/components/chat/Suggestions.svelte';
+	import { user } from '$lib/stores';
 
 
 	let API_BASE_URL = BUILD_TIME_API_BASE_URL;
 	let API_BASE_URL = BUILD_TIME_API_BASE_URL;
 	let db;
 	let db;
 
 
-	// let selectedModel = '';
 	let selectedModels = [''];
 	let selectedModels = [''];
 	let settings = {
 	let settings = {
 		system: null,
 		system: null,
@@ -619,7 +619,8 @@
 			headers: {
 			headers: {
 				Accept: 'application/json',
 				Accept: 'application/json',
 				'Content-Type': 'application/json',
 				'Content-Type': 'application/json',
-				...(settings.authHeader && { Authorization: settings.authHeader })
+				...(settings.authHeader && { Authorization: settings.authHeader }),
+				...($user && { Authorization: `Bearer ${localStorage.token}` })
 			}
 			}
 		})
 		})
 			.then(async (res) => {
 			.then(async (res) => {
@@ -628,7 +629,11 @@
 			})
 			})
 			.catch((error) => {
 			.catch((error) => {
 				console.log(error);
 				console.log(error);
-				toast.error('Server connection failed');
+				if ('detail' in error) {
+					toast.error(error.detail);
+				} else {
+					toast.error('Server connection failed');
+				}
 				return null;
 				return null;
 			});
 			});
 
 
@@ -687,13 +692,6 @@
 				}
 				}
 			})
 			})
 		);
 		);
-
-		// if (selectedModel.includes('gpt-')) {
-		// 	await sendPromptOpenAI(userPrompt, parentId);
-		// } else {
-		// 	await sendPromptOllama(userPrompt, parentId);
-		// }
-
 		console.log(history);
 		console.log(history);
 	};
 	};
 
 
@@ -724,7 +722,8 @@
 			method: 'POST',
 			method: 'POST',
 			headers: {
 			headers: {
 				'Content-Type': 'text/event-stream',
 				'Content-Type': 'text/event-stream',
-				...(settings.authHeader && { Authorization: settings.authHeader })
+				...(settings.authHeader && { Authorization: settings.authHeader }),
+				...($user && { Authorization: `Bearer ${localStorage.token}` })
 			},
 			},
 			body: JSON.stringify({
 			body: JSON.stringify({
 				model: model,
 				model: model,
@@ -779,6 +778,8 @@
 								responseMessage.content += data.response;
 								responseMessage.content += data.response;
 								messages = messages;
 								messages = messages;
 							}
 							}
+						} else if ('detail' in data) {
+							throw data;
 						} else {
 						} else {
 							responseMessage.done = true;
 							responseMessage.done = true;
 							responseMessage.context = data.context;
 							responseMessage.context = data.context;
@@ -791,6 +792,10 @@
 				}
 				}
 			} catch (error) {
 			} catch (error) {
 				console.log(error);
 				console.log(error);
+				if ('detail' in error) {
+					toast.error(error.detail);
+				}
+				break;
 			}
 			}
 
 
 			if (autoScroll) {
 			if (autoScroll) {
@@ -817,7 +822,7 @@
 			window.scrollTo({ top: document.body.scrollHeight });
 			window.scrollTo({ top: document.body.scrollHeight });
 		}
 		}
 
 
-		if (messages.length == 2) {
+		if (messages.length == 2 && messages.at(1).content !== '') {
 			await generateChatTitle(chatId, userPrompt);
 			await generateChatTitle(chatId, userPrompt);
 		}
 		}
 	};
 	};
@@ -1034,7 +1039,8 @@
 			method: 'POST',
 			method: 'POST',
 			headers: {
 			headers: {
 				'Content-Type': 'text/event-stream',
 				'Content-Type': 'text/event-stream',
-				...(settings.authHeader && { Authorization: settings.authHeader })
+				...(settings.authHeader && { Authorization: settings.authHeader }),
+				...($user && { Authorization: `Bearer ${localStorage.token}` })
 			},
 			},
 			body: JSON.stringify({
 			body: JSON.stringify({
 				model: selectedModels[0],
 				model: selectedModels[0],
@@ -1047,6 +1053,9 @@
 				return res.json();
 				return res.json();
 			})
 			})
 			.catch((error) => {
 			.catch((error) => {
+				if ('detail' in error) {
+					toast.error(error.detail);
+				}
 				console.log(error);
 				console.log(error);
 				return null;
 				return null;
 			});
 			});

+ 0 - 0
src/routes/(app)/c/[id]/+page.svelte


+ 61 - 3
src/routes/+layout.svelte

@@ -1,13 +1,71 @@
 <script>
 <script>
-	import { Toaster } from 'svelte-french-toast';
+	import { onMount, tick } from 'svelte';
+	import { config, user } from '$lib/stores';
+	import { goto } from '$app/navigation';
+	import { WEBUI_API_BASE_URL } from '$lib/constants';
+	import toast, { Toaster } from 'svelte-french-toast';
 
 
 	import '../app.css';
 	import '../app.css';
 	import '../tailwind.css';
 	import '../tailwind.css';
+
+	let loaded = false;
+
+	onMount(async () => {
+		const webBackendStatus = await fetch(`${WEBUI_API_BASE_URL}/`, {
+			method: 'GET',
+			headers: {
+				'Content-Type': 'application/json'
+			}
+		})
+			.then(async (res) => {
+				if (!res.ok) throw await res.json();
+				return res.json();
+			})
+			.catch((error) => {
+				console.log(error);
+				return null;
+			});
+
+		console.log(webBackendStatus);
+		await config.set(webBackendStatus);
+
+		if (webBackendStatus) {
+			if (webBackendStatus.auth) {
+				if (localStorage.token) {
+					const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, {
+						method: 'GET',
+						headers: {
+							'Content-Type': 'application/json',
+							Authorization: `Bearer ${localStorage.token}`
+						}
+					})
+						.then(async (res) => {
+							if (!res.ok) throw await res.json();
+							return res.json();
+						})
+						.catch((error) => {
+							console.log(error);
+							toast.error(error.detail);
+							return null;
+						});
+
+					await user.set(res);
+				} else {
+					goto('/auth');
+				}
+			}
+		}
+
+		await tick();
+		loaded = true;
+	});
 </script>
 </script>
 
 
 <svelte:head>
 <svelte:head>
 	<title>Ollama</title>
 	<title>Ollama</title>
 </svelte:head>
 </svelte:head>
-
-<slot />
 <Toaster />
 <Toaster />
+
+{#if $config !== undefined && loaded}
+	<slot />
+{/if}

+ 1091 - 0
src/routes/auth/+page.svelte

@@ -0,0 +1,1091 @@
+<script>
+	import { goto } from '$app/navigation';
+	import { WEBUI_API_BASE_URL } from '$lib/constants';
+	import { config, user } from '$lib/stores';
+	import toast from 'svelte-french-toast';
+
+	let mode = 'signin';
+
+	let name = '';
+	let email = '';
+	let password = '';
+
+	const signInHandler = async () => {
+		const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signin`, {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/json'
+			},
+			body: JSON.stringify({
+				email: email,
+				password: password
+			})
+		})
+			.then(async (res) => {
+				if (!res.ok) throw await res.json();
+				return res.json();
+			})
+			.catch((error) => {
+				console.log(error);
+				toast.error(error.detail);
+				return null;
+			});
+
+		if (res) {
+			console.log(res);
+			toast.success(`You're now logged in. Redirecting you to the main page."`);
+			localStorage.token = res.token;
+			await user.set(res);
+			goto('/');
+		}
+	};
+
+	const signUpHandler = async () => {
+		const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/json'
+			},
+			body: JSON.stringify({
+				name: name,
+				email: email,
+				password: password
+			})
+		})
+			.then(async (res) => {
+				if (!res.ok) throw await res.json();
+				return res.json();
+			})
+			.catch((error) => {
+				console.log(error);
+				toast.error(error.detail);
+				return null;
+			});
+
+		if (res) {
+			console.log(res);
+			toast.success(`Account creation successful. Redirecting you to the main page."`);
+			localStorage.token = res.token;
+			await user.set(res);
+			goto('/');
+		}
+	};
+
+	if ($config === null || !$config.auth || ($config.auth && $user !== undefined)) {
+		goto('/');
+	}
+</script>
+
+{#if $config && $config.auth}
+	<div class="fixed m-10 z-50">
+		<div class="flex space-x-2">
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-8" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" bg-white min-h-screen w-full flex justify-center font-mona">
+		<div class="hidden lg:flex lg:flex-1 px-10 md:px-16 w-full bg-yellow-50 justify-center">
+			<div class=" my-auto pb-16 text-left">
+				<div>
+					<div class=" font-bold text-yellow-600 text-4xl">
+						Get up and running with <br />large language models, locally.
+					</div>
+
+					<div class="mt-2 text-yellow-600 text-xl">
+						Run Llama 2, Code Llama, and other models. Customize and create your own.
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<div class="w-full max-w-xl px-10 md:px-16 bg-white min-h-screen w-full flex flex-col">
+			<div class=" my-auto pb-10 w-full">
+				<form
+					class=" flex flex-col justify-center"
+					on:submit|preventDefault={() => {
+						if (mode === 'signin') {
+							signInHandler();
+						} else {
+							signUpHandler();
+						}
+					}}
+				>
+					<div class=" text-2xl md:text-3xl font-semibold">
+						{mode === 'signin' ? 'Sign in' : 'Sign up'} to Ollama Web UI
+					</div>
+
+					<hr class="my-8" />
+
+					<div class="flex flex-col space-y-4">
+						{#if mode === 'signup'}
+							<div>
+								<div class=" text-sm font-bold text-left mb-2">Name</div>
+								<input
+									bind:value={name}
+									type="text"
+									class=" border px-5 py-4 rounded-2xl w-full text-sm"
+									autocomplete="name"
+									required
+								/>
+							</div>
+						{/if}
+
+						<div>
+							<div class=" text-sm font-bold text-left mb-2">Email</div>
+							<input
+								bind:value={email}
+								type="email"
+								class=" border px-5 py-4 rounded-2xl w-full text-sm"
+								autocomplete="email"
+								required
+							/>
+						</div>
+
+						<div>
+							<div class=" text-sm font-bold text-left mb-2">Password</div>
+							<input
+								bind:value={password}
+								type="password"
+								class=" border px-5 py-4 rounded-2xl w-full text-sm"
+								autocomplete="current-password"
+								required
+							/>
+						</div>
+					</div>
+
+					<div class="mt-8">
+						<button
+							class=" bg-gray-900 hover:bg-gray-800 w-full rounded-full text-white font-semibold text-sm py-5 transition"
+							type="submit"
+						>
+							{mode === 'signin' ? 'Sign In' : 'Create Account'}
+						</button>
+
+						<div class=" mt-4 text-sm text-center">
+							{mode === 'signin' ? `Don't have an account?` : `Already have an account?`}
+
+							<button
+								class=" font-medium underline"
+								type="button"
+								on:click={() => {
+									if (mode === 'signin') {
+										mode = 'signup';
+									} else {
+										mode = 'signin';
+									}
+								}}
+							>
+								{mode === 'signin' ? `Sign up` : `Sign In`}
+							</button>
+						</div>
+					</div>
+				</form>
+			</div>
+		</div>
+	</div>
+
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<style>
+		.font-mona {
+			font-family: 'Mona Sans';
+		}
+	</style>
+{/if}

二进制
static/assets/fonts/Mona-Sans.woff2