forked from open-webui/open-webui
Merge pull request #216 from ollama-webui/dev
feat: full backend support (including auth/rbac)
This commit is contained in:
commit
fbdae0f769
46 changed files with 2633 additions and 1269 deletions
16
.dockerignore
Normal file
16
.dockerignore
Normal file
|
@ -0,0 +1,16 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
__pycache__
|
||||
.env
|
||||
_old
|
||||
uploads
|
||||
.ipynb_checkpoints
|
||||
**/*.db
|
||||
_test
|
115
README.md
115
README.md
|
@ -57,9 +57,9 @@ Also check our sibling project, [OllamaHub](https://ollamahub.com/), where you c
|
|||
|
||||
- ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs.
|
||||
|
||||
- 🔐 **Auth Header Support**: Effortlessly enhance security by adding Authorization headers to Ollama requests directly from the web UI settings, ensuring access to secured Ollama servers.
|
||||
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable.
|
||||
|
||||
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable during the Docker build phase. Additionally, you can also set the external server connection URL from the web UI post-build.
|
||||
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
|
||||
|
||||
- 🔒 **Backend Reverse Proxy Support**: Strengthen security by enabling direct communication between Ollama Web UI backend and Ollama, eliminating the need to expose Ollama over LAN.
|
||||
|
||||
|
@ -82,13 +82,17 @@ docker compose up -d --build
|
|||
This command will install both Ollama and Ollama Web UI on your system.
|
||||
|
||||
#### Enable GPU
|
||||
|
||||
Use the additional Docker Compose file designed to enable GPU support by running the following command:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build
|
||||
```
|
||||
|
||||
#### Expose Ollama API outside the container stack
|
||||
|
||||
Deploy the service with an additional Docker Compose file designed for API exposure:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.api.yml up -d --build
|
||||
```
|
||||
|
@ -105,17 +109,19 @@ After installing Ollama, verify that Ollama is running by accessing the followin
|
|||
|
||||
#### Using Docker 🐳
|
||||
|
||||
**Important:** When using Docker to install Ollama Web UI, make sure to include the `-v ollama-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
|
||||
|
||||
If Ollama is hosted on your local machine and accessible at [http://127.0.0.1:11434/](http://127.0.0.1:11434/), run the following command:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main
|
||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v ollama-webui:/app/backend/data --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main
|
||||
```
|
||||
|
||||
Alternatively, if you prefer to build the container yourself, use the following command:
|
||||
|
||||
```bash
|
||||
docker build -t ollama-webui .
|
||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway --name ollama-webui --restart always ollama-webui
|
||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v ollama-webui:/app/backend/data --name ollama-webui --restart always ollama-webui
|
||||
```
|
||||
|
||||
Your Ollama Web UI should now be hosted at [http://localhost:3000](http://localhost:3000) and accessible over LAN (or Network). Enjoy! 😄
|
||||
|
@ -125,23 +131,25 @@ Your Ollama Web UI should now be hosted at [http://localhost:3000](http://localh
|
|||
Change `OLLAMA_API_BASE_URL` environment variable to match the external Ollama Server url:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main
|
||||
docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v ollama-webui:/app/backend/data --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main
|
||||
```
|
||||
|
||||
Alternatively, if you prefer to build the container yourself, use the following command:
|
||||
|
||||
```bash
|
||||
docker build -t ollama-webui .
|
||||
docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api --name ollama-webui --restart always ollama-webui
|
||||
docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v ollama-webui:/app/backend/data --name ollama-webui --restart always 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.
|
||||
|
||||
**Warning: Backend Dependency for Proper Functionality**
|
||||
### Project Components
|
||||
|
||||
In order to ensure the seamless operation of our application, it is crucial to run both the backend and frontend components simultaneously. Serving only the frontend in isolation is not supported and may lead to unpredictable outcomes, rendering the application inoperable. Attempting to raise an issue when solely serving the frontend will not be addressed, as it falls outside the intended usage. To achieve optimal results, please strictly adhere to the specified steps outlined in this documentation. Utilize the frontend solely for building static files, and subsequently run the complete application with the provided backend commands. Failure to follow these instructions may result in unsupported configurations, and we may not be able to provide assistance in such cases. Your cooperation in following the prescribed procedures is essential for a smooth user experience and effective issue resolution.
|
||||
The Ollama Web UI 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.
|
||||
|
||||
**Warning: Backend Dependency for Proper Functionality**
|
||||
|
||||
### TL;DR 🚀
|
||||
|
||||
|
@ -166,86 +174,6 @@ sh start.sh
|
|||
|
||||
You should have the Ollama Web UI up and running at http://localhost:8080/. Enjoy! 😄
|
||||
|
||||
### Project Components
|
||||
|
||||
The Ollama Web UI 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 using `npm run dev`. Alternatively, you can set the `PUBLIC_API_BASE_URL` during the build process to have the frontend connect directly to your Ollama instance or build the frontend as static files and serve them with the backend.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Clone and Enter the Project:**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ollama-webui/ollama-webui.git
|
||||
cd ollama-webui/
|
||||
```
|
||||
|
||||
2. **Create and Edit `.env`:**
|
||||
|
||||
```sh
|
||||
cp -RPp example.env .env
|
||||
```
|
||||
|
||||
### Building Ollama Web UI Frontend
|
||||
|
||||
1. **Install Node Dependencies:**
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Run in Dev Mode or Build for Deployment:**
|
||||
|
||||
- Dev Mode (requires the backend to be running simultaneously):
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- Build for Deployment:
|
||||
|
||||
```sh
|
||||
# `PUBLIC_API_BASE_URL` overwrites the value in `.env`
|
||||
PUBLIC_API_BASE_URL='https://example.com/api' npm run build
|
||||
```
|
||||
|
||||
3. **Test the Build with `Caddy` (or your preferred server):**
|
||||
|
||||
```sh
|
||||
curl https://webi.sh/caddy | sh
|
||||
|
||||
PUBLIC_API_BASE_URL='https://localhost/api' npm run build
|
||||
caddy run --envfile .env --config ./Caddyfile.localhost
|
||||
```
|
||||
|
||||
### Running Ollama Web UI Backend
|
||||
|
||||
If you wish to run the backend for deployment, ensure that the frontend is built so that the backend can serve the frontend files along with the API route.
|
||||
|
||||
#### Setup Instructions
|
||||
|
||||
1. **Install Python Requirements:**
|
||||
|
||||
```sh
|
||||
cd ./backend
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **Run Python Backend:**
|
||||
|
||||
- Dev Mode with Hot Reloading:
|
||||
|
||||
```sh
|
||||
sh dev.sh
|
||||
```
|
||||
|
||||
- Deployment:
|
||||
|
||||
```sh
|
||||
sh start.sh
|
||||
```
|
||||
|
||||
Now, you should have the Ollama Web UI up and running at [http://localhost:8080/](http://localhost:8080/). Feel free to explore the features and functionalities of Ollama! If you encounter any issues, please refer to the instructions above or reach out to the community for assistance.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [TROUBLESHOOTING.md](/TROUBLESHOOTING.md) for information on how to troubleshoot and/or join our [Ollama Web UI Discord community](https://discord.gg/5rJgQTnV4s).
|
||||
|
@ -257,7 +185,10 @@ See [TROUBLESHOOTING.md](/TROUBLESHOOTING.md) for information on how to troubles
|
|||
Here are some exciting tasks on our roadmap:
|
||||
|
||||
- 📚 **RAG Integration**: Experience first-class retrieval augmented generation support, enabling chat with your documents.
|
||||
- 🔐 **Access Control**: Securely manage requests to Ollama by utilizing the backend as a reverse proxy gateway, ensuring only authenticated users can send specific requests.
|
||||
- 🌐 **Web Browsing Capability**: Experience the convenience of seamlessly integrating web content directly into your chat. Easily browse and share information without leaving the conversation.
|
||||
- 🔄 **Function Calling**: Empower your interactions by running code directly within the chat. Execute functions and commands effortlessly, enhancing the functionality of your conversations.
|
||||
- ⚙️ **Custom Python Backend Actions**: Empower your Ollama Web UI by creating or downloading custom Python backend actions. Unleash the full potential of your web interface with tailored actions that suit your specific needs, enhancing functionality and versatility.
|
||||
- 🧠 **Long-Term Memory**: Witness the power of persistent memory in our agents. Enjoy conversations that feel continuous as agents remember and reference past interactions, creating a more cohesive and personalized user experience.
|
||||
- 🧪 **Research-Centric Features**: Empower researchers in the fields of LLM and HCI with a comprehensive web UI for conducting user studies. Stay tuned for ongoing feature enhancements (e.g., surveys, analytics, and participant tracking) to facilitate their research.
|
||||
- 📈 **User Study Tools**: Providing specialized tools, like heat maps and behavior tracking modules, to empower researchers in capturing and analyzing user behavior patterns with precision and accuracy.
|
||||
- 📚 **Enhanced Documentation**: Elevate your setup and customization experience with improved, comprehensive documentation.
|
||||
|
@ -270,7 +201,11 @@ A big shoutout to our amazing supporters who's helping to make this project poss
|
|||
|
||||
### Platinum Sponsors 🤍
|
||||
|
||||
- [Prof. Lawrence Kim @ SFU](https://www.lhkim.com/)
|
||||
- We're looking for Sponsors!
|
||||
|
||||
### Acknowledgments
|
||||
|
||||
Special thanks to [Prof. Lawrence Kim @ SFU](https://www.lhkim.com/) and [Prof. Nick Vincent @ SFU](https://www.nickmvincent.com/) for their invaluable support and guidance in shaping this project into a research endeavor. Grateful for your mentorship throughout the journey! 🙌
|
||||
|
||||
## License 📜
|
||||
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
# Ollama Web UI Troubleshooting Guide
|
||||
|
||||
## Ollama WebUI: Server Connection Error
|
||||
|
||||
If you're running ollama-webui and have chosen to install webui and ollama separately, you might encounter connection issues. This is often due to the docker container being unable to reach the Ollama server at 127.0.0.1:11434(host.docker.internal:11434). To resolve this, you can use the `--network=host` flag in the docker command. When done so port would be changed from 3000 to 8080, and the link would be: http://localhost:8080.
|
||||
|
||||
Here's an example of the command you should run:
|
||||
|
||||
```bash
|
||||
docker run -d --network=host -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main
|
||||
```
|
||||
|
||||
## Connection Errors
|
||||
|
||||
Make sure you have the **latest version of Ollama** installed before proceeding with the installation. You can find the latest version of Ollama at [https://ollama.ai/](https://ollama.ai/).
|
||||
|
||||
If you encounter difficulties connecting to the Ollama server, please follow these steps to diagnose and resolve the issue:
|
||||
|
||||
**1. Verify Ollama Server Configuration**
|
||||
|
||||
Ensure that the Ollama server is properly configured to accept incoming connections from all origins. To do this, make sure the server is launched with the `OLLAMA_ORIGINS=*` environment variable, as shown in the following command:
|
||||
|
||||
```bash
|
||||
OLLAMA_HOST=0.0.0.0 OLLAMA_ORIGINS=* ollama serve
|
||||
```
|
||||
|
||||
This configuration allows Ollama to accept connections from any source.
|
||||
|
||||
**2. Check Ollama URL Format**
|
||||
**1. Check Ollama URL Format**
|
||||
|
||||
Ensure that the Ollama URL is correctly formatted in the application settings. Follow these steps:
|
||||
|
||||
|
@ -28,33 +28,3 @@ Ensure that the Ollama URL is correctly formatted in the application settings. F
|
|||
It is crucial to include the `/api` at the end of the URL to ensure that the Ollama Web UI can communicate with the server.
|
||||
|
||||
By following these troubleshooting steps, you should be able to identify and resolve connection issues with your Ollama server configuration. If you require further assistance or have additional questions, please don't hesitate to reach out or refer to our documentation for comprehensive guidance.
|
||||
|
||||
## Running ollama-webui as a container on Apple Silicon Mac
|
||||
|
||||
If you are running Docker on a M{1..3} based Mac and have taken the steps to run an x86 container, add "--platform linux/amd64" to the docker run command to prevent a warning.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=http://example.com:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main
|
||||
```
|
||||
|
||||
Becomes
|
||||
|
||||
```
|
||||
docker run --platform linux/amd64 -d -p 3000:8080 -e OLLAMA_API_BASE_URL=http://example.com:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main
|
||||
```
|
||||
|
||||
## Running ollama-webui as a container on WSL Ubuntu
|
||||
If you're running ollama-webui via docker on WSL Ubuntu and have chosen to install webui and ollama separately, you might encounter connection issues. This is often due to the docker container being unable to reach the Ollama server at 127.0.0.1:11434. To resolve this, you can use the `--network=host` flag in the docker command. When done so port would be changed from 3000 to 8080, and the link would be: http://localhost:8080.
|
||||
|
||||
Here's an example of the command you should run:
|
||||
|
||||
```bash
|
||||
docker run -d --network=host -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
[Change Docker Desktop Settings on Mac](https://docs.docker.com/desktop/settings/mac/) Search for "x86" in that page.
|
||||
[Run x86 (Intel) and ARM based images on Apple Silicon (M1) Macs?](https://forums.docker.com/t/run-x86-intel-and-arm-based-images-on-apple-silicon-m1-macs/117123)
|
||||
|
|
7
backend/.dockerignore
Normal file
7
backend/.dockerignore
Normal file
|
@ -0,0 +1,7 @@
|
|||
__pycache__
|
||||
.env
|
||||
_old
|
||||
uploads
|
||||
.ipynb_checkpoints
|
||||
*.db
|
||||
_test
|
5
backend/.gitignore
vendored
5
backend/.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
__pycache__
|
||||
.env
|
||||
_old
|
||||
uploads
|
||||
uploads
|
||||
.ipynb_checkpoints
|
||||
*.db
|
||||
_test
|
|
@ -25,7 +25,7 @@ TARGET_SERVER_URL = OLLAMA_API_BASE_URL
|
|||
def proxy(path):
|
||||
# Combine the base URL of the target server with the requested path
|
||||
target_url = f"{TARGET_SERVER_URL}/{path}"
|
||||
print(path)
|
||||
print(target_url)
|
||||
|
||||
# Get data from the original request
|
||||
data = request.get_data()
|
||||
|
@ -61,6 +61,11 @@ def proxy(path):
|
|||
|
||||
r = None
|
||||
|
||||
headers.pop("Host", None)
|
||||
headers.pop("Authorization", None)
|
||||
headers.pop("Origin", None)
|
||||
headers.pop("Referer", None)
|
||||
|
||||
try:
|
||||
# Make a request to the target server
|
||||
r = requests.request(
|
||||
|
@ -86,8 +91,10 @@ def proxy(path):
|
|||
|
||||
return response
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Ollama WebUI: Server Connection Error"
|
||||
if r != None:
|
||||
print(r.text)
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
|
|
4
backend/apps/web/internal/db.py
Normal file
4
backend/apps/web/internal/db.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from peewee import *
|
||||
|
||||
DB = SqliteDatabase("./data/ollama.db")
|
||||
DB.connect()
|
|
@ -1,7 +1,7 @@
|
|||
from fastapi import FastAPI, Request, Depends, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from apps.web.routers import auths, users, utils
|
||||
from apps.web.routers import auths, users, chats, modelfiles, utils
|
||||
from config import WEBUI_VERSION, WEBUI_AUTH
|
||||
|
||||
app = FastAPI()
|
||||
|
@ -19,6 +19,10 @@ app.add_middleware(
|
|||
|
||||
app.include_router(auths.router, prefix="/auths", tags=["auths"])
|
||||
app.include_router(users.router, prefix="/users", tags=["users"])
|
||||
app.include_router(chats.router, prefix="/chats", tags=["chats"])
|
||||
app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"])
|
||||
|
||||
|
||||
app.include_router(utils.router, prefix="/utils", tags=["utils"])
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ from pydantic import BaseModel
|
|||
from typing import List, Union, Optional
|
||||
import time
|
||||
import uuid
|
||||
from peewee import *
|
||||
|
||||
|
||||
from apps.web.models.users import UserModel, Users
|
||||
|
@ -12,15 +13,23 @@ from utils.utils import (
|
|||
create_token,
|
||||
)
|
||||
|
||||
import config
|
||||
|
||||
DB = config.DB
|
||||
from apps.web.internal.db import DB
|
||||
|
||||
####################
|
||||
# DB MODEL
|
||||
####################
|
||||
|
||||
|
||||
class Auth(Model):
|
||||
id = CharField(unique=True)
|
||||
email = CharField()
|
||||
password = CharField()
|
||||
active = BooleanField()
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class AuthModel(BaseModel):
|
||||
id: str
|
||||
email: str
|
||||
|
@ -64,7 +73,7 @@ class SignupForm(BaseModel):
|
|||
class AuthsTable:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.table = db.auths
|
||||
self.db.create_tables([Auth])
|
||||
|
||||
def insert_new_auth(
|
||||
self, email: str, password: str, name: str, role: str = "pending"
|
||||
|
@ -76,27 +85,28 @@ class AuthsTable:
|
|||
auth = AuthModel(
|
||||
**{"id": id, "email": email, "password": password, "active": True}
|
||||
)
|
||||
result = self.table.insert_one(auth.model_dump())
|
||||
result = Auth.create(**auth.model_dump())
|
||||
|
||||
user = Users.insert_new_user(id, name, email, role)
|
||||
|
||||
print(result, user)
|
||||
if result and user:
|
||||
return user
|
||||
else:
|
||||
return None
|
||||
|
||||
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
|
||||
print("authenticate_user")
|
||||
|
||||
auth = self.table.find_one({"email": email, "active": True})
|
||||
|
||||
if auth:
|
||||
if verify_password(password, auth["password"]):
|
||||
user = self.db.users.find_one({"id": auth["id"]})
|
||||
return UserModel(**user)
|
||||
print("authenticate_user", email)
|
||||
try:
|
||||
auth = Auth.get(Auth.email == email, Auth.active == True)
|
||||
if auth:
|
||||
if verify_password(password, auth.password):
|
||||
user = Users.get_user_by_id(auth.id)
|
||||
return user
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
|
|
157
backend/apps/web/models/chats.py
Normal file
157
backend/apps/web/models/chats.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import List, Union, Optional
|
||||
from peewee import *
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
|
||||
|
||||
####################
|
||||
# Chat DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class Chat(Model):
|
||||
id = CharField(unique=True)
|
||||
user_id = CharField()
|
||||
title = CharField()
|
||||
chat = TextField() # Save Chat JSON as Text
|
||||
timestamp = DateField()
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class ChatModel(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
title: str
|
||||
chat: str
|
||||
timestamp: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class ChatForm(BaseModel):
|
||||
chat: dict
|
||||
|
||||
|
||||
class ChatTitleForm(BaseModel):
|
||||
title: str
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
title: str
|
||||
chat: dict
|
||||
timestamp: int # timestamp in epoch
|
||||
|
||||
|
||||
class ChatTitleIdResponse(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
|
||||
|
||||
class ChatTable:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
db.create_tables([Chat])
|
||||
|
||||
def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
|
||||
id = str(uuid.uuid4())
|
||||
chat = ChatModel(
|
||||
**{
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
"title": form_data.chat["title"]
|
||||
if "title" in form_data.chat
|
||||
else "New Chat",
|
||||
"chat": json.dumps(form_data.chat),
|
||||
"timestamp": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
result = Chat.create(**chat.model_dump())
|
||||
return chat if result else None
|
||||
|
||||
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
|
||||
try:
|
||||
query = Chat.update(
|
||||
chat=json.dumps(chat),
|
||||
title=chat["title"] if "title" in chat else "New Chat",
|
||||
timestamp=int(time.time()),
|
||||
).where(Chat.id == id)
|
||||
query.execute()
|
||||
|
||||
chat = Chat.get(Chat.id == id)
|
||||
return ChatModel(**model_to_dict(chat))
|
||||
except:
|
||||
return None
|
||||
|
||||
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
|
||||
try:
|
||||
query = Chat.update(
|
||||
chat=json.dumps(chat),
|
||||
title=chat["title"] if "title" in chat else "New Chat",
|
||||
timestamp=int(time.time()),
|
||||
).where(Chat.id == id)
|
||||
query.execute()
|
||||
|
||||
chat = Chat.get(Chat.id == id)
|
||||
return ChatModel(**model_to_dict(chat))
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_chat_lists_by_user_id(
|
||||
self, user_id: str, skip: int = 0, limit: int = 50
|
||||
) -> List[ChatModel]:
|
||||
return [
|
||||
ChatModel(**model_to_dict(chat))
|
||||
for chat in Chat.select()
|
||||
.where(Chat.user_id == user_id)
|
||||
.order_by(Chat.timestamp.desc())
|
||||
# .limit(limit)
|
||||
# .offset(skip)
|
||||
]
|
||||
|
||||
def get_all_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||
return [
|
||||
ChatModel(**model_to_dict(chat))
|
||||
for chat in Chat.select()
|
||||
.where(Chat.user_id == user_id)
|
||||
.order_by(Chat.timestamp.desc())
|
||||
]
|
||||
|
||||
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
|
||||
try:
|
||||
chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
|
||||
return ChatModel(**model_to_dict(chat))
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
|
||||
return [
|
||||
ChatModel(**model_to_dict(chat))
|
||||
for chat in Chat.select().limit(limit).offset(skip)
|
||||
]
|
||||
|
||||
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||
try:
|
||||
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
|
||||
query.execute() # Remove the rows, return number of rows removed.
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
Chats = ChatTable(DB)
|
135
backend/apps/web/models/modelfiles.py
Normal file
135
backend/apps/web/models/modelfiles.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
from pydantic import BaseModel
|
||||
from peewee import *
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from typing import List, Union, Optional
|
||||
import time
|
||||
|
||||
from utils.utils import decode_token
|
||||
from utils.misc import get_gravatar_url
|
||||
|
||||
from apps.web.internal.db import DB
|
||||
|
||||
import json
|
||||
|
||||
####################
|
||||
# User DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class Modelfile(Model):
|
||||
tag_name = CharField(unique=True)
|
||||
user_id = CharField()
|
||||
modelfile = TextField()
|
||||
timestamp = DateField()
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class ModelfileModel(BaseModel):
|
||||
tag_name: str
|
||||
user_id: str
|
||||
modelfile: str
|
||||
timestamp: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
|
||||
|
||||
class ModelfileForm(BaseModel):
|
||||
modelfile: dict
|
||||
|
||||
|
||||
class ModelfileTagNameForm(BaseModel):
|
||||
tag_name: str
|
||||
|
||||
|
||||
class ModelfileUpdateForm(ModelfileForm, ModelfileTagNameForm):
|
||||
pass
|
||||
|
||||
|
||||
class ModelfileResponse(BaseModel):
|
||||
tag_name: str
|
||||
user_id: str
|
||||
modelfile: dict
|
||||
timestamp: int # timestamp in epoch
|
||||
|
||||
|
||||
class ModelfilesTable:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.db.create_tables([Modelfile])
|
||||
|
||||
def insert_new_modelfile(
|
||||
self, user_id: str, form_data: ModelfileForm
|
||||
) -> Optional[ModelfileModel]:
|
||||
if "tagName" in form_data.modelfile:
|
||||
modelfile = ModelfileModel(
|
||||
**{
|
||||
"user_id": user_id,
|
||||
"tag_name": form_data.modelfile["tagName"],
|
||||
"modelfile": json.dumps(form_data.modelfile),
|
||||
"timestamp": int(time.time()),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
result = Modelfile.create(**modelfile.model_dump())
|
||||
if result:
|
||||
return modelfile
|
||||
else:
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]:
|
||||
try:
|
||||
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
|
||||
return ModelfileModel(**model_to_dict(modelfile))
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]:
|
||||
return [
|
||||
ModelfileResponse(
|
||||
**{
|
||||
**model_to_dict(modelfile),
|
||||
"modelfile": json.loads(modelfile.modelfile),
|
||||
}
|
||||
)
|
||||
for modelfile in Modelfile.select()
|
||||
# .limit(limit).offset(skip)
|
||||
]
|
||||
|
||||
def update_modelfile_by_tag_name(
|
||||
self, tag_name: str, modelfile: dict
|
||||
) -> Optional[ModelfileModel]:
|
||||
try:
|
||||
query = Modelfile.update(
|
||||
modelfile=json.dumps(modelfile),
|
||||
timestamp=int(time.time()),
|
||||
).where(Modelfile.tag_name == tag_name)
|
||||
|
||||
query.execute()
|
||||
|
||||
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
|
||||
return ModelfileModel(**model_to_dict(modelfile))
|
||||
except:
|
||||
return None
|
||||
|
||||
def delete_modelfile_by_tag_name(self, tag_name: str) -> bool:
|
||||
try:
|
||||
query = Modelfile.delete().where((Modelfile.tag_name == tag_name))
|
||||
query.execute() # Remove the rows, return number of rows removed.
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
Modelfiles = ModelfilesTable(DB)
|
|
@ -1,25 +1,38 @@
|
|||
from pydantic import BaseModel
|
||||
from peewee import *
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from typing import List, Union, Optional
|
||||
from pymongo import ReturnDocument
|
||||
import time
|
||||
|
||||
from utils.utils import decode_token
|
||||
from utils.misc import get_gravatar_url
|
||||
|
||||
from config import DB
|
||||
from apps.web.internal.db import DB
|
||||
|
||||
####################
|
||||
# User DB Schema
|
||||
####################
|
||||
|
||||
|
||||
class User(Model):
|
||||
id = CharField(unique=True)
|
||||
name = CharField()
|
||||
email = CharField()
|
||||
role = CharField()
|
||||
profile_image_url = CharField()
|
||||
timestamp = DateField()
|
||||
|
||||
class Meta:
|
||||
database = DB
|
||||
|
||||
|
||||
class UserModel(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
role: str = "pending"
|
||||
profile_image_url: str = "/user.png"
|
||||
created_at: int # timestamp in epoch
|
||||
timestamp: int # timestamp in epoch
|
||||
|
||||
|
||||
####################
|
||||
|
@ -35,7 +48,7 @@ class UserRoleUpdateForm(BaseModel):
|
|||
class UsersTable:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.table = db.users
|
||||
self.db.create_tables([User])
|
||||
|
||||
def insert_new_user(
|
||||
self, id: str, name: str, email: str, role: str = "pending"
|
||||
|
@ -47,22 +60,27 @@ class UsersTable:
|
|||
"email": email,
|
||||
"role": role,
|
||||
"profile_image_url": get_gravatar_url(email),
|
||||
"created_at": int(time.time()),
|
||||
"timestamp": int(time.time()),
|
||||
}
|
||||
)
|
||||
result = self.table.insert_one(user.model_dump())
|
||||
|
||||
result = User.create(**user.model_dump())
|
||||
if result:
|
||||
return user
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
||||
user = self.table.find_one({"email": email}, {"_id": False})
|
||||
def get_user_by_id(self, id: str) -> Optional[UserModel]:
|
||||
try:
|
||||
user = User.get(User.id == id)
|
||||
return UserModel(**model_to_dict(user))
|
||||
except:
|
||||
return None
|
||||
|
||||
if user:
|
||||
return UserModel(**user)
|
||||
else:
|
||||
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
||||
try:
|
||||
user = User.get(User.email == email)
|
||||
return UserModel(**model_to_dict(user))
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_user_by_token(self, token: str) -> Optional[UserModel]:
|
||||
|
@ -75,23 +93,22 @@ class UsersTable:
|
|||
|
||||
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
|
||||
return [
|
||||
UserModel(**user)
|
||||
for user in list(
|
||||
self.table.find({}, {"_id": False}).skip(skip).limit(limit)
|
||||
)
|
||||
UserModel(**model_to_dict(user))
|
||||
for user in User.select().limit(limit).offset(skip)
|
||||
]
|
||||
|
||||
def get_num_users(self) -> Optional[int]:
|
||||
return self.table.count_documents({})
|
||||
|
||||
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
|
||||
user = self.table.find_one_and_update(
|
||||
{"id": id}, {"$set": updated}, return_document=ReturnDocument.AFTER
|
||||
)
|
||||
return UserModel(**user)
|
||||
return User.select().count()
|
||||
|
||||
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
|
||||
return self.update_user_by_id(id, {"role": role})
|
||||
try:
|
||||
query = User.update(role=role).where(User.id == id)
|
||||
query.execute()
|
||||
|
||||
user = User.get(User.id == id)
|
||||
return UserModel(**model_to_dict(user))
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
Users = UsersTable(DB)
|
||||
|
|
|
@ -104,8 +104,8 @@ async def signup(form_data: SignupForm):
|
|||
"profile_image_url": user.profile_image_url,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||
except Exception as err:
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||
else:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT())
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
|
161
backend/apps/web/routers/chats.py
Normal file
161
backend/apps/web/routers/chats.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
from fastapi import Response
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from apps.web.models.chats import (
|
||||
ChatModel,
|
||||
ChatResponse,
|
||||
ChatTitleForm,
|
||||
ChatForm,
|
||||
ChatTitleIdResponse,
|
||||
Chats,
|
||||
)
|
||||
|
||||
from utils.utils import (
|
||||
bearer_scheme,
|
||||
)
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
############################
|
||||
# GetChats
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ChatTitleIdResponse])
|
||||
async def get_user_chats(skip: int = 0, limit: int = 50, cred=Depends(bearer_scheme)):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
return Chats.get_chat_lists_by_user_id(user.id, skip, limit)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetAllChats
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[ChatResponse])
|
||||
async def get_all_user_chats(cred=Depends(bearer_scheme)):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
return [
|
||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
for chat in Chats.get_all_chats_by_user_id(user.id)
|
||||
]
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# CreateNewChat
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/new", response_model=Optional[ChatResponse])
|
||||
async def create_new_chat(form_data: ChatForm, cred=Depends(bearer_scheme)):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
chat = Chats.insert_new_chat(user.id, form_data)
|
||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChatById
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=Optional[ChatResponse])
|
||||
async def get_chat_by_id(id: str, cred=Depends(bearer_scheme)):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||
|
||||
if chat:
|
||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateChatById
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/{id}", response_model=Optional[ChatResponse])
|
||||
async def update_chat_by_id(id: str, form_data: ChatForm, cred=Depends(bearer_scheme)):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
|
||||
if chat:
|
||||
updated_chat = {**json.loads(chat.chat), **form_data.chat}
|
||||
|
||||
chat = Chats.update_chat_by_id(id, updated_chat)
|
||||
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# DeleteChatById
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/{id}", response_model=bool)
|
||||
async def delete_chat_by_id(id: str, cred=Depends(bearer_scheme)):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
result = Chats.delete_chat_by_id_and_user_id(id, user.id)
|
||||
return result
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
191
backend/apps/web/routers/modelfiles.py
Normal file
191
backend/apps/web/routers/modelfiles.py
Normal file
|
@ -0,0 +1,191 @@
|
|||
from fastapi import Response
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from apps.web.models.modelfiles import (
|
||||
Modelfiles,
|
||||
ModelfileForm,
|
||||
ModelfileTagNameForm,
|
||||
ModelfileUpdateForm,
|
||||
ModelfileResponse,
|
||||
)
|
||||
|
||||
from utils.utils import (
|
||||
bearer_scheme,
|
||||
)
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
############################
|
||||
# GetModelfiles
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ModelfileResponse])
|
||||
async def get_modelfiles(skip: int = 0, limit: int = 50, cred=Depends(bearer_scheme)):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
return Modelfiles.get_modelfiles(skip, limit)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# CreateNewModelfile
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/create", response_model=Optional[ModelfileResponse])
|
||||
async def create_new_modelfile(form_data: ModelfileForm, cred=Depends(bearer_scheme)):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
# Admin Only
|
||||
if user.role == "admin":
|
||||
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
|
||||
|
||||
if modelfile:
|
||||
return ModelfileResponse(
|
||||
**{
|
||||
**modelfile.model_dump(),
|
||||
"modelfile": json.loads(modelfile.modelfile),
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetModelfileByTagName
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/", response_model=Optional[ModelfileResponse])
|
||||
async def get_modelfile_by_tag_name(
|
||||
form_data: ModelfileTagNameForm, cred=Depends(bearer_scheme)
|
||||
):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
||||
|
||||
if modelfile:
|
||||
return ModelfileResponse(
|
||||
**{
|
||||
**modelfile.model_dump(),
|
||||
"modelfile": json.loads(modelfile.modelfile),
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateModelfileByTagName
|
||||
############################
|
||||
|
||||
|
||||
@router.post("/update", response_model=Optional[ModelfileResponse])
|
||||
async def update_modelfile_by_tag_name(
|
||||
form_data: ModelfileUpdateForm, cred=Depends(bearer_scheme)
|
||||
):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
if user.role == "admin":
|
||||
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
|
||||
if modelfile:
|
||||
updated_modelfile = {
|
||||
**json.loads(modelfile.modelfile),
|
||||
**form_data.modelfile,
|
||||
}
|
||||
|
||||
modelfile = Modelfiles.update_modelfile_by_tag_name(
|
||||
form_data.tag_name, updated_modelfile
|
||||
)
|
||||
|
||||
return ModelfileResponse(
|
||||
**{
|
||||
**modelfile.model_dump(),
|
||||
"modelfile": json.loads(modelfile.modelfile),
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# DeleteModelfileByTagName
|
||||
############################
|
||||
|
||||
|
||||
@router.delete("/delete", response_model=bool)
|
||||
async def delete_modelfile_by_tag_name(
|
||||
form_data: ModelfileTagNameForm, cred=Depends(bearer_scheme)
|
||||
):
|
||||
token = cred.credentials
|
||||
user = Users.get_user_by_token(token)
|
||||
|
||||
if user:
|
||||
if user.role == "admin":
|
||||
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
|
||||
return result
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
|
@ -1,9 +1,10 @@
|
|||
from dotenv import load_dotenv, find_dotenv
|
||||
from pymongo import MongoClient
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from secrets import token_bytes
|
||||
from base64 import b64encode
|
||||
|
||||
import os
|
||||
|
||||
load_dotenv(find_dotenv("../.env"))
|
||||
|
@ -30,30 +31,13 @@ if ENV == "prod":
|
|||
# WEBUI_VERSION
|
||||
####################################
|
||||
|
||||
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.40")
|
||||
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.42")
|
||||
|
||||
####################################
|
||||
# WEBUI_AUTH
|
||||
# WEBUI_AUTH (Required for security)
|
||||
####################################
|
||||
|
||||
|
||||
WEBUI_AUTH = True if os.environ.get("WEBUI_AUTH", "FALSE") == "TRUE" else False
|
||||
|
||||
|
||||
####################################
|
||||
# WEBUI_DB (Deprecated, Should be removed)
|
||||
####################################
|
||||
|
||||
|
||||
WEBUI_DB_URL = os.environ.get("WEBUI_DB_URL", "mongodb://root:root@localhost:27017/")
|
||||
|
||||
if WEBUI_AUTH and WEBUI_DB_URL == "":
|
||||
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
|
||||
|
||||
|
||||
DB_CLIENT = MongoClient(f"{WEBUI_DB_URL}?authSource=admin")
|
||||
DB = DB_CLIENT["ollama-webui"]
|
||||
|
||||
WEBUI_AUTH = True
|
||||
|
||||
####################################
|
||||
# WEBUI_JWT_SECRET_KEY
|
||||
|
|
|
@ -11,6 +11,11 @@ class ERROR_MESSAGES(str, Enum):
|
|||
|
||||
DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
|
||||
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
|
||||
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
|
||||
EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
|
||||
USERNAME_TAKEN = (
|
||||
"Uh-oh! This username is already registered. Please choose another username."
|
||||
)
|
||||
INVALID_TOKEN = (
|
||||
"Your session has expired or the token is invalid. Please sign in again."
|
||||
)
|
||||
|
@ -20,5 +25,6 @@ class ERROR_MESSAGES(str, Enum):
|
|||
ACTION_PROHIBITED = (
|
||||
"The requested action has been restricted as a security measure."
|
||||
)
|
||||
NOT_FOUND = "We could not find what you're looking for :/"
|
||||
USER_NOT_FOUND = "We could not find what you're looking for :/"
|
||||
MALICIOUS = "Unusual activities detected, please try again in a few minutes."
|
||||
|
|
1
backend/data/readme.txt
Normal file
1
backend/data/readme.txt
Normal file
|
@ -0,0 +1 @@
|
|||
dir for backend files (db, documents, etc.)
|
|
@ -1 +1 @@
|
|||
uvicorn main:app --port 8080 --reload
|
||||
uvicorn main:app --port 8080 --host 0.0.0.0 --reload
|
|
@ -13,8 +13,8 @@ uuid
|
|||
|
||||
requests
|
||||
aiohttp
|
||||
pymongo
|
||||
peewee
|
||||
bcrypt
|
||||
|
||||
PyJWT
|
||||
pyjwt[crypto]
|
||||
pyjwt[crypto]
|
||||
|
|
|
@ -18,6 +18,8 @@ services:
|
|||
dockerfile: Dockerfile
|
||||
image: ollama-webui:latest
|
||||
container_name: ollama-webui
|
||||
volumes:
|
||||
- ollama-webui:/app/backend/data
|
||||
depends_on:
|
||||
- ollama
|
||||
ports:
|
||||
|
@ -30,3 +32,4 @@ services:
|
|||
|
||||
volumes:
|
||||
ollama: {}
|
||||
ollama-webui: {}
|
||||
|
|
2
run.sh
2
run.sh
|
@ -1,5 +1,5 @@
|
|||
docker stop ollama-webui || true
|
||||
docker rm ollama-webui || true
|
||||
docker build -t ollama-webui .
|
||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway --name ollama-webui --restart always ollama-webui
|
||||
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v ollama-webui:/app/backend/data --name ollama-webui --restart always ollama-webui
|
||||
docker image prune -f
|
90
src/lib/apis/auths/index.ts
Normal file
90
src/lib/apis/auths/index.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getSessionUser = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, {
|
||||
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 userSignIn = async (email: string, password: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signin`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const userSignUp = async (name: string, email: string, password: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
193
src/lib/apis/chats/index.ts
Normal file
193
src/lib/apis/chats/index.ts
Normal file
|
@ -0,0 +1,193 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const createNewChat = async (token: string, chat: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/new`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat: chat
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getChatList = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, {
|
||||
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();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAllChats = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all`, {
|
||||
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();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getChatById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, {
|
||||
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();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateChatById = async (token: string, id: string, chat: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat: chat
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteChatById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, {
|
||||
method: 'DELETE',
|
||||
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();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
23
src/lib/apis/index.ts
Normal file
23
src/lib/apis/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getBackendConfig = async () => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
173
src/lib/apis/modelfiles/index.ts
Normal file
173
src/lib/apis/modelfiles/index.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const createNewModelfile = async (token: string, modelfile: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
modelfile: modelfile
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getModelfiles = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.map((modelfile) => modelfile.modelfile);
|
||||
};
|
||||
|
||||
export const getModelfileByTagName = async (token: string, tagName: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tag_name: tagName
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.modelfile;
|
||||
};
|
||||
|
||||
export const updateModelfileByTagName = async (
|
||||
token: string,
|
||||
tagName: string,
|
||||
modelfile: object
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tag_name: tagName,
|
||||
modelfile: modelfile
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteModelfileByTagName = async (token: string, tagName: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/delete`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tag_name: tagName
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
204
src/lib/apis/ollama/index.ts
Normal file
204
src/lib/apis/ollama/index.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getOllamaVersion = async (
|
||||
base_url: string = OLLAMA_API_BASE_URL,
|
||||
token: string = ''
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${base_url}/version`, {
|
||||
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?.version ?? '';
|
||||
};
|
||||
|
||||
export const getOllamaModels = async (
|
||||
base_url: string = OLLAMA_API_BASE_URL,
|
||||
token: string = ''
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${base_url}/tags`, {
|
||||
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?.models ?? [];
|
||||
};
|
||||
|
||||
export const generateTitle = async (
|
||||
base_url: string = OLLAMA_API_BASE_URL,
|
||||
token: string = '',
|
||||
model: string,
|
||||
prompt: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${base_url}/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${prompt}`,
|
||||
stream: false
|
||||
})
|
||||
})
|
||||
.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;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.response ?? 'New Chat';
|
||||
};
|
||||
|
||||
export const generateChatCompletion = async (
|
||||
base_url: string = OLLAMA_API_BASE_URL,
|
||||
token: string = '',
|
||||
body: object
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${base_url}/chat`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}).catch((err) => {
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const createModel = async (
|
||||
base_url: string = OLLAMA_API_BASE_URL,
|
||||
token: string,
|
||||
tagName: string,
|
||||
content: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${base_url}/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName,
|
||||
modelfile: content
|
||||
})
|
||||
}).catch((err) => {
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteModel = async (
|
||||
base_url: string = OLLAMA_API_BASE_URL,
|
||||
token: string,
|
||||
tagName: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${base_url}/delete`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
console.log(json);
|
||||
return true;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.error;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
33
src/lib/apis/openai/index.ts
Normal file
33
src/lib/apis/openai/index.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
export const getOpenAIModels = async (
|
||||
base_url: string = 'https://api.openai.com/v1',
|
||||
api_key: string = ''
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${base_url}/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${api_key}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let models = Array.isArray(res) ? res : res?.data ?? null;
|
||||
|
||||
return models
|
||||
.map((model) => ({ name: model.id, external: true }))
|
||||
.filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true));
|
||||
};
|
58
src/lib/apis/users/index.ts
Normal file
58
src/lib/apis/users/index.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const updateUserRole = async (token: string, id: string, role: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: id,
|
||||
role: role
|
||||
})
|
||||
})
|
||||
.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 getUsers = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res ? res : [];
|
||||
};
|
|
@ -8,11 +8,13 @@
|
|||
import auto_render from 'katex/dist/contrib/auto-render.mjs';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
import { chatId, config, db, modelfiles, settings, user } from '$lib/stores';
|
||||
import { chats, config, db, modelfiles, settings, user } from '$lib/stores';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
import toast from 'svelte-french-toast';
|
||||
import { getChatList, updateChatById } from '$lib/apis/chats';
|
||||
|
||||
export let chatId = '';
|
||||
export let sendPrompt: Function;
|
||||
export let regenerateResponse: Function;
|
||||
|
||||
|
@ -27,14 +29,25 @@
|
|||
$: if (messages && messages.length > 0 && (messages.at(-1).done ?? false)) {
|
||||
(async () => {
|
||||
await tick();
|
||||
|
||||
[...document.querySelectorAll('*')].forEach((node) => {
|
||||
if (node._tippy) {
|
||||
node._tippy.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('rendering message');
|
||||
|
||||
renderLatex();
|
||||
hljs.highlightAll();
|
||||
createCopyCodeBlockButton();
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.info) {
|
||||
console.log(message);
|
||||
|
||||
tippy(`#info-${message.id}`, {
|
||||
content: `<span class="text-xs">token/s: ${
|
||||
content: `<span class="text-xs" id="tooltip-${message.id}">token/s: ${
|
||||
`${
|
||||
Math.round(
|
||||
((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100
|
||||
|
@ -81,7 +94,7 @@
|
|||
blocks.forEach((block) => {
|
||||
// only add button if browser supports Clipboard API
|
||||
|
||||
if (navigator.clipboard && block.childNodes.length < 2 && block.id !== 'user-message') {
|
||||
if (block.childNodes.length < 2 && block.id !== 'user-message') {
|
||||
let code = block.querySelector('code');
|
||||
code.style.borderTopRightRadius = 0;
|
||||
code.style.borderTopLeftRadius = 0;
|
||||
|
@ -119,10 +132,6 @@
|
|||
topBarDiv.appendChild(button);
|
||||
|
||||
block.prepend(topBarDiv);
|
||||
|
||||
// button.addEventListener('click', async () => {
|
||||
// await copyCode(block, button);
|
||||
// });
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -130,7 +139,7 @@
|
|||
let code = block.querySelector('code');
|
||||
let text = code.innerText;
|
||||
|
||||
await navigator.clipboard.writeText(text);
|
||||
await copyToClipboard(text);
|
||||
|
||||
// visual feedback that task is completed
|
||||
button.innerText = 'Copied!';
|
||||
|
@ -239,7 +248,7 @@
|
|||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId, $chatId);
|
||||
await sendPrompt(userPrompt, userMessageId, chatId);
|
||||
};
|
||||
|
||||
const confirmEditResponseMessage = async (messageId) => {
|
||||
|
@ -253,6 +262,7 @@
|
|||
};
|
||||
|
||||
const rateMessage = async (messageIdx, rating) => {
|
||||
// TODO: Move this function to parent
|
||||
messages = messages.map((message, idx) => {
|
||||
if (messageIdx === idx) {
|
||||
message.rating = rating;
|
||||
|
@ -260,10 +270,12 @@
|
|||
return message;
|
||||
});
|
||||
|
||||
$db.updateChatById(chatId, {
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
const showPreviousMessage = async (message) => {
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
<script lang="ts">
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { config, models, settings, user, chats } from '$lib/stores';
|
||||
import { splitStream, getGravatarURL } from '$lib/utils';
|
||||
|
||||
import { getOllamaVersion } from '$lib/apis/ollama';
|
||||
import { createNewChat, getAllChats, getChatList } from '$lib/apis/chats';
|
||||
import {
|
||||
WEB_UI_VERSION,
|
||||
OLLAMA_API_BASE_URL,
|
||||
WEBUI_API_BASE_URL,
|
||||
WEBUI_BASE_URL
|
||||
} from '$lib/constants';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { onMount } from 'svelte';
|
||||
import { config, info, models, settings, user } from '$lib/stores';
|
||||
import { splitStream, getGravatarURL } from '$lib/utils';
|
||||
|
||||
import Advanced from './Settings/Advanced.svelte';
|
||||
import { stringify } from 'postcss';
|
||||
import Modal from '../common/Modal.svelte';
|
||||
|
||||
export let show = false;
|
||||
|
||||
|
@ -74,11 +79,48 @@
|
|||
let OPENAI_API_KEY = '';
|
||||
let OPENAI_API_BASE_URL = '';
|
||||
|
||||
// Chats
|
||||
|
||||
let importFiles;
|
||||
let showDeleteHistoryConfirm = false;
|
||||
|
||||
const importChats = async (_chats) => {
|
||||
for (const chat of _chats) {
|
||||
console.log(chat);
|
||||
await createNewChat(localStorage.token, chat);
|
||||
}
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
const exportChats = async () => {
|
||||
let blob = new Blob([JSON.stringify(await getAllChats(localStorage.token))], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `chat-export-${Date.now()}.json`);
|
||||
};
|
||||
|
||||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
let chats = JSON.parse(event.target.result);
|
||||
console.log(chats);
|
||||
importChats(chats);
|
||||
};
|
||||
|
||||
reader.readAsText(importFiles[0]);
|
||||
}
|
||||
|
||||
// Auth
|
||||
let authEnabled = false;
|
||||
let authType = 'Basic';
|
||||
let authContent = '';
|
||||
|
||||
// About
|
||||
let ollamaVersion = '';
|
||||
|
||||
const checkOllamaConnection = async () => {
|
||||
if (API_BASE_URL === '') {
|
||||
API_BASE_URL = OLLAMA_API_BASE_URL;
|
||||
|
@ -553,7 +595,7 @@
|
|||
return models;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
console.log(settings);
|
||||
|
||||
|
@ -586,6 +628,13 @@
|
|||
authType = settings.authHeader.split(' ')[0];
|
||||
authContent = settings.authHeader.split(' ')[1];
|
||||
}
|
||||
|
||||
ollamaVersion = await getOllamaVersion(
|
||||
API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token
|
||||
).catch((error) => {
|
||||
return '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -741,6 +790,32 @@
|
|||
<div class=" self-center">Add-ons</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 ===
|
||||
'chats'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'chats';
|
||||
}}
|
||||
>
|
||||
<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="M8 2C4.262 2 1 4.57 1 8c0 1.86.98 3.486 2.455 4.566a3.472 3.472 0 0 1-.469 1.26.75.75 0 0 0 .713 1.14 6.961 6.961 0 0 0 3.06-1.06c.403.062.818.094 1.241.094 3.738 0 7-2.57 7-6s-3.262-6-7-6ZM5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm7-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Chats</div>
|
||||
</button>
|
||||
|
||||
{#if !$config || ($config && !$config.auth)}
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
|
@ -1089,13 +1164,65 @@
|
|||
</div>
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Delete a 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={deleteModelTag}
|
||||
placeholder="Select a model"
|
||||
>
|
||||
{#if !deleteModelTag}
|
||||
<option value="" disabled selected>Select a model</option>
|
||||
{/if}
|
||||
{#each $models.filter((m) => m.size != null) as model}
|
||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
||||
>{model.name +
|
||||
' (' +
|
||||
(model.size / 1024 ** 3).toFixed(1) +
|
||||
' GB)'}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition"
|
||||
on:click={() => {
|
||||
deleteModelHandler();
|
||||
}}
|
||||
>
|
||||
<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="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<form
|
||||
on:submit|preventDefault={() => {
|
||||
uploadModelHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" mb-2 flex w-full justify-between">
|
||||
<div class=" text-sm font-medium">Upload a GGUF model</div>
|
||||
<div class=" text-sm font-medium">
|
||||
Upload a GGUF model <a
|
||||
class=" text-xs font-medium text-gray-500 underline"
|
||||
href="https://github.com/jmorganca/ollama/blob/main/README.md#import-from-gguf"
|
||||
target="_blank">(Experimental)</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
|
@ -1252,51 +1379,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Delete a 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={deleteModelTag}
|
||||
placeholder="Select a model"
|
||||
>
|
||||
{#if !deleteModelTag}
|
||||
<option value="" disabled selected>Select a model</option>
|
||||
{/if}
|
||||
{#each $models.filter((m) => m.size != null) as model}
|
||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
||||
>{model.name +
|
||||
' (' +
|
||||
(model.size / 1024 ** 3).toFixed(1) +
|
||||
' GB)'}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition"
|
||||
on:click={() => {
|
||||
deleteModelHandler();
|
||||
}}
|
||||
>
|
||||
<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="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedTab === 'external'}
|
||||
|
@ -1472,6 +1554,150 @@
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{:else if selectedTab === 'chats'}
|
||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="chat-import-input"
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
hidden
|
||||
/>
|
||||
<button
|
||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
document.getElementById('chat-import-input').click();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<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="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">Import Chats</div>
|
||||
</button>
|
||||
<button
|
||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
exportChats();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<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="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">Export Chats</div>
|
||||
</button>
|
||||
</div>
|
||||
<!-- {#if showDeleteHistoryConfirm}
|
||||
<div
|
||||
class="flex justify-between rounded-md items-center py-3 px-3.5 w-full transition"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 mr-3"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M14.74 9l-.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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
<span>Are you sure?</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5 items-center">
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
deleteChatHistory();
|
||||
showDeleteHistoryConfirm = false;
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
showDeleteHistoryConfirm = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
showDeleteHistoryConfirm = true;
|
||||
}}
|
||||
>
|
||||
<div class="mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M14.74 9l-.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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>Clear conversations</span>
|
||||
</button>
|
||||
{/if} -->
|
||||
</div>
|
||||
{:else if selectedTab === 'auth'}
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
|
@ -1607,7 +1833,7 @@
|
|||
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
||||
{$info?.ollama?.version ?? 'N/A'}
|
||||
{ollamaVersion ?? 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { getChatById } from '$lib/apis/chats';
|
||||
import { chatId, db, modelfiles } from '$lib/stores';
|
||||
import toast from 'svelte-french-toast';
|
||||
|
||||
export let initNewChat: Function;
|
||||
export let title: string = 'Ollama Web UI';
|
||||
export let shareEnabled: boolean = false;
|
||||
|
||||
const shareChat = async () => {
|
||||
const chat = await $db.getChatById($chatId);
|
||||
const chat = (await getChatById(localStorage.token, $chatId)).chat;
|
||||
console.log('share', chat);
|
||||
toast.success('Redirecting you to OllamaHub');
|
||||
|
||||
toast.success('Redirecting you to OllamaHub');
|
||||
const url = 'https://ollamahub.com';
|
||||
// const url = 'http://localhost:5173';
|
||||
|
||||
|
@ -44,12 +43,9 @@
|
|||
<div class="flex w-full max-w-full">
|
||||
<div class="pr-2 self-center">
|
||||
<button
|
||||
id="new-chat-button"
|
||||
class=" cursor-pointer p-1 flex dark:hover:bg-gray-700 rounded-lg transition"
|
||||
on:click={async () => {
|
||||
console.log('newChat');
|
||||
goto('/');
|
||||
await chatId.set(uuidv4());
|
||||
}}
|
||||
on:click={initNewChat}
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<svg
|
||||
|
|
|
@ -6,32 +6,28 @@
|
|||
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { user, db, chats, showSettings, chatId } from '$lib/stores';
|
||||
import { user, chats, showSettings, chatId } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { deleteChatById, getChatList, updateChatById } from '$lib/apis/chats';
|
||||
|
||||
let show = false;
|
||||
let navElement;
|
||||
let importFileInputElement;
|
||||
let importFiles;
|
||||
|
||||
let title: string = 'Ollama Web UI';
|
||||
let search = '';
|
||||
|
||||
let chatDeleteId = null;
|
||||
|
||||
let chatTitleEditId = null;
|
||||
let chatTitle = '';
|
||||
|
||||
let showDropdown = false;
|
||||
|
||||
let showDeleteHistoryConfirm = false;
|
||||
|
||||
onMount(async () => {
|
||||
if (window.innerWidth > 1280) {
|
||||
show = true;
|
||||
}
|
||||
|
||||
await chats.set(await $db.getChats());
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
});
|
||||
|
||||
const loadChat = async (id) => {
|
||||
|
@ -39,49 +35,27 @@
|
|||
};
|
||||
|
||||
const editChatTitle = async (id, _title) => {
|
||||
await $db.updateChatById(id, {
|
||||
title = _title;
|
||||
|
||||
await updateChatById(localStorage.token, id, {
|
||||
title: _title
|
||||
});
|
||||
title = _title;
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
const deleteChat = async (id) => {
|
||||
goto('/');
|
||||
$db.deleteChatById(id);
|
||||
|
||||
await deleteChatById(localStorage.token, id);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
const deleteChatHistory = async () => {
|
||||
await $db.deleteAllChat();
|
||||
};
|
||||
|
||||
const importChats = async (chatHistory) => {
|
||||
await $db.addChats(chatHistory);
|
||||
};
|
||||
|
||||
const exportChats = async () => {
|
||||
let blob = new Blob([JSON.stringify(await $db.exportChats())], { type: 'application/json' });
|
||||
saveAs(blob, `chat-export-${Date.now()}.json`);
|
||||
};
|
||||
|
||||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
let chats = JSON.parse(event.target.result);
|
||||
console.log(chats);
|
||||
importChats(chats);
|
||||
};
|
||||
|
||||
reader.readAsText(importFiles[0]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={navElement}
|
||||
class="h-screen {show
|
||||
? ''
|
||||
: '-translate-x-[260px]'} w-[260px] fixed top-0 left-0 z-40 transition bg-[#0a0a0a] text-gray-200 shadow-2xl text-sm
|
||||
: '-translate-x-[260px]'} w-[260px] fixed top-0 left-0 z-40 transition bg-black text-gray-200 shadow-2xl text-sm
|
||||
"
|
||||
>
|
||||
<div class="py-2.5 my-auto flex flex-col justify-between h-screen">
|
||||
|
@ -91,8 +65,11 @@
|
|||
on:click={async () => {
|
||||
goto('/');
|
||||
|
||||
await chatId.set(uuidv4());
|
||||
// createNewChat();
|
||||
const newChatButton = document.getElementById('new-chat-button');
|
||||
|
||||
if (newChatButton) {
|
||||
newChatButton.click();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex self-center">
|
||||
|
@ -121,39 +98,41 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-2.5 flex justify-center my-1">
|
||||
<button
|
||||
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
|
||||
on:click={async () => {
|
||||
goto('/modelfiles');
|
||||
}}
|
||||
>
|
||||
<div class="self-center">
|
||||
<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="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 002.25-2.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v2.25A2.25 2.25 0 006 10.5zm0 9.75h2.25A2.25 2.25 0 0010.5 18v-2.25a2.25 2.25 0 00-2.25-2.25H6a2.25 2.25 0 00-2.25 2.25V18A2.25 2.25 0 006 20.25zm9.75-9.75H18a2.25 2.25 0 002.25-2.25V6A2.25 2.25 0 0018 3.75h-2.25A2.25 2.25 0 0013.5 6v2.25a2.25 2.25 0 002.25 2.25z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class="px-2.5 flex justify-center my-1">
|
||||
<button
|
||||
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
|
||||
on:click={async () => {
|
||||
goto('/modelfiles');
|
||||
}}
|
||||
>
|
||||
<div class="self-center">
|
||||
<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="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 002.25-2.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v2.25A2.25 2.25 0 006 10.5zm0 9.75h2.25A2.25 2.25 0 0010.5 18v-2.25a2.25 2.25 0 00-2.25-2.25H6a2.25 2.25 0 00-2.25 2.25V18A2.25 2.25 0 006 20.25zm9.75-9.75H18a2.25 2.25 0 002.25-2.25V6A2.25 2.25 0 0018 3.75h-2.25A2.25 2.25 0 0013.5 6v2.25a2.25 2.25 0 002.25 2.25z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="flex self-center">
|
||||
<div class=" self-center font-medium text-sm">Modelfiles</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex self-center">
|
||||
<div class=" self-center font-medium text-sm">Modelfiles</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2">
|
||||
<div class="flex w-full">
|
||||
<div class="self-center pl-3 py-2 rounded-l bg-gray-900">
|
||||
<div class="self-center pl-3 py-2 rounded-l bg-gray-950">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
|
@ -169,7 +148,7 @@
|
|||
</div>
|
||||
|
||||
<input
|
||||
class="w-full rounded-r py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-900 outline-none"
|
||||
class="w-full rounded-r py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
|
||||
placeholder="Search"
|
||||
bind:value={search}
|
||||
/>
|
||||
|
@ -394,148 +373,9 @@
|
|||
</div>
|
||||
|
||||
<div class="px-2.5">
|
||||
<hr class=" border-gray-800 mb-2 w-full" />
|
||||
<hr class=" border-gray-900 mb-1 w-full" />
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="flex">
|
||||
<input bind:this={importFileInputElement} bind:files={importFiles} type="file" hidden />
|
||||
<button
|
||||
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
importFileInputElement.click();
|
||||
// importChats();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m6.75 12l-3-3m0 0l-3 3m3-3v6m-1.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Import</div>
|
||||
</button>
|
||||
<button
|
||||
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
exportChats();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m.75 12l3 3m0 0l3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Export</div>
|
||||
</button>
|
||||
</div>
|
||||
{#if showDeleteHistoryConfirm}
|
||||
<div class="flex justify-between rounded-md items-center py-3 px-3.5 w-full transition">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 mr-3"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M14.74 9l-.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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
<span>Are you sure?</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5 items-center">
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
deleteChatHistory();
|
||||
showDeleteHistoryConfirm = false;
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
showDeleteHistoryConfirm = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
showDeleteHistoryConfirm = true;
|
||||
}}
|
||||
>
|
||||
<div class="mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M14.74 9l-.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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>Clear conversations</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if $user !== undefined}
|
||||
<button
|
||||
class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import { dev, browser } from '$app/environment';
|
||||
import { PUBLIC_API_BASE_URL } from '$env/static/public';
|
||||
|
||||
export const OLLAMA_API_BASE_URL =
|
||||
PUBLIC_API_BASE_URL === ''
|
||||
? browser
|
||||
? `http://${location.hostname}:11434/api`
|
||||
: `http://localhost:11434/api`
|
||||
: PUBLIC_API_BASE_URL;
|
||||
export const OLLAMA_API_BASE_URL = dev
|
||||
? `http://${location.hostname}:8080/ollama/api`
|
||||
: PUBLIC_API_BASE_URL === ''
|
||||
? browser
|
||||
? `http://${location.hostname}:11434/api`
|
||||
: `http://localhost:11434/api`
|
||||
: PUBLIC_API_BASE_URL;
|
||||
|
||||
export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
|
||||
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
|
||||
|
||||
export const WEB_UI_VERSION = 'v1.0.0-alpha-static';
|
||||
|
||||
export const REQUIRED_OLLAMA_VERSION = '0.1.16';
|
||||
|
||||
// Source: https://kit.svelte.dev/docs/modules#$env-static-public
|
||||
// This feature, akin to $env/static/private, exclusively incorporates environment variables
|
||||
// that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
// Backend
|
||||
export const info = writable({});
|
||||
export const config = writable(undefined);
|
||||
export const user = writable(undefined);
|
||||
|
||||
|
|
|
@ -66,9 +66,9 @@ export const getGravatarURL = (email) => {
|
|||
return `https://www.gravatar.com/avatar/${hash}`;
|
||||
};
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
export const copyToClipboard = (text) => {
|
||||
if (!navigator.clipboard) {
|
||||
var textArea = document.createElement('textarea');
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
|
@ -81,8 +81,8 @@ const copyToClipboard = (text) => {
|
|||
textArea.select();
|
||||
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
var msg = successful ? 'successful' : 'unsuccessful';
|
||||
const successful = document.execCommand('copy');
|
||||
const msg = successful ? 'successful' : 'unsuccessful';
|
||||
console.log('Fallback: Copying text command was ' + msg);
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err);
|
||||
|
@ -100,3 +100,14 @@ const copyToClipboard = (text) => {
|
|||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const checkVersion = (required, current) => {
|
||||
// Returns true when current version is below required
|
||||
return current === '0.0.0'
|
||||
? false
|
||||
: current.localeCompare(required, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'case',
|
||||
caseFirst: 'upper'
|
||||
}) < 0;
|
||||
};
|
||||
|
|
|
@ -1,246 +1,163 @@
|
|||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { openDB, deleteDB } from 'idb';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import {
|
||||
config,
|
||||
info,
|
||||
user,
|
||||
showSettings,
|
||||
settings,
|
||||
models,
|
||||
db,
|
||||
chats,
|
||||
chatId,
|
||||
modelfiles
|
||||
} from '$lib/stores';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
|
||||
import { getModelfiles } from '$lib/apis/modelfiles';
|
||||
|
||||
import { getOpenAIModels } from '$lib/apis/openai';
|
||||
|
||||
import { user, showSettings, settings, models, modelfiles } from '$lib/stores';
|
||||
import { OLLAMA_API_BASE_URL, REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
|
||||
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { OLLAMA_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { checkVersion } from '$lib/utils';
|
||||
|
||||
let requiredOllamaVersion = '0.1.16';
|
||||
let ollamaVersion = '';
|
||||
let loaded = false;
|
||||
|
||||
let DB = null;
|
||||
let localDBChats = [];
|
||||
|
||||
const getModels = async () => {
|
||||
let models = [];
|
||||
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/tags`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...($settings.authHeader && { Authorization: $settings.authHeader }),
|
||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
if ('detail' in error) {
|
||||
toast.error(error.detail);
|
||||
} else {
|
||||
toast.error('Server connection failed');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
console.log(res);
|
||||
models.push(...(res?.models ?? []));
|
||||
|
||||
models.push(
|
||||
...(await getOllamaModels(
|
||||
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return [];
|
||||
}))
|
||||
);
|
||||
// If OpenAI API Key exists
|
||||
if ($settings.OPENAI_API_KEY) {
|
||||
// Validate OPENAI_API_KEY
|
||||
const openAIModels = await getOpenAIModels(
|
||||
$settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1',
|
||||
$settings.OPENAI_API_KEY
|
||||
).catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
const API_BASE_URL = $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1';
|
||||
const openaiModelRes = await fetch(`${API_BASE_URL}/models`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
const openAIModels = Array.isArray(openaiModelRes)
|
||||
? openaiModelRes
|
||||
: openaiModelRes?.data ?? null;
|
||||
|
||||
models.push(
|
||||
...(openAIModels
|
||||
? [
|
||||
{ name: 'hr' },
|
||||
...openAIModels
|
||||
.map((model) => ({ name: model.id, external: true }))
|
||||
.filter((model) =>
|
||||
API_BASE_URL.includes('openai') ? model.name.includes('gpt') : true
|
||||
)
|
||||
]
|
||||
: [])
|
||||
);
|
||||
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
|
||||
}
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
const getDB = async () => {
|
||||
const DB = await openDB('Chats', 1, {
|
||||
upgrade(db) {
|
||||
const store = db.createObjectStore('chats', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true
|
||||
});
|
||||
store.createIndex('timestamp', 'timestamp');
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
db: DB,
|
||||
getChatById: async function (id) {
|
||||
return await this.db.get('chats', id);
|
||||
},
|
||||
getChats: async function () {
|
||||
let chats = await this.db.getAllFromIndex('chats', 'timestamp');
|
||||
chats = chats.map((item, idx) => ({
|
||||
title: chats[chats.length - 1 - idx].title,
|
||||
id: chats[chats.length - 1 - idx].id
|
||||
}));
|
||||
return chats;
|
||||
},
|
||||
exportChats: async function () {
|
||||
let chats = await this.db.getAllFromIndex('chats', 'timestamp');
|
||||
chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
|
||||
return chats;
|
||||
},
|
||||
addChats: async function (_chats) {
|
||||
for (const chat of _chats) {
|
||||
console.log(chat);
|
||||
await this.addChat(chat);
|
||||
}
|
||||
await chats.set(await this.getChats());
|
||||
},
|
||||
addChat: async function (chat) {
|
||||
await this.db.put('chats', {
|
||||
...chat
|
||||
});
|
||||
},
|
||||
createNewChat: async function (chat) {
|
||||
await this.addChat({ ...chat, timestamp: Date.now() });
|
||||
await chats.set(await this.getChats());
|
||||
},
|
||||
updateChatById: async function (id, updated) {
|
||||
const chat = await this.getChatById(id);
|
||||
|
||||
await this.db.put('chats', {
|
||||
...chat,
|
||||
...updated,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
await chats.set(await this.getChats());
|
||||
},
|
||||
deleteChatById: async function (id) {
|
||||
if ($chatId === id) {
|
||||
goto('/');
|
||||
await chatId.set(uuidv4());
|
||||
}
|
||||
await this.db.delete('chats', id);
|
||||
await chats.set(await this.getChats());
|
||||
},
|
||||
deleteAllChat: async function () {
|
||||
const tx = this.db.transaction('chats', 'readwrite');
|
||||
await Promise.all([tx.store.clear(), tx.done]);
|
||||
|
||||
await chats.set(await this.getChats());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getOllamaVersion = async () => {
|
||||
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/version`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...($settings.authHeader && { Authorization: $settings.authHeader }),
|
||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
if ('detail' in error) {
|
||||
toast.error(error.detail);
|
||||
} else {
|
||||
toast.error('Server connection failed');
|
||||
}
|
||||
return null;
|
||||
const setOllamaVersion = async (version: string = '') => {
|
||||
if (version === '') {
|
||||
version = await getOllamaVersion(
|
||||
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token
|
||||
).catch((error) => {
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
console.log(res);
|
||||
ollamaVersion = version;
|
||||
|
||||
return res?.version ?? '0';
|
||||
};
|
||||
|
||||
const setOllamaVersion = async (ollamaVersion) => {
|
||||
await info.set({ ...$info, ollama: { version: ollamaVersion } });
|
||||
|
||||
if (
|
||||
ollamaVersion.localeCompare(requiredOllamaVersion, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'case',
|
||||
caseFirst: 'upper'
|
||||
}) < 0
|
||||
) {
|
||||
toast.error(`Ollama Version: ${ollamaVersion}`);
|
||||
console.log(ollamaVersion);
|
||||
if (checkVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion)) {
|
||||
toast.error(`Ollama Version: ${ollamaVersion !== '' ? ollamaVersion : 'Not Detected'}`);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($config && $config.auth && $user === undefined) {
|
||||
if ($user === undefined) {
|
||||
await goto('/auth');
|
||||
} else if (['user', 'admin'].includes($user.role)) {
|
||||
try {
|
||||
// Check if IndexedDB exists
|
||||
DB = await openDB('Chats', 1);
|
||||
|
||||
if (DB) {
|
||||
const chats = await DB.getAllFromIndex('chats', 'timestamp');
|
||||
localDBChats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
|
||||
|
||||
if (localDBChats.length === 0) {
|
||||
await deleteDB('Chats');
|
||||
}
|
||||
|
||||
console.log('localdb', localDBChats);
|
||||
}
|
||||
|
||||
console.log(DB);
|
||||
} catch (error) {
|
||||
// IndexedDB Not Found
|
||||
console.log('IDB Not Found');
|
||||
}
|
||||
|
||||
console.log();
|
||||
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
|
||||
await modelfiles.set(await getModelfiles(localStorage.token));
|
||||
console.log($modelfiles);
|
||||
|
||||
modelfiles.subscribe(async () => {
|
||||
// should fetch models
|
||||
await models.set(await getModels());
|
||||
});
|
||||
|
||||
await setOllamaVersion();
|
||||
await tick();
|
||||
}
|
||||
|
||||
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
|
||||
|
||||
await models.set(await getModels());
|
||||
await modelfiles.set(JSON.parse(localStorage.getItem('modelfiles') ?? '[]'));
|
||||
|
||||
modelfiles.subscribe(async () => {
|
||||
await models.set(await getModels());
|
||||
});
|
||||
|
||||
let _db = await getDB();
|
||||
await db.set(_db);
|
||||
|
||||
await setOllamaVersion(await getOllamaVersion());
|
||||
|
||||
await tick();
|
||||
loaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<div class="app relative">
|
||||
{#if ($info?.ollama?.version ?? '0').localeCompare( requiredOllamaVersion, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' } ) < 0}
|
||||
<div class="absolute w-full h-full flex z-50">
|
||||
{#if !['user', 'admin'].includes($user.role)}
|
||||
<div class="fixed w-full h-full flex z-50">
|
||||
<div
|
||||
class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-900/60 flex justify-center"
|
||||
class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center"
|
||||
>
|
||||
<div class="m-auto pb-44 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<div class="text-center dark:text-white text-2xl font-medium z-50">
|
||||
Account Activation Pending<br /> Contact Admin for WebUI Access
|
||||
</div>
|
||||
|
||||
<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
|
||||
Your account status is currently pending activation. To access the WebUI, please
|
||||
reach out to the administrator. Admins can manage user statuses from the Admin
|
||||
Panel.
|
||||
</div>
|
||||
|
||||
<div class=" mt-6 mx-auto relative group w-fit">
|
||||
<button
|
||||
class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
|
||||
on:click={async () => {
|
||||
location.href = '/';
|
||||
}}
|
||||
>
|
||||
Check Again
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-xs text-center w-full mt-2 text-gray-400 underline"
|
||||
on:click={async () => {
|
||||
localStorage.removeItem('token');
|
||||
location.href = '/auth';
|
||||
}}>Sign Out</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if checkVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion ?? '0')}
|
||||
<div class="fixed w-full h-full flex z-50">
|
||||
<div
|
||||
class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center"
|
||||
>
|
||||
<div class="m-auto pb-44 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
|
@ -254,15 +171,16 @@
|
|||
/>We've detected either a connection hiccup or observed that you're using an older
|
||||
version. Ensure you're on the latest Ollama version
|
||||
<br class=" hidden sm:flex" />(version
|
||||
<span class=" dark:text-white font-medium">{requiredOllamaVersion} or higher</span>)
|
||||
or check your connection.
|
||||
<span class=" dark:text-white font-medium">{REQUIRED_OLLAMA_VERSION} or higher</span
|
||||
>) or check your connection.
|
||||
</div>
|
||||
|
||||
<div class=" mt-6 mx-auto relative group w-fit">
|
||||
<button
|
||||
class="relative z-20 flex px-5 py-2 rounded-full bg-gray-100 hover:bg-gray-200 transition font-medium text-sm"
|
||||
class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
|
||||
on:click={async () => {
|
||||
await setOllamaVersion(await getOllamaVersion());
|
||||
location.href = '/';
|
||||
// await setOllamaVersion();
|
||||
}}
|
||||
>
|
||||
Check Again
|
||||
|
@ -271,7 +189,57 @@
|
|||
<button
|
||||
class="text-xs text-center w-full mt-2 text-gray-400 underline"
|
||||
on:click={async () => {
|
||||
await setOllamaVersion(requiredOllamaVersion);
|
||||
await setOllamaVersion(REQUIRED_OLLAMA_VERSION);
|
||||
}}>Close</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if localDBChats.length > 0}
|
||||
<div class="fixed w-full h-full flex z-50">
|
||||
<div
|
||||
class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center"
|
||||
>
|
||||
<div class="m-auto pb-44 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<div class="text-center dark:text-white text-2xl font-medium z-50">
|
||||
Important Update<br /> Action Required for Chat Log Storage
|
||||
</div>
|
||||
|
||||
<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
|
||||
Saving chat logs directly to your browser's storage is no longer supported. Please
|
||||
take a moment to download and delete your chat logs by clicking the button below.
|
||||
Don't worry, you can easily re-import your chat logs to the backend through <span
|
||||
class="font-semibold dark:text-white">Settings > Chats > Import Chats</span
|
||||
>. This ensures that your valuable conversations are securely saved to your backend
|
||||
database. Thank you!
|
||||
</div>
|
||||
|
||||
<div class=" mt-6 mx-auto relative group w-fit">
|
||||
<button
|
||||
class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
|
||||
on:click={async () => {
|
||||
let blob = new Blob([JSON.stringify(localDBChats)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `chat-export-${Date.now()}.json`);
|
||||
|
||||
const tx = DB.transaction('chats', 'readwrite');
|
||||
await Promise.all([tx.store.clear(), tx.done]);
|
||||
await deleteDB('Chats');
|
||||
|
||||
localDBChats = [];
|
||||
}}
|
||||
>
|
||||
Download & Delete
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-xs text-center w-full mt-2 text-gray-400 underline"
|
||||
on:click={async () => {
|
||||
localDBChats = [];
|
||||
}}>Close</button
|
||||
>
|
||||
</div>
|
||||
|
@ -285,9 +253,7 @@
|
|||
class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-800 min-h-screen overflow-auto flex flex-row"
|
||||
>
|
||||
<Sidebar />
|
||||
|
||||
<SettingsModal bind:show={$showSettings} />
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,23 +2,27 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import toast from 'svelte-french-toast';
|
||||
|
||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { splitStream } from '$lib/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { config, models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores';
|
||||
import { models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores';
|
||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
import { generateChatCompletion, generateTitle } from '$lib/apis/ollama';
|
||||
import { copyToClipboard, splitStream } from '$lib/utils';
|
||||
|
||||
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
||||
import Messages from '$lib/components/chat/Messages.svelte';
|
||||
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
||||
import Navbar from '$lib/components/layout/Navbar.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { createNewChat, getChatList, updateChatById } from '$lib/apis/chats';
|
||||
|
||||
let stopResponseFlag = false;
|
||||
let autoScroll = true;
|
||||
|
||||
let selectedModels = [''];
|
||||
|
||||
let selectedModelfile = null;
|
||||
$: selectedModelfile =
|
||||
selectedModels.length === 1 &&
|
||||
|
@ -26,10 +30,11 @@
|
|||
? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
|
||||
: null;
|
||||
|
||||
let chat = null;
|
||||
|
||||
let title = '';
|
||||
let prompt = '';
|
||||
let files = [];
|
||||
|
||||
let messages = [];
|
||||
let history = {
|
||||
messages: {},
|
||||
|
@ -50,16 +55,8 @@
|
|||
messages = [];
|
||||
}
|
||||
|
||||
$: if (files) {
|
||||
console.log(files);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await chatId.set(uuidv4());
|
||||
|
||||
chatId.subscribe(async () => {
|
||||
await initNewChat();
|
||||
});
|
||||
await initNewChat();
|
||||
});
|
||||
|
||||
//////////////////////////
|
||||
|
@ -67,6 +64,11 @@
|
|||
//////////////////////////
|
||||
|
||||
const initNewChat = async () => {
|
||||
window.history.replaceState(history.state, '', `/`);
|
||||
|
||||
console.log('initNewChat');
|
||||
|
||||
await chatId.set('');
|
||||
console.log($chatId);
|
||||
|
||||
autoScroll = true;
|
||||
|
@ -82,68 +84,33 @@
|
|||
: $settings.models ?? [''];
|
||||
|
||||
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
console.log(_settings);
|
||||
settings.set({
|
||||
..._settings
|
||||
});
|
||||
};
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
if (!navigator.clipboard) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
var msg = successful ? 'successful' : 'unsuccessful';
|
||||
console.log('Fallback: Copying text command was ' + msg);
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(
|
||||
function () {
|
||||
console.log('Async: Copying to clipboard was successful!');
|
||||
},
|
||||
function (err) {
|
||||
console.error('Async: Could not copy text: ', err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
//////////////////////////
|
||||
// Ollama functions
|
||||
//////////////////////////
|
||||
|
||||
const sendPrompt = async (userPrompt, parentId, _chatId) => {
|
||||
const sendPrompt = async (prompt, parentId) => {
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
await Promise.all(
|
||||
selectedModels.map(async (model) => {
|
||||
console.log(model);
|
||||
if ($models.filter((m) => m.name === model)[0].external) {
|
||||
await sendPromptOpenAI(model, userPrompt, parentId, _chatId);
|
||||
await sendPromptOpenAI(model, prompt, parentId, _chatId);
|
||||
} else {
|
||||
await sendPromptOllama(model, userPrompt, parentId, _chatId);
|
||||
await sendPromptOllama(model, prompt, parentId, _chatId);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await chats.set(await $db.getChats());
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => {
|
||||
console.log('sendPromptOllama');
|
||||
// Create response message
|
||||
let responseMessageId = uuidv4();
|
||||
let responseMessage = {
|
||||
parentId: parentId,
|
||||
|
@ -154,8 +121,11 @@
|
|||
model: model
|
||||
};
|
||||
|
||||
// Add message to history and Set currentId to messageId
|
||||
history.messages[responseMessageId] = responseMessage;
|
||||
history.currentId = responseMessageId;
|
||||
|
||||
// Append messageId to childrenIds of parent message
|
||||
if (parentId !== null) {
|
||||
history.messages[parentId].childrenIds = [
|
||||
...history.messages[parentId].childrenIds,
|
||||
|
@ -163,17 +133,16 @@
|
|||
];
|
||||
}
|
||||
|
||||
// Wait until history/message have been updated
|
||||
await tick();
|
||||
|
||||
// Scroll down
|
||||
window.scrollTo({ top: document.body.scrollHeight });
|
||||
|
||||
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/chat`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
...($settings.authHeader && { Authorization: $settings.authHeader }),
|
||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const res = await generateChatCompletion(
|
||||
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token,
|
||||
{
|
||||
model: model,
|
||||
messages: [
|
||||
$settings.system
|
||||
|
@ -195,20 +164,11 @@
|
|||
})
|
||||
})),
|
||||
options: {
|
||||
seed: $settings.seed ?? undefined,
|
||||
temperature: $settings.temperature ?? undefined,
|
||||
repeat_penalty: $settings.repeat_penalty ?? undefined,
|
||||
top_k: $settings.top_k ?? undefined,
|
||||
top_p: $settings.top_p ?? undefined,
|
||||
num_ctx: $settings.num_ctx ?? undefined,
|
||||
...($settings.options ?? {})
|
||||
},
|
||||
format: $settings.requestFormat ?? undefined
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (res && res.ok) {
|
||||
const reader = res.body
|
||||
|
@ -297,23 +257,14 @@
|
|||
if (autoScroll) {
|
||||
window.scrollTo({ top: document.body.scrollHeight });
|
||||
}
|
||||
}
|
||||
|
||||
await $db.updateChatById(_chatId, {
|
||||
title: title === '' ? 'New Chat' : title,
|
||||
models: selectedModels,
|
||||
system: $settings.system ?? undefined,
|
||||
options: {
|
||||
seed: $settings.seed ?? undefined,
|
||||
temperature: $settings.temperature ?? undefined,
|
||||
repeat_penalty: $settings.repeat_penalty ?? undefined,
|
||||
top_k: $settings.top_k ?? undefined,
|
||||
top_p: $settings.top_p ?? undefined,
|
||||
num_ctx: $settings.num_ctx ?? undefined,
|
||||
...($settings.options ?? {})
|
||||
},
|
||||
if ($chatId == _chatId) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
} else {
|
||||
if (res !== null) {
|
||||
|
@ -339,6 +290,7 @@
|
|||
|
||||
stopResponseFlag = false;
|
||||
await tick();
|
||||
|
||||
if (autoScroll) {
|
||||
window.scrollTo({ top: document.body.scrollHeight });
|
||||
}
|
||||
|
@ -481,23 +433,14 @@
|
|||
if (autoScroll) {
|
||||
window.scrollTo({ top: document.body.scrollHeight });
|
||||
}
|
||||
}
|
||||
|
||||
await $db.updateChatById(_chatId, {
|
||||
title: title === '' ? 'New Chat' : title,
|
||||
models: selectedModels,
|
||||
system: $settings.system ?? undefined,
|
||||
options: {
|
||||
seed: $settings.seed ?? undefined,
|
||||
temperature: $settings.temperature ?? undefined,
|
||||
repeat_penalty: $settings.repeat_penalty ?? undefined,
|
||||
top_k: $settings.top_k ?? undefined,
|
||||
top_p: $settings.top_p ?? undefined,
|
||||
num_ctx: $settings.num_ctx ?? undefined,
|
||||
...($settings.options ?? {})
|
||||
},
|
||||
if ($chatId == _chatId) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
} else {
|
||||
if (res !== null) {
|
||||
|
@ -542,16 +485,18 @@
|
|||
};
|
||||
|
||||
const submitPrompt = async (userPrompt) => {
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
console.log('submitPrompt', _chatId);
|
||||
console.log('submitPrompt', $chatId);
|
||||
|
||||
if (selectedModels.includes('')) {
|
||||
toast.error('Model not selected');
|
||||
} else if (messages.length != 0 && messages.at(-1).done != true) {
|
||||
// Response not done
|
||||
console.log('wait');
|
||||
} else {
|
||||
// Reset chat message textarea height
|
||||
document.getElementById('chat-textarea').style.height = '';
|
||||
|
||||
// Create user message
|
||||
let userMessageId = uuidv4();
|
||||
let userMessage = {
|
||||
id: userMessageId,
|
||||
|
@ -562,42 +507,43 @@
|
|||
files: files.length > 0 ? files : undefined
|
||||
};
|
||||
|
||||
// Add message to history and Set currentId to messageId
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
// Append messageId to childrenIds of parent message
|
||||
if (messages.length !== 0) {
|
||||
history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
|
||||
}
|
||||
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
// Wait until history/message have been updated
|
||||
await tick();
|
||||
|
||||
// Create new chat if only one message in messages
|
||||
if (messages.length == 1) {
|
||||
await $db.createNewChat({
|
||||
id: _chatId,
|
||||
chat = await createNewChat(localStorage.token, {
|
||||
id: $chatId,
|
||||
title: 'New Chat',
|
||||
models: selectedModels,
|
||||
system: $settings.system ?? undefined,
|
||||
options: {
|
||||
seed: $settings.seed ?? undefined,
|
||||
temperature: $settings.temperature ?? undefined,
|
||||
repeat_penalty: $settings.repeat_penalty ?? undefined,
|
||||
top_k: $settings.top_k ?? undefined,
|
||||
top_p: $settings.top_p ?? undefined,
|
||||
num_ctx: $settings.num_ctx ?? undefined,
|
||||
...($settings.options ?? {})
|
||||
},
|
||||
messages: messages,
|
||||
history: history
|
||||
history: history,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await chatId.set(chat.id);
|
||||
await tick();
|
||||
}
|
||||
|
||||
// Reset chat input textarea
|
||||
prompt = '';
|
||||
files = [];
|
||||
|
||||
setTimeout(() => {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
}, 50);
|
||||
|
||||
await sendPrompt(userPrompt, userMessageId, _chatId);
|
||||
// Send prompt
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -607,9 +553,7 @@
|
|||
};
|
||||
|
||||
const regenerateResponse = async () => {
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
console.log('regenerateResponse', _chatId);
|
||||
|
||||
console.log('regenerateResponse');
|
||||
if (messages.length != 0 && messages.at(-1).done == true) {
|
||||
messages.splice(messages.length - 1, 1);
|
||||
messages = messages;
|
||||
|
@ -617,41 +561,21 @@
|
|||
let userMessage = messages.at(-1);
|
||||
let userPrompt = userMessage.content;
|
||||
|
||||
await sendPrompt(userPrompt, userMessage.id, _chatId);
|
||||
await sendPrompt(userPrompt, userMessage.id);
|
||||
}
|
||||
};
|
||||
|
||||
const generateChatTitle = async (_chatId, userPrompt) => {
|
||||
if ($settings.titleAutoGenerate ?? true) {
|
||||
console.log('generateChatTitle');
|
||||
const title = await generateTitle(
|
||||
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token,
|
||||
selectedModels[0],
|
||||
userPrompt
|
||||
);
|
||||
|
||||
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
...($settings.authHeader && { Authorization: $settings.authHeader }),
|
||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: selectedModels[0],
|
||||
prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`,
|
||||
stream: false
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
if ('detail' in error) {
|
||||
toast.error(error.detail);
|
||||
}
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
|
||||
if (title) {
|
||||
await setChatTitle(_chatId, title);
|
||||
}
|
||||
} else {
|
||||
await setChatTitle(_chatId, `${userPrompt}`);
|
||||
|
@ -659,10 +583,12 @@
|
|||
};
|
||||
|
||||
const setChatTitle = async (_chatId, _title) => {
|
||||
await $db.updateChatById(_chatId, { title: _title });
|
||||
if (_chatId === $chatId) {
|
||||
title = _title;
|
||||
}
|
||||
|
||||
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -672,7 +598,7 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<Navbar {title} shareEnabled={messages.length > 0} />
|
||||
<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} />
|
||||
<div class="min-h-screen w-full flex justify-center">
|
||||
<div class=" py-2.5 flex flex-col justify-between w-full">
|
||||
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10">
|
||||
|
@ -681,6 +607,7 @@
|
|||
|
||||
<div class=" h-full mt-10 mb-32 w-full flex flex-col">
|
||||
<Messages
|
||||
chatId={$chatId}
|
||||
{selectedModels}
|
||||
{selectedModelfile}
|
||||
bind:history
|
||||
|
|
|
@ -6,62 +6,27 @@
|
|||
|
||||
import toast from 'svelte-french-toast';
|
||||
|
||||
import { updateUserRole, getUsers } from '$lib/apis/users';
|
||||
|
||||
let loaded = false;
|
||||
let users = [];
|
||||
|
||||
const updateUserRole = async (id, role) => {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: id,
|
||||
role: role
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error.detail);
|
||||
return null;
|
||||
});
|
||||
const updateRoleHandler = async (id, role) => {
|
||||
const res = await updateUserRole(localStorage.token, id, role).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
await getUsers();
|
||||
users = await getUsers(localStorage.token);
|
||||
}
|
||||
};
|
||||
|
||||
const getUsers = async () => {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error.detail);
|
||||
return null;
|
||||
});
|
||||
|
||||
users = res ? res : [];
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($config === null || !$config.auth || ($config.auth && $user && $user.role !== 'admin')) {
|
||||
if ($user?.role !== 'admin') {
|
||||
await goto('/');
|
||||
} else {
|
||||
await getUsers();
|
||||
users = await getUsers(localStorage.token);
|
||||
}
|
||||
loaded = true;
|
||||
});
|
||||
|
@ -115,11 +80,11 @@
|
|||
class=" dark:text-white underline"
|
||||
on:click={() => {
|
||||
if (user.role === 'user') {
|
||||
updateUserRole(user.id, 'admin');
|
||||
updateRoleHandler(user.id, 'admin');
|
||||
} else if (user.role === 'pending') {
|
||||
updateUserRole(user.id, 'user');
|
||||
updateRoleHandler(user.id, 'user');
|
||||
} else {
|
||||
updateUserRole(user.id, 'pending');
|
||||
updateRoleHandler(user.id, 'pending');
|
||||
}
|
||||
}}>{user.role}</button
|
||||
>
|
|
@ -2,17 +2,21 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import toast from 'svelte-french-toast';
|
||||
|
||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { convertMessagesToHistory, splitStream } from '$lib/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
import { config, models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores';
|
||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
import { generateChatCompletion, generateTitle } from '$lib/apis/ollama';
|
||||
import { copyToClipboard, splitStream } from '$lib/utils';
|
||||
|
||||
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
||||
import Messages from '$lib/components/chat/Messages.svelte';
|
||||
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
||||
import Navbar from '$lib/components/layout/Navbar.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { createNewChat, getChatById, getChatList, updateChatById } from '$lib/apis/chats';
|
||||
|
||||
let loaded = false;
|
||||
let stopResponseFlag = false;
|
||||
|
@ -27,6 +31,8 @@
|
|||
? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
|
||||
: null;
|
||||
|
||||
let chat = null;
|
||||
|
||||
let title = '';
|
||||
let prompt = '';
|
||||
let files = [];
|
||||
|
@ -53,10 +59,8 @@
|
|||
|
||||
$: if ($page.params.id) {
|
||||
(async () => {
|
||||
let chat = await loadChat();
|
||||
|
||||
await tick();
|
||||
if (chat) {
|
||||
if (await loadChat()) {
|
||||
await tick();
|
||||
loaded = true;
|
||||
} else {
|
||||
await goto('/');
|
||||
|
@ -70,94 +74,70 @@
|
|||
|
||||
const loadChat = async () => {
|
||||
await chatId.set($page.params.id);
|
||||
const chat = await $db.getChatById($chatId);
|
||||
chat = await getChatById(localStorage.token, $chatId).catch(async (error) => {
|
||||
await goto('/');
|
||||
return null;
|
||||
});
|
||||
|
||||
if (chat) {
|
||||
console.log(chat);
|
||||
const chatContent = chat.chat;
|
||||
|
||||
selectedModels = (chat?.models ?? undefined) !== undefined ? chat.models : [chat.model ?? ''];
|
||||
history =
|
||||
(chat?.history ?? undefined) !== undefined
|
||||
? chat.history
|
||||
: convertMessagesToHistory(chat.messages);
|
||||
title = chat.title;
|
||||
if (chatContent) {
|
||||
console.log(chatContent);
|
||||
|
||||
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
await settings.set({
|
||||
..._settings,
|
||||
system: chat.system ?? _settings.system,
|
||||
options: chat.options ?? _settings.options
|
||||
});
|
||||
autoScroll = true;
|
||||
selectedModels =
|
||||
(chatContent?.models ?? undefined) !== undefined
|
||||
? chatContent.models
|
||||
: [chatContent.model ?? ''];
|
||||
history =
|
||||
(chatContent?.history ?? undefined) !== undefined
|
||||
? chatContent.history
|
||||
: convertMessagesToHistory(chatContent.messages);
|
||||
title = chatContent.title;
|
||||
|
||||
await tick();
|
||||
if (messages.length > 0) {
|
||||
history.messages[messages.at(-1).id].done = true;
|
||||
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
await settings.set({
|
||||
..._settings,
|
||||
system: chatContent.system ?? _settings.system,
|
||||
options: chatContent.options ?? _settings.options
|
||||
});
|
||||
autoScroll = true;
|
||||
await tick();
|
||||
|
||||
if (messages.length > 0) {
|
||||
history.messages[messages.at(-1).id].done = true;
|
||||
}
|
||||
await tick();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
await tick();
|
||||
|
||||
return chat;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
if (!navigator.clipboard) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
var msg = successful ? 'successful' : 'unsuccessful';
|
||||
console.log('Fallback: Copying text command was ' + msg);
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(
|
||||
function () {
|
||||
console.log('Async: Copying to clipboard was successful!');
|
||||
},
|
||||
function (err) {
|
||||
console.error('Async: Could not copy text: ', err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
//////////////////////////
|
||||
// Ollama functions
|
||||
//////////////////////////
|
||||
|
||||
const sendPrompt = async (userPrompt, parentId, _chatId) => {
|
||||
const sendPrompt = async (prompt, parentId) => {
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
await Promise.all(
|
||||
selectedModels.map(async (model) => {
|
||||
console.log(model);
|
||||
if ($models.filter((m) => m.name === model)[0].external) {
|
||||
await sendPromptOpenAI(model, userPrompt, parentId, _chatId);
|
||||
await sendPromptOpenAI(model, prompt, parentId, _chatId);
|
||||
} else {
|
||||
await sendPromptOllama(model, userPrompt, parentId, _chatId);
|
||||
await sendPromptOllama(model, prompt, parentId, _chatId);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await chats.set(await $db.getChats());
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => {
|
||||
console.log('sendPromptOllama');
|
||||
// Create response message
|
||||
let responseMessageId = uuidv4();
|
||||
let responseMessage = {
|
||||
parentId: parentId,
|
||||
|
@ -168,8 +148,11 @@
|
|||
model: model
|
||||
};
|
||||
|
||||
// Add message to history and Set currentId to messageId
|
||||
history.messages[responseMessageId] = responseMessage;
|
||||
history.currentId = responseMessageId;
|
||||
|
||||
// Append messageId to childrenIds of parent message
|
||||
if (parentId !== null) {
|
||||
history.messages[parentId].childrenIds = [
|
||||
...history.messages[parentId].childrenIds,
|
||||
|
@ -177,17 +160,16 @@
|
|||
];
|
||||
}
|
||||
|
||||
// Wait until history/message have been updated
|
||||
await tick();
|
||||
|
||||
// Scroll down
|
||||
window.scrollTo({ top: document.body.scrollHeight });
|
||||
|
||||
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/chat`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
...($settings.authHeader && { Authorization: $settings.authHeader }),
|
||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const res = await generateChatCompletion(
|
||||
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token,
|
||||
{
|
||||
model: model,
|
||||
messages: [
|
||||
$settings.system
|
||||
|
@ -209,20 +191,11 @@
|
|||
})
|
||||
})),
|
||||
options: {
|
||||
seed: $settings.seed ?? undefined,
|
||||
temperature: $settings.temperature ?? undefined,
|
||||
repeat_penalty: $settings.repeat_penalty ?? undefined,
|
||||
top_k: $settings.top_k ?? undefined,
|
||||
top_p: $settings.top_p ?? undefined,
|
||||
num_ctx: $settings.num_ctx ?? undefined,
|
||||
...($settings.options ?? {})
|
||||
},
|
||||
format: $settings.requestFormat ?? undefined
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (res && res.ok) {
|
||||
const reader = res.body
|
||||
|
@ -311,23 +284,14 @@
|
|||
if (autoScroll) {
|
||||
window.scrollTo({ top: document.body.scrollHeight });
|
||||
}
|
||||
}
|
||||
|
||||
await $db.updateChatById(_chatId, {
|
||||
title: title === '' ? 'New Chat' : title,
|
||||
models: selectedModels,
|
||||
system: $settings.system ?? undefined,
|
||||
options: {
|
||||
seed: $settings.seed ?? undefined,
|
||||
temperature: $settings.temperature ?? undefined,
|
||||
repeat_penalty: $settings.repeat_penalty ?? undefined,
|
||||
top_k: $settings.top_k ?? undefined,
|
||||
top_p: $settings.top_p ?? undefined,
|
||||
num_ctx: $settings.num_ctx ?? undefined,
|
||||
...($settings.options ?? {})
|
||||
},
|
||||
if ($chatId == _chatId) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
} else {
|
||||
if (res !== null) {
|
||||
|
@ -353,6 +317,7 @@
|
|||
|
||||
stopResponseFlag = false;
|
||||
await tick();
|
||||
|
||||
if (autoScroll) {
|
||||
window.scrollTo({ top: document.body.scrollHeight });
|
||||
}
|
||||
|
@ -495,23 +460,14 @@
|
|||
if (autoScroll) {
|
||||
window.scrollTo({ top: document.body.scrollHeight });
|
||||
}
|
||||
}
|
||||
|
||||
await $db.updateChatById(_chatId, {
|
||||
title: title === '' ? 'New Chat' : title,
|
||||
models: selectedModels,
|
||||
system: $settings.system ?? undefined,
|
||||
options: {
|
||||
seed: $settings.seed ?? undefined,
|
||||
temperature: $settings.temperature ?? undefined,
|
||||
repeat_penalty: $settings.repeat_penalty ?? undefined,
|
||||
top_k: $settings.top_k ?? undefined,
|
||||
top_p: $settings.top_p ?? undefined,
|
||||
num_ctx: $settings.num_ctx ?? undefined,
|
||||
...($settings.options ?? {})
|
||||
},
|
||||
if ($chatId == _chatId) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
} else {
|
||||
if (res !== null) {
|
||||
|
@ -556,16 +512,18 @@
|
|||
};
|
||||
|
||||
const submitPrompt = async (userPrompt) => {
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
console.log('submitPrompt', _chatId);
|
||||
console.log('submitPrompt', $chatId);
|
||||
|
||||
if (selectedModels.includes('')) {
|
||||
toast.error('Model not selected');
|
||||
} else if (messages.length != 0 && messages.at(-1).done != true) {
|
||||
// Response not done
|
||||
console.log('wait');
|
||||
} else {
|
||||
// Reset chat message textarea height
|
||||
document.getElementById('chat-textarea').style.height = '';
|
||||
|
||||
// Create user message
|
||||
let userMessageId = uuidv4();
|
||||
let userMessage = {
|
||||
id: userMessageId,
|
||||
|
@ -576,42 +534,43 @@
|
|||
files: files.length > 0 ? files : undefined
|
||||
};
|
||||
|
||||
// Add message to history and Set currentId to messageId
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
// Append messageId to childrenIds of parent message
|
||||
if (messages.length !== 0) {
|
||||
history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
|
||||
}
|
||||
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
// Wait until history/message have been updated
|
||||
await tick();
|
||||
|
||||
// Create new chat if only one message in messages
|
||||
if (messages.length == 1) {
|
||||
await $db.createNewChat({
|
||||
id: _chatId,
|
||||
chat = await createNewChat(localStorage.token, {
|
||||
id: $chatId,
|
||||
title: 'New Chat',
|
||||
models: selectedModels,
|
||||
system: $settings.system ?? undefined,
|
||||
options: {
|
||||
seed: $settings.seed ?? undefined,
|
||||
temperature: $settings.temperature ?? undefined,
|
||||
repeat_penalty: $settings.repeat_penalty ?? undefined,
|
||||
top_k: $settings.top_k ?? undefined,
|
||||
top_p: $settings.top_p ?? undefined,
|
||||
num_ctx: $settings.num_ctx ?? undefined,
|
||||
...($settings.options ?? {})
|
||||
},
|
||||
messages: messages,
|
||||
history: history
|
||||
history: history,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await chatId.set(chat.id);
|
||||
await tick();
|
||||
}
|
||||
|
||||
// Reset chat input textarea
|
||||
prompt = '';
|
||||
files = [];
|
||||
|
||||
setTimeout(() => {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
}, 50);
|
||||
|
||||
await sendPrompt(userPrompt, userMessageId, _chatId);
|
||||
// Send prompt
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -621,9 +580,7 @@
|
|||
};
|
||||
|
||||
const regenerateResponse = async () => {
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
console.log('regenerateResponse', _chatId);
|
||||
|
||||
console.log('regenerateResponse');
|
||||
if (messages.length != 0 && messages.at(-1).done == true) {
|
||||
messages.splice(messages.length - 1, 1);
|
||||
messages = messages;
|
||||
|
@ -631,41 +588,21 @@
|
|||
let userMessage = messages.at(-1);
|
||||
let userPrompt = userMessage.content;
|
||||
|
||||
await sendPrompt(userPrompt, userMessage.id, _chatId);
|
||||
await sendPrompt(userPrompt, userMessage.id);
|
||||
}
|
||||
};
|
||||
|
||||
const generateChatTitle = async (_chatId, userPrompt) => {
|
||||
if ($settings.titleAutoGenerate ?? true) {
|
||||
console.log('generateChatTitle');
|
||||
const title = await generateTitle(
|
||||
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token,
|
||||
selectedModels[0],
|
||||
userPrompt
|
||||
);
|
||||
|
||||
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
...($settings.authHeader && { Authorization: $settings.authHeader }),
|
||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: selectedModels[0],
|
||||
prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`,
|
||||
stream: false
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
if ('detail' in error) {
|
||||
toast.error(error.detail);
|
||||
}
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
|
||||
if (title) {
|
||||
await setChatTitle(_chatId, title);
|
||||
}
|
||||
} else {
|
||||
await setChatTitle(_chatId, `${userPrompt}`);
|
||||
|
@ -673,10 +610,12 @@
|
|||
};
|
||||
|
||||
const setChatTitle = async (_chatId, _title) => {
|
||||
await $db.updateChatById(_chatId, { title: _title });
|
||||
if (_chatId === $chatId) {
|
||||
title = _title;
|
||||
}
|
||||
|
||||
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -687,7 +626,13 @@
|
|||
/>
|
||||
|
||||
{#if loaded}
|
||||
<Navbar {title} shareEnabled={messages.length > 0} />
|
||||
<Navbar
|
||||
{title}
|
||||
shareEnabled={messages.length > 0}
|
||||
initNewChat={() => {
|
||||
goto('/');
|
||||
}}
|
||||
/>
|
||||
<div class="min-h-screen w-full flex justify-center">
|
||||
<div class=" py-2.5 flex flex-col justify-between w-full">
|
||||
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10">
|
||||
|
@ -696,6 +641,7 @@
|
|||
|
||||
<div class=" h-full mt-10 mb-32 w-full flex flex-col">
|
||||
<Messages
|
||||
chatId={$chatId}
|
||||
{selectedModels}
|
||||
{selectedModelfile}
|
||||
bind:history
|
||||
|
|
|
@ -1,46 +1,41 @@
|
|||
<script lang="ts">
|
||||
import { modelfiles, settings, user } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { modelfiles, settings, user } from '$lib/stores';
|
||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
import { createModel, deleteModel } from '$lib/apis/ollama';
|
||||
import {
|
||||
createNewModelfile,
|
||||
deleteModelfileByTagName,
|
||||
getModelfiles
|
||||
} from '$lib/apis/modelfiles';
|
||||
|
||||
let localModelfiles = [];
|
||||
|
||||
const deleteModelHandler = async (tagName) => {
|
||||
let success = null;
|
||||
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/delete`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
...($settings.authHeader && { Authorization: $settings.authHeader }),
|
||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
console.log(json);
|
||||
toast.success(`Deleted ${tagName}`);
|
||||
success = true;
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
toast.error(err.error);
|
||||
return null;
|
||||
});
|
||||
|
||||
success = await deleteModel(
|
||||
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token,
|
||||
tagName
|
||||
);
|
||||
|
||||
if (success) {
|
||||
toast.success(`Deleted ${tagName}`);
|
||||
}
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
const deleteModelfilebyTagName = async (tagName) => {
|
||||
const deleteModelfile = async (tagName) => {
|
||||
await deleteModelHandler(tagName);
|
||||
await modelfiles.set($modelfiles.filter((modelfile) => modelfile.tagName != tagName));
|
||||
localStorage.setItem('modelfiles', JSON.stringify($modelfiles));
|
||||
await deleteModelfileByTagName(localStorage.token, tagName);
|
||||
await modelfiles.set(await getModelfiles(localStorage.token));
|
||||
};
|
||||
|
||||
const shareModelfile = async (modelfile) => {
|
||||
|
@ -60,6 +55,21 @@
|
|||
false
|
||||
);
|
||||
};
|
||||
|
||||
const saveModelfiles = async (modelfiles) => {
|
||||
let blob = new Blob([JSON.stringify(modelfiles)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `modelfiles-export-${Date.now()}.json`);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
|
||||
|
||||
if (localModelfiles) {
|
||||
console.log(localModelfiles);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen w-full flex justify-center dark:text-white">
|
||||
|
@ -167,7 +177,7 @@
|
|||
class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
deleteModelfilebyTagName(modelfile.tagName);
|
||||
deleteModelfile(modelfile.tagName);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
@ -189,6 +199,79 @@
|
|||
</div>
|
||||
{/each}
|
||||
|
||||
{#if localModelfiles.length > 0}
|
||||
<hr class=" dark:border-gray-700 my-2.5" />
|
||||
|
||||
<div class=" flex justify-end space-x-4 w-full mb-3">
|
||||
<div class=" self-center text-sm font-medium">
|
||||
{localModelfiles.length} Local Modelfiles Detected
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1">
|
||||
<button
|
||||
class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex"
|
||||
on:click={async () => {
|
||||
for (const modelfile of localModelfiles) {
|
||||
await createNewModelfile(localStorage.token, modelfile).catch((error) => {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
saveModelfiles(localModelfiles);
|
||||
localStorage.removeItem('modelfiles');
|
||||
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
|
||||
await modelfiles.set(await getModelfiles(localStorage.token));
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium">Sync All</div>
|
||||
|
||||
<div class=" self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
|
||||
on:click={async () => {
|
||||
saveModelfiles(localModelfiles);
|
||||
|
||||
localStorage.removeItem('modelfiles');
|
||||
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
|
||||
await modelfiles.set(await getModelfiles(localStorage.token));
|
||||
}}
|
||||
>
|
||||
<div class=" self-center">
|
||||
<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 9l-.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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" my-16">
|
||||
<div class=" text-2xl font-semibold mb-6">Made by OllamaHub Community</div>
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import Advanced from '$lib/components/chat/Settings/Advanced.svelte';
|
||||
import { splitStream } from '$lib/utils';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { createModel } from '$lib/apis/ollama';
|
||||
import { createNewModelfile, getModelfileByTagName, getModelfiles } from '$lib/apis/modelfiles';
|
||||
|
||||
let loading = false;
|
||||
|
||||
|
@ -93,11 +95,8 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
|
|||
};
|
||||
|
||||
const saveModelfile = async (modelfile) => {
|
||||
await modelfiles.set([
|
||||
...$modelfiles.filter((m) => m.tagName !== modelfile.tagName),
|
||||
modelfile
|
||||
]);
|
||||
localStorage.setItem('modelfiles', JSON.stringify($modelfiles));
|
||||
await createNewModelfile(localStorage.token, modelfile);
|
||||
await modelfiles.set(await getModelfiles(localStorage.token));
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
|
@ -112,7 +111,10 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
|
|||
return success;
|
||||
}
|
||||
|
||||
if ($models.includes(tagName)) {
|
||||
if (
|
||||
$models.map((model) => model.name).includes(tagName) ||
|
||||
(await getModelfileByTagName(localStorage.token, tagName).catch(() => false))
|
||||
) {
|
||||
toast.error(
|
||||
`Uh-oh! It looks like you already have a model named '${tagName}'. Please choose a different name to complete your modelfile.`
|
||||
);
|
||||
|
@ -128,18 +130,12 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, '');
|
|||
Object.keys(categories).filter((category) => categories[category]).length > 0 &&
|
||||
!$models.includes(tagName)
|
||||
) {
|
||||
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
...($settings.authHeader && { Authorization: $settings.authHeader }),
|
||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName,
|
||||
modelfile: content
|
||||
})
|
||||
});
|
||||
const res = await createModel(
|
||||
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token,
|
||||
tagName,
|
||||
content
|
||||
);
|
||||
|
||||
if (res) {
|
||||
const reader = res.body
|
||||
|
|
|
@ -2,14 +2,20 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { toast } from 'svelte-french-toast';
|
||||
import { goto } from '$app/navigation';
|
||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
import { settings, db, user, config, modelfiles } from '$lib/stores';
|
||||
|
||||
import Advanced from '$lib/components/chat/Settings/Advanced.svelte';
|
||||
import { splitStream } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { settings, db, user, config, modelfiles } from '$lib/stores';
|
||||
|
||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
import { splitStream } from '$lib/utils';
|
||||
|
||||
import { createModel } from '$lib/apis/ollama';
|
||||
import { getModelfiles, updateModelfileByTagName } from '$lib/apis/modelfiles';
|
||||
|
||||
import Advanced from '$lib/components/chat/Settings/Advanced.svelte';
|
||||
|
||||
let loading = false;
|
||||
|
||||
let filesInputElement;
|
||||
|
@ -78,17 +84,9 @@
|
|||
}
|
||||
});
|
||||
|
||||
const saveModelfile = async (modelfile) => {
|
||||
await modelfiles.set(
|
||||
$modelfiles.map((e) => {
|
||||
if (e.tagName === modelfile.tagName) {
|
||||
return modelfile;
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
})
|
||||
);
|
||||
localStorage.setItem('modelfiles', JSON.stringify($modelfiles));
|
||||
const updateModelfile = async (modelfile) => {
|
||||
await updateModelfileByTagName(localStorage.token, modelfile.tagName, modelfile);
|
||||
await modelfiles.set(await getModelfiles(localStorage.token));
|
||||
};
|
||||
|
||||
const updateHandler = async () => {
|
||||
|
@ -106,18 +104,12 @@
|
|||
content !== '' &&
|
||||
Object.keys(categories).filter((category) => categories[category]).length > 0
|
||||
) {
|
||||
const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
...($settings.authHeader && { Authorization: $settings.authHeader }),
|
||||
...($user && { Authorization: `Bearer ${localStorage.token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName,
|
||||
modelfile: content
|
||||
})
|
||||
});
|
||||
const res = await createModel(
|
||||
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
|
||||
localStorage.token,
|
||||
tagName,
|
||||
content
|
||||
);
|
||||
|
||||
if (res) {
|
||||
const reader = res.body
|
||||
|
@ -178,7 +170,7 @@
|
|||
}
|
||||
|
||||
if (success) {
|
||||
await saveModelfile({
|
||||
await updateModelfile({
|
||||
tagName: tagName,
|
||||
imageUrl: imageUrl,
|
||||
title: title,
|
||||
|
|
|
@ -2,56 +2,39 @@
|
|||
import { onMount, tick } from 'svelte';
|
||||
import { config, user } from '$lib/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import toast, { Toaster } from 'svelte-french-toast';
|
||||
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
import { getSessionUser } from '$lib/apis/auths';
|
||||
|
||||
import '../app.css';
|
||||
import '../tailwind.css';
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
|
||||
let loaded = false;
|
||||
|
||||
onMount(async () => {
|
||||
const resBackend = await fetch(`${WEBUI_API_BASE_URL}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
// Check Backend Status
|
||||
const backendConfig = await getBackendConfig();
|
||||
|
||||
console.log(resBackend);
|
||||
await config.set(resBackend);
|
||||
if (backendConfig) {
|
||||
// Save Backend Status to Store
|
||||
await config.set(backendConfig);
|
||||
console.log(backendConfig);
|
||||
|
||||
if ($config) {
|
||||
if ($config.auth) {
|
||||
if ($config) {
|
||||
if (localStorage.token) {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error.detail);
|
||||
return null;
|
||||
});
|
||||
// Get Session User Info
|
||||
const sessionUser = await getSessionUser(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
await user.set(res);
|
||||
if (sessionUser) {
|
||||
// Save Session User to Store
|
||||
await user.set(sessionUser);
|
||||
} else {
|
||||
// Redirect Invalid Session User to /auth Page
|
||||
localStorage.removeItem('token');
|
||||
await goto('/auth');
|
||||
}
|
||||
|
@ -59,6 +42,9 @@
|
|||
await goto('/auth');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Redirect to /error when Backend Not Detected
|
||||
await goto(`/error`);
|
||||
}
|
||||
|
||||
await tick();
|
||||
|
@ -69,8 +55,9 @@
|
|||
<svelte:head>
|
||||
<title>Ollama</title>
|
||||
</svelte:head>
|
||||
<Toaster />
|
||||
|
||||
{#if $config !== undefined && loaded}
|
||||
{#if loaded}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
<Toaster />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { userSignIn, userSignUp } from '$lib/apis/auths';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { config, user } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
@ -12,76 +13,51 @@
|
|||
let email = '';
|
||||
let password = '';
|
||||
|
||||
const signInHandler = async () => {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signin`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error.detail);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
const setSessionUser = async (sessionUser) => {
|
||||
if (sessionUser) {
|
||||
console.log(sessionUser);
|
||||
toast.success(`You're now logged in.`);
|
||||
localStorage.token = res.token;
|
||||
await user.set(res);
|
||||
localStorage.token = sessionUser.token;
|
||||
await user.set(sessionUser);
|
||||
goto('/');
|
||||
}
|
||||
};
|
||||
|
||||
const signUpHandler = async () => {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error.detail);
|
||||
return null;
|
||||
});
|
||||
const signInHandler = async () => {
|
||||
const sessionUser = await userSignIn(email, password).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
toast.success(`Account creation successful."`);
|
||||
localStorage.token = res.token;
|
||||
await user.set(res);
|
||||
goto('/');
|
||||
await setSessionUser(sessionUser);
|
||||
};
|
||||
|
||||
const signUpHandler = async () => {
|
||||
const sessionUser = await userSignUp(name, email, password).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
await setSessionUser(sessionUser);
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (mode === 'signin') {
|
||||
await signInHandler();
|
||||
} else {
|
||||
await signUpHandler();
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($config === null || !$config.auth || ($config.auth && $user !== undefined)) {
|
||||
if ($user !== undefined) {
|
||||
await goto('/');
|
||||
}
|
||||
loaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if loaded && $config && $config.auth}
|
||||
{#if loaded}
|
||||
<div class="fixed m-10 z-50">
|
||||
<div class="flex space-x-2">
|
||||
<div class=" self-center">
|
||||
|
@ -91,7 +67,7 @@
|
|||
</div>
|
||||
|
||||
<div class=" bg-white min-h-screen w-full flex justify-center font-mona">
|
||||
<div class="hidden lg:flex lg:flex-1 px-10 md:px-16 w-full bg-yellow-50 justify-center">
|
||||
<!-- <div class="hidden lg:flex lg:flex-1 px-10 md:px-16 w-full bg-yellow-50 justify-center">
|
||||
<div class=" my-auto pb-16 text-left">
|
||||
<div>
|
||||
<div class=" font-bold text-yellow-600 text-4xl">
|
||||
|
@ -103,66 +79,65 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="w-full max-w-xl px-10 md:px-16 bg-white min-h-screen w-full flex flex-col">
|
||||
<div class="w-full max-w-lg px-10 md:px-16 bg-white min-h-screen flex flex-col">
|
||||
<div class=" my-auto pb-10 w-full">
|
||||
<form
|
||||
class=" flex flex-col justify-center"
|
||||
on:submit|preventDefault={() => {
|
||||
if (mode === 'signin') {
|
||||
signInHandler();
|
||||
} else {
|
||||
signUpHandler();
|
||||
}
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" text-2xl md:text-3xl font-semibold">
|
||||
<div class=" text-xl md:text-2xl font-bold">
|
||||
{mode === 'signin' ? 'Sign in' : 'Sign up'} to Ollama Web UI
|
||||
</div>
|
||||
|
||||
<hr class="my-8" />
|
||||
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col mt-4">
|
||||
{#if mode === 'signup'}
|
||||
<div>
|
||||
<div class=" text-sm font-bold text-left mb-2">Name</div>
|
||||
<div class=" text-sm font-semibold text-left mb-1">Name</div>
|
||||
<input
|
||||
bind:value={name}
|
||||
type="text"
|
||||
class=" border px-5 py-4 rounded-2xl w-full text-sm"
|
||||
class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
|
||||
autocomplete="name"
|
||||
placeholder="Enter Your Full Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr class=" my-3" />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class=" text-sm font-bold text-left mb-2">Email</div>
|
||||
<div class="mb-2">
|
||||
<div class=" text-sm font-semibold text-left mb-1">Email</div>
|
||||
<input
|
||||
bind:value={email}
|
||||
type="email"
|
||||
class=" border px-5 py-4 rounded-2xl w-full text-sm"
|
||||
class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
|
||||
autocomplete="email"
|
||||
placeholder="Enter Your Email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" text-sm font-bold text-left mb-2">Password</div>
|
||||
<div class=" text-sm font-semibold text-left mb-1">Password</div>
|
||||
<input
|
||||
bind:value={password}
|
||||
type="password"
|
||||
class=" border px-5 py-4 rounded-2xl w-full text-sm"
|
||||
class=" border px-4 py-2.5 rounded-2xl w-full text-sm"
|
||||
placeholder="Enter Your Password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div class="mt-5">
|
||||
<button
|
||||
class=" bg-gray-900 hover:bg-gray-800 w-full rounded-full text-white font-semibold text-sm py-5 transition"
|
||||
class=" bg-gray-900 hover:bg-gray-800 w-full rounded-full text-white font-semibold text-sm py-3 transition"
|
||||
type="submit"
|
||||
>
|
||||
{mode === 'signin' ? 'Sign In' : 'Create Account'}
|
||||
|
|
55
src/routes/error/+page.svelte
Normal file
55
src/routes/error/+page.svelte
Normal file
|
@ -0,0 +1,55 @@
|
|||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { config } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let loaded = false;
|
||||
|
||||
onMount(async () => {
|
||||
if ($config) {
|
||||
await goto('/');
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<div class="absolute w-full h-full flex z-50">
|
||||
<div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center">
|
||||
<div class="m-auto pb-44 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<div class="text-center text-2xl font-medium z-50">Ollama WebUI Backend Required</div>
|
||||
|
||||
<div class=" mt-4 text-center text-sm w-full">
|
||||
Oops! You're using an unsupported method (frontend only). Please serve the WebUI from
|
||||
the backend.
|
||||
|
||||
<br class=" " />
|
||||
<br class=" " />
|
||||
<a
|
||||
class=" font-semibold underline"
|
||||
href="https://github.com/ollama-webui/ollama-webui#how-to-install-"
|
||||
target="_blank">See readme.md for instructions</a
|
||||
>
|
||||
or
|
||||
<a class=" font-semibold underline" href="https://discord.gg/5rJgQTnV4s" target="_blank"
|
||||
>join our Discord for help.</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class=" mt-6 mx-auto relative group w-fit">
|
||||
<button
|
||||
class="relative z-20 flex px-5 py-2 rounded-full bg-gray-100 hover:bg-gray-200 transition font-medium text-sm"
|
||||
on:click={() => {
|
||||
location.href = '/';
|
||||
}}
|
||||
>
|
||||
Check Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
Loading…
Reference in a new issue