Commit 25e0f0de authored by Ased Mammad's avatar Ased Mammad
Browse files

Merge remote-tracking branch 'upstream/dev' into feat/add-i18n

parents df8aeb39 3455f899
...@@ -32,7 +32,7 @@ assignees: '' ...@@ -32,7 +32,7 @@ assignees: ''
**Confirmation:** **Confirmation:**
- [ ] I have read and followed all the instructions provided in the README.md. - [ ] 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 browser console logs.
- [ ] I have included the Docker container logs. - [ ] I have included the Docker container logs.
......
This diff is collapsed.
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,
)
...@@ -108,7 +108,7 @@ class StoreWebForm(CollectionNameForm): ...@@ -108,7 +108,7 @@ class StoreWebForm(CollectionNameForm):
url: str 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( text_splitter = RecursiveCharacterTextSplitter(
chunk_size=app.state.CHUNK_SIZE, chunk_overlap=app.state.CHUNK_OVERLAP chunk_size=app.state.CHUNK_SIZE, chunk_overlap=app.state.CHUNK_OVERLAP
) )
...@@ -118,6 +118,12 @@ def store_data_in_vector_db(data, collection_name) -> bool: ...@@ -118,6 +118,12 @@ def store_data_in_vector_db(data, collection_name) -> bool:
metadatas = [doc.metadata for doc in docs] metadatas = [doc.metadata for doc in docs]
try: 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( collection = CHROMA_CLIENT.create_collection(
name=collection_name, name=collection_name,
embedding_function=app.state.sentence_transformer_ef, embedding_function=app.state.sentence_transformer_ef,
...@@ -355,7 +361,7 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)): ...@@ -355,7 +361,7 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
if collection_name == "": if collection_name == "":
collection_name = calculate_sha256_string(form_data.url)[:63] 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 { return {
"status": True, "status": True,
"collection_name": collection_name, "collection_name": collection_name,
......
...@@ -48,3 +48,5 @@ class ERROR_MESSAGES(str, Enum): ...@@ -48,3 +48,5 @@ class ERROR_MESSAGES(str, Enum):
lambda err="": f"Invalid format. Please use the correct format{err if err else ''}" lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
) )
RATE_LIMIT_EXCEEDED = "API rate limit exceeded" RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
...@@ -104,7 +104,7 @@ async def auth_middleware(request: Request, call_next): ...@@ -104,7 +104,7 @@ async def auth_middleware(request: Request, call_next):
app.mount("/api/v1", webui_app) app.mount("/api/v1", webui_app)
app.mount("/litellm/api", litellm_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("/openai/api", openai_app)
app.mount("/images/api/v1", images_app) app.mount("/images/api/v1", images_app)
...@@ -125,6 +125,14 @@ async def get_app_config(): ...@@ -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") @app.get("/api/changelog")
async def get_app_changelog(): async def get_app_changelog():
return CHANGELOG return CHANGELOG
......
...@@ -22,6 +22,7 @@ google-generativeai ...@@ -22,6 +22,7 @@ google-generativeai
langchain langchain
langchain-community langchain-community
fake_useragent
chromadb chromadb
sentence_transformers sentence_transformers
pypdf pypdf
......
import { OLLAMA_API_BASE_URL } from '$lib/constants'; import { OLLAMA_API_BASE_URL } from '$lib/constants';
export const getOllamaAPIUrl = async (token: string = '') => { export const getOllamaUrls = async (token: string = '') => {
let error = null; let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/url`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/urls`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
...@@ -29,13 +29,13 @@ export const getOllamaAPIUrl = async (token: string = '') => { ...@@ -29,13 +29,13 @@ export const getOllamaAPIUrl = async (token: string = '') => {
throw error; throw error;
} }
return res.OLLAMA_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; 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', method: 'POST',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
...@@ -43,7 +43,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => { ...@@ -43,7 +43,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
...(token && { authorization: `Bearer ${token}` }) ...(token && { authorization: `Bearer ${token}` })
}, },
body: JSON.stringify({ body: JSON.stringify({
url: url urls: urls
}) })
}) })
.then(async (res) => { .then(async (res) => {
...@@ -64,7 +64,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => { ...@@ -64,7 +64,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
throw error; throw error;
} }
return res.OLLAMA_BASE_URL; return res.OLLAMA_BASE_URLS;
}; };
export const getOllamaVersion = async (token: string = '') => { export const getOllamaVersion = async (token: string = '') => {
...@@ -151,7 +151,8 @@ export const generateTitle = async ( ...@@ -151,7 +151,8 @@ export const generateTitle = async (
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/event-stream', Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
...@@ -189,7 +190,8 @@ export const generatePrompt = async (token: string = '', model: string, conversa ...@@ -189,7 +190,8 @@ export const generatePrompt = async (token: string = '', model: string, conversa
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/event-stream', Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
...@@ -223,7 +225,8 @@ export const generateTextCompletion = async (token: string = '', model: string, ...@@ -223,7 +225,8 @@ export const generateTextCompletion = async (token: string = '', model: string,
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/event-stream', Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
...@@ -251,7 +254,8 @@ export const generateChatCompletion = async (token: string = '', body: object) = ...@@ -251,7 +254,8 @@ export const generateChatCompletion = async (token: string = '', body: object) =
signal: controller.signal, signal: controller.signal,
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/event-stream', Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
body: JSON.stringify(body) body: JSON.stringify(body)
...@@ -294,7 +298,8 @@ export const createModel = async (token: string, tagName: string, content: strin ...@@ -294,7 +298,8 @@ export const createModel = async (token: string, tagName: string, content: strin
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/event-stream', Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
...@@ -313,19 +318,23 @@ export const createModel = async (token: string, tagName: string, content: strin ...@@ -313,19 +318,23 @@ export const createModel = async (token: string, tagName: string, content: strin
return res; 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; let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/delete`, { const res = await fetch(
method: 'DELETE', `${OLLAMA_API_BASE_URL}/api/delete${urlIdx !== null ? `/${urlIdx}` : ''}`,
headers: { {
'Content-Type': 'text/event-stream', method: 'DELETE',
Authorization: `Bearer ${token}` headers: {
}, Accept: 'application/json',
body: JSON.stringify({ 'Content-Type': 'application/json',
name: tagName Authorization: `Bearer ${token}`
}) },
}) body: JSON.stringify({
name: tagName
})
}
)
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
...@@ -336,7 +345,12 @@ export const deleteModel = async (token: string, tagName: string) => { ...@@ -336,7 +345,12 @@ export const deleteModel = async (token: string, tagName: string) => {
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err);
error = err.error; error = err;
if ('detail' in err) {
error = err.detail;
}
return null; return null;
}); });
...@@ -347,13 +361,14 @@ export const deleteModel = async (token: string, tagName: string) => { ...@@ -347,13 +361,14 @@ export const deleteModel = async (token: string, tagName: string) => {
return res; 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; let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull${urlIdx !== null ? `/${urlIdx}` : ''}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/event-stream', Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
export let suggestionPrompts = []; export let suggestionPrompts = [];
export let autoScroll = true; export let autoScroll = true;
let chatTextAreaElement:HTMLTextAreaElement
let filesInputElement; let filesInputElement;
let promptsElement; let promptsElement;
...@@ -45,11 +45,9 @@ ...@@ -45,11 +45,9 @@
let speechRecognition; let speechRecognition;
$: if (prompt) { $: if (prompt) {
const chatInput = document.getElementById('chat-textarea'); if (chatTextAreaElement) {
chatTextAreaElement.style.height = '';
if (chatInput) { chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
chatInput.style.height = '';
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
} }
} }
...@@ -88,9 +86,7 @@ ...@@ -88,9 +86,7 @@
if (res) { if (res) {
prompt = res.text; prompt = res.text;
await tick(); await tick();
chatTextAreaElement?.focus();
const inputElement = document.getElementById('chat-textarea');
inputElement?.focus();
if (prompt !== '' && $settings?.speechAutoSend === true) { if (prompt !== '' && $settings?.speechAutoSend === true) {
submitPrompt(prompt, user); submitPrompt(prompt, user);
...@@ -193,8 +189,7 @@ ...@@ -193,8 +189,7 @@
prompt = `${prompt}${transcript}`; prompt = `${prompt}${transcript}`;
await tick(); await tick();
const inputElement = document.getElementById('chat-textarea'); chatTextAreaElement?.focus();
inputElement?.focus();
// Restart the inactivity timeout // Restart the inactivity timeout
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
...@@ -296,8 +291,7 @@ ...@@ -296,8 +291,7 @@
}; };
onMount(() => { onMount(() => {
const chatInput = document.getElementById('chat-textarea'); window.setTimeout(() => chatTextAreaElement?.focus(), 0);
window.setTimeout(() => chatInput?.focus(), 0);
const dropZone = document.querySelector('body'); const dropZone = document.querySelector('body');
...@@ -671,6 +665,7 @@ ...@@ -671,6 +665,7 @@
<textarea <textarea
id="chat-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 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]" : ' pl-4'} rounded-xl resize-none h-[48px]"
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
let edit = false; let edit = false;
let editedContent = ''; let editedContent = '';
let editTextAreaElement: HTMLTextAreaElement;
let tooltipInstance = null; let tooltipInstance = null;
let sentencesAudio = {}; let sentencesAudio = {};
...@@ -249,10 +249,9 @@ ...@@ -249,10 +249,9 @@
editedContent = message.content; editedContent = message.content;
await tick(); await tick();
const editElement = document.getElementById(`message-edit-${message.id}`);
editElement.style.height = ''; editTextAreaElement.style.height = '';
editElement.style.height = `${editElement.scrollHeight}px`; editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
}; };
const editMessageConfirmHandler = async () => { const editMessageConfirmHandler = async () => {
...@@ -343,6 +342,7 @@ ...@@ -343,6 +342,7 @@
<div class=" w-full"> <div class=" w-full">
<textarea <textarea
id="message-edit-{message.id}" id="message-edit-{message.id}"
bind:this={editTextAreaElement}
class=" bg-transparent outline-none w-full resize-none" class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent} bind:value={editedContent}
on:input={(e) => { on:input={(e) => {
......
...@@ -22,18 +22,17 @@ ...@@ -22,18 +22,17 @@
let edit = false; let edit = false;
let editedContent = ''; let editedContent = '';
let messageEditTextAreaElement: HTMLTextAreaElement;
const editMessageHandler = async () => { const editMessageHandler = async () => {
edit = true; edit = true;
editedContent = message.content; editedContent = message.content;
await tick(); await tick();
const editElement = document.getElementById(`message-edit-${message.id}`);
editElement.style.height = ''; messageEditTextAreaElement.style.height = '';
editElement.style.height = `${editElement.scrollHeight}px`; messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
editElement?.focus(); messageEditTextAreaElement?.focus();
}; };
const editMessageConfirmHandler = async () => { const editMessageConfirmHandler = async () => {
...@@ -168,10 +167,11 @@ ...@@ -168,10 +167,11 @@
<div class=" w-full"> <div class=" w-full">
<textarea <textarea
id="message-edit-{message.id}" id="message-edit-{message.id}"
bind:this={messageEditTextAreaElement}
class=" bg-transparent outline-none w-full resize-none" class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent} bind:value={editedContent}
on:input={(e) => { on:input={(e) => {
e.target.style.height = `${e.target.scrollHeight}px`; messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
}} }}
/> />
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
let name = ''; let name = '';
let showJWTToken = false; let showJWTToken = false;
let JWTTokenCopied = false; let JWTTokenCopied = false;
let profileImageInputElement: HTMLInputElement;
const submitHandler = async () => { const submitHandler = async () => {
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch( const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
...@@ -42,11 +43,12 @@ ...@@ -42,11 +43,12 @@
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<input <input
id="profile-image-input" id="profile-image-input"
bind:this={profileImageInputElement}
type="file" type="file"
hidden hidden
accept="image/*" accept="image/*"
on:change={(e) => { on:change={(e) => {
const files = e?.target?.files ?? []; const files = profileImageInputElement.files ?? [];
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
let originalImageUrl = `${event.target.result}`; let originalImageUrl = `${event.target.result}`;
...@@ -88,7 +90,7 @@ ...@@ -88,7 +90,7 @@
// Display the compressed image // Display the compressed image
profileImageUrl = compressedSrc; profileImageUrl = compressedSrc;
e.target.files = null; profileImageInputElement.files = null;
}; };
}; };
...@@ -109,9 +111,7 @@ ...@@ -109,9 +111,7 @@
<button <button
class="relative rounded-full dark:bg-gray-700" class="relative rounded-full dark:bg-gray-700"
type="button" type="button"
on:click={() => { on:click={profileImageInputElement.click}
document.getElementById('profile-image-input')?.click();
}}
> >
<img <img
src={profileImageUrl !== '' ? profileImageUrl : '/user.png'} src={profileImageUrl !== '' ? profileImageUrl : '/user.png'}
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
let saveChatHistory = true; let saveChatHistory = true;
let importFiles; let importFiles;
let showDeleteConfirm = false; let showDeleteConfirm = false;
let chatImportInputElement: HTMLInputElement;
$: if (importFiles) { $: if (importFiles) {
console.log(importFiles); console.log(importFiles);
...@@ -161,12 +162,17 @@ ...@@ -161,12 +162,17 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div class="flex flex-col"> <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 <button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={chatImportInputElement.click}
document.getElementById('chat-import-input').click();
}}
> >
<div class=" self-center mr-3"> <div class=" self-center mr-3">
<svg <svg
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
const dispatch = createEventDispatcher(); 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 { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
...@@ -12,7 +12,8 @@ ...@@ -12,7 +12,8 @@
export let getModels: Function; export let getModels: Function;
// External // External
let API_BASE_URL = ''; let OLLAMA_BASE_URL = '';
let OLLAMA_BASE_URLS = [''];
let OPENAI_API_KEY = ''; let OPENAI_API_KEY = '';
let OPENAI_API_BASE_URL = ''; let OPENAI_API_BASE_URL = '';
...@@ -27,8 +28,8 @@ ...@@ -27,8 +28,8 @@
await models.set(await getModels()); await models.set(await getModels());
}; };
const updateOllamaAPIUrlHandler = async () => { const updateOllamaUrlsHandler = async () => {
API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL); OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => { const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
toast.error(error); toast.error(error);
...@@ -43,7 +44,7 @@ ...@@ -43,7 +44,7 @@
onMount(async () => { onMount(async () => {
if ($user.role === 'admin') { 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_BASE_URL = await getOpenAIUrl(localStorage.token);
OPENAI_API_KEY = await getOpenAIKey(localStorage.token); OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
} }
...@@ -55,11 +56,6 @@ ...@@ -55,11 +56,6 @@
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
updateOpenAIHandler(); updateOpenAIHandler();
dispatch('save'); 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"> <div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3">
...@@ -116,35 +112,82 @@ ...@@ -116,35 +112,82 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama API URL')}</div> <div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div>
<div class="flex w-full"> <div class="flex w-full gap-1.5">
<div class="flex-1 mr-2"> <div class="flex-1 flex flex-col gap-2">
<input {#each OLLAMA_BASE_URLS as url, idx}
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" <div class="flex gap-1.5">
placeholder="Enter URL (e.g. http://localhost:11434)" <input
bind:value={API_BASE_URL} 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> </div>
<button
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition" <div class="">
on:click={() => { <button
updateOllamaAPIUrlHandler(); 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={() => {
type="button" updateOllamaUrlsHandler();
> }}
<svg type="button"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
> >
<path <svg
fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
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" viewBox="0 0 20 20"
clip-rule="evenodd" fill="currentColor"
/> class="w-4 h-4"
</svg> >
</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>
</div>
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
......
...@@ -29,6 +29,6 @@ ...@@ -29,6 +29,6 @@
}); });
</script> </script>
<div bind:this={tooltipElement}> <div bind:this={tooltipElement} aria-label={content}>
<slot /> <slot />
</div> </div>
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
export let show = false; export let show = false;
export let selectedDoc; export let selectedDoc;
let uploadDocInputElement: HTMLInputElement;
let inputFiles; let inputFiles;
let tags = []; let tags = [];
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
} }
inputFiles = null; inputFiles = null;
document.getElementById('upload-doc-input').value = ''; uploadDocInputElement.value = '';
} else { } else {
toast.error($i18n.t(`File not found.`)); toast.error($i18n.t(`File not found.`));
} }
...@@ -128,14 +128,19 @@ ...@@ -128,14 +128,19 @@
}} }}
> >
<div class="mb-3 w-full"> <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 <button
class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl" class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
type="button" type="button"
on:click={() => { on:click={uploadDocInputElement.click}
document.getElementById('upload-doc-input')?.click();
}}
> >
{#if inputFiles} {#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected. {inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
......
...@@ -4,12 +4,11 @@ ...@@ -4,12 +4,11 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let messages = []; export let messages = [];
let textAreaElement: HTMLTextAreaElement;
onMount(() => { onMount(() => {
messages.forEach((message, idx) => { messages.forEach((message, idx) => {
let textareaElement = document.getElementById(`${message.role}-${idx}-textarea`); textAreaElement.style.height = '';
textareaElement.style.height = ''; textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
textareaElement.style.height = textareaElement.scrollHeight + 'px';
}); });
}); });
</script> </script>
...@@ -29,18 +28,19 @@ ...@@ -29,18 +28,19 @@
<div class="flex-1"> <div class="flex-1">
<textarea <textarea
id="{message.role}-{idx}-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" class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
placeholder={$i18n.t( placeholder={$i18n.t(
`Enter ${message.role === 'user' ? 'a user' : 'an assistant'} message here` `Enter ${message.role === 'user' ? 'a user' : 'an assistant'} message here`
)} )}
rows="1" rows="1"
on:input={(e) => { on:input={(e) => {
e.target.style.height = ''; textAreaElement.style.height = '';
e.target.style.height = e.target.scrollHeight + 'px'; textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
}} }}
on:focus={(e) => { on:focus={(e) => {
e.target.style.height = ''; textAreaElement.style.height = '';
e.target.style.height = e.target.scrollHeight + 'px'; textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; // e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}} }}
......
...@@ -7,7 +7,7 @@ export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``; ...@@ -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 WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
export const LITELLM_API_BASE_URL = `${WEBUI_BASE_URL}/litellm/api`; 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 OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`; export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`;
export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`; export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`;
......
...@@ -34,12 +34,13 @@ ...@@ -34,12 +34,13 @@
import Sidebar from '$lib/components/layout/Sidebar.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte';
import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte'; import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
import ChangelogModal from '$lib/components/ChangelogModal.svelte'; import ChangelogModal from '$lib/components/ChangelogModal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
let ollamaVersion = ''; let ollamaVersion = '';
let loaded = false; let loaded = false;
let showShortcutsButtonElement: HTMLButtonElement;
let DB = null; let DB = null;
let localDBChats = []; let localDBChats = [];
...@@ -186,7 +187,7 @@ ...@@ -186,7 +187,7 @@
if (isCtrlPressed && event.key === '/') { if (isCtrlPressed && event.key === '/') {
event.preventDefault(); event.preventDefault();
console.log('showShortcuts'); console.log('showShortcuts');
document.getElementById('show-shortcuts-button')?.click(); showShortcutsButtonElement.click();
} }
}); });
...@@ -203,15 +204,18 @@ ...@@ -203,15 +204,18 @@
{#if loaded} {#if loaded}
<div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10"> <div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10">
<button <Tooltip content="help" placement="left">
id="show-shortcuts-button" <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" id="show-shortcuts-button"
on:click={() => { bind:this={showShortcutsButtonElement}
showShortcuts = !showShortcuts; 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> >
?
</button>
</Tooltip>
</div> </div>
<ShortcutsModal bind:show={showShortcuts} /> <ShortcutsModal bind:show={showShortcuts} />
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment