Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92e21acb4c | ||
|
|
ad237b61f3 | ||
|
|
c9173b71b6 | ||
|
|
f741adc6c9 | ||
|
|
5ae121b087 | ||
|
|
434595b3e6 | ||
|
|
534ba9e4f0 | ||
|
|
34b6a99601 | ||
|
|
0aa4e3aafb | ||
|
|
7258a794f1 | ||
|
|
e4eb32709d | ||
|
|
20284dc66d | ||
|
|
2b016adc6e | ||
|
|
a9d74b669e | ||
|
|
8fb5f54751 | ||
|
|
2111398d13 | ||
|
|
089a63e0c6 | ||
|
|
3455f8993b | ||
|
|
132e05ad57 | ||
|
|
95b20f3cb8 | ||
|
|
1fad6e10cd | ||
|
|
124020f824 | ||
|
|
fbdac832bb | ||
|
|
8181d98e99 | ||
|
|
8626ae60dd | ||
|
|
8d34324d12 | ||
|
|
b2dd2f191d | ||
|
|
8fd7bc342d | ||
|
|
f04d60b6d9 | ||
|
|
03907f9a8a | ||
|
|
475105473c | ||
|
|
2d59847fb9 | ||
|
|
32495256fa | ||
|
|
8c01197483 | ||
|
|
6782e95c75 | ||
|
|
7a30dbc839 | ||
|
|
b986c2aefd | ||
|
|
20263f742a | ||
|
|
5d4ff85228 | ||
|
|
50acdb09de | ||
|
|
0def6f8724 | ||
|
|
15e0f9de86 | ||
|
|
fe040ce82b | ||
|
|
f4f6724d96 | ||
|
|
13f0052122 | ||
|
|
a5c1ac9680 | ||
|
|
0fc76d46db | ||
|
|
cc471597cd | ||
|
|
fe19f125d4 | ||
|
|
05221ea135 | ||
|
|
2250387382 | ||
|
|
f9f3fe5ec0 | ||
|
|
eb51ad14e4 | ||
|
|
65804e657a | ||
|
|
25c0316fc6 | ||
|
|
4a1e506a35 | ||
|
|
47a05a47b4 | ||
|
|
9694c6569f | ||
|
|
9201769ba5 | ||
|
|
79d7e135ec | ||
|
|
bf3f3c580e | ||
|
|
901e7a33fa | ||
|
|
656f8dab05 | ||
|
|
06ce5aed4a | ||
|
|
2379e94cbf | ||
|
|
9b86e0bb41 | ||
|
|
b9444d00b3 | ||
|
|
0bc3e0a302 | ||
|
|
f9482275f2 | ||
|
|
63c0927022 | ||
|
|
edb63c2280 | ||
|
|
6d854125cc | ||
|
|
74809e7330 | ||
|
|
a4c6a8d5a4 | ||
|
|
9caf6c26ff | ||
|
|
a12b5ffab3 | ||
|
|
b85c6ef667 | ||
|
|
6c70d0f770 | ||
|
|
96e2ee48f7 | ||
|
|
63e7246c97 | ||
|
|
6f4a863562 | ||
|
|
a7ae141301 | ||
|
|
2564b154fc | ||
|
|
8310392c9d | ||
|
|
78d9c469a2 | ||
|
|
e5c812f1d2 | ||
|
|
8461b92f08 | ||
|
|
3396723057 | ||
|
|
4fe47f43be | ||
|
|
705f5aecd7 | ||
|
|
19d49df76f | ||
|
|
30c94ff10f | ||
|
|
941ab5d6cf | ||
|
|
5a81ca7e39 | ||
|
|
f4122fd0ce | ||
|
|
5745b9c5db | ||
|
|
3fd55ebc32 | ||
|
|
776b463ff3 | ||
|
|
5640b38694 | ||
|
|
a181b2b63b | ||
|
|
32361dc3da | ||
|
|
59ae5b6d34 | ||
|
|
a36627cbce | ||
|
|
8a63d22000 | ||
|
|
781be2779b | ||
|
|
f347d664ac | ||
|
|
260a8b8b83 | ||
|
|
1d4d1ddbea | ||
|
|
8f2d580991 | ||
|
|
6bbc3721b4 | ||
|
|
7fe412374c | ||
|
|
03210c9204 | ||
|
|
d24b6fa9af | ||
|
|
7c7e16c382 | ||
|
|
70f807e8c9 | ||
|
|
210ae331cc | ||
|
|
bbeaa92904 | ||
|
|
a7a817d12b | ||
|
|
0f6bdaa602 | ||
|
|
a016171573 | ||
|
|
7d0d504390 | ||
|
|
6bc627bbfb | ||
|
|
5adff7767f |
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Ollama URL for the backend to connect
|
||||
# The path '/ollama/api' will be redirected to the specified backend URL
|
||||
OLLAMA_API_BASE_URL='http://localhost:11434/api'
|
||||
# The path '/ollama' will be redirected to the specified backend URL
|
||||
OLLAMA_BASE_URL='http://localhost:11434'
|
||||
|
||||
OPENAI_API_BASE_URL=''
|
||||
OPENAI_API_KEY=''
|
||||
|
||||
@@ -32,7 +32,7 @@ assignees: ''
|
||||
**Confirmation:**
|
||||
|
||||
- [ ] I have read and followed all the instructions provided in the README.md.
|
||||
- [ ] I have reviewed the troubleshooting.md document.
|
||||
- [ ] I am on the latest version of both Open WebUI and Ollama.
|
||||
- [ ] I have included the browser console logs.
|
||||
- [ ] I have included the Docker container logs.
|
||||
|
||||
|
||||
@@ -19,24 +19,34 @@ jobs:
|
||||
echo "No changes to package.json"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
- name: Get version number from package.json
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(jq -r '.version' package.json)
|
||||
echo "::set-output name=version::$VERSION"
|
||||
|
||||
- name: Extract latest CHANGELOG entry
|
||||
id: changelog
|
||||
run: |
|
||||
CHANGELOG_CONTENT=$(awk '/^## \[/{n++} n==1' CHANGELOG.md)
|
||||
echo "CHANGELOG_CONTENT<<EOF"
|
||||
echo "$CHANGELOG_CONTENT"
|
||||
echo "EOF"
|
||||
echo "::set-output name=content::${CHANGELOG_CONTENT}"
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changelog = `${{ steps.changelog.outputs.content }}`;
|
||||
const release = await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: `v${{ steps.get_version.outputs.version }}`,
|
||||
name: `v${{ steps.get_version.outputs.version }}`,
|
||||
body: 'Automatically created new release',
|
||||
body: changelog,
|
||||
})
|
||||
console.log(`Created release ${release.data.html_url}`)
|
||||
|
||||
|
||||
@@ -5,6 +5,76 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.1.109] - 2024-03-06
|
||||
|
||||
### Added
|
||||
|
||||
- **🔄 Multiple Ollama Servers Support**: Enjoy enhanced scalability and performance with support for multiple Ollama servers in a single WebUI. Load balancing features are now available, providing improved efficiency (#788, #278).
|
||||
- **🔧 Support for Claude 3 and Gemini**: Responding to user requests, we've expanded our toolset to include Claude 3 and Gemini, offering a wider range of functionalities within our platform (#1064).
|
||||
- **🔍 OCR Functionality for PDF Loader**: We've augmented our PDF loader with Optical Character Recognition (OCR) capabilities. Now, extract text from scanned documents and images within PDFs, broadening the scope of content processing (#1050).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031).
|
||||
- **📝 User Agent Headers**: Fixed issue of RAG web requests being sent with empty user_agent headers, reducing rejections from certain websites. Realistic headers are now utilized for these requests (#1024).
|
||||
- **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006).
|
||||
- **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061).
|
||||
|
||||
### Changed
|
||||
|
||||
- **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004).
|
||||
- **⚠️ Deprecation of `OLLAMA_API_BASE_URL`**: Deprecated `OLLAMA_API_BASE_URL` environment variable; recommend using `OLLAMA_BASE_URL` instead. Refer to our documentation for further details.
|
||||
|
||||
## [0.1.108] - 2024-03-02
|
||||
|
||||
### Added
|
||||
|
||||
- **🎮 Playground Feature (Beta)**: Explore the full potential of the raw API through an intuitive UI with our new playground feature, accessible to admins. Simply click on the bottom name area of the sidebar to access it. The playground feature offers two modes text completion (notebook) and chat completion. As it's in beta, please report any issues you encounter.
|
||||
- **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings.
|
||||
- **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes.
|
||||
- **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use.
|
||||
- Corrected numbered list display issue in Safari (#963).
|
||||
- Restricted user ability to delete chats without proper permissions (#993).
|
||||
|
||||
### Changed
|
||||
|
||||
- **Simplified Ollama Settings**: Ollama settings now don't require the `/api` suffix. You can now utilize the Ollama base URL directly, e.g., `http://localhost:11434`. Also, an `OLLAMA_BASE_URL` environment variable has been added.
|
||||
- **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`.
|
||||
|
||||
## [0.1.107] - 2024-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- **🚀 Makefile and LLM Update Script**: Included Makefile and a script for LLM updates in the repository.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Corrected issue where links in the settings modal didn't appear clickable (#960).
|
||||
- Fixed problem with web UI port not taking effect due to incorrect environment variable name in run-compose.sh (#996).
|
||||
- Enhanced user experience by displaying chat in browser title and enabling automatic scrolling to the bottom (#992).
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded toast library from `svelte-french-toast` to `svelte-sonner` for a more polished UI.
|
||||
- Enhanced accessibility with the addition of dark mode on the authentication page.
|
||||
|
||||
## [0.1.106] - 2024-02-27
|
||||
|
||||
### Added
|
||||
|
||||
- **🎯 Auto-focus Feature**: The input area now automatically focuses when initiating or opening a chat conversation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Corrected typo from "HuggingFace" to "Hugging Face" (Issue #924).
|
||||
- Resolved bug causing errors in chat completion API calls to OpenAI due to missing "num_ctx" parameter (Issue #927).
|
||||
- Fixed issues preventing text editing, selection, and cursor retention in the input field (Issue #940).
|
||||
- Fixed a bug where defining an OpenAI-compatible API server using 'OPENAI_API_BASE_URL' containing 'openai' string resulted in hiding models not containing 'gpt' string from the model menu. (Issue #930)
|
||||
|
||||
## [0.1.105] - 2024-02-25
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ FROM python:3.11-slim-bookworm as base
|
||||
ENV ENV=prod
|
||||
ENV PORT ""
|
||||
|
||||
ENV OLLAMA_API_BASE_URL "/ollama/api"
|
||||
ENV OLLAMA_BASE_URL "/ollama"
|
||||
|
||||
ENV OPENAI_API_BASE_URL ""
|
||||
ENV OPENAI_API_KEY ""
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
install:
|
||||
@docker-compose up -d
|
||||
|
||||
remove:
|
||||
@chmod +x confirm_remove.sh
|
||||
@./confirm_remove.sh
|
||||
|
||||
|
||||
start:
|
||||
@docker-compose start
|
||||
|
||||
stop:
|
||||
@docker-compose stop
|
||||
|
||||
update:
|
||||
# Calls the LLM update script
|
||||
chmod +x update_ollama_models.sh
|
||||
@./update_ollama_models.sh
|
||||
@git pull
|
||||
@docker-compose down
|
||||
# Make sure the ollama-webui container is stopped before rebuilding
|
||||
@docker stop open-webui || true
|
||||
@docker-compose up --build -d
|
||||
@docker-compose start
|
||||
|
||||
@@ -95,10 +95,10 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
|
||||
|
||||
- **If Ollama is on a Different Server**, use this command:
|
||||
|
||||
- To connect to Ollama on another server, change the `OLLAMA_API_BASE_URL` to the server's URL:
|
||||
- To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
- After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
|
||||
@@ -110,7 +110,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c
|
||||
**Example Docker Command**:
|
||||
|
||||
```bash
|
||||
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
### Other Installation Methods
|
||||
@@ -160,6 +160,16 @@ This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LI
|
||||
If you have any questions, suggestions, or need assistance, please open an issue or join our
|
||||
[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#open-webui/open-webui&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open Web UI even more amazing together! 💪
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@
|
||||
|
||||
The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
|
||||
|
||||
- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama/api` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_API_BASE_URL` environment variable. Therefore, a request made to `/ollama/api` in the WebUI is effectively the same as making a request to `OLLAMA_API_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_API_BASE_URL/tags` in the backend.
|
||||
- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend.
|
||||
|
||||
- **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
|
||||
|
||||
@@ -15,7 +15,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c
|
||||
**Example Docker Command**:
|
||||
|
||||
```bash
|
||||
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
### General Connection Errors
|
||||
@@ -25,8 +25,8 @@ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_
|
||||
**Troubleshooting Steps**:
|
||||
|
||||
1. **Verify Ollama URL Format**:
|
||||
- When running the Web UI container, ensure the `OLLAMA_API_BASE_URL` is correctly set, including the `/api` suffix. (e.g., `http://192.168.1.1:11434/api` for different host setups).
|
||||
- When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups).
|
||||
- In the Open WebUI, navigate to "Settings" > "General".
|
||||
- Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]/api` (e.g., `http://localhost:11434/api`), including the `/api` suffix.
|
||||
- Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
|
||||
|
||||
By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
|
||||
|
||||
+844
-16
@@ -3,15 +3,22 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
import random
|
||||
import requests
|
||||
import json
|
||||
import uuid
|
||||
from pydantic import BaseModel
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import decode_token, get_current_user, get_admin_user
|
||||
from config import OLLAMA_API_BASE_URL, WEBUI_AUTH
|
||||
from config import OLLAMA_BASE_URLS
|
||||
|
||||
from typing import Optional, List, Union
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
@@ -22,27 +29,44 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL
|
||||
|
||||
# TARGET_SERVER_URL = OLLAMA_API_BASE_URL
|
||||
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
||||
app.state.MODELS = {}
|
||||
|
||||
|
||||
REQUEST_POOL = []
|
||||
|
||||
|
||||
@app.get("/url")
|
||||
async def get_ollama_api_url(user=Depends(get_admin_user)):
|
||||
return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL}
|
||||
# TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances.
|
||||
# Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin,
|
||||
# least connections, or least response time for better resource utilization and performance optimization.
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def check_url(request: Request, call_next):
|
||||
if len(app.state.MODELS) == 0:
|
||||
await get_all_models()
|
||||
else:
|
||||
pass
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
@app.get("/urls")
|
||||
async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
||||
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
||||
|
||||
|
||||
class UrlUpdateForm(BaseModel):
|
||||
url: str
|
||||
urls: List[str]
|
||||
|
||||
|
||||
@app.post("/url/update")
|
||||
@app.post("/urls/update")
|
||||
async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
|
||||
app.state.OLLAMA_API_BASE_URL = form_data.url
|
||||
return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL}
|
||||
app.state.OLLAMA_BASE_URLS = form_data.urls
|
||||
|
||||
print(app.state.OLLAMA_BASE_URLS)
|
||||
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
|
||||
|
||||
|
||||
@app.get("/cancel/{request_id}")
|
||||
@@ -55,9 +79,806 @@ async def cancel_ollama_request(request_id: str, user=Depends(get_current_user))
|
||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
||||
|
||||
|
||||
async def fetch_url(url):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
return await response.json()
|
||||
except Exception as e:
|
||||
# Handle connection error here
|
||||
print(f"Connection error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def merge_models_lists(model_lists):
|
||||
merged_models = {}
|
||||
|
||||
for idx, model_list in enumerate(model_lists):
|
||||
for model in model_list:
|
||||
digest = model["digest"]
|
||||
if digest not in merged_models:
|
||||
model["urls"] = [idx]
|
||||
merged_models[digest] = model
|
||||
else:
|
||||
merged_models[digest]["urls"].append(idx)
|
||||
|
||||
return list(merged_models.values())
|
||||
|
||||
|
||||
# user=Depends(get_current_user)
|
||||
|
||||
|
||||
async def get_all_models():
|
||||
print("get_all_models")
|
||||
tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
responses = list(filter(lambda x: x is not None, responses))
|
||||
|
||||
models = {
|
||||
"models": merge_models_lists(
|
||||
map(lambda response: response["models"], responses)
|
||||
)
|
||||
}
|
||||
app.state.MODELS = {model["model"]: model for model in models["models"]}
|
||||
|
||||
return models
|
||||
|
||||
|
||||
@app.get("/api/tags")
|
||||
@app.get("/api/tags/{url_idx}")
|
||||
async def get_ollama_tags(
|
||||
url_idx: Optional[int] = None, user=Depends(get_current_user)
|
||||
):
|
||||
|
||||
if url_idx == None:
|
||||
return await get_all_models()
|
||||
else:
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/tags")
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/version")
|
||||
@app.get("/api/version/{url_idx}")
|
||||
async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||
|
||||
if url_idx == None:
|
||||
|
||||
# returns lowest version
|
||||
tasks = [fetch_url(f"{url}/api/version") for url in app.state.OLLAMA_BASE_URLS]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
responses = list(filter(lambda x: x is not None, responses))
|
||||
|
||||
lowest_version = min(
|
||||
responses, key=lambda x: tuple(map(int, x["version"].split(".")))
|
||||
)
|
||||
|
||||
return {"version": lowest_version["version"]}
|
||||
else:
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/version")
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
class ModelNameForm(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
@app.post("/api/pull")
|
||||
@app.post("/api/pull/{url_idx}")
|
||||
async def pull_model(
|
||||
form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
|
||||
):
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal url
|
||||
nonlocal r
|
||||
try:
|
||||
|
||||
def stream_content():
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
yield chunk
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/pull",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
class PushModelForm(BaseModel):
|
||||
name: str
|
||||
insecure: Optional[bool] = None
|
||||
stream: Optional[bool] = None
|
||||
|
||||
|
||||
@app.delete("/api/push")
|
||||
@app.delete("/api/push/{url_idx}")
|
||||
async def push_model(
|
||||
form_data: PushModelForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if form_data.name in app.state.MODELS:
|
||||
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal url
|
||||
nonlocal r
|
||||
try:
|
||||
|
||||
def stream_content():
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
yield chunk
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/push",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
class CreateModelForm(BaseModel):
|
||||
name: str
|
||||
modelfile: Optional[str] = None
|
||||
stream: Optional[bool] = None
|
||||
path: Optional[str] = None
|
||||
|
||||
|
||||
@app.post("/api/create")
|
||||
@app.post("/api/create/{url_idx}")
|
||||
async def create_model(
|
||||
form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
|
||||
):
|
||||
print(form_data)
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal url
|
||||
nonlocal r
|
||||
try:
|
||||
|
||||
def stream_content():
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
yield chunk
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/create",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
print(r)
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
class CopyModelForm(BaseModel):
|
||||
source: str
|
||||
destination: str
|
||||
|
||||
|
||||
@app.post("/api/copy")
|
||||
@app.post("/api/copy/{url_idx}")
|
||||
async def copy_model(
|
||||
form_data: CopyModelForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if form_data.source in app.state.MODELS:
|
||||
url_idx = app.state.MODELS[form_data.source]["urls"][0]
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
try:
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/copy",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
print(r.text)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
@app.delete("/api/delete")
|
||||
@app.delete("/api/delete/{url_idx}")
|
||||
async def delete_model(
|
||||
form_data: ModelNameForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if form_data.name in app.state.MODELS:
|
||||
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
try:
|
||||
r = requests.request(
|
||||
method="DELETE",
|
||||
url=f"{url}/api/delete",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
print(r.text)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/show")
|
||||
async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_user)):
|
||||
if form_data.name not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
||||
)
|
||||
|
||||
url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
try:
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/show",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
class GenerateEmbeddingsForm(BaseModel):
|
||||
model: str
|
||||
prompt: str
|
||||
options: Optional[dict] = None
|
||||
keep_alive: Optional[Union[int, str]] = None
|
||||
|
||||
|
||||
@app.post("/api/embeddings")
|
||||
@app.post("/api/embeddings/{url_idx}")
|
||||
async def generate_embeddings(
|
||||
form_data: GenerateEmbeddingsForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if form_data.model in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
try:
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/embeddings",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
class GenerateCompletionForm(BaseModel):
|
||||
model: str
|
||||
prompt: str
|
||||
images: Optional[List[str]] = None
|
||||
format: Optional[str] = None
|
||||
options: Optional[dict] = None
|
||||
system: Optional[str] = None
|
||||
template: Optional[str] = None
|
||||
context: Optional[str] = None
|
||||
stream: Optional[bool] = True
|
||||
raw: Optional[bool] = None
|
||||
keep_alive: Optional[Union[int, str]] = None
|
||||
|
||||
|
||||
@app.post("/api/generate")
|
||||
@app.post("/api/generate/{url_idx}")
|
||||
async def generate_completion(
|
||||
form_data: GenerateCompletionForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
|
||||
if url_idx == None:
|
||||
if form_data.model in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="error_detail",
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal form_data
|
||||
nonlocal r
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
try:
|
||||
REQUEST_POOL.append(request_id)
|
||||
|
||||
def stream_content():
|
||||
try:
|
||||
if form_data.stream:
|
||||
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
||||
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if request_id in REQUEST_POOL:
|
||||
yield chunk
|
||||
else:
|
||||
print("User: canceled request")
|
||||
break
|
||||
finally:
|
||||
if hasattr(r, "close"):
|
||||
r.close()
|
||||
if request_id in REQUEST_POOL:
|
||||
REQUEST_POOL.remove(request_id)
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/generate",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
images: Optional[List[str]] = None
|
||||
|
||||
|
||||
class GenerateChatCompletionForm(BaseModel):
|
||||
model: str
|
||||
messages: List[ChatMessage]
|
||||
format: Optional[str] = None
|
||||
options: Optional[dict] = None
|
||||
template: Optional[str] = None
|
||||
stream: Optional[bool] = True
|
||||
keep_alive: Optional[Union[int, str]] = None
|
||||
|
||||
|
||||
@app.post("/api/chat")
|
||||
@app.post("/api/chat/{url_idx}")
|
||||
async def generate_chat_completion(
|
||||
form_data: GenerateChatCompletionForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
|
||||
if url_idx == None:
|
||||
if form_data.model in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
r = None
|
||||
|
||||
print(form_data.model_dump_json(exclude_none=True))
|
||||
|
||||
def get_request():
|
||||
nonlocal form_data
|
||||
nonlocal r
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
try:
|
||||
REQUEST_POOL.append(request_id)
|
||||
|
||||
def stream_content():
|
||||
try:
|
||||
if form_data.stream:
|
||||
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
||||
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if request_id in REQUEST_POOL:
|
||||
yield chunk
|
||||
else:
|
||||
print("User: canceled request")
|
||||
break
|
||||
finally:
|
||||
if hasattr(r, "close"):
|
||||
r.close()
|
||||
if request_id in REQUEST_POOL:
|
||||
REQUEST_POOL.remove(request_id)
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/chat",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
# TODO: we should update this part once Ollama supports other types
|
||||
class OpenAIChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class OpenAIChatCompletionForm(BaseModel):
|
||||
model: str
|
||||
messages: List[OpenAIChatMessage]
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
@app.post("/v1/chat/completions")
|
||||
@app.post("/v1/chat/completions/{url_idx}")
|
||||
async def generate_openai_chat_completion(
|
||||
form_data: OpenAIChatCompletionForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
|
||||
if url_idx == None:
|
||||
if form_data.model in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.OLLAMA_BASE_URLS[url_idx]
|
||||
print(url)
|
||||
|
||||
r = None
|
||||
|
||||
def get_request():
|
||||
nonlocal form_data
|
||||
nonlocal r
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
try:
|
||||
REQUEST_POOL.append(request_id)
|
||||
|
||||
def stream_content():
|
||||
try:
|
||||
if form_data.stream:
|
||||
yield json.dumps(
|
||||
{"request_id": request_id, "done": False}
|
||||
) + "\n"
|
||||
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
if request_id in REQUEST_POOL:
|
||||
yield chunk
|
||||
else:
|
||||
print("User: canceled request")
|
||||
break
|
||||
finally:
|
||||
if hasattr(r, "close"):
|
||||
r.close()
|
||||
if request_id in REQUEST_POOL:
|
||||
REQUEST_POOL.remove(request_id)
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/v1/chat/completions",
|
||||
data=form_data.model_dump_json(exclude_none=True),
|
||||
stream=True,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=r.status_code,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
try:
|
||||
return await run_in_threadpool(get_request)
|
||||
except Exception as e:
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def proxy(path: str, request: Request, user=Depends(get_current_user)):
|
||||
target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}"
|
||||
async def deprecated_proxy(path: str, request: Request, user=Depends(get_current_user)):
|
||||
url = app.state.OLLAMA_BASE_URLS[0]
|
||||
target_url = f"{url}/{path}"
|
||||
|
||||
body = await request.body()
|
||||
headers = dict(request.headers)
|
||||
@@ -91,7 +912,13 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)):
|
||||
|
||||
def stream_content():
|
||||
try:
|
||||
if path in ["chat"]:
|
||||
if path == "generate":
|
||||
data = json.loads(body.decode("utf-8"))
|
||||
|
||||
if not ("stream" in data and data["stream"] == False):
|
||||
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
||||
|
||||
elif path == "chat":
|
||||
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
||||
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
@@ -103,7 +930,8 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)):
|
||||
finally:
|
||||
if hasattr(r, "close"):
|
||||
r.close()
|
||||
REQUEST_POOL.remove(request_id)
|
||||
if request_id in REQUEST_POOL:
|
||||
REQUEST_POOL.remove(request_id)
|
||||
|
||||
r = requests.request(
|
||||
method=request.method,
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
import requests
|
||||
import json
|
||||
from pydantic import BaseModel
|
||||
|
||||
from apps.web.models.users import Users
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import decode_token, get_current_user
|
||||
from config import OLLAMA_API_BASE_URL, WEBUI_AUTH
|
||||
|
||||
import aiohttp
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL
|
||||
|
||||
# TARGET_SERVER_URL = OLLAMA_API_BASE_URL
|
||||
|
||||
|
||||
@app.get("/url")
|
||||
async def get_ollama_api_url(user=Depends(get_current_user)):
|
||||
if user and user.role == "admin":
|
||||
return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL}
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
||||
|
||||
|
||||
class UrlUpdateForm(BaseModel):
|
||||
url: str
|
||||
|
||||
|
||||
@app.post("/url/update")
|
||||
async def update_ollama_api_url(
|
||||
form_data: UrlUpdateForm, user=Depends(get_current_user)
|
||||
):
|
||||
if user and user.role == "admin":
|
||||
app.state.OLLAMA_API_BASE_URL = form_data.url
|
||||
return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL}
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
||||
|
||||
|
||||
# async def fetch_sse(method, target_url, body, headers):
|
||||
# async with aiohttp.ClientSession() as session:
|
||||
# try:
|
||||
# async with session.request(
|
||||
# method, target_url, data=body, headers=headers
|
||||
# ) as response:
|
||||
# print(response.status)
|
||||
# async for line in response.content:
|
||||
# yield line
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
# error_detail = "Open WebUI: Server Connection Error"
|
||||
# yield json.dumps({"error": error_detail, "message": str(e)}).encode()
|
||||
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def proxy(path: str, request: Request, user=Depends(get_current_user)):
|
||||
target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}"
|
||||
print(target_url)
|
||||
|
||||
body = await request.body()
|
||||
headers = dict(request.headers)
|
||||
|
||||
if user.role in ["user", "admin"]:
|
||||
if path in ["pull", "delete", "push", "copy", "create"]:
|
||||
if user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
||||
|
||||
headers.pop("Host", None)
|
||||
headers.pop("Authorization", None)
|
||||
headers.pop("Origin", None)
|
||||
headers.pop("Referer", None)
|
||||
|
||||
session = aiohttp.ClientSession()
|
||||
response = None
|
||||
try:
|
||||
response = await session.request(
|
||||
request.method, target_url, data=body, headers=headers
|
||||
)
|
||||
|
||||
print(response)
|
||||
if not response.ok:
|
||||
data = await response.json()
|
||||
print(data)
|
||||
response.raise_for_status()
|
||||
|
||||
async def generate():
|
||||
async for line in response.content:
|
||||
print(line)
|
||||
yield line
|
||||
await session.close()
|
||||
|
||||
return StreamingResponse(generate(), response.status)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
|
||||
if response is not None:
|
||||
try:
|
||||
res = await response.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
await session.close()
|
||||
raise HTTPException(
|
||||
status_code=response.status if response else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
@@ -146,6 +146,13 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
body["max_tokens"] = 4000
|
||||
print("Modified body_dict:", body)
|
||||
|
||||
# Fix for ChatGPT calls failing because the num_ctx key is in body
|
||||
if "num_ctx" in body:
|
||||
# If 'num_ctx' is in the dictionary, delete it
|
||||
# Leaving it there generates an error with the
|
||||
# OpenAI API (Feb 2024)
|
||||
del body["num_ctx"]
|
||||
|
||||
# Convert the modified body back to JSON
|
||||
body = json.dumps(body)
|
||||
except json.JSONDecodeError as e:
|
||||
@@ -184,7 +191,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
|
||||
response_data = r.json()
|
||||
|
||||
if "openai" in app.state.OPENAI_API_BASE_URL and path == "models":
|
||||
if "api.openai.com" in app.state.OPENAI_API_BASE_URL and path == "models":
|
||||
response_data["data"] = list(
|
||||
filter(lambda model: "gpt" in model["id"], response_data["data"])
|
||||
)
|
||||
|
||||
+43
-17
@@ -79,6 +79,8 @@ app.state.CHUNK_SIZE = CHUNK_SIZE
|
||||
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||
app.state.RAG_TEMPLATE = RAG_TEMPLATE
|
||||
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||
app.state.TOP_K = 4
|
||||
|
||||
app.state.sentence_transformer_ef = (
|
||||
embedding_functions.SentenceTransformerEmbeddingFunction(
|
||||
model_name=app.state.RAG_EMBEDDING_MODEL,
|
||||
@@ -106,7 +108,7 @@ class StoreWebForm(CollectionNameForm):
|
||||
url: str
|
||||
|
||||
|
||||
def store_data_in_vector_db(data, collection_name) -> bool:
|
||||
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=app.state.CHUNK_SIZE, chunk_overlap=app.state.CHUNK_OVERLAP
|
||||
)
|
||||
@@ -116,6 +118,12 @@ def store_data_in_vector_db(data, collection_name) -> bool:
|
||||
metadatas = [doc.metadata for doc in docs]
|
||||
|
||||
try:
|
||||
if overwrite:
|
||||
for collection in CHROMA_CLIENT.list_collections():
|
||||
if collection_name == collection.name:
|
||||
print(f"deleting existing collection {collection_name}")
|
||||
CHROMA_CLIENT.delete_collection(name=collection_name)
|
||||
|
||||
collection = CHROMA_CLIENT.create_collection(
|
||||
name=collection_name,
|
||||
embedding_function=app.state.sentence_transformer_ef,
|
||||
@@ -210,23 +218,33 @@ async def get_rag_template(user=Depends(get_current_user)):
|
||||
}
|
||||
|
||||
|
||||
class RAGTemplateForm(BaseModel):
|
||||
template: str
|
||||
@app.get("/query/settings")
|
||||
async def get_query_settings(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"status": True,
|
||||
"template": app.state.RAG_TEMPLATE,
|
||||
"k": app.state.TOP_K,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/template/update")
|
||||
async def update_rag_template(form_data: RAGTemplateForm, user=Depends(get_admin_user)):
|
||||
# TODO: check template requirements
|
||||
app.state.RAG_TEMPLATE = (
|
||||
form_data.template if form_data.template != "" else RAG_TEMPLATE
|
||||
)
|
||||
class QuerySettingsForm(BaseModel):
|
||||
k: Optional[int] = None
|
||||
template: Optional[str] = None
|
||||
|
||||
|
||||
@app.post("/query/settings/update")
|
||||
async def update_query_settings(
|
||||
form_data: QuerySettingsForm, user=Depends(get_admin_user)
|
||||
):
|
||||
app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE
|
||||
app.state.TOP_K = form_data.k if form_data.k else 4
|
||||
return {"status": True, "template": app.state.RAG_TEMPLATE}
|
||||
|
||||
|
||||
class QueryDocForm(BaseModel):
|
||||
collection_name: str
|
||||
query: str
|
||||
k: Optional[int] = 4
|
||||
k: Optional[int] = None
|
||||
|
||||
|
||||
@app.post("/query/doc")
|
||||
@@ -240,7 +258,10 @@ def query_doc(
|
||||
name=form_data.collection_name,
|
||||
embedding_function=app.state.sentence_transformer_ef,
|
||||
)
|
||||
result = collection.query(query_texts=[form_data.query], n_results=form_data.k)
|
||||
result = collection.query(
|
||||
query_texts=[form_data.query],
|
||||
n_results=form_data.k if form_data.k else app.state.TOP_K,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -253,7 +274,7 @@ def query_doc(
|
||||
class QueryCollectionsForm(BaseModel):
|
||||
collection_names: List[str]
|
||||
query: str
|
||||
k: Optional[int] = 4
|
||||
k: Optional[int] = None
|
||||
|
||||
|
||||
def merge_and_sort_query_results(query_results, k):
|
||||
@@ -317,13 +338,16 @@ def query_collection(
|
||||
)
|
||||
|
||||
result = collection.query(
|
||||
query_texts=[form_data.query], n_results=form_data.k
|
||||
query_texts=[form_data.query],
|
||||
n_results=form_data.k if form_data.k else app.state.TOP_K,
|
||||
)
|
||||
results.append(result)
|
||||
except:
|
||||
pass
|
||||
|
||||
return merge_and_sort_query_results(results, form_data.k)
|
||||
return merge_and_sort_query_results(
|
||||
results, form_data.k if form_data.k else app.state.TOP_K
|
||||
)
|
||||
|
||||
|
||||
@app.post("/web")
|
||||
@@ -337,7 +361,7 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
|
||||
if collection_name == "":
|
||||
collection_name = calculate_sha256_string(form_data.url)[:63]
|
||||
|
||||
store_data_in_vector_db(data, collection_name)
|
||||
store_data_in_vector_db(data, collection_name, overwrite=True)
|
||||
return {
|
||||
"status": True,
|
||||
"collection_name": collection_name,
|
||||
@@ -401,7 +425,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||
]
|
||||
|
||||
if file_ext == "pdf":
|
||||
loader = PyPDFLoader(file_path)
|
||||
loader = PyPDFLoader(file_path, extract_images=True)
|
||||
elif file_ext == "csv":
|
||||
loader = CSVLoader(file_path)
|
||||
elif file_ext == "rst":
|
||||
@@ -423,7 +447,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
] or file_ext in ["xls", "xlsx"]:
|
||||
loader = UnstructuredExcelLoader(file_path)
|
||||
elif file_ext in known_source_ext or (file_content_type and file_content_type.find("text/") >= 0):
|
||||
elif file_ext in known_source_ext or (
|
||||
file_content_type and file_content_type.find("text/") >= 0
|
||||
):
|
||||
loader = TextLoader(file_path)
|
||||
else:
|
||||
loader = TextLoader(file_path)
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
from peewee import *
|
||||
from config import DATA_DIR
|
||||
import os
|
||||
|
||||
|
||||
DB = SqliteDatabase(f"{DATA_DIR}/ollama.db")
|
||||
# Check if the file exists
|
||||
if os.path.exists(f"{DATA_DIR}/ollama.db"):
|
||||
# Rename the file
|
||||
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
|
||||
print("File renamed successfully.")
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
|
||||
DB.connect()
|
||||
|
||||
@@ -271,6 +271,16 @@ async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)):
|
||||
|
||||
|
||||
@router.delete("/", response_model=bool)
|
||||
async def delete_all_user_chats(user=Depends(get_current_user)):
|
||||
async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
|
||||
|
||||
if (
|
||||
user.role == "user"
|
||||
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
result = Chats.delete_chats_by_user_id(user.id)
|
||||
return result
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter, UploadFile, File, BackgroundTasks
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from starlette.responses import StreamingResponse
|
||||
from starlette.responses import StreamingResponse, FileResponse
|
||||
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -9,9 +10,11 @@ import os
|
||||
import aiohttp
|
||||
import json
|
||||
|
||||
|
||||
from utils.utils import get_admin_user
|
||||
from utils.misc import calculate_sha256, get_gravatar_url
|
||||
|
||||
from config import OLLAMA_API_BASE_URL, DATA_DIR, UPLOAD_DIR
|
||||
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
|
||||
@@ -72,7 +75,7 @@ async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024
|
||||
hashed = calculate_sha256(file)
|
||||
file.seek(0)
|
||||
|
||||
url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}"
|
||||
url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}"
|
||||
response = requests.post(url, data=file)
|
||||
|
||||
if response.ok:
|
||||
@@ -144,7 +147,7 @@ def upload(file: UploadFile = File(...)):
|
||||
hashed = calculate_sha256(f)
|
||||
f.seek(0)
|
||||
|
||||
url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}"
|
||||
url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}"
|
||||
response = requests.post(url, data=f)
|
||||
|
||||
if response.ok:
|
||||
@@ -172,3 +175,13 @@ async def get_gravatar(
|
||||
email: str,
|
||||
):
|
||||
return get_gravatar_url(email)
|
||||
|
||||
|
||||
@router.get("/db/download")
|
||||
async def download_db(user=Depends(get_admin_user)):
|
||||
|
||||
return FileResponse(
|
||||
f"{DATA_DIR}/webui.db",
|
||||
media_type="application/octet-stream",
|
||||
filename="webui.db",
|
||||
)
|
||||
|
||||
+20
-4
@@ -200,16 +200,32 @@ if not os.path.exists(LITELLM_CONFIG_PATH):
|
||||
|
||||
|
||||
####################################
|
||||
# OLLAMA_API_BASE_URL
|
||||
# OLLAMA_BASE_URL
|
||||
####################################
|
||||
|
||||
OLLAMA_API_BASE_URL = os.environ.get(
|
||||
"OLLAMA_API_BASE_URL", "http://localhost:11434/api"
|
||||
)
|
||||
|
||||
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
||||
|
||||
if ENV == "prod":
|
||||
if OLLAMA_API_BASE_URL == "/ollama/api":
|
||||
OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api"
|
||||
if OLLAMA_BASE_URL == "/ollama":
|
||||
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
|
||||
|
||||
|
||||
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
|
||||
OLLAMA_BASE_URL = (
|
||||
OLLAMA_API_BASE_URL[:-4]
|
||||
if OLLAMA_API_BASE_URL.endswith("/api")
|
||||
else OLLAMA_API_BASE_URL
|
||||
)
|
||||
|
||||
OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
|
||||
|
||||
OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(",")]
|
||||
|
||||
|
||||
####################################
|
||||
# OPENAI_API
|
||||
@@ -226,7 +242,7 @@ if OPENAI_API_BASE_URL == "":
|
||||
# WEBUI
|
||||
####################################
|
||||
|
||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", True)
|
||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
|
||||
DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
|
||||
|
||||
|
||||
|
||||
@@ -48,3 +48,5 @@ class ERROR_MESSAGES(str, Enum):
|
||||
lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
|
||||
)
|
||||
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
|
||||
|
||||
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
|
||||
|
||||
+9
-1
@@ -104,7 +104,7 @@ async def auth_middleware(request: Request, call_next):
|
||||
app.mount("/api/v1", webui_app)
|
||||
app.mount("/litellm/api", litellm_app)
|
||||
|
||||
app.mount("/ollama/api", ollama_app)
|
||||
app.mount("/ollama", ollama_app)
|
||||
app.mount("/openai/api", openai_app)
|
||||
|
||||
app.mount("/images/api/v1", images_app)
|
||||
@@ -125,6 +125,14 @@ async def get_app_config():
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/version")
|
||||
async def get_app_config():
|
||||
|
||||
return {
|
||||
"version": VERSION,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/changelog")
|
||||
async def get_app_changelog():
|
||||
return CHANGELOG
|
||||
|
||||
@@ -22,6 +22,7 @@ google-generativeai
|
||||
|
||||
langchain
|
||||
langchain-community
|
||||
fake_useragent
|
||||
chromadb
|
||||
sentence_transformers
|
||||
pypdf
|
||||
@@ -33,6 +34,7 @@ pandas
|
||||
openpyxl
|
||||
pyxlsb
|
||||
xlrd
|
||||
rapidocr-onnxruntime
|
||||
|
||||
faster-whisper
|
||||
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
echo "Warning: This will remove all containers and volumes, including persistent data. Do you want to continue? [Y/N]"
|
||||
read ans
|
||||
if [ "$ans" == "Y" ] || [ "$ans" == "y" ]; then
|
||||
docker-compose down -v
|
||||
else
|
||||
echo "Operation cancelled."
|
||||
fi
|
||||
+2
-2
@@ -14,7 +14,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
OLLAMA_API_BASE_URL: '/ollama/api'
|
||||
OLLAMA_BASE_URL: '/ollama'
|
||||
dockerfile: Dockerfile
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
container_name: open-webui
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
ports:
|
||||
- ${OPEN_WEBUI_PORT-3000}:8080
|
||||
environment:
|
||||
- 'OLLAMA_API_BASE_URL=http://ollama:11434/api'
|
||||
- 'OLLAMA_BASE_URL=http://ollama:11434'
|
||||
- 'WEBUI_SECRET_KEY='
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
|
||||
@@ -40,7 +40,7 @@ spec:
|
||||
- name: data
|
||||
mountPath: /app/backend/data
|
||||
env:
|
||||
- name: OLLAMA_API_BASE_URL
|
||||
- name: OLLAMA_BASE_URL
|
||||
value: {{ include "ollama.url" . | quote }}
|
||||
tty: true
|
||||
{{- with .Values.webui.nodeSelector }}
|
||||
|
||||
@@ -26,8 +26,8 @@ spec:
|
||||
cpu: "1000m"
|
||||
memory: "1Gi"
|
||||
env:
|
||||
- name: OLLAMA_API_BASE_URL
|
||||
value: "http://ollama-service.open-webui.svc.cluster.local:11434/api"
|
||||
- name: OLLAMA_BASE_URL
|
||||
value: "http://ollama-service.open-webui.svc.cluster.local:11434"
|
||||
tty: true
|
||||
volumeMounts:
|
||||
- name: webui-volume
|
||||
|
||||
Generated
+12
-34
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "v1.0.0-alpha.101",
|
||||
"version": "0.1.106",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-webui",
|
||||
"version": "v1.0.0-alpha.101",
|
||||
"version": "0.1.106",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"async": "^3.2.5",
|
||||
@@ -17,7 +17,7 @@
|
||||
"js-sha256": "^0.10.1",
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.0",
|
||||
"svelte-french-toast": "^1.2.0",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"tippy.js": "^6.3.7",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
@@ -3211,17 +3211,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-french-toast": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-french-toast/-/svelte-french-toast-1.2.0.tgz",
|
||||
"integrity": "sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==",
|
||||
"dependencies": {
|
||||
"svelte-writable-derived": "^3.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.57.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-hmr": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||
@@ -3307,15 +3296,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-writable-derived": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",
|
||||
"integrity": "sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==",
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/pixievoltno1"
|
||||
},
|
||||
"node_modules/svelte-sonner": {
|
||||
"version": "0.3.19",
|
||||
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz",
|
||||
"integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==",
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.2.1 || ^4.0.0-next.1"
|
||||
"svelte": ">=3 <5"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
@@ -5882,14 +5868,6 @@
|
||||
"postcss-scss": "^4.0.8"
|
||||
}
|
||||
},
|
||||
"svelte-french-toast": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-french-toast/-/svelte-french-toast-1.2.0.tgz",
|
||||
"integrity": "sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==",
|
||||
"requires": {
|
||||
"svelte-writable-derived": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"svelte-hmr": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||
@@ -5920,10 +5898,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"svelte-writable-derived": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",
|
||||
"integrity": "sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==",
|
||||
"svelte-sonner": {
|
||||
"version": "0.3.19",
|
||||
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz",
|
||||
"integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"tailwindcss": {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.1.105",
|
||||
"version": "0.1.109",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
@@ -49,7 +49,7 @@
|
||||
"js-sha256": "^0.10.1",
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.0",
|
||||
"svelte-french-toast": "^1.2.0",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"tippy.js": "^6.3.7",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
|
||||
+1
-1
@@ -182,7 +182,7 @@ else
|
||||
export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable
|
||||
fi
|
||||
if [[ -n $webui_port ]]; then
|
||||
export OLLAMA_WEBUI_PORT=$webui_port # Set OLLAMA_WEBUI_PORT environment variable
|
||||
export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable
|
||||
fi
|
||||
DEFAULT_COMPOSE_COMMAND+=" up -d"
|
||||
DEFAULT_COMPOSE_COMMAND+=" --remove-orphans"
|
||||
|
||||
+15
@@ -28,6 +28,21 @@ math {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
ol > li {
|
||||
counter-increment: list-number;
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.prose ol > li::before {
|
||||
content: counters(list-number, '.') '.';
|
||||
padding-right: 0.5rem;
|
||||
color: var(--tw-prose-counters);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
--tw-border-opacity: 1;
|
||||
background-color: rgba(217, 217, 227, 0.8);
|
||||
|
||||
@@ -439,7 +439,7 @@ export const deleteAllChats = async (token: string) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
error = err.detail;
|
||||
|
||||
console.log(err);
|
||||
return null;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getOllamaAPIUrl = async (token: string = '') => {
|
||||
export const getOllamaUrls = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/url`, {
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/urls`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -29,13 +29,13 @@ export const getOllamaAPIUrl = async (token: string = '') => {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.OLLAMA_API_BASE_URL;
|
||||
return res.OLLAMA_BASE_URLS;
|
||||
};
|
||||
|
||||
export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
|
||||
export const updateOllamaUrls = async (token: string = '', urls: string[]) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/url/update`, {
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/urls/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -43,7 +43,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url
|
||||
urls: urls
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
@@ -64,13 +64,13 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.OLLAMA_API_BASE_URL;
|
||||
return res.OLLAMA_BASE_URLS;
|
||||
};
|
||||
|
||||
export const getOllamaVersion = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/version`, {
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/version`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -102,7 +102,7 @@ export const getOllamaVersion = async (token: string = '') => {
|
||||
export const getOllamaModels = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/tags`, {
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -148,10 +148,11 @@ export const generateTitle = async (
|
||||
|
||||
console.log(template);
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -186,10 +187,11 @@ export const generatePrompt = async (token: string = '', model: string, conversa
|
||||
conversation = '[no existing conversation]';
|
||||
}
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -217,15 +219,43 @@ export const generatePrompt = async (token: string = '', model: string, conversa
|
||||
return res;
|
||||
};
|
||||
|
||||
export const generateTextCompletion = async (token: string = '', model: string, text: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
prompt: text,
|
||||
stream: true
|
||||
})
|
||||
}).catch((err) => {
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const generateChatCompletion = async (token: string = '', body: object) => {
|
||||
let controller = new AbortController();
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/chat`, {
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/chat`, {
|
||||
signal: controller.signal,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
@@ -265,10 +295,11 @@ export const cancelChatCompletion = async (token: string = '', requestId: string
|
||||
export const createModel = async (token: string, tagName: string, content: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/create`, {
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -287,19 +318,23 @@ export const createModel = async (token: string, tagName: string, content: strin
|
||||
return res;
|
||||
};
|
||||
|
||||
export const deleteModel = async (token: string, tagName: string) => {
|
||||
export const deleteModel = async (token: string, tagName: string, urlIdx: string | null = null) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/delete`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName
|
||||
})
|
||||
})
|
||||
const res = await fetch(
|
||||
`${OLLAMA_API_BASE_URL}/api/delete${urlIdx !== null ? `/${urlIdx}` : ''}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName
|
||||
})
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
@@ -310,7 +345,12 @@ export const deleteModel = async (token: string, tagName: string) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.error;
|
||||
error = err;
|
||||
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -321,13 +361,14 @@ export const deleteModel = async (token: string, tagName: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const pullModel = async (token: string, tagName: string) => {
|
||||
export const pullModel = async (token: string, tagName: string, urlIdx: string | null = null) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull${urlIdx !== null ? `/${urlIdx}` : ''}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -85,17 +85,49 @@ export const getRAGTemplate = async (token: string) => {
|
||||
return res?.template ?? '';
|
||||
};
|
||||
|
||||
export const updateRAGTemplate = async (token: string, template: string) => {
|
||||
export const getQuerySettings = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/template/update`, {
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/query/settings`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
type QuerySettings = {
|
||||
k: number | null;
|
||||
template: string | null;
|
||||
};
|
||||
|
||||
export const updateQuerySettings = async (token: string, settings: QuerySettings) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/query/settings/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template: template
|
||||
...settings
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
@@ -183,7 +215,7 @@ export const queryDoc = async (
|
||||
token: string,
|
||||
collection_name: string,
|
||||
query: string,
|
||||
k: number
|
||||
k: number | null = null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -21,3 +21,35 @@ export const getGravatarUrl = async (email: string) => {
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const downloadDatabase = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/db/download`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then((blob) => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'webui.db';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs from 'dayjs';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { downloadDatabase } from '$lib/apis/utils';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
onMount(async () => {
|
||||
// permissions = await getUserPermissions(localStorage.token);
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">Database</div>
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<!-- <div class=" self-center text-xs font-medium">Allow Chat Deletion</div> -->
|
||||
|
||||
<button
|
||||
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
// exportAllUserChats();
|
||||
|
||||
downloadDatabase(localStorage.token);
|
||||
}}
|
||||
>
|
||||
<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 d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">Download Database</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div> -->
|
||||
</form>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Database from './Settings/Database.svelte';
|
||||
|
||||
import General from './Settings/General.svelte';
|
||||
import Users from './Settings/Users.svelte';
|
||||
@@ -86,6 +87,34 @@
|
||||
</div>
|
||||
<div class=" self-center">Users</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 ===
|
||||
'db'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'db';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M8 7c3.314 0 6-1.343 6-3s-2.686-3-6-3-6 1.343-6 3 2.686 3 6 3Z" />
|
||||
<path
|
||||
d="M8 8.5c1.84 0 3.579-.37 4.914-1.037A6.33 6.33 0 0 0 14 6.78V8c0 1.657-2.686 3-6 3S2 9.657 2 8V6.78c.346.273.72.5 1.087.683C4.42 8.131 6.16 8.5 8 8.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M8 12.5c1.84 0 3.579-.37 4.914-1.037.366-.183.74-.41 1.086-.684V12c0 1.657-2.686 3-6 3s-6-1.343-6-3v-1.22c.346.273.72.5 1.087.683C4.42 12.131 6.16 12.5 8 12.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Database</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 md:min-h-[380px]">
|
||||
{#if selectedTab === 'general'}
|
||||
@@ -100,6 +129,12 @@
|
||||
show = false;
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'db'}
|
||||
<Database
|
||||
saveHandler={() => {
|
||||
show = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { settings } from '$lib/stores';
|
||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||
@@ -12,13 +12,14 @@
|
||||
import Documents from './MessageInput/Documents.svelte';
|
||||
import Models from './MessageInput/Models.svelte';
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
|
||||
export let submitPrompt: Function;
|
||||
export let stopResponse: Function;
|
||||
|
||||
export let suggestionPrompts = [];
|
||||
export let autoScroll = true;
|
||||
|
||||
let chatTextAreaElement:HTMLTextAreaElement
|
||||
let filesInputElement;
|
||||
|
||||
let promptsElement;
|
||||
@@ -42,11 +43,9 @@
|
||||
let speechRecognition;
|
||||
|
||||
$: if (prompt) {
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
|
||||
if (chatInput) {
|
||||
chatInput.style.height = '';
|
||||
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
|
||||
if (chatTextAreaElement) {
|
||||
chatTextAreaElement.style.height = '';
|
||||
chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +84,7 @@
|
||||
if (res) {
|
||||
prompt = res.text;
|
||||
await tick();
|
||||
|
||||
const inputElement = document.getElementById('chat-textarea');
|
||||
inputElement?.focus();
|
||||
chatTextAreaElement?.focus();
|
||||
|
||||
if (prompt !== '' && $settings?.speechAutoSend === true) {
|
||||
submitPrompt(prompt, user);
|
||||
@@ -190,8 +187,7 @@
|
||||
prompt = `${prompt}${transcript}`;
|
||||
|
||||
await tick();
|
||||
const inputElement = document.getElementById('chat-textarea');
|
||||
inputElement?.focus();
|
||||
chatTextAreaElement?.focus();
|
||||
|
||||
// Restart the inactivity timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
@@ -293,6 +289,8 @@
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
|
||||
|
||||
const dropZone = document.querySelector('body');
|
||||
|
||||
const onDragOver = (e) => {
|
||||
@@ -634,29 +632,32 @@
|
||||
<div class=" flex">
|
||||
{#if fileUploadEnabled}
|
||||
<div class=" self-end mb-2 ml-1">
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-[1.2rem] h-[1.2rem]"
|
||||
<Tooltip content="Upload files">
|
||||
<button
|
||||
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-[1.2rem] h-[1.2rem]"
|
||||
>
|
||||
<path
|
||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<textarea
|
||||
id="chat-textarea"
|
||||
bind:this={chatTextAreaElement}
|
||||
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
|
||||
? ''
|
||||
: ' pl-4'} rounded-xl resize-none h-[48px]"
|
||||
@@ -803,87 +804,97 @@
|
||||
|
||||
<div class="self-end mb-2 flex space-x-1 mr-1">
|
||||
{#if messages.length == 0 || messages.at(-1).done == true}
|
||||
{#if speechRecognitionEnabled}
|
||||
<Tooltip content="Record voice">
|
||||
{#if speechRecognitionEnabled}
|
||||
<button
|
||||
id="voice-input-button"
|
||||
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
speechRecognitionHandler();
|
||||
}}
|
||||
>
|
||||
{#if isRecording}
|
||||
<svg
|
||||
class=" w-5 h-5 translate-y-[0.5px]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="2.5"
|
||||
/><circle
|
||||
class="spinner_qM83 spinner_ZTLf"
|
||||
cx="20"
|
||||
cy="12"
|
||||
r="2.5"
|
||||
/></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5 translate-y-[0.5px]"
|
||||
>
|
||||
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
|
||||
<path
|
||||
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Send message">
|
||||
<button
|
||||
id="voice-input-button"
|
||||
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
speechRecognitionHandler();
|
||||
}}
|
||||
class="{prompt !== ''
|
||||
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
||||
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
|
||||
type="submit"
|
||||
disabled={prompt === ''}
|
||||
>
|
||||
{#if isRecording}
|
||||
<svg
|
||||
class=" w-5 h-5 translate-y-[0.5px]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="2.5"
|
||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5 translate-y-[0.5px]"
|
||||
>
|
||||
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
|
||||
<path
|
||||
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="{prompt !== ''
|
||||
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
||||
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
|
||||
type="submit"
|
||||
disabled={prompt === ''}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { documents } from '$lib/stores';
|
||||
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
|
||||
import { tick } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let prompt = '';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { models } from '$lib/stores';
|
||||
import { splitStream } from '$lib/utils';
|
||||
import { tick } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let prompt = '';
|
||||
export let user = null;
|
||||
@@ -79,14 +79,18 @@
|
||||
throw data;
|
||||
}
|
||||
|
||||
if (data.done == false) {
|
||||
if (prompt == '' && data.response == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
prompt += data.response;
|
||||
console.log(data.response);
|
||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||
await tick();
|
||||
if ('id' in data) {
|
||||
console.log(data);
|
||||
} else {
|
||||
if (data.done == false) {
|
||||
if (prompt == '' && data.response == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
prompt += data.response;
|
||||
console.log(data.response);
|
||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { prompts } from '$lib/stores';
|
||||
import { findWordIndices } from '$lib/utils';
|
||||
import { tick } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let prompt = '';
|
||||
let selectedCommandIdx = 0;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { chats, config, modelfiles, settings, user } from '$lib/stores';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getChatList, updateChatById } from '$lib/apis/chats';
|
||||
|
||||
import UserMessage from './Messages/UserMessage.svelte';
|
||||
@@ -223,33 +223,80 @@
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// TODO: change delete behaviour
|
||||
// const deleteMessageAndDescendants = async (messageId: string) => {
|
||||
// if (history.messages[messageId]) {
|
||||
// history.messages[messageId].deleted = true;
|
||||
|
||||
// for (const childId of history.messages[messageId].childrenIds) {
|
||||
// await deleteMessageAndDescendants(childId);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// const triggerDeleteMessageRecursive = async (messageId: string) => {
|
||||
// await deleteMessageAndDescendants(messageId);
|
||||
// await updateChatById(localStorage.token, chatId, { history });
|
||||
// await chats.set(await getChatList(localStorage.token));
|
||||
// };
|
||||
|
||||
const messageDeleteHandler = async (messageId) => {
|
||||
if (history.messages[messageId]) {
|
||||
history.messages[messageId].deleted = true;
|
||||
|
||||
for (const childId of history.messages[messageId].childrenIds) {
|
||||
history.messages[childId].deleted = true;
|
||||
const messageToDelete = history.messages[messageId];
|
||||
const messageParentId = messageToDelete.parentId;
|
||||
const messageChildrenIds = messageToDelete.childrenIds ?? [];
|
||||
const hasSibling = messageChildrenIds.some(
|
||||
(childId) => history.messages[childId]?.childrenIds?.length > 0
|
||||
);
|
||||
messageChildrenIds.forEach((childId) => {
|
||||
const child = history.messages[childId];
|
||||
if (child && child.childrenIds) {
|
||||
if (child.childrenIds.length === 0 && !hasSibling) {
|
||||
// if last prompt/response pair
|
||||
history.messages[messageParentId].childrenIds = [];
|
||||
history.currentId = messageParentId;
|
||||
} else {
|
||||
child.childrenIds.forEach((grandChildId) => {
|
||||
if (history.messages[grandChildId]) {
|
||||
history.messages[grandChildId].parentId = messageParentId;
|
||||
history.messages[messageParentId].childrenIds.push(grandChildId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
await updateChatById(localStorage.token, chatId, { history });
|
||||
// remove response
|
||||
history.messages[messageParentId].childrenIds = history.messages[
|
||||
messageParentId
|
||||
].childrenIds.filter((id) => id !== childId);
|
||||
});
|
||||
// remove prompt
|
||||
history.messages[messageParentId].childrenIds = history.messages[
|
||||
messageParentId
|
||||
].childrenIds.filter((id) => id !== messageId);
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
};
|
||||
|
||||
// const messageDeleteHandler = async (messageId) => {
|
||||
// const message = history.messages[messageId];
|
||||
// const parentId = message.parentId;
|
||||
// const childrenIds = message.childrenIds ?? [];
|
||||
// const grandchildrenIds = [];
|
||||
|
||||
// // Iterate through childrenIds to find grandchildrenIds
|
||||
// for (const childId of childrenIds) {
|
||||
// const childMessage = history.messages[childId];
|
||||
// const grandChildrenIds = childMessage.childrenIds ?? [];
|
||||
|
||||
// for (const grandchildId of grandchildrenIds) {
|
||||
// const childMessage = history.messages[grandchildId];
|
||||
// childMessage.parentId = parentId;
|
||||
// }
|
||||
// grandchildrenIds.push(...grandChildrenIds);
|
||||
// }
|
||||
|
||||
// history.messages[parentId].childrenIds.push(...grandchildrenIds);
|
||||
// history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
|
||||
// (id) => id !== messageId
|
||||
// );
|
||||
|
||||
// // Select latest message
|
||||
// let currentMessageId = grandchildrenIds.at(-1);
|
||||
// if (currentMessageId) {
|
||||
// let messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||
// while (messageChildrenIds.length !== 0) {
|
||||
// currentMessageId = messageChildrenIds.at(-1);
|
||||
// messageChildrenIds = history.messages[currentMessageId].childrenIds;
|
||||
// }
|
||||
// history.currentId = currentMessageId;
|
||||
// }
|
||||
|
||||
// await updateChatById(localStorage.token, chatId, { messages, history });
|
||||
// };
|
||||
</script>
|
||||
|
||||
{#if messages.length == 0}
|
||||
@@ -258,57 +305,55 @@
|
||||
<div class=" pb-10">
|
||||
{#key chatId}
|
||||
{#each messages as message, messageIdx}
|
||||
{#if !message.deleted}
|
||||
<div class=" w-full">
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
||||
? 'max-w-full'
|
||||
: 'max-w-3xl'} mx-auto rounded-lg group"
|
||||
>
|
||||
{#if message.role === 'user'}
|
||||
<UserMessage
|
||||
on:delete={() => messageDeleteHandler(message.id)}
|
||||
user={$user}
|
||||
{message}
|
||||
isFirstMessage={messageIdx === 0}
|
||||
siblings={message.parentId !== null
|
||||
? history.messages[message.parentId]?.childrenIds ?? []
|
||||
: Object.values(history.messages)
|
||||
.filter((message) => message.parentId === null)
|
||||
.map((message) => message.id) ?? []}
|
||||
{confirmEditMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{copyToClipboard}
|
||||
/>
|
||||
{:else}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
modelfiles={selectedModelfiles}
|
||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{confirmEditResponseMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
<div class=" w-full">
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
||||
? 'max-w-full'
|
||||
: 'max-w-3xl'} mx-auto rounded-lg group"
|
||||
>
|
||||
{#if message.role === 'user'}
|
||||
<UserMessage
|
||||
on:delete={() => messageDeleteHandler(message.id)}
|
||||
user={$user}
|
||||
{message}
|
||||
isFirstMessage={messageIdx === 0}
|
||||
siblings={message.parentId !== null
|
||||
? history.messages[message.parentId]?.childrenIds ?? []
|
||||
: Object.values(history.messages)
|
||||
.filter((message) => message.parentId === null)
|
||||
.map((message) => message.id) ?? []}
|
||||
{confirmEditMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{copyToClipboard}
|
||||
/>
|
||||
{:else}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
modelfiles={selectedModelfiles}
|
||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{confirmEditResponseMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if bottomPadding}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs from 'dayjs';
|
||||
import { marked } from 'marked';
|
||||
import tippy from 'tippy.js';
|
||||
@@ -22,6 +22,7 @@
|
||||
import CodeBlock from './CodeBlock.svelte';
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
export let modelfiles = [];
|
||||
export let message;
|
||||
@@ -40,7 +41,7 @@
|
||||
|
||||
let edit = false;
|
||||
let editedContent = '';
|
||||
|
||||
let editTextAreaElement: HTMLTextAreaElement;
|
||||
let tooltipInstance = null;
|
||||
|
||||
let sentencesAudio = {};
|
||||
@@ -247,10 +248,9 @@
|
||||
editedContent = message.content;
|
||||
|
||||
await tick();
|
||||
const editElement = document.getElementById(`message-edit-${message.id}`);
|
||||
|
||||
editElement.style.height = '';
|
||||
editElement.style.height = `${editElement.scrollHeight}px`;
|
||||
editTextAreaElement.style.height = '';
|
||||
editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
|
||||
};
|
||||
|
||||
const editMessageConfirmHandler = async () => {
|
||||
@@ -334,16 +334,18 @@
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
|
||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
|
||||
>
|
||||
<div>
|
||||
{#if edit === true}
|
||||
<div class=" w-full">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={editTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
@@ -462,189 +464,125 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
>
|
||||
<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="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, 1);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, -1);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="speak-button-{message.id}"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
if (!loadingSpeech) {
|
||||
toggleSpeakMessage(message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if loadingSpeech}
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_S1WN {
|
||||
animation: spinner_MGfb 0.8s linear infinite;
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
.spinner_Km9P {
|
||||
animation-delay: -0.65s;
|
||||
}
|
||||
.spinner_JApP {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes spinner_MGfb {
|
||||
93.75%,
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
|
||||
class="spinner_S1WN spinner_Km9P"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="3"
|
||||
/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
|
||||
>
|
||||
{:else if speaking}
|
||||
<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="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if $config.images}
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
if (!generatingImage) {
|
||||
generateImage(message);
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Copy" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
>
|
||||
<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="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Good Response" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, 1);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Bad Response" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''} dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
rateMessage(message.id, -1);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Read Aloud" placement="bottom">
|
||||
<button
|
||||
id="speak-button-{message.id}"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
if (!loadingSpeech) {
|
||||
toggleSpeakMessage(message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if generatingImage}
|
||||
{#if loadingSpeech}
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
fill="currentColor"
|
||||
@@ -679,6 +617,21 @@
|
||||
r="3"
|
||||
/></svg
|
||||
>
|
||||
{:else if speaking}
|
||||
<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="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -691,93 +644,166 @@
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
|
||||
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if $config.images}
|
||||
<Tooltip content="Generate Image" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
if (!generatingImage) {
|
||||
generateImage(message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if generatingImage}
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_S1WN {
|
||||
animation: spinner_MGfb 0.8s linear infinite;
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
.spinner_Km9P {
|
||||
animation-delay: -0.65s;
|
||||
}
|
||||
.spinner_JApP {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes spinner_MGfb {
|
||||
93.75%,
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
|
||||
class="spinner_S1WN spinner_Km9P"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="3"
|
||||
/><circle
|
||||
class="spinner_S1WN spinner_JApP"
|
||||
cx="20"
|
||||
cy="12"
|
||||
r="3"
|
||||
/></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if message.info}
|
||||
<button
|
||||
class=" {isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
||||
on:click={() => {
|
||||
console.log(message);
|
||||
}}
|
||||
id="info-{message.id}"
|
||||
>
|
||||
<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"
|
||||
<Tooltip content="Generation Info" placement="bottom">
|
||||
<button
|
||||
class=" {isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
||||
on:click={() => {
|
||||
console.log(message);
|
||||
}}
|
||||
id="info-{message.id}"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<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="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if isLastMessage}
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
continueGeneration();
|
||||
}}
|
||||
>
|
||||
<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"
|
||||
<Tooltip content="Continue Response" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
continueGeneration();
|
||||
}}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<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="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={regenerateResponse}
|
||||
>
|
||||
<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"
|
||||
<Tooltip content="Regenerate" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={regenerateResponse}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<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="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
import { modelfiles, settings } from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -20,18 +21,17 @@
|
||||
|
||||
let edit = false;
|
||||
let editedContent = '';
|
||||
|
||||
let messageEditTextAreaElement: HTMLTextAreaElement;
|
||||
const editMessageHandler = async () => {
|
||||
edit = true;
|
||||
editedContent = message.content;
|
||||
|
||||
await tick();
|
||||
const editElement = document.getElementById(`message-edit-${message.id}`);
|
||||
|
||||
editElement.style.height = '';
|
||||
editElement.style.height = `${editElement.scrollHeight}px`;
|
||||
messageEditTextAreaElement.style.height = '';
|
||||
messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
|
||||
|
||||
editElement?.focus();
|
||||
messageEditTextAreaElement?.focus();
|
||||
};
|
||||
|
||||
const editMessageConfirmHandler = async () => {
|
||||
@@ -165,9 +165,11 @@
|
||||
<div class=" w-full">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={messageEditTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
@@ -245,55 +247,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
>
|
||||
<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="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if !isFirstMessage}
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
on:click={() => {
|
||||
deleteMessageHandler();
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -307,10 +265,60 @@
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Copy" placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
>
|
||||
<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="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if !isFirstMessage}
|
||||
<Tooltip content="Delete" placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
deleteMessageHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { setDefaultModels } from '$lib/apis/configs';
|
||||
import { models, showSettings, settings, user } from '$lib/stores';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let selectedModels = [''];
|
||||
export let disabled = false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
@@ -15,6 +15,7 @@
|
||||
let name = '';
|
||||
let showJWTToken = false;
|
||||
let JWTTokenCopied = false;
|
||||
let profileImageInputElement: HTMLInputElement;
|
||||
|
||||
const submitHandler = async () => {
|
||||
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
||||
@@ -40,11 +41,12 @@
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<input
|
||||
id="profile-image-input"
|
||||
bind:this={profileImageInputElement}
|
||||
type="file"
|
||||
hidden
|
||||
accept="image/*"
|
||||
on:change={(e) => {
|
||||
const files = e?.target?.files ?? [];
|
||||
const files = profileImageInputElement.files ?? [];
|
||||
let reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
let originalImageUrl = `${event.target.result}`;
|
||||
@@ -86,7 +88,7 @@
|
||||
// Display the compressed image
|
||||
profileImageUrl = compressedSrc;
|
||||
|
||||
e.target.files = null;
|
||||
profileImageInputElement.files = null;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -107,9 +109,7 @@
|
||||
<button
|
||||
class="relative rounded-full dark:bg-gray-700"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('profile-image-input')?.click();
|
||||
}}
|
||||
on:click={profileImageInputElement.click}
|
||||
>
|
||||
<img
|
||||
src={profileImageUrl !== '' ? profileImageUrl : '/user.png'}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { updateUserPassword } from '$lib/apis/auths';
|
||||
|
||||
let show = false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let saveSettings: Function;
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let saveSettings: Function;
|
||||
// Chats
|
||||
let saveChatHistory = true;
|
||||
let importFiles;
|
||||
let showDeleteConfirm = false;
|
||||
let chatImportInputElement: HTMLInputElement;
|
||||
|
||||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
@@ -75,7 +76,9 @@
|
||||
|
||||
const deleteChats = async () => {
|
||||
await goto('/');
|
||||
await deleteAllChats(localStorage.token);
|
||||
await deleteAllChats(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
@@ -157,12 +160,17 @@
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div class="flex flex-col">
|
||||
<input id="chat-import-input" bind:files={importFiles} type="file" accept=".json" hidden />
|
||||
<input
|
||||
id="chat-import-input"
|
||||
bind:this={chatImportInputElement}
|
||||
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();
|
||||
}}
|
||||
on:click={chatImportInputElement.click}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { getOllamaAPIUrl, getOllamaVersion, updateOllamaAPIUrl } from '$lib/apis/ollama';
|
||||
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
|
||||
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let getModels: Function;
|
||||
|
||||
// External
|
||||
let API_BASE_URL = '';
|
||||
let OLLAMA_BASE_URL = '';
|
||||
let OLLAMA_BASE_URLS = [''];
|
||||
|
||||
let OPENAI_API_KEY = '';
|
||||
let OPENAI_API_BASE_URL = '';
|
||||
@@ -25,8 +26,8 @@
|
||||
await models.set(await getModels());
|
||||
};
|
||||
|
||||
const updateOllamaAPIUrlHandler = async () => {
|
||||
API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
|
||||
const updateOllamaUrlsHandler = async () => {
|
||||
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
|
||||
|
||||
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
@@ -41,7 +42,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
|
||||
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
|
||||
OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
|
||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
||||
}
|
||||
@@ -53,11 +54,6 @@
|
||||
on:submit|preventDefault={() => {
|
||||
updateOpenAIHandler();
|
||||
dispatch('save');
|
||||
|
||||
// saveSettings({
|
||||
// OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined,
|
||||
// OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined
|
||||
// });
|
||||
}}
|
||||
>
|
||||
<div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3">
|
||||
@@ -114,35 +110,82 @@
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
placeholder="Enter URL (e.g. http://localhost:11434/api)"
|
||||
bind:value={API_BASE_URL}
|
||||
/>
|
||||
<div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div>
|
||||
<div class="flex w-full gap-1.5">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
{#each OLLAMA_BASE_URLS as url, idx}
|
||||
<div class="flex gap-1.5">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder="Enter URL (e.g. http://localhost:11434)"
|
||||
bind:value={url}
|
||||
/>
|
||||
|
||||
<div class="self-center flex items-center">
|
||||
{#if idx === 0}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
|
||||
on:click={() => {
|
||||
updateOllamaAPIUrlHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
|
||||
<div class="">
|
||||
<button
|
||||
class="p-2.5 bg-gray-200 hover:bg-gray-300 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-lg transition"
|
||||
on:click={() => {
|
||||
updateOllamaUrlsHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { config, user } from '$lib/stores';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
|
||||
import { config, models, user } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let saveSettings: Function;
|
||||
@@ -191,10 +191,12 @@
|
||||
placeholder="Select a model"
|
||||
>
|
||||
<option value="" selected>Current Model</option>
|
||||
{#each $models.filter((m) => m.size != null) as model}
|
||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
||||
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
|
||||
>
|
||||
{#each $models as model}
|
||||
{#if model.size != null}
|
||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
|
||||
{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}
|
||||
</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<script lang="ts">
|
||||
import queue from 'async/queue';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { createModel, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
|
||||
import {
|
||||
createModel,
|
||||
deleteModel,
|
||||
getOllamaUrls,
|
||||
getOllamaVersion,
|
||||
pullModel
|
||||
} from '$lib/apis/ollama';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_NAME, models, user } from '$lib/stores';
|
||||
import { splitStream } from '$lib/utils';
|
||||
@@ -13,7 +19,7 @@
|
||||
|
||||
let showLiteLLM = false;
|
||||
let showLiteLLMParams = false;
|
||||
|
||||
let modelUploadInputElement: HTMLInputElement;
|
||||
let liteLLMModelInfo = [];
|
||||
|
||||
let liteLLMModel = '';
|
||||
@@ -27,6 +33,9 @@
|
||||
$: liteLLMModelName = liteLLMModel;
|
||||
|
||||
// Models
|
||||
|
||||
let OLLAMA_URLS = [];
|
||||
let selectedOllamaUrlIdx: string | null = null;
|
||||
let showExperimentalOllama = false;
|
||||
let ollamaVersion = '';
|
||||
const MAX_PARALLEL_DOWNLOADS = 3;
|
||||
@@ -45,7 +54,7 @@
|
||||
let modelUploadMode = 'file';
|
||||
let modelInputFile = '';
|
||||
let modelFileUrl = '';
|
||||
let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`;
|
||||
let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
|
||||
let modelFileDigest = '';
|
||||
let uploadProgress = null;
|
||||
|
||||
@@ -236,9 +245,11 @@
|
||||
};
|
||||
|
||||
const deleteModelHandler = async () => {
|
||||
const res = await deleteModel(localStorage.token, deleteModelTag).catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success(`Deleted ${deleteModelTag}`);
|
||||
@@ -249,10 +260,12 @@
|
||||
};
|
||||
|
||||
const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => {
|
||||
const res = await pullModel(localStorage.token, opts.modelName).catch((error) => {
|
||||
opts.callback({ success: false, error, modelName: opts.modelName });
|
||||
return null;
|
||||
});
|
||||
const res = await pullModel(localStorage.token, opts.modelName, selectedOllamaUrlIdx).catch(
|
||||
(error) => {
|
||||
opts.callback({ success: false, error, modelName: opts.modelName });
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
const reader = res.body
|
||||
@@ -358,6 +371,15 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
if (OLLAMA_URLS.length > 1) {
|
||||
selectedOllamaUrlIdx = 0;
|
||||
}
|
||||
|
||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
|
||||
});
|
||||
@@ -367,52 +389,137 @@
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[23rem]">
|
||||
{#if ollamaVersion}
|
||||
<div class="space-y-2 pr-1.5">
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">Manage Ollama Models</div>
|
||||
<div class="text-sm font-medium">Manage Ollama Models</div>
|
||||
|
||||
<div class=" mb-2 text-sm font-medium">Pull a model from Ollama.com</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder="Enter model tag (e.g. mistral:7b)"
|
||||
bind:value={modelTag}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
|
||||
on:click={() => {
|
||||
pullModelHandler();
|
||||
}}
|
||||
disabled={modelTransferring}
|
||||
{#if OLLAMA_URLS.length > 1}
|
||||
<div class="flex-1 pb-1">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={selectedOllamaUrlIdx}
|
||||
placeholder="Select an Ollama instance"
|
||||
>
|
||||
{#if modelTransferring}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
{#each OLLAMA_URLS as url, idx}
|
||||
<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">Pull a model from Ollama.com</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder="Enter model tag (e.g. mistral:7b)"
|
||||
bind:value={modelTag}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
pullModelHandler();
|
||||
}}
|
||||
disabled={modelTransferring}
|
||||
>
|
||||
{#if modelTransferring}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
To access the available model names for downloading, <a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium underline"
|
||||
href="https://ollama.com/library"
|
||||
target="_blank">click here.</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if Object.keys(modelDownloadStatus).length > 0}
|
||||
{#each Object.keys(modelDownloadStatus) as model}
|
||||
<div class="flex flex-col">
|
||||
<div class="font-medium mb-1">{model}</div>
|
||||
<div class="">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
|
||||
>
|
||||
{modelDownloadStatus[model].pullProgress ?? 0}%
|
||||
</div>
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{modelDownloadStatus[model].digest}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">Delete a model</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={deleteModelTag}
|
||||
placeholder="Select a model"
|
||||
>
|
||||
{#if !deleteModelTag}
|
||||
<option value="" disabled selected>Select a model</option>
|
||||
{/if}
|
||||
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) 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-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
deleteModelHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -420,262 +527,192 @@
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
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>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
To access the available model names for downloading, <a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium"
|
||||
href="https://ollama.com/library"
|
||||
target="_blank">click here.</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if Object.keys(modelDownloadStatus).length > 0}
|
||||
{#each Object.keys(modelDownloadStatus) as model}
|
||||
<div class="flex flex-col">
|
||||
<div class="font-medium mb-1">{model}</div>
|
||||
<div class="">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
|
||||
>
|
||||
{modelDownloadStatus[model].pullProgress ?? 0}%
|
||||
</div>
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{modelDownloadStatus[model].digest}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 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-850 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-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark: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 class="flex justify-between items-center text-xs">
|
||||
<div class=" text-sm font-medium">Experimental</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showExperimentalOllama = !showExperimentalOllama;
|
||||
}}>{showExperimentalOllama ? 'Show' : 'Hide'}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showExperimentalOllama}
|
||||
<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>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
if (modelUploadMode === 'file') {
|
||||
modelUploadMode = 'url';
|
||||
} else {
|
||||
modelUploadMode = 'file';
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if modelUploadMode === 'file'}
|
||||
<span class="ml-2 self-center">File Mode</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">URL Mode</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full mb-1.5">
|
||||
<div class="flex flex-col w-full">
|
||||
{#if modelUploadMode === 'file'}
|
||||
<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
|
||||
<input
|
||||
id="model-upload-input"
|
||||
type="file"
|
||||
bind:files={modelInputFile}
|
||||
on:change={() => {
|
||||
console.log(modelInputFile);
|
||||
}}
|
||||
accept=".gguf"
|
||||
required
|
||||
hidden
|
||||
/>
|
||||
<div class="pt-1">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-sm font-medium">Experimental</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showExperimentalOllama = !showExperimentalOllama;
|
||||
}}>{showExperimentalOllama ? 'Hide' : 'Show'}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
|
||||
on:click={() => {
|
||||
document.getElementById('model-upload-input').click();
|
||||
}}
|
||||
>
|
||||
{#if modelInputFile && modelInputFile.length > 0}
|
||||
{modelInputFile[0].name}
|
||||
{:else}
|
||||
Click here to select
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
||||
<input
|
||||
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
||||
''
|
||||
? 'mr-2'
|
||||
: ''}"
|
||||
type="url"
|
||||
required
|
||||
bind:value={modelFileUrl}
|
||||
placeholder="Type HuggingFace Resolve (Download) URL"
|
||||
/>
|
||||
</div>
|
||||
{#if showExperimentalOllama}
|
||||
<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>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
if (modelUploadMode === 'file') {
|
||||
modelUploadMode = 'url';
|
||||
} else {
|
||||
modelUploadMode = 'file';
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if modelUploadMode === 'file'}
|
||||
<span class="ml-2 self-center">File Mode</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">URL Mode</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full mb-1.5">
|
||||
<div class="flex flex-col w-full">
|
||||
{#if modelUploadMode === 'file'}
|
||||
<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
|
||||
<input
|
||||
id="model-upload-input"
|
||||
bind:this={modelUploadInputElement}
|
||||
type="file"
|
||||
bind:files={modelInputFile}
|
||||
on:change={() => {
|
||||
console.log(modelInputFile);
|
||||
}}
|
||||
accept=".gguf"
|
||||
required
|
||||
hidden
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
|
||||
on:click={modelUploadInputElement.click}
|
||||
>
|
||||
{#if modelInputFile && modelInputFile.length > 0}
|
||||
{modelInputFile[0].name}
|
||||
{:else}
|
||||
Click here to select
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
|
||||
<input
|
||||
class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
|
||||
''
|
||||
? 'mr-2'
|
||||
: ''}"
|
||||
type="url"
|
||||
required
|
||||
bind:value={modelFileUrl}
|
||||
placeholder="Type Hugging Face Resolve (Download) URL"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||
<button
|
||||
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
|
||||
type="submit"
|
||||
disabled={modelTransferring}
|
||||
>
|
||||
{#if modelTransferring}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||
<button
|
||||
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
|
||||
type="submit"
|
||||
disabled={modelTransferring}
|
||||
>
|
||||
{#if modelTransferring}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
|
||||
<div>
|
||||
<div>
|
||||
<div class=" my-2.5 text-sm font-medium">Modelfile Content</div>
|
||||
<textarea
|
||||
bind:value={modelFileContent}
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
||||
rows="6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
To access the GGUF models available for downloading, <a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium"
|
||||
href="https://huggingface.co/models?search=gguf"
|
||||
target="_blank">click here.</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if uploadProgress !== null}
|
||||
<div class="mt-2">
|
||||
<div class=" mb-2 text-xs">Upload Progress</div>
|
||||
|
||||
<div class="w-full rounded-full dark:bg-gray-800">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: {Math.max(15, uploadProgress ?? 0)}%"
|
||||
>
|
||||
{uploadProgress ?? 0}%
|
||||
<div>
|
||||
<div class=" my-2.5 text-sm font-medium">Modelfile Content</div>
|
||||
<textarea
|
||||
bind:value={modelFileContent}
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
||||
rows="6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{modelFileDigest}
|
||||
</div>
|
||||
{/if}
|
||||
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
To access the GGUF models available for downloading, <a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium underline"
|
||||
href="https://huggingface.co/models?search=gguf"
|
||||
target="_blank">click here.</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if uploadProgress !== null}
|
||||
<div class="mt-2">
|
||||
<div class=" mb-2 text-xs">Upload Progress</div>
|
||||
|
||||
<div class="w-full rounded-full dark:bg-gray-800">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: {Math.max(15, uploadProgress ?? 0)}%"
|
||||
>
|
||||
{uploadProgress ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{modelFileDigest}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-700 my-2" />
|
||||
{/if}
|
||||
@@ -693,7 +730,7 @@
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showLiteLLMParams = !showLiteLLMParams;
|
||||
}}>{showLiteLLMParams ? 'Advanced' : 'Default'}</button
|
||||
}}>{showLiteLLMParams ? 'Hide Additional Params' : 'Show Additional Params'}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -702,7 +739,7 @@
|
||||
<div class="flex w-full mb-1.5">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder="Enter LiteLLM Model (litellm_params.model)"
|
||||
bind:value={liteLLMModel}
|
||||
autocomplete="off"
|
||||
@@ -710,7 +747,7 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
addLiteLLMModelHandler();
|
||||
}}
|
||||
@@ -734,7 +771,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder="Enter Model Name (model_name)"
|
||||
bind:value={liteLLMModelName}
|
||||
autocomplete="off"
|
||||
@@ -748,7 +785,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder="Enter LiteLLM API Base URL (litellm_params.api_base)"
|
||||
bind:value={liteLLMAPIBase}
|
||||
autocomplete="off"
|
||||
@@ -762,7 +799,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder="Enter LiteLLM API Key (litellm_params.api_key)"
|
||||
bind:value={liteLLMAPIKey}
|
||||
autocomplete="off"
|
||||
@@ -776,7 +813,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder="Enter LiteLLM API RPM (litellm_params.rpm)"
|
||||
bind:value={liteLLMRPM}
|
||||
autocomplete="off"
|
||||
@@ -790,7 +827,7 @@
|
||||
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Not sure what to add?
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
|
||||
target="_blank"
|
||||
>
|
||||
@@ -803,7 +840,7 @@
|
||||
<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-850 outline-none"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={deleteLiteLLMModelId}
|
||||
placeholder="Select a model"
|
||||
>
|
||||
@@ -818,7 +855,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
deleteLiteLLMModelHandler();
|
||||
}}
|
||||
@@ -913,7 +950,7 @@
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Not sure what to add?
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { models, settings, user } from '$lib/stores';
|
||||
|
||||
import { getOllamaModels } from '$lib/apis/ollama';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
export let placement = 'top';
|
||||
export let content = `I'm a tooltip!`;
|
||||
export let touch = true;
|
||||
|
||||
let tooltipElement;
|
||||
let tooltipInstance;
|
||||
|
||||
$: if (tooltipElement && content) {
|
||||
if (tooltipInstance) {
|
||||
tooltipInstance.setContent(content);
|
||||
} else {
|
||||
tooltipInstance = tippy(tooltipElement, {
|
||||
content: content,
|
||||
placement: placement,
|
||||
allowHTML: true,
|
||||
touch: touch
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (tooltipInstance) {
|
||||
tooltipInstance.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={tooltipElement} aria-label={content}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs from 'dayjs';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
export let show = false;
|
||||
export let selectedDoc;
|
||||
|
||||
let uploadDocInputElement: HTMLInputElement;
|
||||
let inputFiles;
|
||||
let tags = [];
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
}
|
||||
|
||||
inputFiles = null;
|
||||
document.getElementById('upload-doc-input').value = '';
|
||||
uploadDocInputElement.value = '';
|
||||
} else {
|
||||
toast.error(`File not found.`);
|
||||
}
|
||||
@@ -126,14 +126,19 @@
|
||||
}}
|
||||
>
|
||||
<div class="mb-3 w-full">
|
||||
<input id="upload-doc-input" hidden bind:files={inputFiles} type="file" multiple />
|
||||
<input
|
||||
id="upload-doc-input"
|
||||
bind:this={uploadDocInputElement}
|
||||
hidden
|
||||
bind:files={inputFiles}
|
||||
type="file"
|
||||
multiple
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('upload-doc-input')?.click();
|
||||
}}
|
||||
on:click={uploadDocInputElement.click}
|
||||
>
|
||||
{#if inputFiles}
|
||||
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs from 'dayjs';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
import { getDocs } from '$lib/apis/documents';
|
||||
import {
|
||||
getChunkParams,
|
||||
getRAGTemplate,
|
||||
getQuerySettings,
|
||||
scanDocs,
|
||||
updateChunkParams,
|
||||
updateRAGTemplate
|
||||
updateQuerySettings
|
||||
} from '$lib/apis/rag';
|
||||
import { documents } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
let chunkSize = 0;
|
||||
let chunkOverlap = 0;
|
||||
|
||||
let template = '';
|
||||
let querySettings = {
|
||||
template: '',
|
||||
k: 4
|
||||
};
|
||||
|
||||
const scanHandler = async () => {
|
||||
loading = true;
|
||||
@@ -33,7 +36,7 @@
|
||||
|
||||
const submitHandler = async () => {
|
||||
const res = await updateChunkParams(localStorage.token, chunkSize, chunkOverlap);
|
||||
await updateRAGTemplate(localStorage.token, template);
|
||||
querySettings = await updateQuerySettings(localStorage.token, querySettings);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
@@ -44,7 +47,7 @@
|
||||
chunkOverlap = res.chunk_overlap;
|
||||
}
|
||||
|
||||
template = await getRAGTemplate(localStorage.token);
|
||||
querySettings = await getQuerySettings(localStorage.token);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -156,10 +159,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-sm font-medium">Query Params</div>
|
||||
|
||||
<div class=" flex">
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class="self-center text-xs font-medium flex-1">Top K</div>
|
||||
|
||||
<div class="self-center p-3">
|
||||
<input
|
||||
class=" w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
type="number"
|
||||
placeholder="Enter Top K"
|
||||
bind:value={querySettings.k}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="flex w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit">Chunk Overlap</div>
|
||||
|
||||
<div class="self-center p-3">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
type="number"
|
||||
placeholder="Enter Chunk Overlap"
|
||||
bind:value={chunkOverlap}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">RAG Template</div>
|
||||
<textarea
|
||||
bind:value={template}
|
||||
bind:value={querySettings.template}
|
||||
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
updateChatById,
|
||||
getAllChatTags
|
||||
} from '$lib/apis/chats';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
|
||||
let show = false;
|
||||
let navElement;
|
||||
@@ -31,9 +32,10 @@
|
||||
let chatTitle = '';
|
||||
|
||||
let showDropdown = false;
|
||||
let isEditing = false;
|
||||
|
||||
onMount(async () => {
|
||||
if (window.innerWidth > 1280) {
|
||||
if (window.innerWidth > 1024) {
|
||||
show = true;
|
||||
}
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
@@ -101,17 +103,16 @@
|
||||
: 'invisible'}"
|
||||
>
|
||||
<div class="px-2 flex justify-center space-x-2">
|
||||
<button
|
||||
<a
|
||||
id="sidebar-new-chat-button"
|
||||
class="flex-grow flex justify-between rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
|
||||
href="/"
|
||||
on:click={async () => {
|
||||
goto('/');
|
||||
|
||||
await goto('/');
|
||||
const newChatButton = document.getElementById('new-chat-button');
|
||||
|
||||
if (newChatButton) {
|
||||
newChatButton.click();
|
||||
}
|
||||
setTimeout(() => {
|
||||
newChatButton?.click();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<div class="flex self-center">
|
||||
@@ -141,7 +142,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if $user?.role === 'admin'}
|
||||
@@ -366,27 +367,39 @@
|
||||
}
|
||||
}) as chat, i}
|
||||
<div class=" w-full pr-2 relative">
|
||||
<a
|
||||
class=" w-full flex justify-between rounded-xl px-3 py-2 hover:bg-gray-900 {chat.id ===
|
||||
$chatId
|
||||
? 'bg-gray-900'
|
||||
: ''} transition whitespace-nowrap text-ellipsis"
|
||||
href="/c/{chat.id}"
|
||||
>
|
||||
<div class=" flex self-center flex-1 w-full">
|
||||
<div
|
||||
class=" text-left self-center overflow-hidden {chat.id === $chatId
|
||||
? 'w-[160px]'
|
||||
: 'w-full'} "
|
||||
>
|
||||
{#if chatTitleEditId === chat.id}
|
||||
<input bind:value={chatTitle} class=" bg-transparent w-full" />
|
||||
{:else}
|
||||
{chat.title}
|
||||
{/if}
|
||||
</div>
|
||||
{#if chatTitleEditId === chat.id}
|
||||
<div
|
||||
class=" w-full flex justify-between rounded-xl px-3 py-2 hover:bg-gray-900 {chat.id ===
|
||||
$chatId
|
||||
? 'bg-gray-900'
|
||||
: ''} transition whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
class=" w-full flex justify-between rounded-xl px-3 py-2 hover:bg-gray-900 {chat.id ===
|
||||
$chatId
|
||||
? 'bg-gray-900'
|
||||
: ''} transition whitespace-nowrap text-ellipsis"
|
||||
href="/c/{chat.id}"
|
||||
on:click={() => {
|
||||
if (window.innerWidth < 1024) {
|
||||
show = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" flex self-center flex-1 w-full">
|
||||
<div
|
||||
class=" text-left self-center overflow-hidden {chat.id === $chatId
|
||||
? 'w-[160px]'
|
||||
: 'w-full'} "
|
||||
>
|
||||
{chat.title}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if chat.id === $chatId}
|
||||
<div class=" absolute right-[22px] top-[10px]">
|
||||
@@ -485,7 +498,6 @@
|
||||
on:click={() => {
|
||||
chatTitle = chat.title;
|
||||
chatTitleEditId = chat.id;
|
||||
// editChatTitle(chat.id, 'a');
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -587,6 +599,32 @@
|
||||
</div>
|
||||
<div class=" self-center font-medium">Admin Panel</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
goto('/playground');
|
||||
showDropdown = false;
|
||||
}}
|
||||
>
|
||||
<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="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center font-medium">Playground</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
@@ -664,30 +702,32 @@
|
||||
<div
|
||||
class="fixed left-0 top-[50dvh] z-40 -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
|
||||
>
|
||||
<button
|
||||
id="sidebar-toggle-button"
|
||||
class=" group"
|
||||
on:click={() => {
|
||||
show = !show;
|
||||
}}
|
||||
><span class="" data-state="closed"
|
||||
><div
|
||||
class="flex h-[72px] w-8 items-center justify-center opacity-20 group-hover:opacity-100 transition"
|
||||
>
|
||||
<div class="flex h-6 w-6 flex-col items-center">
|
||||
<div
|
||||
class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[0.15rem] {show
|
||||
? 'group-hover:rotate-[15deg]'
|
||||
: 'group-hover:rotate-[-15deg]'}"
|
||||
/>
|
||||
<div
|
||||
class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[-0.15rem] {show
|
||||
? 'group-hover:rotate-[-15deg]'
|
||||
: 'group-hover:rotate-[15deg]'}"
|
||||
/>
|
||||
<Tooltip placement="right" content={`${show ? 'Close' : 'Open'} sidebar`} touch={false}>
|
||||
<button
|
||||
id="sidebar-toggle-button"
|
||||
class=" group"
|
||||
on:click={() => {
|
||||
show = !show;
|
||||
}}
|
||||
><span class="" data-state="closed"
|
||||
><div
|
||||
class="flex h-[72px] w-8 items-center justify-center opacity-20 group-hover:opacity-100 transition"
|
||||
>
|
||||
<div class="flex h-6 w-6 flex-col items-center">
|
||||
<div
|
||||
class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[0.15rem] {show
|
||||
? 'group-hover:rotate-[15deg]'
|
||||
: 'group-hover:rotate-[-15deg]'}"
|
||||
/>
|
||||
<div
|
||||
class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[-0.15rem] {show
|
||||
? 'group-hover:rotate-[-15deg]'
|
||||
: 'group-hover:rotate-[15deg]'}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let messages = [];
|
||||
let textAreaElement: HTMLTextAreaElement;
|
||||
onMount(() => {
|
||||
messages.forEach((message, idx) => {
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="py-3 space-y-3">
|
||||
{#each messages as message, idx}
|
||||
<div class="flex gap-2 group">
|
||||
<div class="flex items-start pt-1">
|
||||
<button
|
||||
class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left dark:group-hover:bg-gray-800 rounded-lg transition"
|
||||
on:click={() => {
|
||||
message.role = message.role === 'user' ? 'assistant' : 'user';
|
||||
}}>{message.role}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<textarea
|
||||
id="{message.role}-{idx}-textarea"
|
||||
bind:this={textAreaElement}
|
||||
class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
|
||||
placeholder="Enter {message.role === 'user' ? 'a user' : 'an assistant'} message here"
|
||||
rows="1"
|
||||
on:input={(e) => {
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
}}
|
||||
on:focus={(e) => {
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
|
||||
// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||
}}
|
||||
bind:value={message.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=" pt-1">
|
||||
<button
|
||||
class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition"
|
||||
on:click={() => {
|
||||
messages = messages.filter((message, messageIdx) => messageIdx !== idx);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800" />
|
||||
{/each}
|
||||
|
||||
<button
|
||||
class="flex items-center gap-2 px-2 py-1"
|
||||
on:click={() => {
|
||||
console.log(messages.at(-1));
|
||||
messages.push({
|
||||
role: (messages.at(-1)?.role ?? 'assistant') === 'user' ? 'assistant' : 'user',
|
||||
content: ''
|
||||
});
|
||||
messages = messages;
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<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="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class=" text-sm font-medium">Add message</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -7,7 +7,7 @@ export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
|
||||
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
|
||||
|
||||
export const LITELLM_API_BASE_URL = `${WEBUI_BASE_URL}/litellm/api`;
|
||||
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`;
|
||||
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama`;
|
||||
export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
|
||||
export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`;
|
||||
export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`;
|
||||
@@ -90,8 +90,3 @@ export const SUPPORTED_FILE_EXTENSIONS = [
|
||||
// This feature, akin to $env/static/private, exclusively incorporates environment variables
|
||||
// that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
|
||||
// Consequently, these variables can be securely exposed to client-side code.
|
||||
|
||||
// Example of the .env configuration:
|
||||
// OLLAMA_API_BASE_URL="http://localhost:11434/api"
|
||||
// # Public
|
||||
// PUBLIC_API_BASE_URL=$OLLAMA_API_BASE_URL
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { openDB, deleteDB } from 'idb';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
@@ -34,10 +34,11 @@
|
||||
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
||||
import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
|
||||
import ChangelogModal from '$lib/components/ChangelogModal.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
let ollamaVersion = '';
|
||||
let loaded = false;
|
||||
|
||||
let showShortcutsButtonElement: HTMLButtonElement;
|
||||
let DB = null;
|
||||
let localDBChats = [];
|
||||
|
||||
@@ -184,7 +185,7 @@
|
||||
if (isCtrlPressed && event.key === '/') {
|
||||
event.preventDefault();
|
||||
console.log('showShortcuts');
|
||||
document.getElementById('show-shortcuts-button')?.click();
|
||||
showShortcutsButtonElement.click();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -201,15 +202,18 @@
|
||||
|
||||
{#if loaded}
|
||||
<div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10">
|
||||
<button
|
||||
id="show-shortcuts-button"
|
||||
class="text-gray-600 dark:text-gray-300 bg-gray-300/20 w-6 h-6 flex items-center justify-center text-xs rounded-full"
|
||||
on:click={() => {
|
||||
showShortcuts = !showShortcuts;
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
<Tooltip content="help" placement="left">
|
||||
<button
|
||||
id="show-shortcuts-button"
|
||||
bind:this={showShortcutsButtonElement}
|
||||
class="text-gray-600 dark:text-gray-300 bg-gray-300/20 w-6 h-6 flex items-center justify-center text-xs rounded-full"
|
||||
on:click={() => {
|
||||
showShortcuts = !showShortcuts;
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<ShortcutsModal bind:show={showShortcuts} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -14,6 +14,7 @@
|
||||
chats,
|
||||
chatId,
|
||||
config,
|
||||
WEBUI_NAME,
|
||||
tags as _tags
|
||||
} from '$lib/stores';
|
||||
import { copyToClipboard, splitStream } from '$lib/utils';
|
||||
@@ -38,11 +39,10 @@
|
||||
import { RAGTemplate } from '$lib/utils/rag';
|
||||
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
let stopResponseFlag = false;
|
||||
let autoScroll = true;
|
||||
let processing = '';
|
||||
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
let currentRequestId = null;
|
||||
|
||||
let selectedModels = [''];
|
||||
@@ -134,11 +134,13 @@
|
||||
settings.set({
|
||||
..._settings
|
||||
});
|
||||
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
setTimeout(() => chatInput?.focus(), 0);
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const element = document.getElementById('messages-container');
|
||||
element.scrollTop = element.scrollHeight;
|
||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||
};
|
||||
|
||||
//////////////////////////
|
||||
@@ -245,19 +247,17 @@
|
||||
let relevantContexts = await Promise.all(
|
||||
docs.map(async (doc) => {
|
||||
if (doc.type === 'collection') {
|
||||
return await queryCollection(localStorage.token, doc.collection_names, query, 4).catch(
|
||||
return await queryCollection(localStorage.token, doc.collection_names, query).catch(
|
||||
(error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return await queryDoc(localStorage.token, doc.collection_name, query, 4).catch(
|
||||
(error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
return await queryDoc(localStorage.token, doc.collection_name, query).catch((error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -339,7 +339,7 @@
|
||||
content: $settings.system
|
||||
}
|
||||
: undefined,
|
||||
...messages.filter((message) => !message.deleted)
|
||||
...messages
|
||||
]
|
||||
.filter((message) => message)
|
||||
.map((message, idx, arr) => ({
|
||||
@@ -547,7 +547,7 @@
|
||||
content: $settings.system
|
||||
}
|
||||
: undefined,
|
||||
...messages.filter((message) => !message.deleted)
|
||||
...messages
|
||||
]
|
||||
.filter((message) => message)
|
||||
.map((message, idx, arr) => ({
|
||||
@@ -806,14 +806,25 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{title
|
||||
? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
|
||||
: `${$WEBUI_NAME}`}
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="h-screen max-h-[100dvh] w-full flex flex-col">
|
||||
<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} {tags} {addTag} {deleteTag} />
|
||||
<div class="flex flex-col flex-auto">
|
||||
<div
|
||||
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
|
||||
id="messages-container"
|
||||
bind:this={messagesContainerElement}
|
||||
on:scroll={(e) => {
|
||||
autoScroll = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50;
|
||||
autoScroll =
|
||||
messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
|
||||
messagesContainerElement.clientHeight + 50;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -821,10 +832,7 @@
|
||||
? 'max-w-full'
|
||||
: 'max-w-2xl md:px-0'} mx-auto w-full px-4"
|
||||
>
|
||||
<ModelSelector
|
||||
bind:selectedModels
|
||||
disabled={messages.length > 0 && !selectedModels.includes('')}
|
||||
/>
|
||||
<ModelSelector bind:selectedModels />
|
||||
</div>
|
||||
|
||||
<div class=" h-full w-full flex flex-col py-8">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { config, user } from '$lib/stores';
|
||||
import { WEBUI_NAME, config, user } from '$lib/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users';
|
||||
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
|
||||
@@ -61,6 +61,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{`Admin Panel | ${$WEBUI_NAME}`}
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
{#key selectedUser}
|
||||
<EditUserModal
|
||||
bind:show={showEditUserModal}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -14,6 +14,7 @@
|
||||
chats,
|
||||
chatId,
|
||||
config,
|
||||
WEBUI_NAME,
|
||||
tags as _tags
|
||||
} from '$lib/stores';
|
||||
import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils';
|
||||
@@ -37,14 +38,14 @@
|
||||
import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
|
||||
import Navbar from '$lib/components/layout/Navbar.svelte';
|
||||
import { RAGTemplate } from '$lib/utils/rag';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
let loaded = false;
|
||||
|
||||
let stopResponseFlag = false;
|
||||
let autoScroll = true;
|
||||
let processing = '';
|
||||
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
let currentRequestId = null;
|
||||
|
||||
// let chatId = $page.params.id;
|
||||
@@ -99,6 +100,10 @@
|
||||
if (await loadChat()) {
|
||||
await tick();
|
||||
loaded = true;
|
||||
|
||||
window.setTimeout(() => scrollToBottom(), 0);
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
chatInput?.focus();
|
||||
} else {
|
||||
await goto('/');
|
||||
}
|
||||
@@ -155,8 +160,7 @@
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const element = document.getElementById('messages-container');
|
||||
element.scrollTop = element.scrollHeight;
|
||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||
};
|
||||
|
||||
//////////////////////////
|
||||
@@ -256,19 +260,17 @@
|
||||
let relevantContexts = await Promise.all(
|
||||
docs.map(async (doc) => {
|
||||
if (doc.type === 'collection') {
|
||||
return await queryCollection(localStorage.token, doc.collection_names, query, 4).catch(
|
||||
return await queryCollection(localStorage.token, doc.collection_names, query).catch(
|
||||
(error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return await queryDoc(localStorage.token, doc.collection_name, query, 4).catch(
|
||||
(error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
return await queryDoc(localStorage.token, doc.collection_name, query).catch((error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -350,7 +352,7 @@
|
||||
content: $settings.system
|
||||
}
|
||||
: undefined,
|
||||
...messages.filter((message) => !message.deleted)
|
||||
...messages
|
||||
]
|
||||
.filter((message) => message)
|
||||
.map((message, idx, arr) => ({
|
||||
@@ -558,7 +560,7 @@
|
||||
content: $settings.system
|
||||
}
|
||||
: undefined,
|
||||
...messages.filter((message) => !message.deleted)
|
||||
...messages
|
||||
]
|
||||
.filter((message) => message)
|
||||
.map((message, idx, arr) => ({
|
||||
@@ -820,6 +822,14 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{title
|
||||
? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
|
||||
: `${$WEBUI_NAME}`}
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if loaded}
|
||||
<div class="min-h-screen max-h-screen w-full flex flex-col">
|
||||
<Navbar
|
||||
@@ -841,8 +851,11 @@
|
||||
<div
|
||||
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
|
||||
id="messages-container"
|
||||
bind:this={messagesContainerElement}
|
||||
on:scroll={(e) => {
|
||||
autoScroll = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50;
|
||||
autoScroll =
|
||||
messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
|
||||
messagesContainerElement.clientHeight + 50;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -850,10 +863,7 @@
|
||||
? 'max-w-full'
|
||||
: 'max-w-2xl md:px-0'} mx-auto w-full px-4"
|
||||
>
|
||||
<ModelSelector
|
||||
bind:selectedModels
|
||||
disabled={messages.length > 0 && !selectedModels.includes('')}
|
||||
/>
|
||||
<ModelSelector bind:selectedModels />
|
||||
</div>
|
||||
|
||||
<div class=" h-full w-full flex flex-col py-8">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { documents } from '$lib/stores';
|
||||
import { WEBUI_NAME, documents } from '$lib/stores';
|
||||
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
|
||||
|
||||
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
let inputFiles = '';
|
||||
let query = '';
|
||||
|
||||
let documentsImportInputElement: HTMLInputElement;
|
||||
let tags = [];
|
||||
|
||||
let showSettingsModal = false;
|
||||
@@ -148,6 +148,12 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{`Documents | ${$WEBUI_NAME}`}
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if dragged}
|
||||
<div
|
||||
class="fixed w-full h-full flex z-50 touch-none pointer-events-none"
|
||||
@@ -518,6 +524,7 @@
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
id="documents-import-input"
|
||||
bind:this={documentsImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
@@ -552,9 +559,7 @@
|
||||
|
||||
<button
|
||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||
on:click={async () => {
|
||||
document.getElementById('documents-import-input')?.click();
|
||||
}}
|
||||
on:click={documentsImportInputElement.click}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium">Import Documents Mapping</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { modelfiles, settings, user } from '$lib/stores';
|
||||
import { WEBUI_NAME, modelfiles, settings, user } from '$lib/stores';
|
||||
import { createModel, deleteModel } from '$lib/apis/ollama';
|
||||
import {
|
||||
createNewModelfile,
|
||||
@@ -16,11 +16,14 @@
|
||||
|
||||
let localModelfiles = [];
|
||||
let importFiles;
|
||||
|
||||
let modelfilesImportInputElement: HTMLInputElement;
|
||||
const deleteModelHandler = async (tagName) => {
|
||||
let success = null;
|
||||
|
||||
success = await deleteModel(localStorage.token, tagName);
|
||||
success = await deleteModel(localStorage.token, tagName).catch((err) => {
|
||||
toast.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (success) {
|
||||
toast.success(`Deleted ${tagName}`);
|
||||
@@ -69,6 +72,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{`Modelfiles | ${$WEBUI_NAME}`}
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
|
||||
<div class="flex flex-col justify-between w-full overflow-y-auto">
|
||||
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
|
||||
@@ -229,6 +238,7 @@
|
||||
<div class="flex space-x-1">
|
||||
<input
|
||||
id="modelfiles-import-input"
|
||||
bind:this={modelfilesImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
@@ -256,9 +266,7 @@
|
||||
|
||||
<button
|
||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||
on:click={async () => {
|
||||
document.getElementById('modelfiles-import-input')?.click();
|
||||
}}
|
||||
on:click={modelfilesImportInputElement.click}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium">Import Modelfiles</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { toast } from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import { settings, user, config, modelfiles, models } from '$lib/stores';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { toast } from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import {
|
||||
LITELLM_API_BASE_URL,
|
||||
OLLAMA_API_BASE_URL,
|
||||
OPENAI_API_BASE_URL,
|
||||
WEBUI_API_BASE_URL
|
||||
} from '$lib/constants';
|
||||
import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
|
||||
|
||||
import { cancelChatCompletion, generateChatCompletion } from '$lib/apis/ollama';
|
||||
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
|
||||
|
||||
import { splitStream } from '$lib/utils';
|
||||
import ChatCompletion from '$lib/components/playground/ChatCompletion.svelte';
|
||||
|
||||
let mode = 'chat';
|
||||
let loaded = false;
|
||||
let text = '';
|
||||
|
||||
let selectedModelId = '';
|
||||
|
||||
let loading = false;
|
||||
let currentRequestId = null;
|
||||
let stopResponseFlag = false;
|
||||
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
let textCompletionAreaElement: HTMLTextAreaElement;
|
||||
|
||||
let system = '';
|
||||
let messages = [
|
||||
{
|
||||
role: 'user',
|
||||
content: ''
|
||||
}
|
||||
];
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const element = mode === 'chat' ? messagesContainerElement : textCompletionAreaElement;
|
||||
|
||||
if (element) {
|
||||
element.scrollTop = element?.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
// const cancelHandler = async () => {
|
||||
// if (currentRequestId) {
|
||||
// const res = await cancelChatCompletion(localStorage.token, currentRequestId);
|
||||
// currentRequestId = null;
|
||||
// loading = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
const stopResponse = () => {
|
||||
stopResponseFlag = true;
|
||||
console.log('stopResponse');
|
||||
};
|
||||
|
||||
const textCompletionHandler = async () => {
|
||||
const model = $models.find((model) => model.id === selectedModelId);
|
||||
|
||||
const res = await generateOpenAIChatCompletion(
|
||||
localStorage.token,
|
||||
{
|
||||
model: model.id,
|
||||
stream: true,
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: text
|
||||
}
|
||||
]
|
||||
},
|
||||
model.external
|
||||
? model.source === 'litellm'
|
||||
? `${LITELLM_API_BASE_URL}/v1`
|
||||
: `${OPENAI_API_BASE_URL}`
|
||||
: `${OLLAMA_API_BASE_URL}/v1`
|
||||
);
|
||||
|
||||
if (res && res.ok) {
|
||||
const reader = res.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(splitStream('\n'))
|
||||
.getReader();
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done || stopResponseFlag) {
|
||||
if (stopResponseFlag) {
|
||||
await cancelChatCompletion(localStorage.token, currentRequestId);
|
||||
}
|
||||
|
||||
currentRequestId = null;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
let lines = value.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line !== '') {
|
||||
if (line === 'data: [DONE]') {
|
||||
// responseMessage.done = true;
|
||||
console.log('done');
|
||||
} else {
|
||||
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||
console.log(data);
|
||||
|
||||
if ('request_id' in data) {
|
||||
currentRequestId = data.request_id;
|
||||
} else {
|
||||
text += data.choices[0].delta.content ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chatCompletionHandler = async () => {
|
||||
const model = $models.find((model) => model.id === selectedModelId);
|
||||
|
||||
const res = await generateOpenAIChatCompletion(
|
||||
localStorage.token,
|
||||
{
|
||||
model: model.id,
|
||||
stream: true,
|
||||
messages: [
|
||||
system
|
||||
? {
|
||||
role: 'system',
|
||||
content: system
|
||||
}
|
||||
: undefined,
|
||||
...messages
|
||||
].filter((message) => message)
|
||||
},
|
||||
model.external
|
||||
? model.source === 'litellm'
|
||||
? `${LITELLM_API_BASE_URL}/v1`
|
||||
: `${OPENAI_API_BASE_URL}`
|
||||
: `${OLLAMA_API_BASE_URL}/v1`
|
||||
);
|
||||
|
||||
let responseMessage;
|
||||
if (messages.at(-1)?.role === 'assistant') {
|
||||
responseMessage = messages.at(-1);
|
||||
} else {
|
||||
responseMessage = {
|
||||
role: 'assistant',
|
||||
content: ''
|
||||
};
|
||||
messages.push(responseMessage);
|
||||
messages = messages;
|
||||
}
|
||||
|
||||
await tick();
|
||||
const textareaElement = document.getElementById(`assistant-${messages.length - 1}-textarea`);
|
||||
|
||||
if (res && res.ok) {
|
||||
const reader = res.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(splitStream('\n'))
|
||||
.getReader();
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done || stopResponseFlag) {
|
||||
if (stopResponseFlag) {
|
||||
await cancelChatCompletion(localStorage.token, currentRequestId);
|
||||
}
|
||||
|
||||
currentRequestId = null;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
let lines = value.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line !== '') {
|
||||
console.log(line);
|
||||
if (line === 'data: [DONE]') {
|
||||
// responseMessage.done = true;
|
||||
messages = messages;
|
||||
} else {
|
||||
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||
console.log(data);
|
||||
|
||||
if ('request_id' in data) {
|
||||
currentRequestId = data.request_id;
|
||||
} else {
|
||||
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
|
||||
responseMessage.content += data.choices[0].delta.content ?? '';
|
||||
messages = messages;
|
||||
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (selectedModelId) {
|
||||
loading = true;
|
||||
|
||||
if (mode === 'complete') {
|
||||
await textCompletionHandler();
|
||||
} else if (mode === 'chat') {
|
||||
await chatCompletionHandler();
|
||||
}
|
||||
|
||||
loading = false;
|
||||
stopResponseFlag = false;
|
||||
currentRequestId = null;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user?.role !== 'admin') {
|
||||
await goto('/');
|
||||
}
|
||||
|
||||
if ($settings?.models) {
|
||||
selectedModelId = $settings?.models[0];
|
||||
} else if ($config?.default_models) {
|
||||
selectedModelId = $config?.default_models.split(',')[0];
|
||||
} else {
|
||||
selectedModelId = '';
|
||||
}
|
||||
loaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{`Playground | ${$WEBUI_NAME}`}
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
|
||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-[100dvh]">
|
||||
<div class="max-w-2xl mx-auto w-full px-3 p-3 md:px-0 h-full">
|
||||
<div class=" flex flex-col h-full">
|
||||
<div class="flex flex-col justify-between mb-2.5 gap-1">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class=" text-2xl font-semibold self-center flex">
|
||||
Playground <span class=" text-xs text-gray-500 self-center ml-1">(Beta)</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class=" flex items-center gap-0.5 text-xs px-2.5 py-0.5 rounded-lg {mode ===
|
||||
'chat' && 'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {mode === 'complete' &&
|
||||
'text-green-600 dark:text-green-200 bg-green-200/30'} "
|
||||
on:click={() => {
|
||||
if (mode === 'complete') {
|
||||
mode = 'chat';
|
||||
} else {
|
||||
mode = 'complete';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if mode === 'complete'}
|
||||
Text Completion
|
||||
{:else if mode === 'chat'}
|
||||
Chat
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.22 10.22a.75.75 0 0 1 1.06 0L8 11.94l1.72-1.72a.75.75 0 1 1 1.06 1.06l-2.25 2.25a.75.75 0 0 1-1.06 0l-2.25-2.25a.75.75 0 0 1 0-1.06ZM10.78 5.78a.75.75 0 0 1-1.06 0L8 4.06 6.28 5.78a.75.75 0 0 1-1.06-1.06l2.25-2.25a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex gap-1 px-1">
|
||||
<select
|
||||
id="models"
|
||||
class="outline-none bg-transparent text-sm font-medium rounded-lg w-full placeholder-gray-400"
|
||||
bind:value={selectedModelId}
|
||||
>
|
||||
<option class=" text-gray-800" value="" selected disabled>Select a model</option>
|
||||
|
||||
{#each $models as model}
|
||||
{#if model.name === 'hr'}
|
||||
<hr />
|
||||
{:else}
|
||||
<option value={model.id} class="text-gray-800 text-lg"
|
||||
>{model.name +
|
||||
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<!-- <button
|
||||
class=" self-center dark:hover:text-gray-300"
|
||||
id="open-settings-button"
|
||||
on:click={async () => {}}
|
||||
>
|
||||
<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="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if mode === 'chat'}
|
||||
<div class="p-1">
|
||||
<div class="p-3 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg">
|
||||
<div class=" text-sm font-medium">System</div>
|
||||
<textarea
|
||||
id="system-textarea"
|
||||
class="w-full h-full bg-transparent resize-none outline-none text-sm"
|
||||
bind:value={system}
|
||||
placeholder="You're a helpful assistant."
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
|
||||
id="messages-container"
|
||||
bind:this={messagesContainerElement}
|
||||
>
|
||||
<div class=" h-full w-full flex flex-col">
|
||||
<div class="flex-1 p-1">
|
||||
{#if mode === 'complete'}
|
||||
<textarea
|
||||
id="text-completion-textarea"
|
||||
bind:this={textCompletionAreaElement}
|
||||
class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm"
|
||||
bind:value={text}
|
||||
placeholder="You're a helpful assistant."
|
||||
/>
|
||||
{:else if mode === 'chat'}
|
||||
<ChatCompletion bind:messages />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pb-2">
|
||||
{#if !loading}
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
|
||||
on:click={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-900 transition rounded-lg"
|
||||
on:click={() => {
|
||||
stopResponse();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.scrollbar-hidden::-webkit-scrollbar {
|
||||
display: none; /* for Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.scrollbar-hidden {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { prompts } from '$lib/stores';
|
||||
import { WEBUI_NAME, prompts } from '$lib/stores';
|
||||
import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let importFiles = '';
|
||||
let query = '';
|
||||
|
||||
let promptsImportInputElement: HTMLInputElement;
|
||||
const sharePrompt = async (prompt) => {
|
||||
toast.success('Redirecting you to OpenWebUI Community');
|
||||
|
||||
@@ -36,6 +36,12 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{`Prompts | ${$WEBUI_NAME}`}
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white">
|
||||
<div class="flex flex-col justify-between w-full overflow-y-auto">
|
||||
<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10">
|
||||
@@ -202,6 +208,7 @@
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
id="prompts-import-input"
|
||||
bind:this={promptsImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
@@ -235,9 +242,7 @@
|
||||
|
||||
<button
|
||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||
on:click={async () => {
|
||||
document.getElementById('prompts-import-input')?.click();
|
||||
}}
|
||||
on:click={promptsImportInputElement.click}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium">Import Prompts</div>
|
||||
|
||||
@@ -260,7 +265,7 @@
|
||||
<button
|
||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
|
||||
on:click={async () => {
|
||||
// document.getElementById('modelfiles-import-input')?.click();
|
||||
// promptsImportInputElement.click();
|
||||
let blob = new Blob([JSON.stringify($prompts)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { prompts } from '$lib/stores';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { prompts } from '$lib/stores';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { config, user, theme, WEBUI_NAME } from '$lib/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import toast, { Toaster } from 'svelte-french-toast';
|
||||
import { Toaster, toast } from 'svelte-sonner';
|
||||
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
import { getSessionUser } from '$lib/apis/auths';
|
||||
@@ -68,4 +68,4 @@
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
<Toaster />
|
||||
<Toaster richColors position="top-center" />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_NAME, config, user } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let loaded = false;
|
||||
let mode = 'signin';
|
||||
@@ -57,6 +57,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{`${$WEBUI_NAME}`}
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if loaded}
|
||||
<div class="fixed m-10 z-50">
|
||||
<div class="flex space-x-2">
|
||||
@@ -66,7 +72,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" bg-white min-h-screen w-full flex justify-center font-mona">
|
||||
<div class=" bg-white dark:bg-gray-900 min-h-screen w-full flex justify-center font-mona">
|
||||
<!-- <div class="hidden lg:flex lg:flex-1 px-10 md:px-16 w-full bg-yellow-50 justify-center">
|
||||
<div class=" my-auto pb-16 text-left">
|
||||
<div>
|
||||
@@ -81,15 +87,15 @@
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="w-full max-w-lg px-10 md:px-16 bg-white min-h-screen flex flex-col">
|
||||
<div class="w-full sm:max-w-lg px-4 min-h-screen flex flex-col">
|
||||
<div class=" my-auto pb-10 w-full">
|
||||
<form
|
||||
class=" flex flex-col justify-center"
|
||||
class=" flex flex-col justify-center bg-white py-6 sm:py-16 px-6 sm:px-16 rounded-2xl"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" text-xl md:text-2xl font-bold">
|
||||
<div class=" text-xl sm:text-2xl font-bold">
|
||||
{mode === 'signin' ? 'Sign in' : 'Sign up'} to {$WEBUI_NAME}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# update_llm.sh
|
||||
|
||||
# Retrieves the list of LLMs installed in the Docker container
|
||||
llm_list=$(docker exec ollama ollama list | tail -n +2 | awk '{print $1}')
|
||||
|
||||
# Loop over each LLM to update it
|
||||
for llm in $llm_list; do
|
||||
docker exec ollama ollama pull $llm
|
||||
done
|
||||
Reference in New Issue
Block a user