forked from open-webui/open-webui
Merge remote-tracking branch 'upstream/dev' into feat/add-i18n
This commit is contained in:
commit
25e0f0de42
26 changed files with 1445 additions and 668 deletions
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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.
|
||||
|
||||
|
|
|
@ -3,16 +3,23 @@ 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_BASE_URL, WEBUI_AUTH
|
||||
|
||||
from typing import Optional, List, Union
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
@ -23,26 +30,44 @@ app.add_middleware(
|
|||
)
|
||||
|
||||
app.state.OLLAMA_BASE_URL = OLLAMA_BASE_URL
|
||||
|
||||
# TARGET_SERVER_URL = OLLAMA_API_BASE_URL
|
||||
app.state.OLLAMA_BASE_URLS = [OLLAMA_BASE_URL]
|
||||
app.state.MODELS = {}
|
||||
|
||||
|
||||
REQUEST_POOL = []
|
||||
|
||||
|
||||
@app.get("/url")
|
||||
async def get_ollama_api_url(user=Depends(get_admin_user)):
|
||||
return {"OLLAMA_BASE_URL": app.state.OLLAMA_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_BASE_URL = form_data.url
|
||||
return {"OLLAMA_BASE_URL": app.state.OLLAMA_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 +80,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_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)
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -108,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
|
||||
)
|
||||
|
@ -118,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,
|
||||
|
@ -355,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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_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,7 +64,7 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
|
|||
throw error;
|
||||
}
|
||||
|
||||
return res.OLLAMA_BASE_URL;
|
||||
return res.OLLAMA_BASE_URLS;
|
||||
};
|
||||
|
||||
export const getOllamaVersion = async (token: string = '') => {
|
||||
|
@ -151,7 +151,8 @@ export const generateTitle = async (
|
|||
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({
|
||||
|
@ -189,7 +190,8 @@ export const generatePrompt = async (token: string = '', model: string, conversa
|
|||
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({
|
||||
|
@ -223,7 +225,8 @@ export const generateTextCompletion = async (token: string = '', model: string,
|
|||
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({
|
||||
|
@ -251,7 +254,8 @@ export const generateChatCompletion = async (token: string = '', body: object) =
|
|||
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)
|
||||
|
@ -294,7 +298,8 @@ export const createModel = async (token: string, tagName: string, content: strin
|
|||
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({
|
||||
|
@ -313,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}/api/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();
|
||||
|
@ -336,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;
|
||||
});
|
||||
|
||||
|
@ -347,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}/api/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({
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
export let suggestionPrompts = [];
|
||||
export let autoScroll = true;
|
||||
|
||||
let chatTextAreaElement:HTMLTextAreaElement
|
||||
let filesInputElement;
|
||||
|
||||
let promptsElement;
|
||||
|
@ -45,11 +45,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';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,9 +86,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);
|
||||
|
@ -193,8 +189,7 @@
|
|||
prompt = `${prompt}${transcript}`;
|
||||
|
||||
await tick();
|
||||
const inputElement = document.getElementById('chat-textarea');
|
||||
inputElement?.focus();
|
||||
chatTextAreaElement?.focus();
|
||||
|
||||
// Restart the inactivity timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
|
@ -296,8 +291,7 @@
|
|||
};
|
||||
|
||||
onMount(() => {
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
window.setTimeout(() => chatInput?.focus(), 0);
|
||||
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
|
||||
|
||||
const dropZone = document.querySelector('body');
|
||||
|
||||
|
@ -671,6 +665,7 @@
|
|||
|
||||
<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]"
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
let edit = false;
|
||||
let editedContent = '';
|
||||
|
||||
let editTextAreaElement: HTMLTextAreaElement;
|
||||
let tooltipInstance = null;
|
||||
|
||||
let sentencesAudio = {};
|
||||
|
@ -249,10 +249,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 () => {
|
||||
|
@ -343,6 +342,7 @@
|
|||
<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) => {
|
||||
|
|
|
@ -22,18 +22,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 () => {
|
||||
|
@ -168,10 +167,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.scrollHeight}px`;
|
||||
messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
let name = '';
|
||||
let showJWTToken = false;
|
||||
let JWTTokenCopied = false;
|
||||
let profileImageInputElement: HTMLInputElement;
|
||||
|
||||
const submitHandler = async () => {
|
||||
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
||||
|
@ -42,11 +43,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}`;
|
||||
|
@ -88,7 +90,7 @@
|
|||
// Display the compressed image
|
||||
profileImageUrl = compressedSrc;
|
||||
|
||||
e.target.files = null;
|
||||
profileImageInputElement.files = null;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -109,9 +111,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'}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
let saveChatHistory = true;
|
||||
let importFiles;
|
||||
let showDeleteConfirm = false;
|
||||
let chatImportInputElement: HTMLInputElement;
|
||||
|
||||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
|
@ -161,12 +162,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,7 +3,7 @@
|
|||
import { createEventDispatcher, onMount, getContext } 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-sonner';
|
||||
|
||||
|
@ -12,7 +12,8 @@
|
|||
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 = '';
|
||||
|
@ -27,8 +28,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);
|
||||
|
@ -43,7 +44,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);
|
||||
}
|
||||
|
@ -55,11 +56,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">
|
||||
|
@ -116,35 +112,82 @@
|
|||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('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)"
|
||||
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">
|
||||
|
|
|
@ -2,7 +2,13 @@
|
|||
import queue from 'async/queue';
|
||||
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';
|
||||
|
@ -15,7 +21,7 @@
|
|||
|
||||
let showLiteLLM = false;
|
||||
let showLiteLLMParams = false;
|
||||
|
||||
let modelUploadInputElement: HTMLInputElement;
|
||||
let liteLLMModelInfo = [];
|
||||
|
||||
let liteLLMModel = '';
|
||||
|
@ -29,6 +35,9 @@
|
|||
$: liteLLMModelName = liteLLMModel;
|
||||
|
||||
// Models
|
||||
|
||||
let OLLAMA_URLS = [];
|
||||
let selectedOllamaUrlIdx: string | null = null;
|
||||
let showExperimentalOllama = false;
|
||||
let ollamaVersion = '';
|
||||
const MAX_PARALLEL_DOWNLOADS = 3;
|
||||
|
@ -246,9 +255,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($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
|
||||
|
@ -259,10 +270,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
|
||||
|
@ -368,6 +381,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);
|
||||
});
|
||||
|
@ -377,52 +399,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">{$i18n.t('Manage Ollama Models')}</div>
|
||||
<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
|
||||
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('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">{$i18n.t('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) 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"
|
||||
|
@ -430,263 +537,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">
|
||||
{$i18n.t('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">{$i18n.t('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">{$i18n.t('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={$i18n.t('Select a model')}
|
||||
>
|
||||
{#if !deleteModelTag}
|
||||
<option value="" disabled selected>{$i18n.t('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">{$i18n.t('Experimental')}</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showExperimentalOllama = !showExperimentalOllama;
|
||||
}}>{showExperimentalOllama ? $i18n.t('Show') : $i18n.t('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">{$i18n.t('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">{$i18n.t('File Mode')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('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 Hugging Face 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">{$i18n.t('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 underline"
|
||||
href="https://huggingface.co/models?search=gguf"
|
||||
target="_blank">{$i18n.t('click here.')}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if uploadProgress !== null}
|
||||
<div class="mt-2">
|
||||
<div class=" mb-2 text-xs">{$i18n.t('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}
|
||||
|
@ -704,7 +740,7 @@
|
|||
type="button"
|
||||
on:click={() => {
|
||||
showLiteLLMParams = !showLiteLLMParams;
|
||||
}}>{showLiteLLMParams ? $i18n.t('Advanced') : $i18n.t('Default')}</button
|
||||
}}>{showLiteLLMParams ? 'Hide Additional Params' : 'Show Additional Params'}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -713,7 +749,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"
|
||||
|
@ -721,7 +757,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();
|
||||
}}
|
||||
|
@ -745,7 +781,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"
|
||||
|
@ -759,7 +795,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"
|
||||
|
@ -773,7 +809,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"
|
||||
|
@ -787,7 +823,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"
|
||||
|
@ -814,7 +850,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={$i18n.t('Select a model')}
|
||||
>
|
||||
|
@ -829,7 +865,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();
|
||||
}}
|
||||
|
|
|
@ -29,6 +29,6 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={tooltipElement}>
|
||||
<div bind:this={tooltipElement} aria-label={content}>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
export let show = false;
|
||||
export let selectedDoc;
|
||||
|
||||
let uploadDocInputElement: HTMLInputElement;
|
||||
let inputFiles;
|
||||
let tags = [];
|
||||
|
||||
|
@ -71,7 +71,7 @@
|
|||
}
|
||||
|
||||
inputFiles = null;
|
||||
document.getElementById('upload-doc-input').value = '';
|
||||
uploadDocInputElement.value = '';
|
||||
} else {
|
||||
toast.error($i18n.t(`File not found.`));
|
||||
}
|
||||
|
@ -128,14 +128,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.
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
const i18n = getContext('i18n');
|
||||
|
||||
export let messages = [];
|
||||
|
||||
let textAreaElement: HTMLTextAreaElement;
|
||||
onMount(() => {
|
||||
messages.forEach((message, idx) => {
|
||||
let textareaElement = document.getElementById(`${message.role}-${idx}-textarea`);
|
||||
textareaElement.style.height = '';
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -29,18 +28,19 @@
|
|||
<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={$i18n.t(
|
||||
`Enter ${message.role === 'user' ? 'a user' : 'an assistant'} message here`
|
||||
)}
|
||||
rows="1"
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = e.target.scrollHeight + 'px';
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
|
||||
}}
|
||||
on:focus={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = e.target.scrollHeight + 'px';
|
||||
textAreaElement.style.height = '';
|
||||
textAreaElement.style.height = textAreaElement.scrollHeight + '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` : ``;
|
|||
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`;
|
||||
|
|
|
@ -34,12 +34,13 @@
|
|||
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';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let ollamaVersion = '';
|
||||
let loaded = false;
|
||||
|
||||
let showShortcutsButtonElement: HTMLButtonElement;
|
||||
let DB = null;
|
||||
let localDBChats = [];
|
||||
|
||||
|
@ -186,7 +187,7 @@
|
|||
if (isCtrlPressed && event.key === '/') {
|
||||
event.preventDefault();
|
||||
console.log('showShortcuts');
|
||||
document.getElementById('show-shortcuts-button')?.click();
|
||||
showShortcutsButtonElement.click();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -203,15 +204,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} />
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
let stopResponseFlag = false;
|
||||
let autoScroll = true;
|
||||
let processing = '';
|
||||
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
let currentRequestId = null;
|
||||
|
||||
let selectedModels = [''];
|
||||
|
@ -143,8 +143,7 @@
|
|||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const element = document.getElementById('messages-container');
|
||||
element.scrollTop = element.scrollHeight;
|
||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||
};
|
||||
|
||||
//////////////////////////
|
||||
|
@ -837,8 +836,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
|
||||
|
@ -846,10 +848,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">
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
let stopResponseFlag = false;
|
||||
let autoScroll = true;
|
||||
let processing = '';
|
||||
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
let currentRequestId = null;
|
||||
|
||||
// let chatId = $page.params.id;
|
||||
|
@ -162,8 +162,7 @@
|
|||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const element = document.getElementById('messages-container');
|
||||
element.scrollTop = element.scrollHeight;
|
||||
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
||||
};
|
||||
|
||||
//////////////////////////
|
||||
|
@ -865,8 +864,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
|
||||
|
@ -874,10 +876,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">
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
let inputFiles = '';
|
||||
let query = '';
|
||||
|
||||
let documentsImportInputElement: HTMLInputElement;
|
||||
let tags = [];
|
||||
|
||||
let showSettingsModal = false;
|
||||
|
@ -527,6 +527,7 @@
|
|||
<div class="flex space-x-2">
|
||||
<input
|
||||
id="documents-import-input"
|
||||
bind:this={documentsImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
|
@ -561,9 +562,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">{$i18n.t('Import Documents Mapping')}</div>
|
||||
|
||||
|
|
|
@ -18,11 +18,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($i18n.t(`Deleted {tagName}`, { tagName }));
|
||||
|
@ -237,6 +240,7 @@
|
|||
<div class="flex space-x-1">
|
||||
<input
|
||||
id="modelfiles-import-input"
|
||||
bind:this={modelfilesImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
|
@ -264,9 +268,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">{$i18n.t('Import Modelfiles')}</div>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
|
@ -23,15 +23,17 @@
|
|||
|
||||
let mode = 'chat';
|
||||
let loaded = false;
|
||||
|
||||
let text = '';
|
||||
|
||||
let selectedModelId = '';
|
||||
|
||||
let loading = false;
|
||||
let currentRequestId;
|
||||
let currentRequestId = null;
|
||||
let stopResponseFlag = false;
|
||||
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
let textCompletionAreaElement: HTMLTextAreaElement;
|
||||
|
||||
let system = '';
|
||||
let messages = [
|
||||
{
|
||||
|
@ -41,13 +43,7 @@
|
|||
];
|
||||
|
||||
const scrollToBottom = () => {
|
||||
let element;
|
||||
|
||||
if (mode === 'chat') {
|
||||
element = document.getElementById('messages-container');
|
||||
} else {
|
||||
element = document.getElementById('text-completion-textarea');
|
||||
}
|
||||
const element = mode === 'chat' ? messagesContainerElement : textCompletionAreaElement;
|
||||
|
||||
if (element) {
|
||||
element.scrollTop = element?.scrollHeight;
|
||||
|
@ -98,6 +94,10 @@
|
|||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done || stopResponseFlag) {
|
||||
if (stopResponseFlag) {
|
||||
await cancelChatCompletion(localStorage.token, currentRequestId);
|
||||
}
|
||||
|
||||
currentRequestId = null;
|
||||
break;
|
||||
}
|
||||
|
@ -114,7 +114,11 @@
|
|||
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||
console.log(data);
|
||||
|
||||
text += data.choices[0].delta.content ?? '';
|
||||
if ('request_id' in data) {
|
||||
currentRequestId = data.request_id;
|
||||
} else {
|
||||
text += data.choices[0].delta.content ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,16 +156,6 @@
|
|||
: `${OLLAMA_API_BASE_URL}/v1`
|
||||
);
|
||||
|
||||
// const [res, controller] = await generateChatCompletion(localStorage.token, {
|
||||
// model: selectedModelId,
|
||||
// messages: [
|
||||
// {
|
||||
// role: 'assistant',
|
||||
// content: text
|
||||
// }
|
||||
// ]
|
||||
// });
|
||||
|
||||
let responseMessage;
|
||||
if (messages.at(-1)?.role === 'assistant') {
|
||||
responseMessage = messages.at(-1);
|
||||
|
@ -186,6 +180,11 @@
|
|||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done || stopResponseFlag) {
|
||||
if (stopResponseFlag) {
|
||||
await cancelChatCompletion(localStorage.token, currentRequestId);
|
||||
}
|
||||
|
||||
currentRequestId = null;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -202,17 +201,21 @@
|
|||
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||
console.log(data);
|
||||
|
||||
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
|
||||
continue;
|
||||
if ('request_id' in data) {
|
||||
currentRequestId = data.request_id;
|
||||
} else {
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
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;
|
||||
responseMessage.content += data.choices[0].delta.content ?? '';
|
||||
messages = messages;
|
||||
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
|
||||
await tick();
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -223,48 +226,6 @@
|
|||
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 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);
|
||||
// let data = JSON.parse(line);
|
||||
|
||||
// if ('detail' in data) {
|
||||
// throw data;
|
||||
// }
|
||||
|
||||
// if ('id' in data) {
|
||||
// console.log(data);
|
||||
// currentRequestId = data.id;
|
||||
// } else {
|
||||
// if (data.done == false) {
|
||||
// text += data.message.content;
|
||||
// } else {
|
||||
// console.log('done');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
|
||||
// scrollToBottom();
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -422,12 +383,14 @@
|
|||
<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={$i18n.t("You're a helpful assistant.")}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
let importFiles = '';
|
||||
let query = '';
|
||||
|
||||
let promptsImportInputElement: HTMLInputElement;
|
||||
const sharePrompt = async (prompt) => {
|
||||
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
||||
|
||||
|
@ -210,6 +210,7 @@
|
|||
<div class="flex space-x-2">
|
||||
<input
|
||||
id="prompts-import-input"
|
||||
bind:this={promptsImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
|
@ -243,9 +244,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">{$i18n.t('Import Prompts')}</div>
|
||||
|
||||
|
@ -268,7 +267,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'
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue