forked from open-webui/open-webui
Merge branch 'main' into functions
This commit is contained in:
commit
837feb4e79
65 changed files with 2439 additions and 481 deletions
|
@ -5,6 +5,8 @@ OLLAMA_API_BASE_URL='http://localhost:11434/api'
|
|||
OPENAI_API_BASE_URL=''
|
||||
OPENAI_API_KEY=''
|
||||
|
||||
# AUTOMATIC1111_BASE_URL="http://localhost:7860"
|
||||
|
||||
# DO NOT TRACK
|
||||
SCARF_NO_ANALYTICS=true
|
||||
DO_NOT_TRACK=true
|
49
.github/workflows/build-release.yml
vendored
Normal file
49
.github/workflows/build-release.yml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # or whatever branch you want to use
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check for changes in package.json
|
||||
run: |
|
||||
git diff --cached --diff-filter=d package.json || {
|
||||
echo "No changes to package.json"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Get version number from package.json
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(jq -r '.version' package.json)
|
||||
echo "::set-output name=version::$VERSION"
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const release = await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: `v${{ steps.get_version.outputs.version }}`,
|
||||
name: `v${{ steps.get_version.outputs.version }}`,
|
||||
body: 'Automatically created new release',
|
||||
})
|
||||
console.log(`Created release ${release.data.html_url}`)
|
||||
|
||||
- name: Upload package to GitHub release
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: package
|
||||
path: .
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
25
CHANGELOG.md
Normal file
25
CHANGELOG.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.1.102] - 2024-02-22
|
||||
|
||||
### Added
|
||||
|
||||
- **🖼️ Image Generation**: Generate Images using the AUTOMATIC1111/stable-diffusion-webui API. You can set this up in Settings > Images.
|
||||
- **📝 Change title generation prompt**: Change the prompt used to generate titles for your chats. You can set this up in the Settings > Interface.
|
||||
- **🤖 Change embedding model**: Change the embedding model used to generate embeddings for your chats in the Dockerfile. Use any sentence transformer model from huggingface.co.
|
||||
- **📢 CHANGELOG.md/Popup**: This popup will show you the latest changes.
|
||||
|
||||
## [0.1.101] - 2024-02-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- LaTex output formatting issue (#828)
|
||||
|
||||
### Changed
|
||||
|
||||
- Instead of having the previous 1.0.0-alpha.101, we switched to semantic versioning as a way to respect global conventions.
|
25
Dockerfile
25
Dockerfile
|
@ -30,10 +30,24 @@ ENV WEBUI_SECRET_KEY ""
|
|||
ENV SCARF_NO_ANALYTICS true
|
||||
ENV DO_NOT_TRACK true
|
||||
|
||||
#Whisper TTS Settings
|
||||
######## Preloaded models ########
|
||||
# whisper TTS Settings
|
||||
ENV WHISPER_MODEL="base"
|
||||
ENV WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
|
||||
|
||||
# RAG Embedding Model Settings
|
||||
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
||||
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
||||
# for better persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
||||
# IMPORTANT: If you change the default model (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.
|
||||
ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2"
|
||||
# device type for whisper tts and ebbeding models - "cpu" (default), "cuda" (nvidia gpu and CUDA required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||
ENV RAG_EMBEDDING_MODEL_DEVICE_TYPE="cpu"
|
||||
ENV RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models"
|
||||
ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR
|
||||
|
||||
######## Preloaded models ########
|
||||
|
||||
WORKDIR /app/backend
|
||||
|
||||
# install python dependencies
|
||||
|
@ -48,9 +62,10 @@ RUN apt-get update \
|
|||
&& apt-get install -y pandoc netcat-openbsd \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# RUN python -c "from sentence_transformers import SentenceTransformer; model = SentenceTransformer('all-MiniLM-L6-v2')"
|
||||
RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"
|
||||
|
||||
# preload embedding model
|
||||
RUN python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device=os.environ['RAG_EMBEDDING_MODEL_DEVICE_TYPE'])"
|
||||
# preload tts model
|
||||
RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='auto', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"
|
||||
|
||||
# copy embedding weight from build
|
||||
RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
|
||||
|
@ -58,6 +73,8 @@ COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onn
|
|||
|
||||
# copy built frontend files
|
||||
COPY --from=build /app/build /app/build
|
||||
COPY --from=build /app/CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY --from=build /app/package.json /app/package.json
|
||||
|
||||
# copy backend files
|
||||
COPY ./backend .
|
||||
|
|
39
README.md
39
README.md
|
@ -1,17 +1,17 @@
|
|||
# Open WebUI (Formerly Ollama WebUI) 👋
|
||||
|
||||
![GitHub stars](https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social)
|
||||
![GitHub forks](https://img.shields.io/github/forks/ollama-webui/ollama-webui?style=social)
|
||||
![GitHub watchers](https://img.shields.io/github/watchers/ollama-webui/ollama-webui?style=social)
|
||||
![GitHub repo size](https://img.shields.io/github/repo-size/ollama-webui/ollama-webui)
|
||||
![GitHub language count](https://img.shields.io/github/languages/count/ollama-webui/ollama-webui)
|
||||
![GitHub top language](https://img.shields.io/github/languages/top/ollama-webui/ollama-webui)
|
||||
![GitHub last commit](https://img.shields.io/github/last-commit/ollama-webui/ollama-webui?color=red)
|
||||
![GitHub stars](https://img.shields.io/github/stars/open-webui/open-webui?style=social)
|
||||
![GitHub forks](https://img.shields.io/github/forks/open-webui/open-webui?style=social)
|
||||
![GitHub watchers](https://img.shields.io/github/watchers/open-webui/open-webui?style=social)
|
||||
![GitHub repo size](https://img.shields.io/github/repo-size/open-webui/open-webui)
|
||||
![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui)
|
||||
![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
|
||||
![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
|
||||
![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Follama-webui%2Follama-wbui&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)
|
||||
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
|
||||
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
|
||||
|
||||
ChatGPT-Style Web Interface for Ollama 🦙
|
||||
User-friendly WebUI for LLMs, Inspired by ChatGPT
|
||||
|
||||
![Open WebUI Demo](./demo.gif)
|
||||
|
||||
|
@ -83,11 +83,11 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
|
|||
|
||||
🌟 **Important Note on User Roles and Privacy:**
|
||||
|
||||
- **Admin Creation:** The very first account to sign up on the Open WebUI will be granted **Administrator privileges**. This account will have comprehensive control over the platform, including user management and system settings.
|
||||
- **Admin Creation:** The very first account to sign up on Open WebUI will be granted **Administrator privileges**. This account will have comprehensive control over the platform, including user management and system settings.
|
||||
|
||||
- **User Registrations:** All subsequent users signing up will initially have their accounts set to **Pending** status by default. These accounts will require approval from the Administrator to gain access to the platform functionalities.
|
||||
|
||||
- **Privacy and Data Security:** We prioritize your privacy and data security above all. Please be reassured that all data entered into the Open WebUI is stored locally on your device. Our system is designed to be privacy-first, ensuring that no external requests are made, and your data does not leave your local environment. We are committed to maintaining the highest standards of data privacy and security, ensuring that your information remains confidential and under your control.
|
||||
- **Privacy and Data Security:** We prioritize your privacy and data security above all. Please be reassured that all data entered into Open WebUI is stored locally on your device. Our system is designed to be privacy-first, ensuring that no external requests are made, and your data does not leave your local environment. We are committed to maintaining the highest standards of data privacy and security, ensuring that your information remains confidential and under your control.
|
||||
|
||||
### Steps to Install Open WebUI
|
||||
|
||||
|
@ -212,14 +212,13 @@ For other ways to install, like using Kustomize or Helm, check out [INSTALLATION
|
|||
|
||||
### Updating your Docker Installation
|
||||
|
||||
In case you want to update your local Docker installation to the latest version, you can do it performing the following actions:
|
||||
In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
|
||||
|
||||
```bash
|
||||
docker rm -f open-webui
|
||||
docker pull ghcr.io/open-webui/open-webui:main
|
||||
[insert command you used to install]
|
||||
docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui
|
||||
```
|
||||
In the last line, you need to use the very same command you used to install (local install, remote server, etc.)
|
||||
|
||||
In the last part of the command, replace `open-webui` with your container name if it is different.
|
||||
|
||||
### Moving from Ollama WebUI to Open WebUI
|
||||
|
||||
|
@ -257,17 +256,13 @@ Once you verify that all the data has been migrated you can erase the old volume
|
|||
docker volume rm ollama-webui
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## How to Install Without Docker
|
||||
|
||||
While we strongly recommend using our convenient Docker container installation for optimal support, we understand that some situations may require a non-Docker setup, especially for development purposes. Please note that non-Docker installations are not officially supported, and you might need to troubleshoot on your own.
|
||||
|
||||
### Project Components
|
||||
|
||||
The Open WebUI consists of two primary components: the frontend and the backend (which serves as a reverse proxy, handling static frontend files, and additional features). Both need to be running concurrently for the development environment.
|
||||
Open WebUI consists of two primary components: the frontend and the backend (which serves as a reverse proxy, handling static frontend files, and additional features). Both need to be running concurrently for the development environment.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The backend is required for proper functionality
|
||||
|
@ -286,7 +281,7 @@ git clone https://github.com/open-webui/open-webui.git
|
|||
cd open-webui/
|
||||
|
||||
# Copying required .env file
|
||||
cp -RPp example.env .env
|
||||
cp -RPp .env.example .env
|
||||
|
||||
# Building Frontend Using Node
|
||||
npm i
|
||||
|
@ -302,7 +297,7 @@ pip install -r requirements.txt -U
|
|||
sh start.sh
|
||||
```
|
||||
|
||||
You should have the Open WebUI up and running at http://localhost:8080/. Enjoy! 😄
|
||||
You should have Open WebUI up and running at http://localhost:8080/. Enjoy! 😄
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
|
@ -7,4 +7,5 @@ uploads
|
|||
_test
|
||||
Pipfile
|
||||
data/*
|
||||
!data/config.json
|
||||
.webui_secret_key
|
|
@ -56,7 +56,7 @@ def transcribe(
|
|||
|
||||
model = WhisperModel(
|
||||
WHISPER_MODEL,
|
||||
device="cpu",
|
||||
device="auto",
|
||||
compute_type="int8",
|
||||
download_root=WHISPER_MODEL_DIR,
|
||||
)
|
||||
|
|
193
backend/apps/images/main.py
Normal file
193
backend/apps/images/main.py
Normal file
|
@ -0,0 +1,193 @@
|
|||
import re
|
||||
import requests
|
||||
from fastapi import (
|
||||
FastAPI,
|
||||
Request,
|
||||
Depends,
|
||||
HTTPException,
|
||||
status,
|
||||
UploadFile,
|
||||
File,
|
||||
Form,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
get_current_user,
|
||||
get_admin_user,
|
||||
)
|
||||
from utils.misc import calculate_sha256
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from config import AUTOMATIC1111_BASE_URL
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
app.state.ENABLED = app.state.AUTOMATIC1111_BASE_URL != ""
|
||||
app.state.IMAGE_SIZE = "512x512"
|
||||
|
||||
|
||||
@app.get("/enabled", response_model=bool)
|
||||
async def get_enable_status(request: Request, user=Depends(get_admin_user)):
|
||||
return app.state.ENABLED
|
||||
|
||||
|
||||
@app.get("/enabled/toggle", response_model=bool)
|
||||
async def toggle_enabled(request: Request, user=Depends(get_admin_user)):
|
||||
try:
|
||||
r = requests.head(app.state.AUTOMATIC1111_BASE_URL)
|
||||
app.state.ENABLED = not app.state.ENABLED
|
||||
return app.state.ENABLED
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
|
||||
class UrlUpdateForm(BaseModel):
|
||||
url: str
|
||||
|
||||
|
||||
@app.get("/url")
|
||||
async def get_openai_url(user=Depends(get_admin_user)):
|
||||
return {"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL}
|
||||
|
||||
|
||||
@app.post("/url/update")
|
||||
async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
|
||||
|
||||
if form_data.url == "":
|
||||
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
else:
|
||||
app.state.AUTOMATIC1111_BASE_URL = form_data.url.strip("/")
|
||||
|
||||
return {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
|
||||
"status": True,
|
||||
}
|
||||
|
||||
|
||||
class ImageSizeUpdateForm(BaseModel):
|
||||
size: str
|
||||
|
||||
|
||||
@app.get("/size")
|
||||
async def get_image_size(user=Depends(get_admin_user)):
|
||||
return {"IMAGE_SIZE": app.state.IMAGE_SIZE}
|
||||
|
||||
|
||||
@app.post("/size/update")
|
||||
async def update_image_size(
|
||||
form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
pattern = r"^\d+x\d+$" # Regular expression pattern
|
||||
if re.match(pattern, form_data.size):
|
||||
app.state.IMAGE_SIZE = form_data.size
|
||||
return {
|
||||
"IMAGE_SIZE": app.state.IMAGE_SIZE,
|
||||
"status": True,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
|
||||
)
|
||||
|
||||
|
||||
@app.get("/models")
|
||||
def get_models(user=Depends(get_current_user)):
|
||||
try:
|
||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models")
|
||||
models = r.json()
|
||||
return models
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
|
||||
@app.get("/models/default")
|
||||
async def get_default_model(user=Depends(get_admin_user)):
|
||||
try:
|
||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
||||
options = r.json()
|
||||
|
||||
return {"model": options["sd_model_checkpoint"]}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
|
||||
class UpdateModelForm(BaseModel):
|
||||
model: str
|
||||
|
||||
|
||||
def set_model_handler(model: str):
|
||||
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
|
||||
options = r.json()
|
||||
|
||||
if model != options["sd_model_checkpoint"]:
|
||||
options["sd_model_checkpoint"] = model
|
||||
r = requests.post(
|
||||
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@app.post("/models/default/update")
|
||||
def update_default_model(
|
||||
form_data: UpdateModelForm,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
return set_model_handler(form_data.model)
|
||||
|
||||
|
||||
class GenerateImageForm(BaseModel):
|
||||
model: Optional[str] = None
|
||||
prompt: str
|
||||
n: int = 1
|
||||
size: str = "512x512"
|
||||
negative_prompt: Optional[str] = None
|
||||
|
||||
|
||||
@app.post("/generations")
|
||||
def generate_image(
|
||||
form_data: GenerateImageForm,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
|
||||
print(form_data)
|
||||
|
||||
try:
|
||||
if form_data.model:
|
||||
set_model_handler(form_data.model)
|
||||
|
||||
width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
|
||||
|
||||
data = {
|
||||
"prompt": form_data.prompt,
|
||||
"batch_size": form_data.n,
|
||||
"width": width,
|
||||
"height": height,
|
||||
}
|
||||
|
||||
if form_data.negative_prompt != None:
|
||||
data["negative_prompt"] = form_data.negative_prompt
|
||||
|
||||
print(data)
|
||||
|
||||
r = requests.post(
|
||||
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
|
||||
json=data,
|
||||
)
|
||||
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e))
|
|
@ -1,6 +1,5 @@
|
|||
from fastapi import (
|
||||
FastAPI,
|
||||
Request,
|
||||
Depends,
|
||||
HTTPException,
|
||||
status,
|
||||
|
@ -14,7 +13,8 @@ import os, shutil
|
|||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
# from chromadb.utils import embedding_functions
|
||||
from sentence_transformers import SentenceTransformer
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
from langchain_community.document_loaders import (
|
||||
WebBaseLoader,
|
||||
|
@ -30,16 +30,12 @@ from langchain_community.document_loaders import (
|
|||
UnstructuredExcelLoader,
|
||||
)
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from langchain.chains import RetrievalQA
|
||||
from langchain_community.vectorstores import Chroma
|
||||
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import mimetypes
|
||||
import uuid
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
from apps.web.models.documents import (
|
||||
|
@ -58,23 +54,37 @@ from utils.utils import get_current_user, get_admin_user
|
|||
from config import (
|
||||
UPLOAD_DIR,
|
||||
DOCS_DIR,
|
||||
EMBED_MODEL,
|
||||
RAG_EMBEDDING_MODEL,
|
||||
RAG_EMBEDDING_MODEL_DEVICE_TYPE,
|
||||
CHROMA_CLIENT,
|
||||
CHUNK_SIZE,
|
||||
CHUNK_OVERLAP,
|
||||
RAG_TEMPLATE,
|
||||
)
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
# EMBEDDING_FUNC = embedding_functions.SentenceTransformerEmbeddingFunction(
|
||||
# model_name=EMBED_MODEL
|
||||
# )
|
||||
#
|
||||
# if RAG_EMBEDDING_MODEL:
|
||||
# sentence_transformer_ef = SentenceTransformer(
|
||||
# model_name_or_path=RAG_EMBEDDING_MODEL,
|
||||
# cache_folder=RAG_EMBEDDING_MODEL_DIR,
|
||||
# device=RAG_EMBEDDING_MODEL_DEVICE_TYPE,
|
||||
# )
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.state.CHUNK_SIZE = CHUNK_SIZE
|
||||
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||
app.state.RAG_TEMPLATE = RAG_TEMPLATE
|
||||
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||
app.state.sentence_transformer_ef = (
|
||||
embedding_functions.SentenceTransformerEmbeddingFunction(
|
||||
model_name=app.state.RAG_EMBEDDING_MODEL,
|
||||
device=RAG_EMBEDDING_MODEL_DEVICE_TYPE,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
origins = ["*"]
|
||||
|
@ -106,7 +116,10 @@ def store_data_in_vector_db(data, collection_name) -> bool:
|
|||
metadatas = [doc.metadata for doc in docs]
|
||||
|
||||
try:
|
||||
collection = CHROMA_CLIENT.create_collection(name=collection_name)
|
||||
collection = CHROMA_CLIENT.create_collection(
|
||||
name=collection_name,
|
||||
embedding_function=app.state.sentence_transformer_ef,
|
||||
)
|
||||
|
||||
collection.add(
|
||||
documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts]
|
||||
|
@ -126,6 +139,38 @@ async def get_status():
|
|||
"status": True,
|
||||
"chunk_size": app.state.CHUNK_SIZE,
|
||||
"chunk_overlap": app.state.CHUNK_OVERLAP,
|
||||
"template": app.state.RAG_TEMPLATE,
|
||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/embedding/model")
|
||||
async def get_embedding_model(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"status": True,
|
||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||
}
|
||||
|
||||
|
||||
class EmbeddingModelUpdateForm(BaseModel):
|
||||
embedding_model: str
|
||||
|
||||
|
||||
@app.post("/embedding/model/update")
|
||||
async def update_embedding_model(
|
||||
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
||||
app.state.sentence_transformer_ef = (
|
||||
embedding_functions.SentenceTransformerEmbeddingFunction(
|
||||
model_name=app.state.RAG_EMBEDDING_MODEL,
|
||||
device=RAG_EMBEDDING_MODEL_DEVICE_TYPE,
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
|
||||
}
|
||||
|
||||
|
||||
|
@ -190,8 +235,10 @@ def query_doc(
|
|||
user=Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
# if you use docker use the model from the environment variable
|
||||
collection = CHROMA_CLIENT.get_collection(
|
||||
name=form_data.collection_name,
|
||||
embedding_function=app.state.sentence_transformer_ef,
|
||||
)
|
||||
result = collection.query(query_texts=[form_data.query], n_results=form_data.k)
|
||||
return result
|
||||
|
@ -263,9 +310,12 @@ def query_collection(
|
|||
|
||||
for collection_name in form_data.collection_names:
|
||||
try:
|
||||
# if you use docker use the model from the environment variable
|
||||
collection = CHROMA_CLIENT.get_collection(
|
||||
name=collection_name,
|
||||
embedding_function=app.state.sentence_transformer_ef,
|
||||
)
|
||||
|
||||
result = collection.query(
|
||||
query_texts=[form_data.query], n_results=form_data.k
|
||||
)
|
||||
|
|
|
@ -26,6 +26,8 @@ app = FastAPI()
|
|||
origins = ["*"]
|
||||
|
||||
app.state.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
app.state.JWT_EXPIRES_IN = "-1"
|
||||
|
||||
app.state.DEFAULT_MODELS = DEFAULT_MODELS
|
||||
app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
|
@ -55,7 +57,6 @@ app.include_router(utils.router, prefix="/utils", tags=["utils"])
|
|||
async def get_status():
|
||||
return {
|
||||
"status": True,
|
||||
"version": WEBUI_VERSION,
|
||||
"auth": WEBUI_AUTH,
|
||||
"default_models": app.state.DEFAULT_MODELS,
|
||||
"default_prompt_suggestions": app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
|
|
|
@ -7,6 +7,7 @@ from fastapi import APIRouter, status
|
|||
from pydantic import BaseModel
|
||||
import time
|
||||
import uuid
|
||||
import re
|
||||
|
||||
from apps.web.models.auths import (
|
||||
SigninForm,
|
||||
|
@ -25,7 +26,7 @@ from utils.utils import (
|
|||
get_admin_user,
|
||||
create_token,
|
||||
)
|
||||
from utils.misc import get_gravatar_url, validate_email_format
|
||||
from utils.misc import parse_duration, validate_email_format
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
router = APIRouter()
|
||||
|
@ -95,10 +96,13 @@ async def update_password(
|
|||
|
||||
|
||||
@router.post("/signin", response_model=SigninResponse)
|
||||
async def signin(form_data: SigninForm):
|
||||
async def signin(request: Request, form_data: SigninForm):
|
||||
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
|
||||
if user:
|
||||
token = create_token(data={"id": user.id})
|
||||
token = create_token(
|
||||
data={"id": user.id},
|
||||
expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
|
||||
)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
|
@ -145,7 +149,10 @@ async def signup(request: Request, form_data: SignupForm):
|
|||
)
|
||||
|
||||
if user:
|
||||
token = create_token(data={"id": user.id})
|
||||
token = create_token(
|
||||
data={"id": user.id},
|
||||
expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
|
||||
)
|
||||
# response.set_cookie(key='token', value=token, httponly=True)
|
||||
|
||||
return {
|
||||
|
@ -200,3 +207,33 @@ async def update_default_user_role(
|
|||
if form_data.role in ["pending", "user", "admin"]:
|
||||
request.app.state.DEFAULT_USER_ROLE = form_data.role
|
||||
return request.app.state.DEFAULT_USER_ROLE
|
||||
|
||||
|
||||
############################
|
||||
# JWT Expiration
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/token/expires")
|
||||
async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.JWT_EXPIRES_IN
|
||||
|
||||
|
||||
class UpdateJWTExpiresDurationForm(BaseModel):
|
||||
duration: str
|
||||
|
||||
|
||||
@router.post("/token/expires/update")
|
||||
async def update_token_expires_duration(
|
||||
request: Request,
|
||||
form_data: UpdateJWTExpiresDurationForm,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"
|
||||
|
||||
# Check if the input string matches the pattern
|
||||
if re.match(pattern, form_data.duration):
|
||||
request.app.state.JWT_EXPIRES_IN = form_data.duration
|
||||
return request.app.state.JWT_EXPIRES_IN
|
||||
else:
|
||||
return request.app.state.JWT_EXPIRES_IN
|
||||
|
|
|
@ -5,6 +5,10 @@ from secrets import token_bytes
|
|||
from base64 import b64encode
|
||||
from constants import ERROR_MESSAGES
|
||||
from pathlib import Path
|
||||
import json
|
||||
import markdown
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv, find_dotenv
|
||||
|
@ -21,6 +25,75 @@ except ImportError:
|
|||
ENV = os.environ.get("ENV", "dev")
|
||||
|
||||
|
||||
try:
|
||||
with open(f"../package.json", "r") as f:
|
||||
PACKAGE_DATA = json.load(f)
|
||||
except:
|
||||
PACKAGE_DATA = {"version": "0.0.0"}
|
||||
|
||||
VERSION = PACKAGE_DATA["version"]
|
||||
|
||||
|
||||
# Function to parse each section
|
||||
def parse_section(section):
|
||||
items = []
|
||||
for li in section.find_all("li"):
|
||||
# Extract raw HTML string
|
||||
raw_html = str(li)
|
||||
|
||||
# Extract text without HTML tags
|
||||
text = li.get_text(separator=" ", strip=True)
|
||||
|
||||
# Split into title and content
|
||||
parts = text.split(": ", 1)
|
||||
title = parts[0].strip() if len(parts) > 1 else ""
|
||||
content = parts[1].strip() if len(parts) > 1 else text
|
||||
|
||||
items.append({"title": title, "content": content, "raw": raw_html})
|
||||
return items
|
||||
|
||||
|
||||
try:
|
||||
with open("../CHANGELOG.md", "r") as file:
|
||||
changelog_content = file.read()
|
||||
except:
|
||||
changelog_content = ""
|
||||
|
||||
# Convert markdown content to HTML
|
||||
html_content = markdown.markdown(changelog_content)
|
||||
|
||||
# Parse the HTML content
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
|
||||
# Initialize JSON structure
|
||||
changelog_json = {}
|
||||
|
||||
# Iterate over each version
|
||||
for version in soup.find_all("h2"):
|
||||
version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets
|
||||
date = version.get_text().strip().split(" - ")[1]
|
||||
|
||||
version_data = {"date": date}
|
||||
|
||||
# Find the next sibling that is a h3 tag (section title)
|
||||
current = version.find_next_sibling()
|
||||
|
||||
print(current)
|
||||
|
||||
while current and current.name != "h2":
|
||||
if current.name == "h3":
|
||||
section_title = current.get_text().lower() # e.g., "added", "fixed"
|
||||
section_items = parse_section(current.find_next_sibling("ul"))
|
||||
version_data[section_title] = section_items
|
||||
|
||||
# Move to the next element
|
||||
current = current.find_next_sibling()
|
||||
|
||||
changelog_json[version_number] = version_data
|
||||
|
||||
|
||||
CHANGELOG = changelog_json
|
||||
|
||||
####################################
|
||||
# DATA/FRONTEND BUILD DIR
|
||||
####################################
|
||||
|
@ -28,6 +101,12 @@ ENV = os.environ.get("ENV", "dev")
|
|||
DATA_DIR = str(Path(os.getenv("DATA_DIR", "./data")).resolve())
|
||||
FRONTEND_BUILD_DIR = str(Path(os.getenv("FRONTEND_BUILD_DIR", "../build")))
|
||||
|
||||
try:
|
||||
with open(f"{DATA_DIR}/config.json", "r") as f:
|
||||
CONFIG_DATA = json.load(f)
|
||||
except:
|
||||
CONFIG_DATA = {}
|
||||
|
||||
####################################
|
||||
# File Upload DIR
|
||||
####################################
|
||||
|
@ -87,9 +166,14 @@ if OPENAI_API_BASE_URL == "":
|
|||
|
||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", True)
|
||||
DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
|
||||
DEFAULT_PROMPT_SUGGESTIONS = os.environ.get(
|
||||
"DEFAULT_PROMPT_SUGGESTIONS",
|
||||
[
|
||||
|
||||
|
||||
DEFAULT_PROMPT_SUGGESTIONS = (
|
||||
CONFIG_DATA["ui"]["prompt_suggestions"]
|
||||
if "ui" in CONFIG_DATA
|
||||
and "prompt_suggestions" in CONFIG_DATA["ui"]
|
||||
and type(CONFIG_DATA["ui"]["prompt_suggestions"]) is list
|
||||
else [
|
||||
{
|
||||
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
||||
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
|
||||
|
@ -106,8 +190,10 @@ DEFAULT_PROMPT_SUGGESTIONS = os.environ.get(
|
|||
"title": ["Show me a code snippet", "of a website's sticky header"],
|
||||
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
|
||||
},
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_USER_ROLE = "pending"
|
||||
USER_PERMISSIONS = {"chat": {"deletion": True}}
|
||||
|
||||
|
@ -143,7 +229,12 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
|
|||
####################################
|
||||
|
||||
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
||||
EMBED_MODEL = "all-MiniLM-L6-v2"
|
||||
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (all-MiniLM-L6-v2)
|
||||
RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2")
|
||||
# device type ebbeding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||
RAG_EMBEDDING_MODEL_DEVICE_TYPE = os.environ.get(
|
||||
"RAG_EMBEDDING_MODEL_DEVICE_TYPE", "cpu"
|
||||
)
|
||||
CHROMA_CLIENT = chromadb.PersistentClient(
|
||||
path=CHROMA_DATA_PATH,
|
||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||
|
@ -172,3 +263,10 @@ Query: [query]"""
|
|||
|
||||
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base")
|
||||
WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
|
||||
|
||||
|
||||
####################################
|
||||
# Images
|
||||
####################################
|
||||
|
||||
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
|
||||
|
|
|
@ -44,3 +44,6 @@ class ERROR_MESSAGES(str, Enum):
|
|||
MALICIOUS = "Unusual activities detected, please try again in a few minutes."
|
||||
|
||||
PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
|
||||
INCORRECT_FORMAT = (
|
||||
lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
|
||||
)
|
||||
|
|
34
backend/data/config.json
Normal file
34
backend/data/config.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"ui": {
|
||||
"prompt_suggestions": [
|
||||
{
|
||||
"title": [
|
||||
"Help me study",
|
||||
"vocabulary for a college entrance exam"
|
||||
],
|
||||
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option."
|
||||
},
|
||||
{
|
||||
"title": [
|
||||
"Give me ideas",
|
||||
"for what to do with my kids' art"
|
||||
],
|
||||
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
|
||||
},
|
||||
{
|
||||
"title": [
|
||||
"Tell me a fun fact",
|
||||
"about the Roman Empire"
|
||||
],
|
||||
"content": "Tell me a random fun fact about the Roman Empire"
|
||||
},
|
||||
{
|
||||
"title": [
|
||||
"Show me a code snippet",
|
||||
"of a website's sticky header"
|
||||
],
|
||||
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
from bs4 import BeautifulSoup
|
||||
import json
|
||||
import markdown
|
||||
import time
|
||||
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi import HTTPException
|
||||
|
@ -12,11 +16,12 @@ from apps.ollama.main import app as ollama_app
|
|||
from apps.openai.main import app as openai_app
|
||||
from apps.audio.main import app as audio_app
|
||||
from apps.functions.main import app as functions_app
|
||||
|
||||
from apps.web.main import app as webui_app
|
||||
from apps.images.main import app as images_app
|
||||
from apps.rag.main import app as rag_app
|
||||
|
||||
from config import ENV, FRONTEND_BUILD_DIR
|
||||
from apps.web.main import app as webui_app
|
||||
|
||||
from config import ENV, VERSION, CHANGELOG, FRONTEND_BUILD_DIR
|
||||
|
||||
|
||||
class SPAStaticFiles(StaticFiles):
|
||||
|
@ -57,12 +62,30 @@ app.mount("/api/v1", webui_app)
|
|||
|
||||
app.mount("/ollama/api", ollama_app)
|
||||
app.mount("/openai/api", openai_app)
|
||||
|
||||
app.mount("/images/api/v1", images_app)
|
||||
app.mount("/audio/api/v1", audio_app)
|
||||
app.mount("/rag/api/v1", rag_app)
|
||||
app.mount("/audio/api/v1", audio_app)
|
||||
app.mount("/functions/api/v1", functions_app)
|
||||
|
||||
|
||||
@app.get("/api/config")
|
||||
async def get_app_config():
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"version": VERSION,
|
||||
"images": images_app.state.ENABLED,
|
||||
"default_models": webui_app.state.DEFAULT_MODELS,
|
||||
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/changelog")
|
||||
async def get_app_changelog():
|
||||
return CHANGELOG
|
||||
|
||||
|
||||
app.mount(
|
||||
"/",
|
||||
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from pathlib import Path
|
||||
import hashlib
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def get_gravatar_url(email):
|
||||
|
@ -76,3 +78,34 @@ def extract_folders_after_data_docs(path):
|
|||
tags.append("/".join(folders[: idx + 1]))
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def parse_duration(duration: str) -> Optional[timedelta]:
|
||||
if duration == "-1" or duration == "0":
|
||||
return None
|
||||
|
||||
# Regular expression to find number and unit pairs
|
||||
pattern = r"(-?\d+(\.\d+)?)(ms|s|m|h|d|w)"
|
||||
matches = re.findall(pattern, duration)
|
||||
|
||||
if not matches:
|
||||
raise ValueError("Invalid duration string")
|
||||
|
||||
total_duration = timedelta()
|
||||
|
||||
for number, _, unit in matches:
|
||||
number = float(number)
|
||||
if unit == "ms":
|
||||
total_duration += timedelta(milliseconds=number)
|
||||
elif unit == "s":
|
||||
total_duration += timedelta(seconds=number)
|
||||
elif unit == "m":
|
||||
total_duration += timedelta(minutes=number)
|
||||
elif unit == "h":
|
||||
total_duration += timedelta(hours=number)
|
||||
elif unit == "d":
|
||||
total_duration += timedelta(days=number)
|
||||
elif unit == "w":
|
||||
total_duration += timedelta(weeks=number)
|
||||
|
||||
return total_duration
|
||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
BIN
demo.gif
BIN
demo.gif
Binary file not shown.
Before Width: | Height: | Size: 5.9 MiB After Width: | Height: | Size: 6.1 MiB |
|
@ -1,5 +1,5 @@
|
|||
apiVersion: v2
|
||||
name: ollama-webui
|
||||
description: "Ollama Web UI: A User-Friendly Web Interface for Chat Interactions 👋"
|
||||
name: open-webui
|
||||
description: "Open WebUI: A User-Friendly Web Interface for Chat Interactions 👋"
|
||||
version: 1.0.0
|
||||
icon: https://raw.githubusercontent.com/ollama-webui/ollama-webui/main/static/favicon.png
|
||||
icon: https://raw.githubusercontent.com/open-webui/open-webui/main/static/favicon.png
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ollama-webui-deployment
|
||||
name: open-webui-deployment
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ollama-webui
|
||||
app: open-webui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ollama-webui
|
||||
app: open-webui
|
||||
spec:
|
||||
containers:
|
||||
- name: ollama-webui
|
||||
- name: open-webui
|
||||
image: {{ .Values.webui.image }}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
|
@ -35,4 +35,4 @@ spec:
|
|||
volumes:
|
||||
- name: webui-volume
|
||||
persistentVolumeClaim:
|
||||
claimName: ollama-webui-pvc
|
||||
claimName: open-webui-pvc
|
|
@ -2,7 +2,7 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ollama-webui-ingress
|
||||
name: open-webui-ingress
|
||||
namespace: {{ .Values.namespace }}
|
||||
{{- if .Values.webui.ingress.annotations }}
|
||||
annotations:
|
||||
|
@ -17,7 +17,7 @@ spec:
|
|||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ollama-webui-service
|
||||
name: open-webui-service
|
||||
port:
|
||||
number: {{ .Values.webui.servicePort }}
|
||||
{{- end }}
|
||||
|
|
|
@ -2,8 +2,8 @@ apiVersion: v1
|
|||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
labels:
|
||||
app: ollama-webui
|
||||
name: ollama-webui-pvc
|
||||
app: open-webui
|
||||
name: open-webui-pvc
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ollama-webui-service
|
||||
name: open-webui-service
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
type: {{ .Values.webui.service.type }} # Default: NodePort # Use LoadBalancer if you're on a cloud that supports it
|
||||
selector:
|
||||
app: ollama-webui
|
||||
app: open-webui
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ .Values.webui.servicePort }}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
namespace: ollama-namespace
|
||||
namespace: open-webui
|
||||
|
||||
ollama:
|
||||
replicaCount: 1
|
||||
|
@ -22,7 +22,7 @@ ollama:
|
|||
|
||||
webui:
|
||||
replicaCount: 1
|
||||
image: ghcr.io/ollama-webui/ollama-webui:main
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
servicePort: 8080
|
||||
resources:
|
||||
requests:
|
||||
|
@ -36,7 +36,7 @@ webui:
|
|||
annotations:
|
||||
# Use appropriate annotations for your Ingress controller, e.g., for NGINX:
|
||||
# nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
host: ollama.minikube.local
|
||||
host: open-webui.minikube.local
|
||||
volumeSize: 2Gi
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
|
|
|
@ -2,7 +2,7 @@ apiVersion: v1
|
|||
kind: Service
|
||||
metadata:
|
||||
name: ollama-service
|
||||
namespace: ollama-namespace
|
||||
namespace: open-webui
|
||||
spec:
|
||||
selector:
|
||||
app: ollama
|
||||
|
|
|
@ -2,7 +2,7 @@ apiVersion: apps/v1
|
|||
kind: StatefulSet
|
||||
metadata:
|
||||
name: ollama
|
||||
namespace: ollama-namespace
|
||||
namespace: open-webui
|
||||
spec:
|
||||
serviceName: "ollama"
|
||||
replicas: 1
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: ollama-namespace
|
||||
name: open-webui
|
|
@ -1,21 +1,21 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ollama-webui-deployment
|
||||
namespace: ollama-namespace
|
||||
name: open-webui-deployment
|
||||
namespace: open-webui
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ollama-webui
|
||||
app: open-webui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ollama-webui
|
||||
app: open-webui
|
||||
spec:
|
||||
containers:
|
||||
- name: ollama-webui
|
||||
image: ghcr.io/ollama-webui/ollama-webui:main
|
||||
- name: open-webui
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
resources:
|
||||
|
@ -27,7 +27,7 @@ spec:
|
|||
memory: "1Gi"
|
||||
env:
|
||||
- name: OLLAMA_API_BASE_URL
|
||||
value: "http://ollama-service.ollama-namespace.svc.cluster.local:11434/api"
|
||||
value: "http://ollama-service.open-webui.svc.cluster.local:11434/api"
|
||||
tty: true
|
||||
volumeMounts:
|
||||
- name: webui-volume
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ollama-webui-ingress
|
||||
namespace: ollama-namespace
|
||||
name: open-webui-ingress
|
||||
namespace: open-webui
|
||||
#annotations:
|
||||
# Use appropriate annotations for your Ingress controller, e.g., for NGINX:
|
||||
# nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- host: ollama.minikube.local
|
||||
- host: open-webui.minikube.local
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ollama-webui-service
|
||||
name: open-webui-service
|
||||
port:
|
||||
number: 8080
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ollama-webui-service
|
||||
namespace: ollama-namespace
|
||||
name: open-webui-service
|
||||
namespace: open-webui
|
||||
spec:
|
||||
type: NodePort # Use LoadBalancer if you're on a cloud that supports it
|
||||
selector:
|
||||
app: ollama-webui
|
||||
app: open-webui
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
resources:
|
||||
- base/ollama-namespace.yaml
|
||||
- base/open-webui.yaml
|
||||
- base/ollama-service.yaml
|
||||
- base/ollama-statefulset.yaml
|
||||
- base/webui-deployment.yaml
|
||||
|
|
|
@ -2,7 +2,7 @@ apiVersion: apps/v1
|
|||
kind: StatefulSet
|
||||
metadata:
|
||||
name: ollama
|
||||
namespace: ollama-namespace
|
||||
namespace: open-webui
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
|
|
25
package-lock.json
generated
25
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "ollama-webui",
|
||||
"version": "0.0.1",
|
||||
"name": "open-webui",
|
||||
"version": "v1.0.0-alpha.101",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ollama-webui",
|
||||
"version": "0.0.1",
|
||||
"name": "open-webui",
|
||||
"version": "v1.0.0-alpha.101",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"async": "^3.2.5",
|
||||
|
@ -38,6 +38,7 @@
|
|||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"svelte-confetti": "^1.3.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
|
@ -3174,6 +3175,15 @@
|
|||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-confetti": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-1.3.2.tgz",
|
||||
"integrity": "sha512-R+JwFTC7hIgWVA/OuXrkj384B7CMoceb0t9VacyW6dORTQg0pWojVBB8Bo3tM30cLEQE48Fekzqgx+XSzHESMA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.33.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz",
|
||||
|
@ -5852,6 +5862,13 @@
|
|||
"typescript": "^5.0.3"
|
||||
}
|
||||
},
|
||||
"svelte-confetti": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-1.3.2.tgz",
|
||||
"integrity": "sha512-R+JwFTC7hIgWVA/OuXrkj384B7CMoceb0t9VacyW6dORTQg0pWojVBB8Bo3tM30cLEQE48Fekzqgx+XSzHESMA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"svelte-eslint-parser": {
|
||||
"version": "0.33.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ollama-webui",
|
||||
"version": "0.0.1",
|
||||
"name": "open-webui",
|
||||
"version": "0.1.102",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
|
@ -32,6 +32,7 @@
|
|||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"svelte-confetti": "^1.3.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
|
|
4
run.sh
4
run.sh
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
image_name="ollama-webui"
|
||||
container_name="ollama-webui"
|
||||
image_name="open-webui"
|
||||
container_name="open-webui"
|
||||
host_port=3000
|
||||
container_port=8080
|
||||
|
||||
|
|
|
@ -261,3 +261,60 @@ export const toggleSignUpEnabledStatus = async (token: string) => {
|
|||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getJWTExpiresDuration = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires`, {
|
||||
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 updateJWTExpiresDuration = async (token: string, duration: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
duration: duration
|
||||
})
|
||||
})
|
||||
.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;
|
||||
};
|
||||
|
|
333
src/lib/apis/images/index.ts
Normal file
333
src/lib/apis/images/index.ts
Normal file
|
@ -0,0 +1,333 @@
|
|||
import { IMAGES_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getImageGenerationEnabledStatus = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/enabled`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const toggleImageGenerationEnabledStatus = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/enabled/toggle`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAUTOMATIC1111Url = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.AUTOMATIC1111_BASE_URL;
|
||||
};
|
||||
|
||||
export const updateAUTOMATIC1111Url = async (token: string = '', url: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.AUTOMATIC1111_BASE_URL;
|
||||
};
|
||||
|
||||
export const getImageSize = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/size`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.IMAGE_SIZE;
|
||||
};
|
||||
|
||||
export const updateImageSize = async (token: string = '', size: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/size/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
size: size
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.IMAGE_SIZE;
|
||||
};
|
||||
|
||||
export const getDiffusionModels = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getDefaultDiffusionModel = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.model;
|
||||
};
|
||||
|
||||
export const updateDefaultDiffusionModel = async (token: string = '', model: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.model;
|
||||
};
|
||||
|
||||
export const imageGenerations = async (token: string = '', prompt: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/generations`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: prompt
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
|
@ -1,9 +1,31 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getBackendConfig = async () => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/`, {
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/config`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getChangelog = async () => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/changelog`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
|
|
@ -133,9 +133,19 @@ export const getOllamaModels = async (token: string = '') => {
|
|||
});
|
||||
};
|
||||
|
||||
export const generateTitle = async (token: string = '', model: string, prompt: string) => {
|
||||
// TODO: migrate to backend
|
||||
export const generateTitle = async (
|
||||
token: string = '',
|
||||
template: string,
|
||||
model: string,
|
||||
prompt: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
template = template.replace(/{{prompt}}/g, prompt);
|
||||
|
||||
console.log(template);
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -144,7 +154,7 @@ export const generateTitle = async (token: string = '', model: string, prompt: s
|
|||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
prompt: `Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': ${prompt}`,
|
||||
prompt: template,
|
||||
stream: false
|
||||
})
|
||||
})
|
||||
|
|
115
src/lib/components/ChangelogModal.svelte
Normal file
115
src/lib/components/ChangelogModal.svelte
Normal file
|
@ -0,0 +1,115 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Confetti } from 'svelte-confetti';
|
||||
|
||||
import { config } from '$lib/stores';
|
||||
|
||||
import { WEBUI_NAME, WEB_UI_VERSION } from '$lib/constants';
|
||||
import { getChangelog } from '$lib/apis';
|
||||
|
||||
import Modal from './common/Modal.svelte';
|
||||
|
||||
export let show = false;
|
||||
|
||||
let changelog = null;
|
||||
|
||||
onMount(async () => {
|
||||
const res = await getChangelog();
|
||||
changelog = res;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal bind:show>
|
||||
<div class="px-5 py-4 dark:text-gray-300">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="text-xl font-bold">
|
||||
What’s New in {WEBUI_NAME}
|
||||
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
|
||||
</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<div class="flex items-center mt-1">
|
||||
<div class="text-sm dark:text-gray-200">Release Notes</div>
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<div class="text-sm dark:text-gray-200">
|
||||
v{WEB_UI_VERSION}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800" />
|
||||
|
||||
<div class=" w-full p-4 px-5">
|
||||
<div class=" overflow-y-scroll max-h-80">
|
||||
<div class="mb-3">
|
||||
{#if changelog}
|
||||
{#each Object.keys(changelog) as version}
|
||||
<div class=" mb-3 pr-2">
|
||||
<div class="font-bold text-xl mb-1 dark:text-white">
|
||||
v{version} - {changelog[version].date}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800 my-2" />
|
||||
|
||||
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
|
||||
<div class="">
|
||||
<div
|
||||
class="font-bold uppercase text-xs {section === 'added'
|
||||
? 'text-white bg-blue-600'
|
||||
: section === 'fixed'
|
||||
? 'text-white bg-green-600'
|
||||
: section === 'changed'
|
||||
? 'text-white bg-yellow-600'
|
||||
: section === 'removed'
|
||||
? 'text-white bg-red-600'
|
||||
: ''} w-fit px-3 rounded-full my-2.5"
|
||||
>
|
||||
{section}
|
||||
</div>
|
||||
|
||||
<div class="my-2.5 px-1.5">
|
||||
{#each Object.keys(changelog[version][section]) as item}
|
||||
<div class="text-sm mb-2">
|
||||
<div class="font-semibold uppercase">
|
||||
{changelog[version][section][item].title}
|
||||
</div>
|
||||
<div class="mb-2 mt-1">{changelog[version][section][item].content}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
on:click={() => {
|
||||
localStorage.version = $config.version;
|
||||
show = false;
|
||||
}}
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
>
|
||||
<span class="relative">Okay, Let's Go!</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
|
@ -1,15 +1,18 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
getDefaultUserRole,
|
||||
getJWTExpiresDuration,
|
||||
getSignUpEnabledStatus,
|
||||
toggleSignUpEnabledStatus,
|
||||
updateDefaultUserRole
|
||||
updateDefaultUserRole,
|
||||
updateJWTExpiresDuration
|
||||
} from '$lib/apis/auths';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let saveHandler: Function;
|
||||
let signUpEnabled = true;
|
||||
let defaultUserRole = 'pending';
|
||||
let JWTExpiresIn = '';
|
||||
|
||||
const toggleSignUpEnabled = async () => {
|
||||
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
|
||||
|
@ -19,9 +22,14 @@
|
|||
defaultUserRole = await updateDefaultUserRole(localStorage.token, role);
|
||||
};
|
||||
|
||||
const updateJWTExpiresDurationHandler = async (duration) => {
|
||||
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
|
||||
defaultUserRole = await getDefaultUserRole(localStorage.token);
|
||||
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -29,6 +37,7 @@
|
|||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
// console.log('submit');
|
||||
updateJWTExpiresDurationHandler(JWTExpiresIn);
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
|
@ -94,6 +103,29 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 my-3" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">JWT Expiration</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
type="text"
|
||||
placeholder={`e.g.) "30m","1h", "10d". `}
|
||||
bind:value={JWTExpiresIn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Valid time units: <span class=" text-gray-300 font-medium"
|
||||
>'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
import ResponseMessage from './Messages/ResponseMessage.svelte';
|
||||
import Placeholder from './Messages/Placeholder.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
|
||||
export let chatId = '';
|
||||
export let sendPrompt: Function;
|
||||
|
@ -221,6 +222,34 @@
|
|||
scrollToBottom();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// TODO: change delete behaviour
|
||||
// const deleteMessageAndDescendants = async (messageId: string) => {
|
||||
// if (history.messages[messageId]) {
|
||||
// history.messages[messageId].deleted = true;
|
||||
|
||||
// for (const childId of history.messages[messageId].childrenIds) {
|
||||
// await deleteMessageAndDescendants(childId);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// const triggerDeleteMessageRecursive = async (messageId: string) => {
|
||||
// await deleteMessageAndDescendants(messageId);
|
||||
// await updateChatById(localStorage.token, chatId, { history });
|
||||
// await chats.set(await getChatList(localStorage.token));
|
||||
// };
|
||||
|
||||
const messageDeleteHandler = async (messageId) => {
|
||||
if (history.messages[messageId]) {
|
||||
history.messages[messageId].deleted = true;
|
||||
|
||||
for (const childId of history.messages[messageId].childrenIds) {
|
||||
history.messages[childId].deleted = true;
|
||||
}
|
||||
}
|
||||
await updateChatById(localStorage.token, chatId, { history });
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if messages.length == 0}
|
||||
|
@ -229,89 +258,103 @@
|
|||
<div class=" pb-10">
|
||||
{#key chatId}
|
||||
{#each messages as message, messageIdx}
|
||||
<div class=" w-full">
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
||||
? 'max-w-full'
|
||||
: 'max-w-3xl'} mx-auto rounded-lg group"
|
||||
>
|
||||
{#if message.role === 'user'}
|
||||
<UserMessage
|
||||
user={$user}
|
||||
{message}
|
||||
siblings={message.parentId !== null
|
||||
? history.messages[message.parentId]?.childrenIds ?? []
|
||||
: Object.values(history.messages)
|
||||
.filter((message) => message.parentId === null)
|
||||
.map((message) => message.id) ?? []}
|
||||
{confirmEditMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{copyToClipboard}
|
||||
/>
|
||||
{#if !message.deleted}
|
||||
<div class=" w-full">
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
||||
? 'max-w-full'
|
||||
: 'max-w-3xl'} mx-auto rounded-lg group"
|
||||
>
|
||||
{#if message.role === 'user'}
|
||||
<UserMessage
|
||||
on:delete={() => messageDeleteHandler(message.id)}
|
||||
user={$user}
|
||||
{message}
|
||||
isFirstMessage={messageIdx === 0}
|
||||
siblings={message.parentId !== null
|
||||
? history.messages[message.parentId]?.childrenIds ?? []
|
||||
: Object.values(history.messages)
|
||||
.filter((message) => message.parentId === null)
|
||||
.map((message) => message.id) ?? []}
|
||||
{confirmEditMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{copyToClipboard}
|
||||
/>
|
||||
|
||||
{#if messages.length - 1 === messageIdx && processing !== ''}
|
||||
<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
|
||||
<div class=" dark:text-blue-100">
|
||||
<svg
|
||||
class=" w-4 h-4 translate-y-[0.5px]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
{#if messages.length - 1 === messageIdx && processing !== ''}
|
||||
<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
|
||||
<div class=" dark:text-blue-100">
|
||||
<svg
|
||||
class=" w-4 h-4 translate-y-[0.5px]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="2.5"
|
||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
|
||||
>
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="2.5"
|
||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
|
||||
>
|
||||
</div>
|
||||
<div class=" text-sm font-medium">
|
||||
{processing}
|
||||
</div>
|
||||
</div>
|
||||
<div class=" text-sm font-medium">
|
||||
{processing}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
modelfiles={selectedModelfiles}
|
||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{confirmEditResponseMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
modelfiles={selectedModelfiles}
|
||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{confirmEditResponseMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if bottomPadding}
|
||||
|
|
|
@ -2,20 +2,25 @@
|
|||
import toast from 'svelte-french-toast';
|
||||
import dayjs from 'dayjs';
|
||||
import { marked } from 'marked';
|
||||
import { settings } from '$lib/stores';
|
||||
import tippy from 'tippy.js';
|
||||
import auto_render from 'katex/dist/contrib/auto-render.mjs';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { config, settings } from '$lib/stores';
|
||||
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
import { extractSentences } from '$lib/utils';
|
||||
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
import CodeBlock from './CodeBlock.svelte';
|
||||
|
||||
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
|
||||
import { extractSentences } from '$lib/utils';
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
|
||||
export let modelfiles = [];
|
||||
export let message;
|
||||
|
@ -42,6 +47,7 @@
|
|||
let speakingIdx = null;
|
||||
|
||||
let loadingSpeech = false;
|
||||
let generatingImage = false;
|
||||
|
||||
$: tokens = marked.lexer(message.content);
|
||||
|
||||
|
@ -81,7 +87,9 @@
|
|||
}<br/>
|
||||
prompt_token/s: ${
|
||||
Math.round(
|
||||
((message.info.prompt_eval_count ?? 0) / (message.info.prompt_eval_duration / 1000000000)) * 100
|
||||
((message.info.prompt_eval_count ?? 0) /
|
||||
(message.info.prompt_eval_duration / 1000000000)) *
|
||||
100
|
||||
) / 100 ?? 'N/A'
|
||||
} tokens<br/>
|
||||
total_duration: ${
|
||||
|
@ -114,10 +122,11 @@
|
|||
// customised options
|
||||
// • auto-render specific keys, e.g.:
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
// { left: '$', right: '$', display: false },
|
||||
{ left: '\\(', right: '\\)', display: true },
|
||||
{ left: '\\[', right: '\\]', display: true }
|
||||
{ left: '$$', right: '$$', display: false },
|
||||
{ left: '$', right: '$', display: false },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: false },
|
||||
{ left: '[ ', right: ' ]', display: false }
|
||||
],
|
||||
// • rendering keys, e.g.:
|
||||
throwOnError: false
|
||||
|
@ -264,6 +273,23 @@
|
|||
renderStyling();
|
||||
};
|
||||
|
||||
const generateImage = async (message) => {
|
||||
generatingImage = true;
|
||||
const res = await imageGenerations(localStorage.token, message.content);
|
||||
console.log(res);
|
||||
|
||||
if (res) {
|
||||
message.files = res.images.map((image) => ({
|
||||
type: 'image',
|
||||
url: `data:image/png;base64,${image}`
|
||||
}));
|
||||
|
||||
dispatch('save', message);
|
||||
}
|
||||
|
||||
generatingImage = false;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
renderStyling();
|
||||
|
@ -292,6 +318,18 @@
|
|||
{#if message.content === ''}
|
||||
<Skeleton />
|
||||
{:else}
|
||||
{#if message.files}
|
||||
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
|
||||
{#each message.files as file}
|
||||
<div>
|
||||
{#if file.type === 'image'}
|
||||
<Image src={file.url} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
|
||||
>
|
||||
|
@ -592,6 +630,71 @@
|
|||
{/if}
|
||||
</button>
|
||||
|
||||
{#if $config.images}
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
if (!generatingImage) {
|
||||
generateImage(message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if generatingImage}
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_S1WN {
|
||||
animation: spinner_MGfb 0.8s linear infinite;
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
.spinner_Km9P {
|
||||
animation-delay: -0.65s;
|
||||
}
|
||||
.spinner_JApP {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes spinner_MGfb {
|
||||
93.75%,
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
|
||||
class="spinner_S1WN spinner_Km9P"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="3"
|
||||
/><circle
|
||||
class="spinner_S1WN spinner_JApP"
|
||||
cx="20"
|
||||
cy="12"
|
||||
r="3"
|
||||
/></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if message.info}
|
||||
<button
|
||||
class=" {isLastMessage
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
<script lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { tick } from 'svelte';
|
||||
import { tick, createEventDispatcher } from 'svelte';
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
import { modelfiles, settings } from '$lib/stores';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let user;
|
||||
export let message;
|
||||
export let siblings;
|
||||
export let isFirstMessage: boolean;
|
||||
|
||||
export let confirmEditMessage: Function;
|
||||
export let showPreviousMessage: Function;
|
||||
|
@ -42,6 +45,10 @@
|
|||
edit = false;
|
||||
editedContent = '';
|
||||
};
|
||||
|
||||
const deleteMessageHandler = async () => {
|
||||
dispatch('delete', message.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class=" flex w-full">
|
||||
|
@ -189,11 +196,11 @@
|
|||
<div class="w-full">
|
||||
<pre id="user-message">{message.content}</pre>
|
||||
|
||||
<div class=" flex justify-start space-x-1">
|
||||
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center">
|
||||
<button
|
||||
class="self-center"
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
|
@ -212,12 +219,12 @@
|
|||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="text-xs font-bold self-center">
|
||||
<div class="text-xs font-bold self-center dark:text-gray-100">
|
||||
{siblings.indexOf(message.id) + 1} / {siblings.length}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="self-center"
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
|
@ -239,7 +246,7 @@
|
|||
{/if}
|
||||
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white transition edit-user-message-button"
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
|
@ -261,7 +268,7 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white transition"
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
|
@ -281,6 +288,30 @@
|
|||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if !isFirstMessage}
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
deleteMessageHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { getOllamaVersion } from '$lib/apis/ollama';
|
||||
import { WEBUI_NAME, WEB_UI_VERSION } from '$lib/constants';
|
||||
import { config } from '$lib/stores';
|
||||
import { config, showChangelog } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let ollamaVersion = '';
|
||||
|
@ -15,10 +15,25 @@
|
|||
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
|
||||
<div class=" space-y-3">
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{WEBUI_NAME} Version</div>
|
||||
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
|
||||
<div>
|
||||
{WEBUI_NAME} Version
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
||||
{$config && $config.version ? $config.version : WEB_UI_VERSION}
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200 flex space-x-1.5 items-center">
|
||||
<div>
|
||||
v{WEB_UI_VERSION}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
|
||||
on:click={() => {
|
||||
showChangelog.set(true);
|
||||
}}
|
||||
>
|
||||
<div>See what's new</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,10 +59,17 @@
|
|||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/ollama-webui/ollama-webui" target="_blank">
|
||||
<a href="https://twitter.com/OpenWebUI" target="_blank">
|
||||
<img
|
||||
alt="X (formerly Twitter) Follow"
|
||||
src="https://img.shields.io/twitter/follow/OpenWebUI"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/open-webui/open-webui" target="_blank">
|
||||
<img
|
||||
alt="Github Repo"
|
||||
src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github"
|
||||
src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -7,11 +7,14 @@
|
|||
|
||||
import UpdatePassword from './Account/UpdatePassword.svelte';
|
||||
import { getGravatarUrl } from '$lib/apis/utils';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
let profileImageUrl = '';
|
||||
let name = '';
|
||||
let showJWTToken = false;
|
||||
let JWTTokenCopied = false;
|
||||
|
||||
const submitHandler = async () => {
|
||||
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
||||
|
@ -160,6 +163,108 @@
|
|||
|
||||
<hr class=" dark:border-gray-700 my-4" />
|
||||
<UpdatePassword />
|
||||
|
||||
<hr class=" dark:border-gray-700 my-4" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">JWT Token</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2">
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
type={showJWTToken ? 'text' : 'password'}
|
||||
value={localStorage.token}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<button
|
||||
class="dark:bg-gray-800 px-2 transition rounded-r-lg"
|
||||
on:click={() => {
|
||||
showJWTToken = !showJWTToken;
|
||||
}}
|
||||
>
|
||||
{#if showJWTToken}
|
||||
<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="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg"
|
||||
on:click={() => {
|
||||
copyToClipboard(localStorage.token);
|
||||
JWTTokenCopied = true;
|
||||
setTimeout(() => {
|
||||
JWTTokenCopied = false;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{#if JWTTokenCopied}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{: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="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
updatePasswordHandler();
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-between mb-2.5 items-center text-sm">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">Change Password</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
|
@ -51,7 +51,7 @@
|
|||
</div>
|
||||
|
||||
{#if show}
|
||||
<div class=" space-y-1.5">
|
||||
<div class=" py-2.5 space-y-1.5">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">Current Password</div>
|
||||
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
|
||||
import { models, user } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { getOllamaAPIUrl, updateOllamaAPIUrl } from '$lib/apis/ollama';
|
||||
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
|
||||
import toast from 'svelte-french-toast';
|
||||
|
||||
export let getModels: Function;
|
||||
|
||||
// External
|
||||
let API_BASE_URL = '';
|
||||
|
||||
let OPENAI_API_KEY = '';
|
||||
let OPENAI_API_BASE_URL = '';
|
||||
|
||||
|
@ -17,8 +22,19 @@
|
|||
await models.set(await getModels());
|
||||
};
|
||||
|
||||
const updateOllamaAPIUrlHandler = async () => {
|
||||
API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
|
||||
const _models = await getModels('ollama');
|
||||
|
||||
if (_models.length > 0) {
|
||||
toast.success('Server connection verified');
|
||||
await models.set(_models);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
|
||||
OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
|
||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
||||
}
|
||||
|
@ -26,7 +42,7 @@
|
|||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
class="flex flex-col h-full space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateOpenAIHandler();
|
||||
dispatch('save');
|
||||
|
@ -37,6 +53,52 @@
|
|||
// });
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
placeholder="Enter URL (e.g. http://localhost:11434/api)"
|
||||
bind:value={API_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
|
||||
on:click={() => {
|
||||
updateOllamaAPIUrlHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Trouble accessing Ollama?
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
href="https://github.com/open-webui/open-webui#troubleshooting"
|
||||
target="_blank"
|
||||
>
|
||||
Click here for help.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div class=" space-y-3">
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div>
|
||||
|
@ -50,13 +112,8 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Adds optional support for online models.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">OpenAI API Base URL</div>
|
||||
<div class="flex w-full">
|
|
@ -3,31 +3,20 @@
|
|||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { getOllamaAPIUrl, updateOllamaAPIUrl } from '$lib/apis/ollama';
|
||||
import { models, user } from '$lib/stores';
|
||||
|
||||
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
||||
|
||||
export let saveSettings: Function;
|
||||
export let getModels: Function;
|
||||
|
||||
// General
|
||||
let API_BASE_URL = '';
|
||||
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
|
||||
let theme = 'dark';
|
||||
let notificationEnabled = false;
|
||||
let system = '';
|
||||
|
||||
const toggleTheme = async () => {
|
||||
if (theme === 'dark') {
|
||||
theme = 'light';
|
||||
} else {
|
||||
theme = 'dark';
|
||||
}
|
||||
|
||||
localStorage.theme = theme;
|
||||
|
||||
document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark');
|
||||
document.documentElement.classList.add(theme);
|
||||
};
|
||||
let showAdvanced = false;
|
||||
|
||||
const toggleNotification = async () => {
|
||||
const permission = await Notification.requestPermission();
|
||||
|
@ -42,170 +31,233 @@
|
|||
}
|
||||
};
|
||||
|
||||
const updateOllamaAPIUrlHandler = async () => {
|
||||
API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
|
||||
const _models = await getModels('ollama');
|
||||
// Advanced
|
||||
let requestFormat = '';
|
||||
let keepAlive = null;
|
||||
|
||||
if (_models.length > 0) {
|
||||
toast.success('Server connection verified');
|
||||
await models.set(_models);
|
||||
let options = {
|
||||
// Advanced
|
||||
seed: 0,
|
||||
temperature: '',
|
||||
repeat_penalty: '',
|
||||
repeat_last_n: '',
|
||||
mirostat: '',
|
||||
mirostat_eta: '',
|
||||
mirostat_tau: '',
|
||||
top_k: '',
|
||||
top_p: '',
|
||||
stop: '',
|
||||
tfs_z: '',
|
||||
num_ctx: '',
|
||||
num_predict: ''
|
||||
};
|
||||
|
||||
const toggleRequestFormat = async () => {
|
||||
if (requestFormat === '') {
|
||||
requestFormat = 'json';
|
||||
} else {
|
||||
requestFormat = '';
|
||||
}
|
||||
|
||||
saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
|
||||
}
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
|
||||
theme = localStorage.theme ?? 'dark';
|
||||
notificationEnabled = settings.notificationEnabled ?? false;
|
||||
system = settings.system ?? '';
|
||||
|
||||
requestFormat = settings.requestFormat ?? '';
|
||||
keepAlive = settings.keepAlive ?? null;
|
||||
|
||||
options.seed = settings.seed ?? 0;
|
||||
options.temperature = settings.temperature ?? '';
|
||||
options.repeat_penalty = settings.repeat_penalty ?? '';
|
||||
options.top_k = settings.top_k ?? '';
|
||||
options.top_p = settings.top_p ?? '';
|
||||
options.num_ctx = settings.num_ctx ?? '';
|
||||
options = { ...options, ...settings.options };
|
||||
options.stop = (settings?.options?.stop ?? []).join(',');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col space-y-3">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">WebUI Settings</div>
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" pr-1.5 overflow-y-scroll max-h-[21rem]">
|
||||
<div class="">
|
||||
<div class=" mb-1 text-sm font-medium">WebUI Settings</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Theme</div>
|
||||
<div class="flex items-center relative">
|
||||
<div class=" absolute right-16">
|
||||
{#if theme === 'dark'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else if theme === 'light'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4 self-center"
|
||||
>
|
||||
<path
|
||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Theme</div>
|
||||
<div class="flex items-center relative">
|
||||
<div class=" absolute right-16">
|
||||
{#if theme === 'dark'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else if theme === 'light'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4 self-center"
|
||||
>
|
||||
<path
|
||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<select
|
||||
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={theme}
|
||||
placeholder="Select a theme"
|
||||
on:change={(e) => {
|
||||
localStorage.theme = theme;
|
||||
<select
|
||||
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={theme}
|
||||
placeholder="Select a theme"
|
||||
on:change={(e) => {
|
||||
localStorage.theme = theme;
|
||||
|
||||
themes
|
||||
.filter((e) => e !== theme)
|
||||
.forEach((e) => {
|
||||
e.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.remove(e);
|
||||
themes
|
||||
.filter((e) => e !== theme)
|
||||
.forEach((e) => {
|
||||
e.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.remove(e);
|
||||
});
|
||||
});
|
||||
|
||||
theme.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.add(e);
|
||||
});
|
||||
|
||||
theme.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.add(e);
|
||||
});
|
||||
|
||||
console.log(theme);
|
||||
}}
|
||||
>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="rose-pine dark">Rosé Pine</option>
|
||||
<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Notification</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleNotification();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if notificationEnabled === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
placeholder="Enter URL (e.g. http://localhost:11434/api)"
|
||||
bind:value={API_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
|
||||
on:click={() => {
|
||||
updateOllamaAPIUrlHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
console.log(theme);
|
||||
}}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="rose-pine dark">Rosé Pine</option>
|
||||
<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Trouble accessing Ollama?
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
|
||||
target="_blank"
|
||||
>
|
||||
Click here for help.
|
||||
</a>
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Notification</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleNotification();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if notificationEnabled === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-700 my-3" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">System Prompt</div>
|
||||
<textarea
|
||||
bind:value={system}
|
||||
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
<div>
|
||||
<div class=" my-2.5 text-sm font-medium">System Prompt</div>
|
||||
<textarea
|
||||
bind:value={system}
|
||||
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 space-y-3 pr-1.5">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">Advanced Parameters</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAdvanced = !showAdvanced;
|
||||
}}>{showAdvanced ? 'Hide' : 'Show'}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if showAdvanced}
|
||||
<AdvancedParams bind:options />
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div class=" py-1 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Keep Alive</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
keepAlive = keepAlive === null ? '5m' : null;
|
||||
}}
|
||||
>
|
||||
{#if keepAlive === null}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if keepAlive !== null}
|
||||
<div class="flex mt-1 space-x-2">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
type="text"
|
||||
placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`}
|
||||
bind:value={keepAlive}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-1 flex w-full justify-between">
|
||||
<div class=" self-center text-sm font-medium">Request Mode</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleRequestFormat();
|
||||
}}
|
||||
>
|
||||
{#if requestFormat === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
{:else if requestFormat === 'json'}
|
||||
<!-- <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4 self-center"
|
||||
>
|
||||
<path
|
||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||
/>
|
||||
</svg> -->
|
||||
<span class="ml-2 self-center"> JSON </span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
|
@ -213,7 +265,23 @@
|
|||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
on:click={() => {
|
||||
saveSettings({
|
||||
system: system !== '' ? system : undefined
|
||||
system: system !== '' ? system : undefined,
|
||||
options: {
|
||||
seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
|
||||
stop: options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined,
|
||||
temperature: options.temperature !== '' ? options.temperature : undefined,
|
||||
repeat_penalty: options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
|
||||
repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
|
||||
mirostat: options.mirostat !== '' ? options.mirostat : undefined,
|
||||
mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
|
||||
mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
|
||||
top_k: options.top_k !== '' ? options.top_k : undefined,
|
||||
top_p: options.top_p !== '' ? options.top_p : undefined,
|
||||
tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
|
||||
num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined,
|
||||
num_predict: options.num_predict !== '' ? options.num_predict : undefined
|
||||
},
|
||||
keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
|
||||
});
|
||||
dispatch('save');
|
||||
}}
|
||||
|
|
255
src/lib/components/chat/Settings/Images.svelte
Normal file
255
src/lib/components/chat/Settings/Images.svelte
Normal file
|
@ -0,0 +1,255 @@
|
|||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { config, user } from '$lib/stores';
|
||||
import {
|
||||
getAUTOMATIC1111Url,
|
||||
getDefaultDiffusionModel,
|
||||
getDiffusionModels,
|
||||
getImageGenerationEnabledStatus,
|
||||
getImageSize,
|
||||
toggleImageGenerationEnabledStatus,
|
||||
updateAUTOMATIC1111Url,
|
||||
updateDefaultDiffusionModel,
|
||||
updateImageSize
|
||||
} from '$lib/apis/images';
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let saveSettings: Function;
|
||||
|
||||
let loading = false;
|
||||
|
||||
let enableImageGeneration = true;
|
||||
let AUTOMATIC1111_BASE_URL = '';
|
||||
|
||||
let selectedModel = '';
|
||||
let models = [];
|
||||
|
||||
let imageSize = '';
|
||||
|
||||
const getModels = async () => {
|
||||
models = await getDiffusionModels(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
selectedModel = await getDefaultDiffusionModel(localStorage.token);
|
||||
};
|
||||
|
||||
const updateAUTOMATIC1111UrlHandler = async () => {
|
||||
const res = await updateAUTOMATIC1111Url(localStorage.token, AUTOMATIC1111_BASE_URL).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
AUTOMATIC1111_BASE_URL = res;
|
||||
|
||||
await getModels();
|
||||
|
||||
if (models) {
|
||||
toast.success('Server connection verified');
|
||||
}
|
||||
} else {
|
||||
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
|
||||
}
|
||||
};
|
||||
const toggleImageGeneration = async () => {
|
||||
if (AUTOMATIC1111_BASE_URL) {
|
||||
enableImageGeneration = await toggleImageGenerationEnabledStatus(localStorage.token).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
if (enableImageGeneration) {
|
||||
config.set(await getBackendConfig(localStorage.token));
|
||||
getModels();
|
||||
}
|
||||
} else {
|
||||
enableImageGeneration = false;
|
||||
toast.error('AUTOMATIC1111_BASE_URL not provided');
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
enableImageGeneration = await getImageGenerationEnabledStatus(localStorage.token);
|
||||
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
|
||||
|
||||
if (enableImageGeneration && AUTOMATIC1111_BASE_URL) {
|
||||
imageSize = await getImageSize(localStorage.token);
|
||||
getModels();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
loading = true;
|
||||
await updateDefaultDiffusionModel(localStorage.token, selectedModel);
|
||||
await updateImageSize(localStorage.token, imageSize).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
dispatch('save');
|
||||
loading = false;
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[21rem]">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">Image Settings</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Image Generation (Experimental)</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleImageGeneration();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if enableImageGeneration === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div class=" mb-2.5 text-sm font-medium">AUTOMATIC1111 Base URL</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
placeholder="Enter URL (e.g. http://127.0.0.1:7860/)"
|
||||
bind:value={AUTOMATIC1111_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
// updateOllamaAPIUrlHandler();
|
||||
|
||||
updateAUTOMATIC1111UrlHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Include `--api` flag when running stable-diffusion-webui
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
|
||||
target="_blank"
|
||||
>
|
||||
(e.g. `sh webui.sh --api`)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if enableImageGeneration}
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Set Default 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={selectedModel}
|
||||
placeholder="Select a model"
|
||||
>
|
||||
{#if !selectedModel}
|
||||
<option value="" disabled selected>Select a model</option>
|
||||
{/if}
|
||||
{#each models as model}
|
||||
<option value={model.title} class="bg-gray-100 dark:bg-gray-700"
|
||||
>{model.model_name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Set Image Size</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
placeholder="Enter Image Size (e.g. 512x512)"
|
||||
bind:value={imageSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded flex flex-row space-x-1 items-center {loading
|
||||
? ' cursor-not-allowed'
|
||||
: ''}"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
Save
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
|
@ -13,6 +13,7 @@
|
|||
let responseAutoCopy = false;
|
||||
let titleAutoGenerateModel = '';
|
||||
let fullScreenMode = false;
|
||||
let titleGenerationPrompt = '';
|
||||
|
||||
// Interface
|
||||
let promptSuggestions = [];
|
||||
|
@ -56,8 +57,14 @@
|
|||
};
|
||||
|
||||
const updateInterfaceHandler = async () => {
|
||||
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
|
||||
await config.set(await getBackendConfig());
|
||||
if ($user.role === 'admin') {
|
||||
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
|
||||
await config.set(await getBackendConfig());
|
||||
}
|
||||
|
||||
saveSettings({
|
||||
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
||||
});
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
|
@ -72,6 +79,9 @@
|
|||
showUsername = settings.showUsername ?? false;
|
||||
fullScreenMode = settings.fullScreenMode ?? false;
|
||||
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
|
||||
titleGenerationPrompt =
|
||||
settings.titleGenerationPrompt ??
|
||||
`Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -212,6 +222,14 @@
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-sm font-medium">Title Generation Prompt</div>
|
||||
<textarea
|
||||
bind:value={titleGenerationPrompt}
|
||||
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Account from './Settings/Account.svelte';
|
||||
import Advanced from './Settings/Advanced.svelte';
|
||||
import About from './Settings/About.svelte';
|
||||
import Models from './Settings/Models.svelte';
|
||||
import General from './Settings/General.svelte';
|
||||
import External from './Settings/External.svelte';
|
||||
import Interface from './Settings/Interface.svelte';
|
||||
import Audio from './Settings/Audio.svelte';
|
||||
import Chats from './Settings/Chats.svelte';
|
||||
import Connections from './Settings/Connections.svelte';
|
||||
import Images from './Settings/Images.svelte';
|
||||
|
||||
export let show = false;
|
||||
|
||||
|
@ -102,31 +102,31 @@
|
|||
<div class=" self-center">General</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'advanced'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'advanced';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M17 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM17 15.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM3.75 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5a.75.75 0 01.75-.75zM4.5 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM10 11a.75.75 0 01.75.75v5.5a.75.75 0 01-1.5 0v-5.5A.75.75 0 0110 11zM10.75 2.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM10 6a2 2 0 100 4 2 2 0 000-4zM3.75 10a2 2 0 100 4 2 2 0 000-4zM16.25 10a2 2 0 100 4 2 2 0 000-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Advanced</div>
|
||||
</button>
|
||||
|
||||
{#if $user?.role === 'admin'}
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'connections'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'connections';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Connections</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'models'
|
||||
|
@ -152,30 +152,6 @@
|
|||
</div>
|
||||
<div class=" self-center">Models</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'external'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'external';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">External</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
|
@ -196,7 +172,7 @@
|
|||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
@ -231,6 +207,34 @@
|
|||
<div class=" self-center">Audio</div>
|
||||
</button>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'images'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'images';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<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="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Images</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'chats'
|
||||
|
@ -318,17 +322,10 @@
|
|||
show = false;
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'advanced'}
|
||||
<Advanced
|
||||
on:save={() => {
|
||||
show = false;
|
||||
}}
|
||||
{saveSettings}
|
||||
/>
|
||||
{:else if selectedTab === 'models'}
|
||||
<Models {getModels} />
|
||||
{:else if selectedTab === 'external'}
|
||||
<External
|
||||
{:else if selectedTab === 'connections'}
|
||||
<Connections
|
||||
{getModels}
|
||||
on:save={() => {
|
||||
show = false;
|
||||
|
@ -348,6 +345,13 @@
|
|||
show = false;
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'images'}
|
||||
<Images
|
||||
{saveSettings}
|
||||
on:save={() => {
|
||||
show = false;
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'chats'}
|
||||
<Chats {saveSettings} />
|
||||
{:else if selectedTab === 'account'}
|
||||
|
|
18
src/lib/components/common/Image.svelte
Normal file
18
src/lib/components/common/Image.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import ImagePreview from './ImagePreview.svelte';
|
||||
|
||||
export let src = '';
|
||||
export let alt = '';
|
||||
|
||||
let showImagePreview = false;
|
||||
</script>
|
||||
|
||||
<ImagePreview bind:show={showImagePreview} {src} {alt} />
|
||||
<button
|
||||
on:click={() => {
|
||||
console.log('image preview');
|
||||
showImagePreview = true;
|
||||
}}
|
||||
>
|
||||
<img {src} {alt} class=" max-h-96 rounded-lg" draggable="false" />
|
||||
</button>
|
62
src/lib/components/common/ImagePreview.svelte
Normal file
62
src/lib/components/common/ImagePreview.svelte
Normal file
|
@ -0,0 +1,62 @@
|
|||
<script lang="ts">
|
||||
export let show = false;
|
||||
export let src = '';
|
||||
export let alt = '';
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="fixed top-0 right-0 left-0 bottom-0 bg-black text-white w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
|
||||
>
|
||||
<div class=" absolute left-0 w-full flex justify-between">
|
||||
<div>
|
||||
<button
|
||||
class=" p-5"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class=" p-5"
|
||||
on:click={() => {
|
||||
const a = document.createElement('a');
|
||||
a.href = src;
|
||||
a.download = 'Image.png';
|
||||
a.click();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<img {src} {alt} class=" mx-auto h-full object-scale-down" />
|
||||
</div>
|
||||
{/if}
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { fade, blur } from 'svelte/transition';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let show = true;
|
||||
export let size = 'md';
|
||||
|
@ -13,7 +13,7 @@
|
|||
} else if (size === 'sm') {
|
||||
return 'w-[30rem]';
|
||||
} else {
|
||||
return 'w-[42rem]';
|
||||
return 'w-[44rem]';
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -34,16 +34,17 @@
|
|||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
|
||||
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
|
||||
in:fade={{ duration: 10 }}
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="m-auto rounded-xl max-w-full {sizeToWidth(
|
||||
class=" modal-content m-auto rounded-xl max-w-full {sizeToWidth(
|
||||
size
|
||||
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
|
||||
transition:fade={{ delay: 100, duration: 200 }}
|
||||
in:fade={{ duration: 10 }}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
@ -52,3 +53,20 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.modal-content {
|
||||
animation: scaleUp 0.1s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes scaleUp {
|
||||
from {
|
||||
transform: scale(0.985);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
updateChatById
|
||||
} from '$lib/apis/chats';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let show = false;
|
||||
let navElement;
|
||||
|
@ -139,11 +140,9 @@
|
|||
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class="px-2.5 flex justify-center mt-0.5">
|
||||
<button
|
||||
<a
|
||||
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
|
||||
on:click={async () => {
|
||||
goto('/modelfiles');
|
||||
}}
|
||||
href="/modelfiles"
|
||||
>
|
||||
<div class="self-center">
|
||||
<svg
|
||||
|
@ -165,15 +164,13 @@
|
|||
<div class="flex self-center">
|
||||
<div class=" self-center font-medium text-sm">Modelfiles</div>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="px-2.5 flex justify-center">
|
||||
<button
|
||||
<a
|
||||
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
|
||||
on:click={async () => {
|
||||
goto('/prompts');
|
||||
}}
|
||||
href="/prompts"
|
||||
>
|
||||
<div class="self-center">
|
||||
<svg
|
||||
|
@ -195,15 +192,13 @@
|
|||
<div class="flex self-center">
|
||||
<div class=" self-center font-medium text-sm">Prompts</div>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="px-2.5 flex justify-center mb-1">
|
||||
<button
|
||||
<a
|
||||
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
|
||||
on:click={async () => {
|
||||
goto('/documents');
|
||||
}}
|
||||
href="/documents"
|
||||
>
|
||||
<div class="self-center">
|
||||
<svg
|
||||
|
@ -225,7 +220,7 @@
|
|||
<div class="flex self-center">
|
||||
<div class=" self-center font-medium text-sm">Documents</div>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -360,22 +355,12 @@
|
|||
}
|
||||
}) as chat, i}
|
||||
<div class=" w-full pr-2 relative">
|
||||
<button
|
||||
<a
|
||||
class=" w-full flex justify-between rounded-md px-3 py-2 hover:bg-gray-900 {chat.id ===
|
||||
$chatId
|
||||
? 'bg-gray-900'
|
||||
: ''} transition whitespace-nowrap text-ellipsis"
|
||||
on:click={() => {
|
||||
// goto(`/c/${chat.id}`);
|
||||
if (chat.id !== chatTitleEditId) {
|
||||
chatTitleEditId = null;
|
||||
chatTitle = '';
|
||||
}
|
||||
|
||||
if (chat.id !== $chatId) {
|
||||
loadChat(chat.id);
|
||||
}
|
||||
}}
|
||||
href="/c/{chat.id}"
|
||||
>
|
||||
<div class=" flex self-center flex-1">
|
||||
<div class=" self-center mr-3">
|
||||
|
@ -406,7 +391,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
{#if chat.id === $chatId}
|
||||
<div class=" absolute right-[22px] top-[10px]">
|
||||
|
@ -578,6 +563,7 @@
|
|||
<div
|
||||
id="dropdownDots"
|
||||
class="absolute z-40 bottom-[70px] 4.5rem rounded-lg shadow w-[240px] bg-gray-900"
|
||||
in:slide={{ duration: 150 }}
|
||||
>
|
||||
<div class="py-2 w-full">
|
||||
{#if $user.role === 'admin'}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { dev } from '$app/environment';
|
||||
// import { version } from '../../package.json';
|
||||
|
||||
export const WEBUI_NAME = 'Open WebUI';
|
||||
export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
|
||||
|
@ -6,11 +7,11 @@ export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
|
|||
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
|
||||
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`;
|
||||
export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
|
||||
export const RAG_API_BASE_URL = `${WEBUI_BASE_URL}/rag/api/v1`;
|
||||
export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`;
|
||||
export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`;
|
||||
export const RAG_API_BASE_URL = `${WEBUI_BASE_URL}/rag/api/v1`;
|
||||
|
||||
export const WEB_UI_VERSION = 'v1.0.0-alpha-static';
|
||||
|
||||
export const WEB_UI_VERSION = APP_VERSION;
|
||||
export const REQUIRED_OLLAMA_VERSION = '0.1.16';
|
||||
|
||||
export const SUPPORTED_FILE_TYPE = [
|
||||
|
|
|
@ -32,3 +32,4 @@ export const documents = writable([
|
|||
|
||||
export const settings = writable({});
|
||||
export const showSettings = writable(false);
|
||||
export const showChangelog = writable(false);
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { openDB, deleteDB } from 'idb';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
|
||||
import { getModelfiles } from '$lib/apis/modelfiles';
|
||||
import { getPrompts } from '$lib/apis/prompts';
|
||||
|
||||
import { getOpenAIModels } from '$lib/apis/openai';
|
||||
import { getDocs } from '$lib/apis/documents';
|
||||
import { getAllChatTags } from '$lib/apis/chats';
|
||||
|
||||
import {
|
||||
user,
|
||||
|
@ -21,16 +22,17 @@
|
|||
modelfiles,
|
||||
prompts,
|
||||
documents,
|
||||
tags
|
||||
tags,
|
||||
showChangelog,
|
||||
config
|
||||
} from '$lib/stores';
|
||||
import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { checkVersion } from '$lib/utils';
|
||||
|
||||
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
|
||||
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
||||
import { checkVersion } from '$lib/utils';
|
||||
import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
|
||||
import { getDocs } from '$lib/apis/documents';
|
||||
import { getAllChatTags } from '$lib/apis/chats';
|
||||
import ChangelogModal from '$lib/components/ChangelogModal.svelte';
|
||||
|
||||
let ollamaVersion = '';
|
||||
let loaded = false;
|
||||
|
@ -182,6 +184,10 @@
|
|||
}
|
||||
});
|
||||
|
||||
if ($user.role === 'admin') {
|
||||
showChangelog.set(localStorage.version !== $config.version);
|
||||
}
|
||||
|
||||
await tick();
|
||||
}
|
||||
|
||||
|
@ -268,7 +274,7 @@
|
|||
Trouble accessing Ollama?
|
||||
<a
|
||||
class=" text-black dark:text-white font-semibold underline"
|
||||
href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
|
||||
href="https://github.com/open-webui/open-webui#troubleshooting"
|
||||
target="_blank"
|
||||
>
|
||||
Click here for help.
|
||||
|
@ -355,6 +361,7 @@
|
|||
>
|
||||
<Sidebar />
|
||||
<SettingsModal bind:show={$showSettings} />
|
||||
<ChangelogModal bind:show={$showChangelog} />
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -334,7 +334,7 @@
|
|||
content: $settings.system
|
||||
}
|
||||
: undefined,
|
||||
...messages
|
||||
...messages.filter(message => !message.deleted)
|
||||
]
|
||||
.filter((message) => message)
|
||||
.map((message, idx, arr) => ({
|
||||
|
@ -540,7 +540,7 @@
|
|||
content: $settings.system
|
||||
}
|
||||
: undefined,
|
||||
...messages
|
||||
...messages.filter(message => !message.deleted)
|
||||
]
|
||||
.filter((message) => message)
|
||||
.map((message, idx, arr) => ({
|
||||
|
@ -742,6 +742,8 @@
|
|||
if ($settings.titleAutoGenerate ?? true) {
|
||||
const title = await generateTitle(
|
||||
localStorage.token,
|
||||
$settings?.titleGenerationPrompt ??
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}",
|
||||
$settings?.titleAutoGenerateModel ?? selectedModels[0],
|
||||
userPrompt
|
||||
);
|
||||
|
|
|
@ -348,7 +348,7 @@
|
|||
content: $settings.system
|
||||
}
|
||||
: undefined,
|
||||
...messages
|
||||
...messages.filter((message) => !message.deleted)
|
||||
]
|
||||
.filter((message) => message)
|
||||
.map((message, idx, arr) => ({
|
||||
|
@ -555,7 +555,7 @@
|
|||
content: $settings.system
|
||||
}
|
||||
: undefined,
|
||||
...messages
|
||||
...messages.filter((message) => !message.deleted)
|
||||
]
|
||||
.filter((message) => message)
|
||||
.map((message, idx, arr) => ({
|
||||
|
@ -755,7 +755,13 @@
|
|||
|
||||
const generateChatTitle = async (_chatId, userPrompt) => {
|
||||
if ($settings.titleAutoGenerate ?? true) {
|
||||
const title = await generateTitle(localStorage.token, selectedModels[0], userPrompt);
|
||||
const title = await generateTitle(
|
||||
localStorage.token,
|
||||
$settings?.titleGenerationPrompt ??
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}",
|
||||
$settings?.titleAutoGenerateModel ?? selectedModels[0],
|
||||
userPrompt
|
||||
);
|
||||
|
||||
if (title) {
|
||||
await setChatTitle(_chatId, title);
|
||||
|
|
|
@ -286,9 +286,9 @@
|
|||
<hr class=" dark:border-gray-700 my-2.5" />
|
||||
|
||||
{#if tags.length > 0}
|
||||
<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
|
||||
<div class="px-2.5 pt-1 flex gap-1 flex-wrap">
|
||||
<button
|
||||
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-600 dark:text-white"
|
||||
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
|
||||
on:click={async () => {
|
||||
selectedTag = '';
|
||||
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
|
||||
|
@ -298,7 +298,7 @@
|
|||
</button>
|
||||
{#each tags as tag}
|
||||
<button
|
||||
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-600 dark:text-white"
|
||||
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
|
||||
on:click={async () => {
|
||||
selectedTag = tag;
|
||||
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
|
||||
|
@ -377,7 +377,7 @@
|
|||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class=" flex-1 self-center flex-1">
|
||||
<div class=" self-center flex-1">
|
||||
<div class=" font-bold line-clamp-1">#{doc.name} ({doc.filename})</div>
|
||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||
{doc.title}
|
||||
|
|
|
@ -296,7 +296,7 @@
|
|||
</div>
|
||||
|
||||
<div class=" my-16">
|
||||
<div class=" text-2xl font-semibold mb-6">Made by OpenWebUI Community</div>
|
||||
<div class=" text-2xl font-semibold mb-3">Made by OpenWebUI Community</div>
|
||||
|
||||
<a
|
||||
class=" flex space-x-4 cursor-pointer w-full mb-3 px-3 py-2"
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<br class=" " />
|
||||
<a
|
||||
class=" font-semibold underline"
|
||||
href="https://github.com/ollama-webui/ollama-webui#how-to-install-"
|
||||
href="https://github.com/open-webui/open-webui#how-to-install-"
|
||||
target="_blank">See readme.md for instructions</a
|
||||
>
|
||||
or
|
||||
|
|
|
@ -2,5 +2,8 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()],
|
||||
define: {
|
||||
APP_VERSION: JSON.stringify(process.env.npm_package_version)
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue