"src/git@developer.sourcefind.cn:chenpangpang/open-webui.git" did not exist on "182ab8b8a22c3f38419a06d0ff94409d20071d02"
Unverified Commit fa598b59 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge branch 'main' into rag

parents becb7b1d fe8c2244
...@@ -24,7 +24,6 @@ dist/ ...@@ -24,7 +24,6 @@ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
......
...@@ -33,6 +33,8 @@ Also check our sibling project, [OllamaHub](https://ollamahub.com/), where you c ...@@ -33,6 +33,8 @@ Also check our sibling project, [OllamaHub](https://ollamahub.com/), where you c
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction. - ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
- 📜 **Prompt Preset Support**: Instantly access preset prompts using the '/' command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [OllamaHub](https://ollamahub.com/) integration.
- 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI. - 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI.
- ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face. - ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face.
......
from fastapi import FastAPI, Depends from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from apps.web.routers import auths, users, chats, modelfiles, utils from apps.web.routers import auths, users, chats, modelfiles, prompts, configs, utils
from config import WEBUI_VERSION, WEBUI_AUTH from config import WEBUI_VERSION, WEBUI_AUTH
app = FastAPI() app = FastAPI()
...@@ -9,6 +9,7 @@ app = FastAPI() ...@@ -9,6 +9,7 @@ app = FastAPI()
origins = ["*"] origins = ["*"]
app.state.ENABLE_SIGNUP = True app.state.ENABLE_SIGNUP = True
app.state.DEFAULT_MODELS = None
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
...@@ -19,13 +20,21 @@ app.add_middleware( ...@@ -19,13 +20,21 @@ app.add_middleware(
) )
app.include_router(auths.router, prefix="/auths", tags=["auths"]) app.include_router(auths.router, prefix="/auths", tags=["auths"])
app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(chats.router, prefix="/chats", tags=["chats"]) app.include_router(chats.router, prefix="/chats", tags=["chats"])
app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"]) app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"])
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
app.include_router(configs.router, prefix="/configs", tags=["configs"])
app.include_router(utils.router, prefix="/utils", tags=["utils"]) app.include_router(utils.router, prefix="/utils", tags=["utils"])
@app.get("/") @app.get("/")
async def get_status(): async def get_status():
return {"status": True, "version": WEBUI_VERSION, "auth": WEBUI_AUTH} return {
"status": True,
"version": WEBUI_VERSION,
"auth": WEBUI_AUTH,
"default_models": app.state.DEFAULT_MODELS,
}
...@@ -12,7 +12,7 @@ from apps.web.internal.db import DB ...@@ -12,7 +12,7 @@ from apps.web.internal.db import DB
import json import json
#################### ####################
# User DB Schema # Modelfile DB Schema
#################### ####################
......
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time
from utils.utils import decode_token
from utils.misc import get_gravatar_url
from apps.web.internal.db import DB
import json
####################
# Prompts DB Schema
####################
class Prompt(Model):
command = CharField(unique=True)
user_id = CharField()
title = CharField()
content = TextField()
timestamp = DateField()
class Meta:
database = DB
class PromptModel(BaseModel):
command: str
user_id: str
title: str
content: str
timestamp: int # timestamp in epoch
####################
# Forms
####################
class PromptForm(BaseModel):
command: str
title: str
content: str
class PromptsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Prompt])
def insert_new_prompt(
self, user_id: str, form_data: PromptForm
) -> Optional[PromptModel]:
prompt = PromptModel(
**{
"user_id": user_id,
"command": form_data.command,
"title": form_data.title,
"content": form_data.content,
"timestamp": int(time.time()),
}
)
try:
result = Prompt.create(**prompt.model_dump())
if result:
return prompt
else:
return None
except:
return None
def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
try:
prompt = Prompt.get(Prompt.command == command)
return PromptModel(**model_to_dict(prompt))
except:
return None
def get_prompts(self) -> List[PromptModel]:
return [
PromptModel(**model_to_dict(prompt))
for prompt in Prompt.select()
# .limit(limit).offset(skip)
]
def update_prompt_by_command(
self, command: str, form_data: PromptForm
) -> Optional[PromptModel]:
try:
query = Prompt.update(
title=form_data.title,
content=form_data.content,
timestamp=int(time.time()),
).where(Prompt.command == command)
query.execute()
prompt = Prompt.get(Prompt.command == command)
return PromptModel(**model_to_dict(prompt))
except:
return None
def delete_prompt_by_command(self, command: str) -> bool:
try:
query = Prompt.delete().where((Prompt.command == command))
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
Prompts = PromptsTable(DB)
...@@ -8,6 +8,7 @@ from pydantic import BaseModel ...@@ -8,6 +8,7 @@ from pydantic import BaseModel
import time import time
import uuid import uuid
from apps.web.models.auths import ( from apps.web.models.auths import (
SigninForm, SigninForm,
SignupForm, SignupForm,
...@@ -20,7 +21,7 @@ from apps.web.models.users import Users ...@@ -20,7 +21,7 @@ from apps.web.models.users import Users
from utils.utils import get_password_hash, get_current_user, create_token from utils.utils import get_password_hash, get_current_user, create_token
from utils.misc import get_gravatar_url from utils.misc import get_gravatar_url, validate_email_format
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
...@@ -95,33 +96,38 @@ async def signin(form_data: SigninForm): ...@@ -95,33 +96,38 @@ async def signin(form_data: SigninForm):
@router.post("/signup", response_model=SigninResponse) @router.post("/signup", response_model=SigninResponse)
async def signup(request: Request, form_data: SignupForm): async def signup(request: Request, form_data: SignupForm):
if request.app.state.ENABLE_SIGNUP: if request.app.state.ENABLE_SIGNUP:
if not Users.get_user_by_email(form_data.email.lower()): if validate_email_format(form_data.email.lower()):
try: if not Users.get_user_by_email(form_data.email.lower()):
role = "admin" if Users.get_num_users() == 0 else "pending" try:
hashed = get_password_hash(form_data.password) role = "admin" if Users.get_num_users() == 0 else "pending"
user = Auths.insert_new_auth( hashed = get_password_hash(form_data.password)
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}) if user:
# response.set_cookie(key='token', value=token, httponly=True) token = create_token(data={"email": user.email})
# response.set_cookie(key='token', value=token, httponly=True)
return {
"token": token, return {
"token_type": "Bearer", "token": token,
"id": user.id, "token_type": "Bearer",
"email": user.email, "id": user.id,
"name": user.name, "email": user.email,
"role": user.role, "name": user.name,
"profile_image_url": user.profile_image_url, "role": user.role,
} "profile_image_url": user.profile_image_url,
else: }
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) else:
except Exception as err: raise HTTPException(
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) 500, detail=ERROR_MESSAGES.CREATE_USER_ERROR
)
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
else:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
else: else:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT)
else: else:
raise HTTPException(400, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) raise HTTPException(400, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
......
from fastapi import Response, Request
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 apps.web.models.users import Users
from utils.utils import get_password_hash, get_current_user, create_token
from utils.misc import get_gravatar_url, validate_email_format
from constants import ERROR_MESSAGES
router = APIRouter()
class SetDefaultModelsForm(BaseModel):
models: str
############################
# SetDefaultModels
############################
@router.post("/default/models", response_model=str)
async def set_global_default_models(
request: Request, form_data: SetDefaultModelsForm, user=Depends(get_current_user)
):
if user.role == "admin":
request.app.state.DEFAULT_MODELS = form_data.models
return request.app.state.DEFAULT_MODELS
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.web.models.prompts import Prompts, PromptForm, PromptModel
from utils.utils import get_current_user
from constants import ERROR_MESSAGES
router = APIRouter()
############################
# GetPrompts
############################
@router.get("/", response_model=List[PromptModel])
async def get_prompts(user=Depends(get_current_user)):
return Prompts.get_prompts()
############################
# CreateNewPrompt
############################
@router.post("/create", response_model=Optional[PromptModel])
async def create_new_prompt(form_data: PromptForm, user=Depends(get_current_user)):
if user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
prompt = Prompts.get_prompt_by_command(form_data.command)
if prompt == None:
prompt = Prompts.insert_new_prompt(user.id, form_data)
if prompt:
return prompt
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.COMMAND_TAKEN,
)
############################
# GetPromptByCommand
############################
@router.get("/{command}", response_model=Optional[PromptModel])
async def get_prompt_by_command(command: str, user=Depends(get_current_user)):
prompt = Prompts.get_prompt_by_command(f"/{command}")
if prompt:
return prompt
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdatePromptByCommand
############################
@router.post("/{command}/update", response_model=Optional[PromptModel])
async def update_prompt_by_command(
command: str, form_data: PromptForm, user=Depends(get_current_user)
):
if user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
if prompt:
return prompt
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
############################
# DeletePromptByCommand
############################
@router.delete("/{command}/delete", response_model=bool)
async def delete_prompt_by_command(command: str, user=Depends(get_current_user)):
if user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
result = Prompts.delete_prompt_by_command(f"/{command}")
return result
...@@ -17,10 +17,12 @@ class ERROR_MESSAGES(str, Enum): ...@@ -17,10 +17,12 @@ class ERROR_MESSAGES(str, Enum):
USERNAME_TAKEN = ( USERNAME_TAKEN = (
"Uh-oh! This username is already registered. Please choose another username." "Uh-oh! This username is already registered. Please choose another username."
) )
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
INVALID_TOKEN = ( INVALID_TOKEN = (
"Your session has expired or the token is invalid. Please sign in again." "Your session has expired or the token is invalid. Please sign in again."
) )
INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again." INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)."
INVALID_PASSWORD = ( INVALID_PASSWORD = (
"The password provided is incorrect. Please check for typos and try again." "The password provided is incorrect. Please check for typos and try again."
) )
...@@ -31,5 +33,4 @@ class ERROR_MESSAGES(str, Enum): ...@@ -31,5 +33,4 @@ class ERROR_MESSAGES(str, Enum):
) )
NOT_FOUND = "We could not find what you're looking for :/" NOT_FOUND = "We could not find what you're looking for :/"
USER_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." MALICIOUS = "Unusual activities detected, please try again in a few minutes."
uvicorn main:app --host 0.0.0.0 --port 8080 --forwarded-allow-ips '*' #!/usr/bin/env bash
\ No newline at end of file
uvicorn main:app --host 0.0.0.0 --port 8080 --forwarded-allow-ips '*'
import hashlib import hashlib
import re
def get_gravatar_url(email): def get_gravatar_url(email):
...@@ -21,3 +22,9 @@ def calculate_sha256(file): ...@@ -21,3 +22,9 @@ def calculate_sha256(file):
for chunk in iter(lambda: file.read(8192), b""): for chunk in iter(lambda: file.read(8192), b""):
sha256.update(chunk) sha256.update(chunk)
return sha256.hexdigest() return sha256.hexdigest()
def validate_email_format(email: str) -> bool:
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
return False
return True
...@@ -12,11 +12,12 @@ ...@@ -12,11 +12,12 @@
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches) (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
) { ) {
document.documentElement.classList.add('light'); document.documentElement.classList.add('light');
} else if (localStorage.theme === 'dark') { } else if (localStorage.theme) {
document.documentElement.classList.add('dark'); localStorage.theme.split(' ').forEach((e) => {
document.documentElement.classList.add(e);
});
} else { } else {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
document.documentElement.classList.add(localStorage.theme);
} }
</script> </script>
......
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const setDefaultModels = async (token: string, models: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/default/models`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
models: models
})
})
.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;
};
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const createNewPrompt = async (
token: string,
command: string,
title: string,
content: string
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/create`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
command: `/${command}`,
title: title,
content: content
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getPrompts = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getPromptByCommand = async (token: string, command: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const updatePromptByCommand = async (
token: string,
command: string,
title: string,
content: string
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
command: `/${command}`,
title: title,
content: content
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deletePromptByCommand = async (token: string, command: string) => {
let error = null;
command = command.charAt(0) === '/' ? command.slice(1) : command;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}/delete`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
<script lang="ts"> <script lang="ts">
import { settings } from '$lib/stores';
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
import { onMount, tick } from 'svelte';
import { settings } from '$lib/stores';
import { findWordIndices } from '$lib/utils';
import Prompts from './MessageInput/PromptCommands.svelte';
import Suggestions from './MessageInput/Suggestions.svelte'; import Suggestions from './MessageInput/Suggestions.svelte';
import { onMount } from 'svelte';
export let submitPrompt: Function; export let submitPrompt: Function;
export let stopResponse: Function; export let stopResponse: Function;
...@@ -11,6 +14,8 @@ ...@@ -11,6 +14,8 @@
export let autoScroll = true; export let autoScroll = true;
let filesInputElement; let filesInputElement;
let promptsElement;
let inputFiles; let inputFiles;
let dragged = false; let dragged = false;
...@@ -154,36 +159,42 @@ ...@@ -154,36 +159,42 @@
<div class="fixed bottom-0 w-full"> <div class="fixed bottom-0 w-full">
<div class="px-2.5 pt-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> <div class="px-2.5 pt-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
{#if messages.length == 0 && suggestionPrompts.length !== 0} <div class="flex flex-col max-w-3xl w-full">
<div class="max-w-3xl w-full"> <div>
<Suggestions {suggestionPrompts} {submitPrompt} /> {#if autoScroll === false && messages.length > 0}
<div class=" flex justify-center mb-4">
<button
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full"
on:click={() => {
autoScroll = true;
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}}
>
<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="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{/if}
</div> </div>
{/if}
<div class="w-full">
{#if autoScroll === false && messages.length > 0} {#if prompt.charAt(0) === '/'}
<div class=" flex justify-center mb-4"> <Prompts bind:this={promptsElement} bind:prompt />
<button {:else if messages.length == 0 && suggestionPrompts.length !== 0}
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full" <Suggestions {suggestionPrompts} {submitPrompt} />
on:click={() => { {/if}
autoScroll = true;
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}}
>
<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="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
clip-rule="evenodd"
/>
</svg>
</button>
</div> </div>
{/if} </div>
</div> </div>
<div class="bg-white dark:bg-gray-800"> <div class="bg-white dark:bg-gray-800">
<div class="max-w-3xl px-2.5 -mb-0.5 mx-auto inset-x-0"> <div class="max-w-3xl px-2.5 -mb-0.5 mx-auto inset-x-0">
...@@ -287,7 +298,7 @@ ...@@ -287,7 +298,7 @@
id="chat-textarea" id="chat-textarea"
class=" dark:bg-gray-800 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled class=" dark:bg-gray-800 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled
? '' ? ''
: ' pl-4'} rounded-xl resize-none" : ' pl-4'} rounded-xl resize-none h-[48px]"
placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'} placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'}
bind:value={prompt} bind:value={prompt}
on:keypress={(e) => { on:keypress={(e) => {
...@@ -298,7 +309,7 @@ ...@@ -298,7 +309,7 @@
submitPrompt(prompt); submitPrompt(prompt);
} }
}} }}
on:keydown={(e) => { on:keydown={async (e) => {
if (prompt === '' && e.key == 'ArrowUp') { if (prompt === '' && e.key == 'ArrowUp') {
e.preventDefault(); e.preventDefault();
...@@ -315,6 +326,61 @@ ...@@ -315,6 +326,61 @@
userMessageElement.scrollIntoView({ block: 'center' }); userMessageElement.scrollIntoView({ block: 'center' });
editButton?.click(); editButton?.click();
} }
if (prompt.charAt(0) === '/' && e.key === 'ArrowUp') {
promptsElement.selectUp();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (prompt.charAt(0) === '/' && e.key === 'ArrowDown') {
promptsElement.selectDown();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (prompt.charAt(0) === '/' && e.key === 'Enter') {
e.preventDefault();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton?.click();
}
if (prompt.charAt(0) === '/' && e.key === 'Tab') {
e.preventDefault();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.at(-1);
commandOptionButton?.click();
} else if (e.key === 'Tab') {
const words = findWordIndices(prompt);
if (words.length > 0) {
const word = words.at(0);
const fullPrompt = prompt;
prompt = prompt.substring(0, word?.endIndex + 1);
await tick();
e.target.scrollTop = e.target.scrollHeight;
prompt = fullPrompt;
await tick();
e.preventDefault();
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
}
}} }}
rows="1" rows="1"
on:input={(e) => { on:input={(e) => {
......
<script lang="ts">
import { prompts } from '$lib/stores';
import { findWordIndices } from '$lib/utils';
import { tick } from 'svelte';
export let prompt = '';
let selectedCommandIdx = 0;
let filteredPromptCommands = [];
$: filteredPromptCommands = $prompts
.filter((p) => p.command.includes(prompt))
.sort((a, b) => a.title.localeCompare(b.title));
$: if (prompt) {
selectedCommandIdx = 0;
}
export const selectUp = () => {
selectedCommandIdx = Math.max(0, selectedCommandIdx - 1);
};
export const selectDown = () => {
selectedCommandIdx = Math.min(selectedCommandIdx + 1, filteredPromptCommands.length - 1);
};
const confirmCommand = async (command) => {
prompt = command.content;
const chatInputElement = document.getElementById('chat-textarea');
await tick();
chatInputElement.style.height = '';
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
chatInputElement?.focus();
await tick();
const words = findWordIndices(prompt);
if (words.length > 0) {
const word = words.at(0);
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
};
</script>
{#if filteredPromptCommands.length > 0}
<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">
<div class=" text-lg font-semibold mt-2">/</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
{#each filteredPromptCommands as command, commandIdx}
<button
class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx
? ' bg-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmCommand(command);
}}
on:mousemove={() => {
selectedCommandIdx = commandIdx;
}}
on:focus={() => {}}
>
<div class=" font-medium text-black">
{command.command}
</div>
<div class=" text-xs text-gray-600">
{command.title}
</div>
</button>
{/each}
</div>
<div
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-lg flex items-center space-x-1"
>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-3 h-3"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</div>
<div class="line-clamp-1">
Tip: Update multiple variable slots consecutively by pressing the tab key in the chat
input after each replacement.
</div>
</div>
</div>
</div>
</div>
{/if}
<script lang="ts"> <script lang="ts">
import { models, showSettings, settings } from '$lib/stores'; import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick } from 'svelte';
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
export let selectedModels = ['']; export let selectedModels = [''];
export let disabled = false; export let disabled = false;
const saveDefaultModel = () => { const saveDefaultModel = async () => {
const hasEmptyModel = selectedModels.filter((it) => it === ''); const hasEmptyModel = selectedModels.filter((it) => it === '');
if (hasEmptyModel.length) { if (hasEmptyModel.length) {
toast.error('Choose a model before saving...'); toast.error('Choose a model before saving...');
...@@ -13,8 +15,19 @@ ...@@ -13,8 +15,19 @@
} }
settings.set({ ...$settings, models: selectedModels }); settings.set({ ...$settings, models: selectedModels });
localStorage.setItem('settings', JSON.stringify($settings)); localStorage.setItem('settings', JSON.stringify($settings));
if ($user.role === 'admin') {
console.log('setting default models globally');
await setDefaultModels(localStorage.token, selectedModels.join(','));
}
toast.success('Default model updated'); toast.success('Default model updated');
}; };
$: if (selectedModels.length > 0 && $models.length > 0) {
selectedModels = selectedModels.map((model) =>
$models.map((m) => m.name).includes(model) ? model : ''
);
}
</script> </script>
<div class="flex flex-col my-2"> <div class="flex flex-col my-2">
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
// General // General
let API_BASE_URL = OLLAMA_API_BASE_URL; let API_BASE_URL = OLLAMA_API_BASE_URL;
let themes = ['dark', 'light', 'rose-pine']; let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
let theme = 'dark'; let theme = 'dark';
let notificationEnabled = false; let notificationEnabled = false;
let system = ''; let system = '';
...@@ -74,17 +74,20 @@ ...@@ -74,17 +74,20 @@
let deleteModelTag = ''; let deleteModelTag = '';
// External
let OPENAI_API_KEY = '';
let OPENAI_API_BASE_URL = '';
// Addons // Addons
let titleAutoGenerate = true; let titleAutoGenerate = true;
let speechAutoSend = false; let speechAutoSend = false;
let responseAutoCopy = false; let responseAutoCopy = false;
let gravatarEmail = ''; let gravatarEmail = '';
let OPENAI_API_KEY = ''; let titleAutoGenerateModel = '';
let OPENAI_API_BASE_URL = '';
// Chats // Chats
let importFiles; let importFiles;
let showDeleteConfirm = false; let showDeleteConfirm = false;
...@@ -656,13 +659,14 @@ ...@@ -656,13 +659,14 @@
options = { ...options, ...settings.options }; options = { ...options, ...settings.options };
options.stop = (settings?.options?.stop ?? []).join(','); options.stop = (settings?.options?.stop ?? []).join(',');
OPENAI_API_KEY = settings.OPENAI_API_KEY ?? '';
OPENAI_API_BASE_URL = settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1';
titleAutoGenerate = settings.titleAutoGenerate ?? true; titleAutoGenerate = settings.titleAutoGenerate ?? true;
speechAutoSend = settings.speechAutoSend ?? false; speechAutoSend = settings.speechAutoSend ?? false;
responseAutoCopy = settings.responseAutoCopy ?? false; responseAutoCopy = settings.responseAutoCopy ?? false;
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
gravatarEmail = settings.gravatarEmail ?? ''; gravatarEmail = settings.gravatarEmail ?? '';
OPENAI_API_KEY = settings.OPENAI_API_KEY ?? '';
OPENAI_API_BASE_URL = settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1';
authEnabled = settings.authHeader !== undefined ? true : false; authEnabled = settings.authHeader !== undefined ? true : false;
if (authEnabled) { if (authEnabled) {
...@@ -757,31 +761,33 @@ ...@@ -757,31 +761,33 @@
<div class=" self-center">Advanced</div> <div class=" self-center">Advanced</div>
</button> </button>
<button {#if $user?.role === 'admin'}
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === <button
'models' class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
? 'bg-gray-200 dark:bg-gray-700' 'models'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" ? 'bg-gray-200 dark:bg-gray-700'
on:click={() => { : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
selectedTab = 'models'; on:click={() => {
}} selectedTab = 'models';
> }}
<div class=" self-center mr-2"> >
<svg <div class=" self-center mr-2">
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 20 20"
class="w-4 h-4" fill="currentColor"
> class="w-4 h-4"
<path >
fill-rule="evenodd" <path
d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z" fill-rule="evenodd"
clip-rule="evenodd" d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
/> clip-rule="evenodd"
</svg> />
</div> </svg>
<div class=" self-center">Models</div> </div>
</button> <div class=" self-center">Models</div>
</button>
{/if}
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
...@@ -994,21 +1000,22 @@ ...@@ -994,21 +1000,22 @@
themes themes
.filter((e) => e !== theme) .filter((e) => e !== theme)
.forEach((e) => { .forEach((e) => {
document.documentElement.classList.remove(e); e.split(' ').forEach((e) => {
document.documentElement.classList.remove(e);
});
}); });
document.documentElement.classList.add(theme); theme.split(' ').forEach((e) => {
document.documentElement.classList.add(e);
if (theme === 'rose-pine') { });
document.documentElement.classList.add('dark');
}
console.log(theme); console.log(theme);
}} }}
> >
<option value="dark">Dark</option> <option value="dark">Dark</option>
<option value="light">Light</option> <option value="light">Light</option>
<option value="rose-pine">Rosé Pine</option> <option value="rose-pine dark">Rosé Pine</option>
<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
</select> </select>
</div> </div>
</div> </div>
...@@ -1547,10 +1554,6 @@ ...@@ -1547,10 +1554,6 @@
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
saveSettings({
gravatarEmail: gravatarEmail !== '' ? gravatarEmail : undefined,
gravatarUrl: gravatarEmail !== '' ? getGravatarURL(gravatarEmail) : undefined
});
show = false; show = false;
}} }}
> >
...@@ -1560,7 +1563,7 @@ ...@@ -1560,7 +1563,7 @@
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Title Auto Generation</div> <div class=" self-center text-xs font-medium">Title Auto-Generation</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -1622,6 +1625,54 @@ ...@@ -1622,6 +1625,54 @@
</div> </div>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Set Title Auto-Generation Model</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
bind:value={titleAutoGenerateModel}
placeholder="Select a model"
>
<option value="" selected>Default</option>
{#each $models.filter((m) => m.size != null) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name +
' (' +
(model.size / 1024 ** 3).toFixed(1) +
' GB)'}</option
>
{/each}
</select>
</div>
<button
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
on:click={() => {
saveSettings({
titleAutoGenerateModel:
titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined
});
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3.5 h-3.5"
>
<path
fill-rule="evenodd"
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
<!-- <hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium"> <div class=" mb-2.5 text-sm font-medium">
Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span> Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span>
...@@ -1644,7 +1695,7 @@ ...@@ -1644,7 +1695,7 @@
target="_blank">Gravatar.</a target="_blank">Gravatar.</a
> >
</div> </div>
</div> </div> -->
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
......
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
</div> </div>
{#if $user?.role === 'admin'} {#if $user?.role === 'admin'}
<div class="px-2.5 flex justify-center my-1"> <div class="px-2.5 flex justify-center mt-1">
<button <button
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition" class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
on:click={async () => { on:click={async () => {
...@@ -129,10 +129,38 @@ ...@@ -129,10 +129,38 @@
</div> </div>
</button> </button>
</div> </div>
<div class="px-2.5 flex justify-center mb-1">
<button
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
on:click={async () => {
goto('/prompts');
}}
>
<div class="self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="flex self-center">
<div class=" self-center font-medium text-sm">Prompts</div>
</div>
</button>
</div>
{/if} {/if}
<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2"> <div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2">
<div class="flex w-full"> <div class="flex w-full" id="chat-search">
<div class="self-center pl-3 py-2 rounded-l bg-gray-950"> <div class="self-center pl-3 py-2 rounded-l bg-gray-950">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
......
...@@ -6,10 +6,13 @@ export const user = writable(undefined); ...@@ -6,10 +6,13 @@ export const user = writable(undefined);
// Frontend // Frontend
export const theme = writable('dark'); export const theme = writable('dark');
export const chatId = writable(''); export const chatId = writable('');
export const chats = writable([]); export const chats = writable([]);
export const models = writable([]); export const models = writable([]);
export const modelfiles = writable([]); export const modelfiles = writable([]);
export const prompts = writable([]);
export const settings = writable({}); export const settings = writable({});
export const showSettings = writable(false); export const showSettings = writable(false);
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment