Unverified Commit bced9073 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge branch 'websearch' into feat/backend-web-search

parents 276b7b90 b6b71c08
...@@ -84,6 +84,8 @@ jobs: ...@@ -84,6 +84,8 @@ jobs:
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
BUILD_HASH=${{ github.sha }}
- name: Export digest - name: Export digest
run: | run: |
...@@ -170,7 +172,9 @@ jobs: ...@@ -170,7 +172,9 @@ jobs:
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: USE_CUDA=true build-args: |
BUILD_HASH=${{ github.sha }}
USE_CUDA=true
- name: Export digest - name: Export digest
run: | run: |
...@@ -257,7 +261,9 @@ jobs: ...@@ -257,7 +261,9 @@ jobs:
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: USE_OLLAMA=true build-args: |
BUILD_HASH=${{ github.sha }}
USE_OLLAMA=true
- name: Export digest - name: Export digest
run: | run: |
......
...@@ -11,12 +11,14 @@ ARG USE_CUDA_VER=cu121 ...@@ -11,12 +11,14 @@ ARG USE_CUDA_VER=cu121
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. # IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
ARG USE_RERANKING_MODEL="" ARG USE_RERANKING_MODEL=""
ARG BUILD_HASH=dev-build
# Override at your own risk - non-root configurations are untested # Override at your own risk - non-root configurations are untested
ARG UID=0 ARG UID=0
ARG GID=0 ARG GID=0
######## WebUI frontend ######## ######## WebUI frontend ########
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
ARG BUILD_HASH
WORKDIR /app WORKDIR /app
...@@ -24,6 +26,7 @@ COPY package.json package-lock.json ./ ...@@ -24,6 +26,7 @@ COPY package.json package-lock.json ./
RUN npm ci RUN npm ci
COPY . . COPY . .
ENV APP_BUILD_HASH=${BUILD_HASH}
RUN npm run build RUN npm run build
######## WebUI backend ######## ######## WebUI backend ########
...@@ -35,6 +38,7 @@ ARG USE_OLLAMA ...@@ -35,6 +38,7 @@ ARG USE_OLLAMA
ARG USE_CUDA_VER ARG USE_CUDA_VER
ARG USE_EMBEDDING_MODEL ARG USE_EMBEDDING_MODEL
ARG USE_RERANKING_MODEL ARG USE_RERANKING_MODEL
ARG BUILD_HASH
ARG UID ARG UID
ARG GID ARG GID
...@@ -150,4 +154,6 @@ HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.stat ...@@ -150,4 +154,6 @@ HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.stat
USER $UID:$GID USER $UID:$GID
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
CMD [ "bash", "start.sh"] CMD [ "bash", "start.sh"]
...@@ -198,7 +198,7 @@ async def fetch_url(url, key): ...@@ -198,7 +198,7 @@ async def fetch_url(url, key):
def merge_models_lists(model_lists): def merge_models_lists(model_lists):
log.info(f"merge_models_lists {model_lists}") log.debug(f"merge_models_lists {model_lists}")
merged_list = [] merged_list = []
for idx, models in enumerate(model_lists): for idx, models in enumerate(model_lists):
...@@ -237,7 +237,7 @@ async def get_all_models(): ...@@ -237,7 +237,7 @@ async def get_all_models():
] ]
responses = await asyncio.gather(*tasks) responses = await asyncio.gather(*tasks)
log.info(f"get_all_models:responses() {responses}") log.debug(f"get_all_models:responses() {responses}")
models = { models = {
"data": merge_models_lists( "data": merge_models_lists(
...@@ -254,7 +254,7 @@ async def get_all_models(): ...@@ -254,7 +254,7 @@ async def get_all_models():
) )
} }
log.info(f"models: {models}") log.debug(f"models: {models}")
app.state.MODELS = {model["id"]: model for model in models["data"]} app.state.MODELS = {model["id"]: model for model in models["data"]}
return models return models
......
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Adding fields settings to the 'user' table
migrator.add_fields("user", settings=pw.TextField(null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
# Remove the settings field
migrator.remove_fields("user", "settings")
...@@ -13,7 +13,7 @@ from apps.webui.routers import ( ...@@ -13,7 +13,7 @@ from apps.webui.routers import (
utils, utils,
) )
from config import ( from config import (
WEBUI_VERSION, WEBUI_BUILD_HASH,
WEBUI_AUTH, WEBUI_AUTH,
DEFAULT_MODELS, DEFAULT_MODELS,
DEFAULT_PROMPT_SUGGESTIONS, DEFAULT_PROMPT_SUGGESTIONS,
...@@ -23,7 +23,9 @@ from config import ( ...@@ -23,7 +23,9 @@ from config import (
WEBHOOK_URL, WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER, WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
JWT_EXPIRES_IN, JWT_EXPIRES_IN,
WEBUI_BANNERS,
AppConfig, AppConfig,
ENABLE_COMMUNITY_SHARING,
) )
app = FastAPI() app = FastAPI()
...@@ -40,7 +42,9 @@ app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS ...@@ -40,7 +42,9 @@ app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
app.state.config.WEBHOOK_URL = WEBHOOK_URL app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.config.BANNERS = WEBUI_BANNERS
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.MODELS = {} app.state.MODELS = {}
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
......
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from peewee import * from peewee import *
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional from typing import List, Union, Optional
import time import time
from utils.misc import get_gravatar_url from utils.misc import get_gravatar_url
from apps.webui.internal.db import DB from apps.webui.internal.db import DB, JSONField
from apps.webui.models.chats import Chats from apps.webui.models.chats import Chats
#################### ####################
...@@ -25,11 +25,18 @@ class User(Model): ...@@ -25,11 +25,18 @@ class User(Model):
created_at = BigIntegerField() created_at = BigIntegerField()
api_key = CharField(null=True, unique=True) api_key = CharField(null=True, unique=True)
settings = JSONField(null=True)
class Meta: class Meta:
database = DB database = DB
class UserSettings(BaseModel):
ui: Optional[dict] = {}
model_config = ConfigDict(extra="allow")
pass
class UserModel(BaseModel): class UserModel(BaseModel):
id: str id: str
name: str name: str
...@@ -42,6 +49,7 @@ class UserModel(BaseModel): ...@@ -42,6 +49,7 @@ class UserModel(BaseModel):
created_at: int # timestamp in epoch created_at: int # timestamp in epoch
api_key: Optional[str] = None api_key: Optional[str] = None
settings: Optional[UserSettings] = None
#################### ####################
......
...@@ -8,6 +8,8 @@ from pydantic import BaseModel ...@@ -8,6 +8,8 @@ from pydantic import BaseModel
import time import time
import uuid import uuid
from config import BannerModel
from apps.webui.models.users import Users from apps.webui.models.users import Users
from utils.utils import ( from utils.utils import (
...@@ -57,3 +59,31 @@ async def set_global_default_suggestions( ...@@ -57,3 +59,31 @@ async def set_global_default_suggestions(
data = form_data.model_dump() data = form_data.model_dump()
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"] request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS
############################
# SetBanners
############################
class SetBannersForm(BaseModel):
banners: List[BannerModel]
@router.post("/banners", response_model=List[BannerModel])
async def set_banners(
request: Request,
form_data: SetBannersForm,
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.config.BANNERS = data["banners"]
return request.app.state.config.BANNERS
@router.get("/banners", response_model=List[BannerModel])
async def get_banners(
request: Request,
user=Depends(get_current_user),
):
return request.app.state.config.BANNERS
...@@ -9,7 +9,13 @@ import time ...@@ -9,7 +9,13 @@ import time
import uuid import uuid
import logging import logging
from apps.webui.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users from apps.webui.models.users import (
UserModel,
UserUpdateForm,
UserRoleUpdateForm,
UserSettings,
Users,
)
from apps.webui.models.auths import Auths from apps.webui.models.auths import Auths
from apps.webui.models.chats import Chats from apps.webui.models.chats import Chats
...@@ -68,6 +74,42 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin ...@@ -68,6 +74,42 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin
) )
############################
# GetUserSettingsBySessionUser
############################
@router.get("/user/settings", response_model=Optional[UserSettings])
async def get_user_settings_by_session_user(user=Depends(get_verified_user)):
user = Users.get_user_by_id(user.id)
if user:
return user.settings
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# UpdateUserSettingsBySessionUser
############################
@router.post("/user/settings/update", response_model=UserSettings)
async def update_user_settings_by_session_user(
form_data: UserSettings, user=Depends(get_verified_user)
):
user = Users.update_user_by_id(user.id, {"settings": form_data.model_dump()})
if user:
return user.settings
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################ ############################
# GetUserById # GetUserById
############################ ############################
...@@ -81,6 +123,8 @@ class UserResponse(BaseModel): ...@@ -81,6 +123,8 @@ class UserResponse(BaseModel):
@router.get("/{user_id}", response_model=UserResponse) @router.get("/{user_id}", response_model=UserResponse)
async def get_user_by_id(user_id: str, user=Depends(get_verified_user)): async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
# Check if user_id is a shared chat
# If it is, get the user_id from the chat
if user_id.startswith("shared-"): if user_id.startswith("shared-"):
chat_id = user_id.replace("shared-", "") chat_id = user_id.replace("shared-", "")
chat = Chats.get_chat_by_id(chat_id) chat = Chats.get_chat_by_id(chat_id)
......
...@@ -8,6 +8,8 @@ from chromadb import Settings ...@@ -8,6 +8,8 @@ from chromadb import Settings
from base64 import b64encode from base64 import b64encode
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from typing import TypeVar, Generic, Union from typing import TypeVar, Generic, Union
from pydantic import BaseModel
from typing import Optional
from pathlib import Path from pathlib import Path
import json import json
...@@ -166,10 +168,10 @@ CHANGELOG = changelog_json ...@@ -166,10 +168,10 @@ CHANGELOG = changelog_json
#################################### ####################################
# WEBUI_VERSION # WEBUI_BUILD_HASH
#################################### ####################################
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100") WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
#################################### ####################################
# DATA/FRONTEND BUILD DIR # DATA/FRONTEND BUILD DIR
...@@ -566,6 +568,27 @@ WEBHOOK_URL = PersistentConfig( ...@@ -566,6 +568,27 @@ WEBHOOK_URL = PersistentConfig(
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true" ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
ENABLE_COMMUNITY_SHARING = PersistentConfig(
"ENABLE_COMMUNITY_SHARING",
"ui.enable_community_sharing",
os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true",
)
class BannerModel(BaseModel):
id: str
type: str
title: Optional[str] = None
content: str
dismissible: bool
timestamp: int
WEBUI_BANNERS = PersistentConfig(
"WEBUI_BANNERS",
"ui.banners",
[BannerModel(**banner) for banner in json.loads("[]")],
)
#################################### ####################################
# WEBUI_SECRET_KEY # WEBUI_SECRET_KEY
#################################### ####################################
......
...@@ -56,6 +56,7 @@ from config import ( ...@@ -56,6 +56,7 @@ from config import (
ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_EXPORT,
RAG_WEB_SEARCH_ENABLED, RAG_WEB_SEARCH_ENABLED,
AppConfig, AppConfig,
WEBUI_BUILD_HASH,
) )
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES
...@@ -85,7 +86,8 @@ print( ...@@ -85,7 +86,8 @@ print(
|_| |_|
v{VERSION} - building the best open-source AI user interface. v{VERSION} - building the best open-source AI user interface.
{f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""}
https://github.com/open-webui/open-webui https://github.com/open-webui/open-webui
""" """
) )
...@@ -357,15 +359,18 @@ async def get_app_config(): ...@@ -357,15 +359,18 @@ async def get_app_config():
"status": True, "status": True,
"name": WEBUI_NAME, "name": WEBUI_NAME,
"version": VERSION, "version": VERSION,
"auth": WEBUI_AUTH,
"auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
"enable_signup": webui_app.state.config.ENABLE_SIGNUP,
"enable_image_generation": images_app.state.config.ENABLED,
"enable_admin_export": ENABLE_ADMIN_EXPORT,
"default_locale": default_locale, "default_locale": default_locale,
"default_models": webui_app.state.config.DEFAULT_MODELS, "default_models": webui_app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS, "default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
"enable_websearch": RAG_WEB_SEARCH_ENABLED, "features": {
"auth": WEBUI_AUTH,
"auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
"enable_signup": webui_app.state.config.ENABLE_SIGNUP,
"enable_websearch": RAG_WEB_SEARCH_ENABLED,
"enable_image_generation": images_app.state.config.ENABLED,
"enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING,
"enable_admin_export": ENABLE_ADMIN_EXPORT,
},
} }
...@@ -416,6 +421,19 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): ...@@ -416,6 +421,19 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
} }
@app.get("/api/community_sharing", response_model=bool)
async def get_community_sharing_status(request: Request, user=Depends(get_admin_user)):
return webui_app.state.config.ENABLE_COMMUNITY_SHARING
@app.get("/api/community_sharing/toggle", response_model=bool)
async def toggle_community_sharing(request: Request, user=Depends(get_admin_user)):
webui_app.state.config.ENABLE_COMMUNITY_SHARING = (
not webui_app.state.config.ENABLE_COMMUNITY_SHARING
)
return webui_app.state.config.ENABLE_COMMUNITY_SHARING
@app.get("/api/version") @app.get("/api/version")
async def get_app_config(): async def get_app_config():
return { return {
......
# noqa: INP001 # noqa: INP001
import os
import shutil import shutil
import subprocess import subprocess
from sys import stderr from sys import stderr
...@@ -18,4 +19,5 @@ class CustomBuildHook(BuildHookInterface): ...@@ -18,4 +19,5 @@ class CustomBuildHook(BuildHookInterface):
stderr.write("### npm install\n") stderr.write("### npm install\n")
subprocess.run([npm, "install"], check=True) # noqa: S603 subprocess.run([npm, "install"], check=True) # noqa: S603
stderr.write("\n### npm run build\n") stderr.write("\n### npm run build\n")
os.environ["APP_BUILD_HASH"] = version
subprocess.run([npm, "run", "build"], check=True) # noqa: S603 subprocess.run([npm, "run", "build"], check=True) # noqa: S603
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.2.0.dev1", "version": "0.2.0.dev2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.2.0.dev1", "version": "0.2.0.dev2",
"dependencies": { "dependencies": {
"@pyscript/core": "^0.4.32", "@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^1.3.1", "@sveltejs/adapter-node": "^1.3.1",
......
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.2.0.dev1", "version": "0.2.0.dev2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run pyodide:fetch && vite dev --host", "dev": "npm run pyodide:fetch && vite dev --host",
......
import { WEBUI_API_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL } from '$lib/constants';
import type { Banner } from '$lib/types';
export const setDefaultModels = async (token: string, models: string) => { export const setDefaultModels = async (token: string, models: string) => {
let error = null; let error = null;
...@@ -59,3 +60,60 @@ export const setDefaultPromptSuggestions = async (token: string, promptSuggestio ...@@ -59,3 +60,60 @@ export const setDefaultPromptSuggestions = async (token: string, promptSuggestio
return res; return res;
}; };
export const getBanners = async (token: string): Promise<Banner[]> => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const setBanners = async (token: string, banners: Banner[]) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
banners: banners
})
})
.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_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
export const getModels = async (token: string = '') => { export const getModels = async (token: string = '') => {
let error = null; let error = null;
...@@ -246,6 +246,60 @@ export const updateWebhookUrl = async (token: string, url: string) => { ...@@ -246,6 +246,60 @@ export const updateWebhookUrl = async (token: string, url: string) => {
return res.url; return res.url;
}; };
export const getCommunitySharingEnabledStatus = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/community_sharing`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const toggleCommunitySharingEnabledStatus = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/community_sharing/toggle`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const getModelConfig = async (token: string): Promise<GlobalModelConfig> => { export const getModelConfig = async (token: string): Promise<GlobalModelConfig> => {
let error = null; let error = null;
......
...@@ -115,6 +115,62 @@ export const getUsers = async (token: string) => { ...@@ -115,6 +115,62 @@ export const getUsers = async (token: string) => {
return res ? res : []; return res ? res : [];
}; };
export const getUserSettings = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const updateUserSettings = async (token: string, settings: object) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
...settings
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const getUserById = async (token: string, userId: string) => { export const getUserById = async (token: string, userId: string) => {
let error = null; let error = null;
......
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { getContext, onMount } from 'svelte';
import { banners as _banners } from '$lib/stores';
import type { Banner } from '$lib/types';
import { getBanners, setBanners } from '$lib/apis/configs';
import type { Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
const i18n: Writable<i18nType> = getContext('i18n');
export let saveHandler: Function;
let banners: Banner[] = [];
onMount(async () => {
banners = await getBanners(localStorage.token);
});
const updateBanners = async () => {
_banners.set(await setBanners(localStorage.token, banners));
};
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
updateBanners();
saveHandler();
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80 h-full">
<div class=" space-y-3 pr-1.5">
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">
{$i18n.t('Banners')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
if (banners.length === 0 || banners.at(-1).content !== '') {
banners = [
...banners,
{
id: uuidv4(),
type: '',
title: '',
content: '',
dismissible: true,
timestamp: Math.floor(Date.now() / 1000)
}
];
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
/>
</svg>
</button>
</div>
<div class="flex flex-col space-y-1">
{#each banners as banner, bannerIdx}
<div class=" flex justify-between">
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
<select
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
bind:value={banner.type}
>
{#if banner.type == ''}
<option value="" selected disabled class="text-gray-900">{$i18n.t('Type')}</option
>
{/if}
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
</select>
<input
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
placeholder={$i18n.t('Content')}
bind:value={banner.content}
/>
<div class="relative top-1.5 -left-2">
<Tooltip content="Dismissible" className="flex h-fit items-center">
<Switch bind:state={banner.dismissible} />
</Tooltip>
</div>
</div>
<button
class="px-2"
type="button"
on:click={() => {
banners.splice(bannerIdx, 1);
banners = banners;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
{/each}
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
Save
</button>
</div>
</form>
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
<div> <div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
{#if $config?.enable_admin_export ?? true} {#if $config?.features.enable_admin_export ?? true}
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> --> <!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
......
<script lang="ts"> <script lang="ts">
import { getWebhookUrl, updateWebhookUrl } from '$lib/apis'; import {
getCommunitySharingEnabledStatus,
getWebhookUrl,
toggleCommunitySharingEnabledStatus,
updateWebhookUrl
} from '$lib/apis';
import { import {
getDefaultUserRole, getDefaultUserRole,
getJWTExpiresDuration, getJWTExpiresDuration,
...@@ -18,6 +23,7 @@ ...@@ -18,6 +23,7 @@
let JWTExpiresIn = ''; let JWTExpiresIn = '';
let webhookUrl = ''; let webhookUrl = '';
let communitySharingEnabled = true;
const toggleSignUpEnabled = async () => { const toggleSignUpEnabled = async () => {
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token); signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
...@@ -35,11 +41,28 @@ ...@@ -35,11 +41,28 @@
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl); webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
}; };
const toggleCommunitySharingEnabled = async () => {
communitySharingEnabled = await toggleCommunitySharingEnabledStatus(localStorage.token);
};
onMount(async () => { onMount(async () => {
signUpEnabled = await getSignUpEnabledStatus(localStorage.token); await Promise.all([
defaultUserRole = await getDefaultUserRole(localStorage.token); (async () => {
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token); signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
webhookUrl = await getWebhookUrl(localStorage.token); })(),
(async () => {
defaultUserRole = await getDefaultUserRole(localStorage.token);
})(),
(async () => {
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
})(),
(async () => {
webhookUrl = await getWebhookUrl(localStorage.token);
})(),
(async () => {
communitySharingEnabled = await getCommunitySharingEnabledStatus(localStorage.token);
})()
]);
}); });
</script> </script>
...@@ -114,6 +137,47 @@ ...@@ -114,6 +137,47 @@
</div> </div>
</div> </div>
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleCommunitySharingEnabled();
}}
type="button"
>
{#if communitySharingEnabled}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
/>
</svg>
<span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
{:else}
<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="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
clip-rule="evenodd"
/>
</svg>
<span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
{/if}
</button>
</div>
<hr class=" dark:border-gray-700 my-3" /> <hr class=" dark:border-gray-700 my-3" />
<div class=" w-full justify-between"> <div class=" w-full justify-between">
......
<script lang="ts"> <script lang="ts">
import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis'; import { getBackendConfig, getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users'; import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { models } from '$lib/stores'; import { models, config } from '$lib/stores';
import Switch from '$lib/components/common/Switch.svelte';
import { setDefaultModels } from '$lib/apis/configs';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
let defaultModelId = '';
let whitelistEnabled = false; let whitelistEnabled = false;
let whitelistModels = ['']; let whitelistModels = [''];
let permissions = { let permissions = {
...@@ -24,9 +28,10 @@ ...@@ -24,9 +28,10 @@
const res = await getModelFilterConfig(localStorage.token); const res = await getModelFilterConfig(localStorage.token);
if (res) { if (res) {
whitelistEnabled = res.enabled; whitelistEnabled = res.enabled;
whitelistModels = res.models.length > 0 ? res.models : ['']; whitelistModels = res.models.length > 0 ? res.models : [''];
} }
defaultModelId = $config.default_models ? $config?.default_models.split(',')[0] : '';
}); });
</script> </script>
...@@ -34,10 +39,13 @@ ...@@ -34,10 +39,13 @@
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={async () => { on:submit|preventDefault={async () => {
// console.log('submit'); // console.log('submit');
await updateUserPermissions(localStorage.token, permissions);
await setDefaultModels(localStorage.token, defaultModelId);
await updateUserPermissions(localStorage.token, permissions);
await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels); await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
saveHandler(); saveHandler();
await config.set(await getBackendConfig());
}} }}
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
...@@ -88,26 +96,40 @@ ...@@ -88,26 +96,40 @@
<hr class=" dark:border-gray-700 my-2" /> <hr class=" dark:border-gray-700 my-2" />
<div class="mt-2 space-y-3 pr-1.5"> <div class="mt-2 space-y-3">
<div> <div>
<div class="mb-2"> <div class="mb-2">
<div class="flex justify-between items-center text-xs"> <div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div> <div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
</div> </div>
</div> </div>
<div class=" space-y-1 mb-3">
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
</div>
</div>
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={defaultModelId}
placeholder="Select a model"
>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
{/each}
</select>
</div>
</div>
<div class=" space-y-3"> <div class=" space-y-1">
<div> <div class="mb-2">
<div class="flex justify-between items-center text-xs"> <div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div> <div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
<button <Switch bind:state={whitelistEnabled} />
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
whitelistEnabled = !whitelistEnabled;
}}>{whitelistEnabled ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div> </div>
</div> </div>
......
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