forked from open-webui/open-webui
		
	Merge branch 'main' into rag
This commit is contained in:
		
						commit
						fa598b59e2
					
				
					 31 changed files with 1917 additions and 143 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -24,7 +24,6 @@ dist/ | ||||||
| downloads/ | downloads/ | ||||||
| eggs/ | eggs/ | ||||||
| .eggs/ | .eggs/ | ||||||
| lib/ |  | ||||||
| lib64/ | lib64/ | ||||||
| parts/ | parts/ | ||||||
| sdist/ | sdist/ | ||||||
|  |  | ||||||
|  | @ -33,6 +33,8 @@ Also check our sibling project, [OllamaHub](https://ollamahub.com/), where you c | ||||||
| 
 | 
 | ||||||
| - ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction. | - ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction. | ||||||
| 
 | 
 | ||||||
|  | - 📜 **Prompt Preset Support**: Instantly access preset prompts using the '/' command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [OllamaHub](https://ollamahub.com/) integration. | ||||||
|  | 
 | ||||||
| - 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI. | - 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI. | ||||||
| 
 | 
 | ||||||
| - ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face. | - ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face. | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| from fastapi import FastAPI, Depends | from fastapi import FastAPI, Depends | ||||||
| from fastapi.routing import APIRoute | from fastapi.routing import APIRoute | ||||||
| from fastapi.middleware.cors import CORSMiddleware | from fastapi.middleware.cors import CORSMiddleware | ||||||
| from apps.web.routers import auths, users, chats, modelfiles, utils | from apps.web.routers import auths, users, chats, modelfiles, prompts, configs, utils | ||||||
| from config import WEBUI_VERSION, WEBUI_AUTH | from config import WEBUI_VERSION, WEBUI_AUTH | ||||||
| 
 | 
 | ||||||
| app = FastAPI() | app = FastAPI() | ||||||
|  | @ -9,6 +9,7 @@ app = FastAPI() | ||||||
| origins = ["*"] | origins = ["*"] | ||||||
| 
 | 
 | ||||||
| app.state.ENABLE_SIGNUP = True | app.state.ENABLE_SIGNUP = True | ||||||
|  | app.state.DEFAULT_MODELS = None | ||||||
| 
 | 
 | ||||||
| app.add_middleware( | app.add_middleware( | ||||||
|     CORSMiddleware, |     CORSMiddleware, | ||||||
|  | @ -19,13 +20,21 @@ app.add_middleware( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| app.include_router(auths.router, prefix="/auths", tags=["auths"]) | app.include_router(auths.router, prefix="/auths", tags=["auths"]) | ||||||
| 
 |  | ||||||
| app.include_router(users.router, prefix="/users", tags=["users"]) | app.include_router(users.router, prefix="/users", tags=["users"]) | ||||||
| app.include_router(chats.router, prefix="/chats", tags=["chats"]) | app.include_router(chats.router, prefix="/chats", tags=["chats"]) | ||||||
| app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"]) | app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"]) | ||||||
|  | app.include_router(prompts.router, prefix="/prompts", tags=["prompts"]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | app.include_router(configs.router, prefix="/configs", tags=["configs"]) | ||||||
| app.include_router(utils.router, prefix="/utils", tags=["utils"]) | app.include_router(utils.router, prefix="/utils", tags=["utils"]) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.get("/") | @app.get("/") | ||||||
| async def get_status(): | async def get_status(): | ||||||
|     return {"status": True, "version": WEBUI_VERSION, "auth": WEBUI_AUTH} |     return { | ||||||
|  |         "status": True, | ||||||
|  |         "version": WEBUI_VERSION, | ||||||
|  |         "auth": WEBUI_AUTH, | ||||||
|  |         "default_models": app.state.DEFAULT_MODELS, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ from apps.web.internal.db import DB | ||||||
| import json | import json | ||||||
| 
 | 
 | ||||||
| #################### | #################### | ||||||
| # User DB Schema | # Modelfile DB Schema | ||||||
| #################### | #################### | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										117
									
								
								backend/apps/web/models/prompts.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								backend/apps/web/models/prompts.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,117 @@ | ||||||
|  | from pydantic import BaseModel | ||||||
|  | from peewee import * | ||||||
|  | from playhouse.shortcuts import model_to_dict | ||||||
|  | from typing import List, Union, Optional | ||||||
|  | import time | ||||||
|  | 
 | ||||||
|  | from utils.utils import decode_token | ||||||
|  | from utils.misc import get_gravatar_url | ||||||
|  | 
 | ||||||
|  | from apps.web.internal.db import DB | ||||||
|  | 
 | ||||||
|  | import json | ||||||
|  | 
 | ||||||
|  | #################### | ||||||
|  | # Prompts DB Schema | ||||||
|  | #################### | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Prompt(Model): | ||||||
|  |     command = CharField(unique=True) | ||||||
|  |     user_id = CharField() | ||||||
|  |     title = CharField() | ||||||
|  |     content = TextField() | ||||||
|  |     timestamp = DateField() | ||||||
|  | 
 | ||||||
|  |     class Meta: | ||||||
|  |         database = DB | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PromptModel(BaseModel): | ||||||
|  |     command: str | ||||||
|  |     user_id: str | ||||||
|  |     title: str | ||||||
|  |     content: str | ||||||
|  |     timestamp: int  # timestamp in epoch | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #################### | ||||||
|  | # Forms | ||||||
|  | #################### | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PromptForm(BaseModel): | ||||||
|  |     command: str | ||||||
|  |     title: str | ||||||
|  |     content: str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PromptsTable: | ||||||
|  |     def __init__(self, db): | ||||||
|  |         self.db = db | ||||||
|  |         self.db.create_tables([Prompt]) | ||||||
|  | 
 | ||||||
|  |     def insert_new_prompt( | ||||||
|  |         self, user_id: str, form_data: PromptForm | ||||||
|  |     ) -> Optional[PromptModel]: | ||||||
|  |         prompt = PromptModel( | ||||||
|  |             **{ | ||||||
|  |                 "user_id": user_id, | ||||||
|  |                 "command": form_data.command, | ||||||
|  |                 "title": form_data.title, | ||||||
|  |                 "content": form_data.content, | ||||||
|  |                 "timestamp": int(time.time()), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             result = Prompt.create(**prompt.model_dump()) | ||||||
|  |             if result: | ||||||
|  |                 return prompt | ||||||
|  |             else: | ||||||
|  |                 return None | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def get_prompt_by_command(self, command: str) -> Optional[PromptModel]: | ||||||
|  |         try: | ||||||
|  |             prompt = Prompt.get(Prompt.command == command) | ||||||
|  |             return PromptModel(**model_to_dict(prompt)) | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def get_prompts(self) -> List[PromptModel]: | ||||||
|  |         return [ | ||||||
|  |             PromptModel(**model_to_dict(prompt)) | ||||||
|  |             for prompt in Prompt.select() | ||||||
|  |             # .limit(limit).offset(skip) | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |     def update_prompt_by_command( | ||||||
|  |         self, command: str, form_data: PromptForm | ||||||
|  |     ) -> Optional[PromptModel]: | ||||||
|  |         try: | ||||||
|  |             query = Prompt.update( | ||||||
|  |                 title=form_data.title, | ||||||
|  |                 content=form_data.content, | ||||||
|  |                 timestamp=int(time.time()), | ||||||
|  |             ).where(Prompt.command == command) | ||||||
|  | 
 | ||||||
|  |             query.execute() | ||||||
|  | 
 | ||||||
|  |             prompt = Prompt.get(Prompt.command == command) | ||||||
|  |             return PromptModel(**model_to_dict(prompt)) | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def delete_prompt_by_command(self, command: str) -> bool: | ||||||
|  |         try: | ||||||
|  |             query = Prompt.delete().where((Prompt.command == command)) | ||||||
|  |             query.execute()  # Remove the rows, return number of rows removed. | ||||||
|  | 
 | ||||||
|  |             return True | ||||||
|  |         except: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Prompts = PromptsTable(DB) | ||||||
|  | @ -8,6 +8,7 @@ from pydantic import BaseModel | ||||||
| import time | import time | ||||||
| import uuid | import uuid | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| from apps.web.models.auths import ( | from apps.web.models.auths import ( | ||||||
|     SigninForm, |     SigninForm, | ||||||
|     SignupForm, |     SignupForm, | ||||||
|  | @ -20,7 +21,7 @@ from apps.web.models.users import Users | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| from utils.utils import get_password_hash, get_current_user, create_token | from utils.utils import get_password_hash, get_current_user, create_token | ||||||
| from utils.misc import get_gravatar_url | from utils.misc import get_gravatar_url, validate_email_format | ||||||
| from constants import ERROR_MESSAGES | from constants import ERROR_MESSAGES | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -95,6 +96,7 @@ async def signin(form_data: SigninForm): | ||||||
| @router.post("/signup", response_model=SigninResponse) | @router.post("/signup", response_model=SigninResponse) | ||||||
| async def signup(request: Request, form_data: SignupForm): | async def signup(request: Request, form_data: SignupForm): | ||||||
|     if request.app.state.ENABLE_SIGNUP: |     if request.app.state.ENABLE_SIGNUP: | ||||||
|  |         if validate_email_format(form_data.email.lower()): | ||||||
|             if not Users.get_user_by_email(form_data.email.lower()): |             if not Users.get_user_by_email(form_data.email.lower()): | ||||||
|                 try: |                 try: | ||||||
|                     role = "admin" if Users.get_num_users() == 0 else "pending" |                     role = "admin" if Users.get_num_users() == 0 else "pending" | ||||||
|  | @ -117,11 +119,15 @@ async def signup(request: Request, form_data: SignupForm): | ||||||
|                             "profile_image_url": user.profile_image_url, |                             "profile_image_url": user.profile_image_url, | ||||||
|                         } |                         } | ||||||
|                     else: |                     else: | ||||||
|                     raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) |                         raise HTTPException( | ||||||
|  |                             500, detail=ERROR_MESSAGES.CREATE_USER_ERROR | ||||||
|  |                         ) | ||||||
|                 except Exception as err: |                 except Exception as err: | ||||||
|                     raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) |                     raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) | ||||||
|             else: |             else: | ||||||
|                 raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) |                 raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) | ||||||
|  |         else: | ||||||
|  |             raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) | ||||||
|     else: |     else: | ||||||
|         raise HTTPException(400, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) |         raise HTTPException(400, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										41
									
								
								backend/apps/web/routers/configs.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								backend/apps/web/routers/configs.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | ||||||
|  | from fastapi import Response, Request | ||||||
|  | from fastapi import Depends, FastAPI, HTTPException, status | ||||||
|  | from datetime import datetime, timedelta | ||||||
|  | from typing import List, Union | ||||||
|  | 
 | ||||||
|  | from fastapi import APIRouter | ||||||
|  | from pydantic import BaseModel | ||||||
|  | import time | ||||||
|  | import uuid | ||||||
|  | 
 | ||||||
|  | from apps.web.models.users import Users | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from utils.utils import get_password_hash, get_current_user, create_token | ||||||
|  | from utils.misc import get_gravatar_url, validate_email_format | ||||||
|  | from constants import ERROR_MESSAGES | ||||||
|  | 
 | ||||||
|  | router = APIRouter() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SetDefaultModelsForm(BaseModel): | ||||||
|  |     models: str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # SetDefaultModels | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.post("/default/models", response_model=str) | ||||||
|  | async def set_global_default_models( | ||||||
|  |     request: Request, form_data: SetDefaultModelsForm, user=Depends(get_current_user) | ||||||
|  | ): | ||||||
|  |     if user.role == "admin": | ||||||
|  |         request.app.state.DEFAULT_MODELS = form_data.models | ||||||
|  |         return request.app.state.DEFAULT_MODELS | ||||||
|  |     else: | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_403_FORBIDDEN, | ||||||
|  |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|  |         ) | ||||||
							
								
								
									
										115
									
								
								backend/apps/web/routers/prompts.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								backend/apps/web/routers/prompts.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | ||||||
|  | from fastapi import Depends, FastAPI, HTTPException, status | ||||||
|  | from datetime import datetime, timedelta | ||||||
|  | from typing import List, Union, Optional | ||||||
|  | 
 | ||||||
|  | from fastapi import APIRouter | ||||||
|  | from pydantic import BaseModel | ||||||
|  | import json | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from apps.web.models.prompts import Prompts, PromptForm, PromptModel | ||||||
|  | 
 | ||||||
|  | from utils.utils import get_current_user | ||||||
|  | from constants import ERROR_MESSAGES | ||||||
|  | 
 | ||||||
|  | router = APIRouter() | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # GetPrompts | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.get("/", response_model=List[PromptModel]) | ||||||
|  | async def get_prompts(user=Depends(get_current_user)): | ||||||
|  |     return Prompts.get_prompts() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # CreateNewPrompt | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.post("/create", response_model=Optional[PromptModel]) | ||||||
|  | async def create_new_prompt(form_data: PromptForm, user=Depends(get_current_user)): | ||||||
|  |     if user.role != "admin": | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     prompt = Prompts.get_prompt_by_command(form_data.command) | ||||||
|  |     if prompt == None: | ||||||
|  |         prompt = Prompts.insert_new_prompt(user.id, form_data) | ||||||
|  | 
 | ||||||
|  |         if prompt: | ||||||
|  |             return prompt | ||||||
|  |         else: | ||||||
|  |             raise HTTPException( | ||||||
|  |                 status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |                 detail=ERROR_MESSAGES.DEFAULT(), | ||||||
|  |             ) | ||||||
|  |     else: | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_400_BAD_REQUEST, | ||||||
|  |             detail=ERROR_MESSAGES.COMMAND_TAKEN, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # GetPromptByCommand | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.get("/{command}", response_model=Optional[PromptModel]) | ||||||
|  | async def get_prompt_by_command(command: str, user=Depends(get_current_user)): | ||||||
|  |     prompt = Prompts.get_prompt_by_command(f"/{command}") | ||||||
|  | 
 | ||||||
|  |     if prompt: | ||||||
|  |         return prompt | ||||||
|  |     else: | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |             detail=ERROR_MESSAGES.NOT_FOUND, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # UpdatePromptByCommand | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.post("/{command}/update", response_model=Optional[PromptModel]) | ||||||
|  | async def update_prompt_by_command( | ||||||
|  |     command: str, form_data: PromptForm, user=Depends(get_current_user) | ||||||
|  | ): | ||||||
|  |     if user.role != "admin": | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     prompt = Prompts.update_prompt_by_command(f"/{command}", form_data) | ||||||
|  |     if prompt: | ||||||
|  |         return prompt | ||||||
|  |     else: | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ############################ | ||||||
|  | # DeletePromptByCommand | ||||||
|  | ############################ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @router.delete("/{command}/delete", response_model=bool) | ||||||
|  | async def delete_prompt_by_command(command: str, user=Depends(get_current_user)): | ||||||
|  |     if user.role != "admin": | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     result = Prompts.delete_prompt_by_command(f"/{command}") | ||||||
|  |     return result | ||||||
|  | @ -17,10 +17,12 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     USERNAME_TAKEN = ( |     USERNAME_TAKEN = ( | ||||||
|         "Uh-oh! This username is already registered. Please choose another username." |         "Uh-oh! This username is already registered. Please choose another username." | ||||||
|     ) |     ) | ||||||
|  |     COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string." | ||||||
|     INVALID_TOKEN = ( |     INVALID_TOKEN = ( | ||||||
|         "Your session has expired or the token is invalid. Please sign in again." |         "Your session has expired or the token is invalid. Please sign in again." | ||||||
|     ) |     ) | ||||||
|     INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again." |     INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again." | ||||||
|  |     INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)." | ||||||
|     INVALID_PASSWORD = ( |     INVALID_PASSWORD = ( | ||||||
|         "The password provided is incorrect. Please check for typos and try again." |         "The password provided is incorrect. Please check for typos and try again." | ||||||
|     ) |     ) | ||||||
|  | @ -31,5 +33,4 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     ) |     ) | ||||||
|     NOT_FOUND = "We could not find what you're looking for :/" |     NOT_FOUND = "We could not find what you're looking for :/" | ||||||
|     USER_NOT_FOUND = "We could not find what you're looking for :/" |     USER_NOT_FOUND = "We could not find what you're looking for :/" | ||||||
| 
 |  | ||||||
|     MALICIOUS = "Unusual activities detected, please try again in a few minutes." |     MALICIOUS = "Unusual activities detected, please try again in a few minutes." | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								backend/start.sh
									
										
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										2
									
								
								backend/start.sh
									
										
									
									
									
										
										
										Normal file → Executable file
									
								
							|  | @ -1 +1,3 @@ | ||||||
|  | #!/usr/bin/env bash | ||||||
|  | 
 | ||||||
| uvicorn main:app --host 0.0.0.0 --port 8080 --forwarded-allow-ips '*' | uvicorn main:app --host 0.0.0.0 --port 8080 --forwarded-allow-ips '*' | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import hashlib | import hashlib | ||||||
|  | import re | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_gravatar_url(email): | def get_gravatar_url(email): | ||||||
|  | @ -21,3 +22,9 @@ def calculate_sha256(file): | ||||||
|     for chunk in iter(lambda: file.read(8192), b""): |     for chunk in iter(lambda: file.read(8192), b""): | ||||||
|         sha256.update(chunk) |         sha256.update(chunk) | ||||||
|     return sha256.hexdigest() |     return sha256.hexdigest() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def validate_email_format(email: str) -> bool: | ||||||
|  |     if not re.match(r"[^@]+@[^@]+\.[^@]+", email): | ||||||
|  |         return False | ||||||
|  |     return True | ||||||
|  |  | ||||||
|  | @ -12,11 +12,12 @@ | ||||||
| 				(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches) | 				(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches) | ||||||
| 			) { | 			) { | ||||||
| 				document.documentElement.classList.add('light'); | 				document.documentElement.classList.add('light'); | ||||||
| 			} else if (localStorage.theme === 'dark') { | 			} else if (localStorage.theme) { | ||||||
| 				document.documentElement.classList.add('dark'); | 				localStorage.theme.split(' ').forEach((e) => { | ||||||
|  | 					document.documentElement.classList.add(e); | ||||||
|  | 				}); | ||||||
| 			} else { | 			} else { | ||||||
| 				document.documentElement.classList.add('dark'); | 				document.documentElement.classList.add('dark'); | ||||||
| 				document.documentElement.classList.add(localStorage.theme); |  | ||||||
| 			} | 			} | ||||||
| 		</script> | 		</script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										31
									
								
								src/lib/apis/configs/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/lib/apis/configs/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | ||||||
|  | import { WEBUI_API_BASE_URL } from '$lib/constants'; | ||||||
|  | 
 | ||||||
|  | export const setDefaultModels = async (token: string, models: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/default/models`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			Authorization: `Bearer ${token}` | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify({ | ||||||
|  | 			models: models | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err); | ||||||
|  | 			error = err.detail; | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
							
								
								
									
										178
									
								
								src/lib/apis/prompts/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/lib/apis/prompts/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,178 @@ | ||||||
|  | import { WEBUI_API_BASE_URL } from '$lib/constants'; | ||||||
|  | 
 | ||||||
|  | export const createNewPrompt = async ( | ||||||
|  | 	token: string, | ||||||
|  | 	command: string, | ||||||
|  | 	title: string, | ||||||
|  | 	content: string | ||||||
|  | ) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/create`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			authorization: `Bearer ${token}` | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify({ | ||||||
|  | 			command: `/${command}`, | ||||||
|  | 			title: title, | ||||||
|  | 			content: content | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			error = err.detail; | ||||||
|  | 			console.log(err); | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getPrompts = async (token: string = '') => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/`, { | ||||||
|  | 		method: 'GET', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			authorization: `Bearer ${token}` | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.then((json) => { | ||||||
|  | 			return json; | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			error = err.detail; | ||||||
|  | 			console.log(err); | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getPromptByCommand = async (token: string, command: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}`, { | ||||||
|  | 		method: 'GET', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			authorization: `Bearer ${token}` | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.then((json) => { | ||||||
|  | 			return json; | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			error = err.detail; | ||||||
|  | 
 | ||||||
|  | 			console.log(err); | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const updatePromptByCommand = async ( | ||||||
|  | 	token: string, | ||||||
|  | 	command: string, | ||||||
|  | 	title: string, | ||||||
|  | 	content: string | ||||||
|  | ) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}/update`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			authorization: `Bearer ${token}` | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify({ | ||||||
|  | 			command: `/${command}`, | ||||||
|  | 			title: title, | ||||||
|  | 			content: content | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.then((json) => { | ||||||
|  | 			return json; | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			error = err.detail; | ||||||
|  | 
 | ||||||
|  | 			console.log(err); | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const deletePromptByCommand = async (token: string, command: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	command = command.charAt(0) === '/' ? command.slice(1) : command; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}/delete`, { | ||||||
|  | 		method: 'DELETE', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			authorization: `Bearer ${token}` | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.then((json) => { | ||||||
|  | 			return json; | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			error = err.detail; | ||||||
|  | 
 | ||||||
|  | 			console.log(err); | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | @ -1,8 +1,11 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { settings } from '$lib/stores'; |  | ||||||
| 	import toast from 'svelte-french-toast'; | 	import toast from 'svelte-french-toast'; | ||||||
|  | 	import { onMount, tick } from 'svelte'; | ||||||
|  | 	import { settings } from '$lib/stores'; | ||||||
|  | 	import { findWordIndices } from '$lib/utils'; | ||||||
|  | 
 | ||||||
|  | 	import Prompts from './MessageInput/PromptCommands.svelte'; | ||||||
| 	import Suggestions from './MessageInput/Suggestions.svelte'; | 	import Suggestions from './MessageInput/Suggestions.svelte'; | ||||||
| 	import { onMount } from 'svelte'; |  | ||||||
| 
 | 
 | ||||||
| 	export let submitPrompt: Function; | 	export let submitPrompt: Function; | ||||||
| 	export let stopResponse: Function; | 	export let stopResponse: Function; | ||||||
|  | @ -11,6 +14,8 @@ | ||||||
| 	export let autoScroll = true; | 	export let autoScroll = true; | ||||||
| 
 | 
 | ||||||
| 	let filesInputElement; | 	let filesInputElement; | ||||||
|  | 	let promptsElement; | ||||||
|  | 
 | ||||||
| 	let inputFiles; | 	let inputFiles; | ||||||
| 	let dragged = false; | 	let dragged = false; | ||||||
| 
 | 
 | ||||||
|  | @ -154,12 +159,8 @@ | ||||||
| 
 | 
 | ||||||
| <div class="fixed bottom-0 w-full"> | <div class="fixed bottom-0 w-full"> | ||||||
| 	<div class="px-2.5 pt-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> | 	<div class="px-2.5 pt-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> | ||||||
| 		{#if messages.length == 0 && suggestionPrompts.length !== 0} | 		<div class="flex flex-col max-w-3xl w-full"> | ||||||
| 			<div class="max-w-3xl w-full"> | 			<div> | ||||||
| 				<Suggestions {suggestionPrompts} {submitPrompt} /> |  | ||||||
| 			</div> |  | ||||||
| 		{/if} |  | ||||||
| 
 |  | ||||||
| 				{#if autoScroll === false && messages.length > 0} | 				{#if autoScroll === false && messages.length > 0} | ||||||
| 					<div class=" flex justify-center mb-4"> | 					<div class=" flex justify-center mb-4"> | ||||||
| 						<button | 						<button | ||||||
|  | @ -185,6 +186,16 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				{/if} | 				{/if} | ||||||
| 			</div> | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div class="w-full"> | ||||||
|  | 				{#if prompt.charAt(0) === '/'} | ||||||
|  | 					<Prompts bind:this={promptsElement} bind:prompt /> | ||||||
|  | 				{:else if messages.length == 0 && suggestionPrompts.length !== 0} | ||||||
|  | 					<Suggestions {suggestionPrompts} {submitPrompt} /> | ||||||
|  | 				{/if} | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
| 	<div class="bg-white dark:bg-gray-800"> | 	<div class="bg-white dark:bg-gray-800"> | ||||||
| 		<div class="max-w-3xl px-2.5 -mb-0.5 mx-auto inset-x-0"> | 		<div class="max-w-3xl px-2.5 -mb-0.5 mx-auto inset-x-0"> | ||||||
| 			<div class="bg-gradient-to-t from-white dark:from-gray-800 from-40% pb-2"> | 			<div class="bg-gradient-to-t from-white dark:from-gray-800 from-40% pb-2"> | ||||||
|  | @ -287,7 +298,7 @@ | ||||||
| 							id="chat-textarea" | 							id="chat-textarea" | ||||||
| 							class=" dark:bg-gray-800 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled | 							class=" dark:bg-gray-800 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled | ||||||
| 								? '' | 								? '' | ||||||
| 								: ' pl-4'} rounded-xl resize-none" | 								: ' pl-4'} rounded-xl resize-none h-[48px]" | ||||||
| 							placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'} | 							placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'} | ||||||
| 							bind:value={prompt} | 							bind:value={prompt} | ||||||
| 							on:keypress={(e) => { | 							on:keypress={(e) => { | ||||||
|  | @ -298,7 +309,7 @@ | ||||||
| 									submitPrompt(prompt); | 									submitPrompt(prompt); | ||||||
| 								} | 								} | ||||||
| 							}} | 							}} | ||||||
| 							on:keydown={(e) => { | 							on:keydown={async (e) => { | ||||||
| 								if (prompt === '' && e.key == 'ArrowUp') { | 								if (prompt === '' && e.key == 'ArrowUp') { | ||||||
| 									e.preventDefault(); | 									e.preventDefault(); | ||||||
| 
 | 
 | ||||||
|  | @ -315,6 +326,61 @@ | ||||||
| 									userMessageElement.scrollIntoView({ block: 'center' }); | 									userMessageElement.scrollIntoView({ block: 'center' }); | ||||||
| 									editButton?.click(); | 									editButton?.click(); | ||||||
| 								} | 								} | ||||||
|  | 
 | ||||||
|  | 								if (prompt.charAt(0) === '/' && e.key === 'ArrowUp') { | ||||||
|  | 									promptsElement.selectUp(); | ||||||
|  | 
 | ||||||
|  | 									const commandOptionButton = [ | ||||||
|  | 										...document.getElementsByClassName('selected-command-option-button') | ||||||
|  | 									]?.at(-1); | ||||||
|  | 									commandOptionButton.scrollIntoView({ block: 'center' }); | ||||||
|  | 								} | ||||||
|  | 
 | ||||||
|  | 								if (prompt.charAt(0) === '/' && e.key === 'ArrowDown') { | ||||||
|  | 									promptsElement.selectDown(); | ||||||
|  | 
 | ||||||
|  | 									const commandOptionButton = [ | ||||||
|  | 										...document.getElementsByClassName('selected-command-option-button') | ||||||
|  | 									]?.at(-1); | ||||||
|  | 									commandOptionButton.scrollIntoView({ block: 'center' }); | ||||||
|  | 								} | ||||||
|  | 
 | ||||||
|  | 								if (prompt.charAt(0) === '/' && e.key === 'Enter') { | ||||||
|  | 									e.preventDefault(); | ||||||
|  | 
 | ||||||
|  | 									const commandOptionButton = [ | ||||||
|  | 										...document.getElementsByClassName('selected-command-option-button') | ||||||
|  | 									]?.at(-1); | ||||||
|  | 
 | ||||||
|  | 									commandOptionButton?.click(); | ||||||
|  | 								} | ||||||
|  | 
 | ||||||
|  | 								if (prompt.charAt(0) === '/' && e.key === 'Tab') { | ||||||
|  | 									e.preventDefault(); | ||||||
|  | 
 | ||||||
|  | 									const commandOptionButton = [ | ||||||
|  | 										...document.getElementsByClassName('selected-command-option-button') | ||||||
|  | 									]?.at(-1); | ||||||
|  | 
 | ||||||
|  | 									commandOptionButton?.click(); | ||||||
|  | 								} else if (e.key === 'Tab') { | ||||||
|  | 									const words = findWordIndices(prompt); | ||||||
|  | 
 | ||||||
|  | 									if (words.length > 0) { | ||||||
|  | 										const word = words.at(0); | ||||||
|  | 										const fullPrompt = prompt; | ||||||
|  | 
 | ||||||
|  | 										prompt = prompt.substring(0, word?.endIndex + 1); | ||||||
|  | 										await tick(); | ||||||
|  | 
 | ||||||
|  | 										e.target.scrollTop = e.target.scrollHeight; | ||||||
|  | 										prompt = fullPrompt; | ||||||
|  | 										await tick(); | ||||||
|  | 
 | ||||||
|  | 										e.preventDefault(); | ||||||
|  | 										e.target.setSelectionRange(word?.startIndex, word.endIndex + 1); | ||||||
|  | 									} | ||||||
|  | 								} | ||||||
| 							}} | 							}} | ||||||
| 							rows="1" | 							rows="1" | ||||||
| 							on:input={(e) => { | 							on:input={(e) => { | ||||||
|  |  | ||||||
							
								
								
									
										111
									
								
								src/lib/components/chat/MessageInput/PromptCommands.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/lib/components/chat/MessageInput/PromptCommands.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,111 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 	import { prompts } from '$lib/stores'; | ||||||
|  | 	import { findWordIndices } from '$lib/utils'; | ||||||
|  | 	import { tick } from 'svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let prompt = ''; | ||||||
|  | 	let selectedCommandIdx = 0; | ||||||
|  | 	let filteredPromptCommands = []; | ||||||
|  | 
 | ||||||
|  | 	$: filteredPromptCommands = $prompts | ||||||
|  | 		.filter((p) => p.command.includes(prompt)) | ||||||
|  | 		.sort((a, b) => a.title.localeCompare(b.title)); | ||||||
|  | 
 | ||||||
|  | 	$: if (prompt) { | ||||||
|  | 		selectedCommandIdx = 0; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	export const selectUp = () => { | ||||||
|  | 		selectedCommandIdx = Math.max(0, selectedCommandIdx - 1); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	export const selectDown = () => { | ||||||
|  | 		selectedCommandIdx = Math.min(selectedCommandIdx + 1, filteredPromptCommands.length - 1); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const confirmCommand = async (command) => { | ||||||
|  | 		prompt = command.content; | ||||||
|  | 
 | ||||||
|  | 		const chatInputElement = document.getElementById('chat-textarea'); | ||||||
|  | 
 | ||||||
|  | 		await tick(); | ||||||
|  | 
 | ||||||
|  | 		chatInputElement.style.height = ''; | ||||||
|  | 		chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px'; | ||||||
|  | 
 | ||||||
|  | 		chatInputElement?.focus(); | ||||||
|  | 
 | ||||||
|  | 		await tick(); | ||||||
|  | 
 | ||||||
|  | 		const words = findWordIndices(prompt); | ||||||
|  | 
 | ||||||
|  | 		if (words.length > 0) { | ||||||
|  | 			const word = words.at(0); | ||||||
|  | 			chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if filteredPromptCommands.length > 0} | ||||||
|  | 	<div class="md:px-2 mb-3 text-left w-full"> | ||||||
|  | 		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700"> | ||||||
|  | 			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center"> | ||||||
|  | 				<div class=" text-lg font-semibold mt-2">/</div> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div class="max-h-60 flex flex-col w-full rounded-r-lg"> | ||||||
|  | 				<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5"> | ||||||
|  | 					{#each filteredPromptCommands as command, commandIdx} | ||||||
|  | 						<button | ||||||
|  | 							class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx | ||||||
|  | 								? ' bg-gray-100 selected-command-option-button' | ||||||
|  | 								: ''}" | ||||||
|  | 							type="button" | ||||||
|  | 							on:click={() => { | ||||||
|  | 								confirmCommand(command); | ||||||
|  | 							}} | ||||||
|  | 							on:mousemove={() => { | ||||||
|  | 								selectedCommandIdx = commandIdx; | ||||||
|  | 							}} | ||||||
|  | 							on:focus={() => {}} | ||||||
|  | 						> | ||||||
|  | 							<div class=" font-medium text-black"> | ||||||
|  | 								{command.command} | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div class=" text-xs text-gray-600"> | ||||||
|  | 								{command.title} | ||||||
|  | 							</div> | ||||||
|  | 						</button> | ||||||
|  | 					{/each} | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div | ||||||
|  | 					class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-lg flex items-center space-x-1" | ||||||
|  | 				> | ||||||
|  | 					<div> | ||||||
|  | 						<svg | ||||||
|  | 							xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 							fill="none" | ||||||
|  | 							viewBox="0 0 24 24" | ||||||
|  | 							stroke-width="1.5" | ||||||
|  | 							stroke="currentColor" | ||||||
|  | 							class="w-3 h-3" | ||||||
|  | 						> | ||||||
|  | 							<path | ||||||
|  | 								stroke-linecap="round" | ||||||
|  | 								stroke-linejoin="round" | ||||||
|  | 								d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" | ||||||
|  | 							/> | ||||||
|  | 						</svg> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="line-clamp-1"> | ||||||
|  | 						Tip: Update multiple variable slots consecutively by pressing the tab key in the chat | ||||||
|  | 						input after each replacement. | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | {/if} | ||||||
|  | @ -1,11 +1,13 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { models, showSettings, settings } from '$lib/stores'; | 	import { setDefaultModels } from '$lib/apis/configs'; | ||||||
|  | 	import { models, showSettings, settings, user } from '$lib/stores'; | ||||||
|  | 	import { onMount, tick } from 'svelte'; | ||||||
| 	import toast from 'svelte-french-toast'; | 	import toast from 'svelte-french-toast'; | ||||||
| 
 | 
 | ||||||
| 	export let selectedModels = ['']; | 	export let selectedModels = ['']; | ||||||
| 	export let disabled = false; | 	export let disabled = false; | ||||||
| 
 | 
 | ||||||
| 	const saveDefaultModel = () => { | 	const saveDefaultModel = async () => { | ||||||
| 		const hasEmptyModel = selectedModels.filter((it) => it === ''); | 		const hasEmptyModel = selectedModels.filter((it) => it === ''); | ||||||
| 		if (hasEmptyModel.length) { | 		if (hasEmptyModel.length) { | ||||||
| 			toast.error('Choose a model before saving...'); | 			toast.error('Choose a model before saving...'); | ||||||
|  | @ -13,8 +15,19 @@ | ||||||
| 		} | 		} | ||||||
| 		settings.set({ ...$settings, models: selectedModels }); | 		settings.set({ ...$settings, models: selectedModels }); | ||||||
| 		localStorage.setItem('settings', JSON.stringify($settings)); | 		localStorage.setItem('settings', JSON.stringify($settings)); | ||||||
|  | 
 | ||||||
|  | 		if ($user.role === 'admin') { | ||||||
|  | 			console.log('setting default models globally'); | ||||||
|  | 			await setDefaultModels(localStorage.token, selectedModels.join(',')); | ||||||
|  | 		} | ||||||
| 		toast.success('Default model updated'); | 		toast.success('Default model updated'); | ||||||
| 	}; | 	}; | ||||||
|  | 
 | ||||||
|  | 	$: if (selectedModels.length > 0 && $models.length > 0) { | ||||||
|  | 		selectedModels = selectedModels.map((model) => | ||||||
|  | 			$models.map((m) => m.name).includes(model) ? model : '' | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="flex flex-col my-2"> | <div class="flex flex-col my-2"> | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ | ||||||
| 
 | 
 | ||||||
| 	// General | 	// General | ||||||
| 	let API_BASE_URL = OLLAMA_API_BASE_URL; | 	let API_BASE_URL = OLLAMA_API_BASE_URL; | ||||||
| 	let themes = ['dark', 'light', 'rose-pine']; | 	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light']; | ||||||
| 	let theme = 'dark'; | 	let theme = 'dark'; | ||||||
| 	let notificationEnabled = false; | 	let notificationEnabled = false; | ||||||
| 	let system = ''; | 	let system = ''; | ||||||
|  | @ -74,17 +74,20 @@ | ||||||
| 
 | 
 | ||||||
| 	let deleteModelTag = ''; | 	let deleteModelTag = ''; | ||||||
| 
 | 
 | ||||||
|  | 	// External | ||||||
|  | 
 | ||||||
|  | 	let OPENAI_API_KEY = ''; | ||||||
|  | 	let OPENAI_API_BASE_URL = ''; | ||||||
|  | 
 | ||||||
| 	// Addons | 	// Addons | ||||||
| 	let titleAutoGenerate = true; | 	let titleAutoGenerate = true; | ||||||
| 	let speechAutoSend = false; | 	let speechAutoSend = false; | ||||||
| 	let responseAutoCopy = false; | 	let responseAutoCopy = false; | ||||||
| 
 | 
 | ||||||
| 	let gravatarEmail = ''; | 	let gravatarEmail = ''; | ||||||
| 	let OPENAI_API_KEY = ''; | 	let titleAutoGenerateModel = ''; | ||||||
| 	let OPENAI_API_BASE_URL = ''; |  | ||||||
| 
 | 
 | ||||||
| 	// Chats | 	// Chats | ||||||
| 
 |  | ||||||
| 	let importFiles; | 	let importFiles; | ||||||
| 	let showDeleteConfirm = false; | 	let showDeleteConfirm = false; | ||||||
| 
 | 
 | ||||||
|  | @ -656,13 +659,14 @@ | ||||||
| 		options = { ...options, ...settings.options }; | 		options = { ...options, ...settings.options }; | ||||||
| 		options.stop = (settings?.options?.stop ?? []).join(','); | 		options.stop = (settings?.options?.stop ?? []).join(','); | ||||||
| 
 | 
 | ||||||
|  | 		OPENAI_API_KEY = settings.OPENAI_API_KEY ?? ''; | ||||||
|  | 		OPENAI_API_BASE_URL = settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'; | ||||||
|  | 
 | ||||||
| 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | ||||||
| 		speechAutoSend = settings.speechAutoSend ?? false; | 		speechAutoSend = settings.speechAutoSend ?? false; | ||||||
| 		responseAutoCopy = settings.responseAutoCopy ?? false; | 		responseAutoCopy = settings.responseAutoCopy ?? false; | ||||||
| 
 | 		titleAutoGenerateModel = settings.titleAutoGenerateModel ?? ''; | ||||||
| 		gravatarEmail = settings.gravatarEmail ?? ''; | 		gravatarEmail = settings.gravatarEmail ?? ''; | ||||||
| 		OPENAI_API_KEY = settings.OPENAI_API_KEY ?? ''; |  | ||||||
| 		OPENAI_API_BASE_URL = settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'; |  | ||||||
| 
 | 
 | ||||||
| 		authEnabled = settings.authHeader !== undefined ? true : false; | 		authEnabled = settings.authHeader !== undefined ? true : false; | ||||||
| 		if (authEnabled) { | 		if (authEnabled) { | ||||||
|  | @ -757,6 +761,7 @@ | ||||||
| 					<div class=" self-center">Advanced</div> | 					<div class=" self-center">Advanced</div> | ||||||
| 				</button> | 				</button> | ||||||
| 
 | 
 | ||||||
|  | 				{#if $user?.role === 'admin'} | ||||||
| 					<button | 					<button | ||||||
| 						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | 						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | ||||||
| 						'models' | 						'models' | ||||||
|  | @ -782,6 +787,7 @@ | ||||||
| 						</div> | 						</div> | ||||||
| 						<div class=" self-center">Models</div> | 						<div class=" self-center">Models</div> | ||||||
| 					</button> | 					</button> | ||||||
|  | 				{/if} | ||||||
| 
 | 
 | ||||||
| 				<button | 				<button | ||||||
| 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | ||||||
|  | @ -994,21 +1000,22 @@ | ||||||
| 											themes | 											themes | ||||||
| 												.filter((e) => e !== theme) | 												.filter((e) => e !== theme) | ||||||
| 												.forEach((e) => { | 												.forEach((e) => { | ||||||
|  | 													e.split(' ').forEach((e) => { | ||||||
| 														document.documentElement.classList.remove(e); | 														document.documentElement.classList.remove(e); | ||||||
| 													}); | 													}); | ||||||
|  | 												}); | ||||||
| 
 | 
 | ||||||
| 											document.documentElement.classList.add(theme); | 											theme.split(' ').forEach((e) => { | ||||||
| 
 | 												document.documentElement.classList.add(e); | ||||||
| 											if (theme === 'rose-pine') { | 											}); | ||||||
| 												document.documentElement.classList.add('dark'); |  | ||||||
| 											} |  | ||||||
| 
 | 
 | ||||||
| 											console.log(theme); | 											console.log(theme); | ||||||
| 										}} | 										}} | ||||||
| 									> | 									> | ||||||
| 										<option value="dark">Dark</option> | 										<option value="dark">Dark</option> | ||||||
| 										<option value="light">Light</option> | 										<option value="light">Light</option> | ||||||
| 										<option value="rose-pine">Rosé Pine</option> | 										<option value="rose-pine dark">Rosé Pine</option> | ||||||
|  | 										<option value="rose-pine-dawn light">Rosé Pine Dawn</option> | ||||||
| 									</select> | 									</select> | ||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
|  | @ -1547,10 +1554,6 @@ | ||||||
| 					<form | 					<form | ||||||
| 						class="flex flex-col h-full justify-between space-y-3 text-sm" | 						class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||||
| 						on:submit|preventDefault={() => { | 						on:submit|preventDefault={() => { | ||||||
| 							saveSettings({ |  | ||||||
| 								gravatarEmail: gravatarEmail !== '' ? gravatarEmail : undefined, |  | ||||||
| 								gravatarUrl: gravatarEmail !== '' ? getGravatarURL(gravatarEmail) : undefined |  | ||||||
| 							}); |  | ||||||
| 							show = false; | 							show = false; | ||||||
| 						}} | 						}} | ||||||
| 					> | 					> | ||||||
|  | @ -1560,7 +1563,7 @@ | ||||||
| 
 | 
 | ||||||
| 								<div> | 								<div> | ||||||
| 									<div class=" py-0.5 flex w-full justify-between"> | 									<div class=" py-0.5 flex w-full justify-between"> | ||||||
| 										<div class=" self-center text-xs font-medium">Title Auto Generation</div> | 										<div class=" self-center text-xs font-medium">Title Auto-Generation</div> | ||||||
| 
 | 
 | ||||||
| 										<button | 										<button | ||||||
| 											class="p-1 px-3 text-xs flex rounded transition" | 											class="p-1 px-3 text-xs flex rounded transition" | ||||||
|  | @ -1622,6 +1625,54 @@ | ||||||
| 							</div> | 							</div> | ||||||
| 
 | 
 | ||||||
| 							<hr class=" dark:border-gray-700" /> | 							<hr class=" dark:border-gray-700" /> | ||||||
|  | 
 | ||||||
|  | 							<div> | ||||||
|  | 								<div class=" mb-2.5 text-sm font-medium">Set Title Auto-Generation Model</div> | ||||||
|  | 								<div class="flex w-full"> | ||||||
|  | 									<div class="flex-1 mr-2"> | ||||||
|  | 										<select | ||||||
|  | 											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||||
|  | 											bind:value={titleAutoGenerateModel} | ||||||
|  | 											placeholder="Select a model" | ||||||
|  | 										> | ||||||
|  | 											<option value="" selected>Default</option> | ||||||
|  | 											{#each $models.filter((m) => m.size != null) as model} | ||||||
|  | 												<option value={model.name} class="bg-gray-100 dark:bg-gray-700" | ||||||
|  | 													>{model.name + | ||||||
|  | 														' (' + | ||||||
|  | 														(model.size / 1024 ** 3).toFixed(1) + | ||||||
|  | 														' GB)'}</option | ||||||
|  | 												> | ||||||
|  | 											{/each} | ||||||
|  | 										</select> | ||||||
|  | 									</div> | ||||||
|  | 									<button | ||||||
|  | 										class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition" | ||||||
|  | 										on:click={() => { | ||||||
|  | 											saveSettings({ | ||||||
|  | 												titleAutoGenerateModel: | ||||||
|  | 													titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined | ||||||
|  | 											}); | ||||||
|  | 										}} | ||||||
|  | 										type="button" | ||||||
|  | 									> | ||||||
|  | 										<svg | ||||||
|  | 											xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 											viewBox="0 0 16 16" | ||||||
|  | 											fill="currentColor" | ||||||
|  | 											class="w-3.5 h-3.5" | ||||||
|  | 										> | ||||||
|  | 											<path | ||||||
|  | 												fill-rule="evenodd" | ||||||
|  | 												d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z" | ||||||
|  | 												clip-rule="evenodd" | ||||||
|  | 											/> | ||||||
|  | 										</svg> | ||||||
|  | 									</button> | ||||||
|  | 								</div> | ||||||
|  | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<!-- <hr class=" dark:border-gray-700" /> | ||||||
| 							<div> | 							<div> | ||||||
| 								<div class=" mb-2.5 text-sm font-medium"> | 								<div class=" mb-2.5 text-sm font-medium"> | ||||||
| 									Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span> | 									Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span> | ||||||
|  | @ -1644,7 +1695,7 @@ | ||||||
| 										target="_blank">Gravatar.</a | 										target="_blank">Gravatar.</a | ||||||
| 									> | 									> | ||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</div> --> | ||||||
| 						</div> | 						</div> | ||||||
| 
 | 
 | ||||||
| 						<div class="flex justify-end pt-3 text-sm font-medium"> | 						<div class="flex justify-end pt-3 text-sm font-medium"> | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		{#if $user?.role === 'admin'} | 		{#if $user?.role === 'admin'} | ||||||
| 			<div class="px-2.5 flex justify-center my-1"> | 			<div class="px-2.5 flex justify-center mt-1"> | ||||||
| 				<button | 				<button | ||||||
| 					class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition" | 					class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition" | ||||||
| 					on:click={async () => { | 					on:click={async () => { | ||||||
|  | @ -129,10 +129,38 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</button> | 				</button> | ||||||
| 			</div> | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div class="px-2.5 flex justify-center mb-1"> | ||||||
|  | 				<button | ||||||
|  | 					class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition" | ||||||
|  | 					on:click={async () => { | ||||||
|  | 						goto('/prompts'); | ||||||
|  | 					}} | ||||||
|  | 				> | ||||||
|  | 					<div class="self-center"> | ||||||
|  | 						<svg | ||||||
|  | 							xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 							viewBox="0 0 16 16" | ||||||
|  | 							fill="currentColor" | ||||||
|  | 							class="w-4 h-4" | ||||||
|  | 						> | ||||||
|  | 							<path | ||||||
|  | 								fill-rule="evenodd" | ||||||
|  | 								d="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z" | ||||||
|  | 								clip-rule="evenodd" | ||||||
|  | 							/> | ||||||
|  | 						</svg> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="flex self-center"> | ||||||
|  | 						<div class=" self-center font-medium text-sm">Prompts</div> | ||||||
|  | 					</div> | ||||||
|  | 				</button> | ||||||
|  | 			</div> | ||||||
| 		{/if} | 		{/if} | ||||||
| 
 | 
 | ||||||
| 		<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2"> | 		<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2"> | ||||||
| 			<div class="flex w-full"> | 			<div class="flex w-full" id="chat-search"> | ||||||
| 				<div class="self-center pl-3 py-2 rounded-l bg-gray-950"> | 				<div class="self-center pl-3 py-2 rounded-l bg-gray-950"> | ||||||
| 					<svg | 					<svg | ||||||
| 						xmlns="http://www.w3.org/2000/svg" | 						xmlns="http://www.w3.org/2000/svg" | ||||||
|  |  | ||||||
|  | @ -6,10 +6,13 @@ export const user = writable(undefined); | ||||||
| 
 | 
 | ||||||
| // Frontend
 | // Frontend
 | ||||||
| export const theme = writable('dark'); | export const theme = writable('dark'); | ||||||
|  | 
 | ||||||
| export const chatId = writable(''); | export const chatId = writable(''); | ||||||
|  | 
 | ||||||
| export const chats = writable([]); | export const chats = writable([]); | ||||||
| export const models = writable([]); | export const models = writable([]); | ||||||
| export const modelfiles = writable([]); | export const modelfiles = writable([]); | ||||||
|  | export const prompts = writable([]); | ||||||
| 
 | 
 | ||||||
| export const settings = writable({}); | export const settings = writable({}); | ||||||
| export const showSettings = writable(false); | export const showSettings = writable(false); | ||||||
|  |  | ||||||
|  | @ -111,3 +111,19 @@ export const checkVersion = (required, current) => { | ||||||
| 				caseFirst: 'upper' | 				caseFirst: 'upper' | ||||||
| 		  }) < 0; | 		  }) < 0; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const findWordIndices = (text) => { | ||||||
|  | 	const regex = /\[([^\]]+)\]/g; | ||||||
|  | 	let matches = []; | ||||||
|  | 	let match; | ||||||
|  | 
 | ||||||
|  | 	while ((match = regex.exec(text)) !== null) { | ||||||
|  | 		matches.push({ | ||||||
|  | 			word: match[1], | ||||||
|  | 			startIndex: match.index, | ||||||
|  | 			endIndex: regex.lastIndex - 1 | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return matches; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -9,10 +9,11 @@ | ||||||
| 
 | 
 | ||||||
| 	import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama'; | 	import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama'; | ||||||
| 	import { getModelfiles } from '$lib/apis/modelfiles'; | 	import { getModelfiles } from '$lib/apis/modelfiles'; | ||||||
|  | 	import { getPrompts } from '$lib/apis/prompts'; | ||||||
| 
 | 
 | ||||||
| 	import { getOpenAIModels } from '$lib/apis/openai'; | 	import { getOpenAIModels } from '$lib/apis/openai'; | ||||||
| 
 | 
 | ||||||
| 	import { user, showSettings, settings, models, modelfiles } from '$lib/stores'; | 	import { user, showSettings, settings, models, modelfiles, prompts } from '$lib/stores'; | ||||||
| 	import { OLLAMA_API_BASE_URL, REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; | 	import { OLLAMA_API_BASE_URL, REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; | ||||||
| 
 | 
 | ||||||
| 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | ||||||
|  | @ -101,6 +102,9 @@ | ||||||
| 			console.log(); | 			console.log(); | ||||||
| 			await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); | 			await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); | ||||||
| 			await modelfiles.set(await getModelfiles(localStorage.token)); | 			await modelfiles.set(await getModelfiles(localStorage.token)); | ||||||
|  | 
 | ||||||
|  | 			await prompts.set(await getPrompts(localStorage.token)); | ||||||
|  | 
 | ||||||
| 			console.log($modelfiles); | 			console.log($modelfiles); | ||||||
| 
 | 
 | ||||||
| 			modelfiles.subscribe(async () => { | 			modelfiles.subscribe(async () => { | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| 	import { goto } from '$app/navigation'; | 	import { goto } from '$app/navigation'; | ||||||
| 	import { page } from '$app/stores'; | 	import { page } from '$app/stores'; | ||||||
| 
 | 
 | ||||||
| 	import { models, modelfiles, user, settings, chats, chatId } from '$lib/stores'; | 	import { models, modelfiles, user, settings, chats, chatId, config } from '$lib/stores'; | ||||||
| 	import { OLLAMA_API_BASE_URL } from '$lib/constants'; | 	import { OLLAMA_API_BASE_URL } from '$lib/constants'; | ||||||
| 
 | 
 | ||||||
| 	import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; | 	import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; | ||||||
|  | @ -90,9 +90,18 @@ | ||||||
| 			messages: {}, | 			messages: {}, | ||||||
| 			currentId: null | 			currentId: null | ||||||
| 		}; | 		}; | ||||||
| 		selectedModels = $page.url.searchParams.get('models') | 
 | ||||||
| 			? $page.url.searchParams.get('models')?.split(',') | 		console.log($config); | ||||||
| 			: $settings.models ?? ['']; | 
 | ||||||
|  | 		if ($page.url.searchParams.get('models')) { | ||||||
|  | 			selectedModels = $page.url.searchParams.get('models')?.split(','); | ||||||
|  | 		} else if ($settings?.models) { | ||||||
|  | 			selectedModels = $settings?.models; | ||||||
|  | 		} else if ($config?.default_models) { | ||||||
|  | 			selectedModels = $config?.default_models.split(','); | ||||||
|  | 		} else { | ||||||
|  | 			selectedModels = ['']; | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | 		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||||
| 		settings.set({ | 		settings.set({ | ||||||
|  | @ -109,10 +118,14 @@ | ||||||
| 		await Promise.all( | 		await Promise.all( | ||||||
| 			selectedModels.map(async (model) => { | 			selectedModels.map(async (model) => { | ||||||
| 				console.log(model); | 				console.log(model); | ||||||
| 				if ($models.filter((m) => m.name === model)[0].external) { | 				const modelTag = $models.filter((m) => m.name === model).at(0); | ||||||
|  | 
 | ||||||
|  | 				if (modelTag?.external) { | ||||||
| 					await sendPromptOpenAI(model, prompt, parentId, _chatId); | 					await sendPromptOpenAI(model, prompt, parentId, _chatId); | ||||||
| 				} else { | 				} else if (modelTag) { | ||||||
| 					await sendPromptOllama(model, prompt, parentId, _chatId); | 					await sendPromptOllama(model, prompt, parentId, _chatId); | ||||||
|  | 				} else { | ||||||
|  | 					toast.error(`Model ${model} not found`); | ||||||
| 				} | 				} | ||||||
| 			}) | 			}) | ||||||
| 		); | 		); | ||||||
|  | @ -379,13 +392,13 @@ | ||||||
| 										  } | 										  } | ||||||
| 										: { content: message.content }) | 										: { content: message.content }) | ||||||
| 								})), | 								})), | ||||||
| 							seed: $settings.options.seed ?? undefined, | 							seed: $settings?.options?.seed ?? undefined, | ||||||
| 							stop: $settings.options.stop ?? undefined, | 							stop: $settings?.options?.stop ?? undefined, | ||||||
| 							temperature: $settings.options.temperature ?? undefined, | 							temperature: $settings?.options?.temperature ?? undefined, | ||||||
| 							top_p: $settings.options.top_p ?? undefined, | 							top_p: $settings?.options?.top_p ?? undefined, | ||||||
| 							num_ctx: $settings.options.num_ctx ?? undefined, | 							num_ctx: $settings?.options?.num_ctx ?? undefined, | ||||||
| 							frequency_penalty: $settings.options.repeat_penalty ?? undefined, | 							frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, | ||||||
| 							max_tokens: $settings.options.num_predict ?? undefined | 							max_tokens: $settings?.options?.num_predict ?? undefined | ||||||
| 						}) | 						}) | ||||||
| 					} | 					} | ||||||
| 				).catch((err) => { | 				).catch((err) => { | ||||||
|  | @ -584,7 +597,7 @@ | ||||||
| 			const title = await generateTitle( | 			const title = await generateTitle( | ||||||
| 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, | 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, | ||||||
| 				localStorage.token, | 				localStorage.token, | ||||||
| 				selectedModels[0], | 				$settings?.titleAutoGenerateModel ?? selectedModels[0], | ||||||
| 				userPrompt | 				userPrompt | ||||||
| 			); | 			); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -136,17 +136,20 @@ | ||||||
| 		await Promise.all( | 		await Promise.all( | ||||||
| 			selectedModels.map(async (model) => { | 			selectedModels.map(async (model) => { | ||||||
| 				console.log(model); | 				console.log(model); | ||||||
| 				if ($models.filter((m) => m.name === model)[0].external) { | 				const modelTag = $models.filter((m) => m.name === model).at(0); | ||||||
|  | 
 | ||||||
|  | 				if (modelTag?.external) { | ||||||
| 					await sendPromptOpenAI(model, prompt, parentId, _chatId); | 					await sendPromptOpenAI(model, prompt, parentId, _chatId); | ||||||
| 				} else { | 				} else if (modelTag) { | ||||||
| 					await sendPromptOllama(model, prompt, parentId, _chatId); | 					await sendPromptOllama(model, prompt, parentId, _chatId); | ||||||
|  | 				} else { | ||||||
|  | 					toast.error(`Model ${model} not found`); | ||||||
| 				} | 				} | ||||||
| 			}) | 			}) | ||||||
| 		); | 		); | ||||||
| 
 | 
 | ||||||
| 		await chats.set(await getChatList(localStorage.token)); | 		await chats.set(await getChatList(localStorage.token)); | ||||||
| 	}; | 	}; | ||||||
| 
 |  | ||||||
| 	const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => { | 	const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => { | ||||||
| 		// Create response message | 		// Create response message | ||||||
| 		let responseMessageId = uuidv4(); | 		let responseMessageId = uuidv4(); | ||||||
|  | @ -406,13 +409,13 @@ | ||||||
| 										  } | 										  } | ||||||
| 										: { content: message.content }) | 										: { content: message.content }) | ||||||
| 								})), | 								})), | ||||||
| 							seed: $settings.options.seed ?? undefined, | 							seed: $settings?.options?.seed ?? undefined, | ||||||
| 							stop: $settings.options.stop ?? undefined, | 							stop: $settings?.options?.stop ?? undefined, | ||||||
| 							temperature: $settings.options.temperature ?? undefined, | 							temperature: $settings?.options?.temperature ?? undefined, | ||||||
| 							top_p: $settings.options.top_p ?? undefined, | 							top_p: $settings?.options?.top_p ?? undefined, | ||||||
| 							num_ctx: $settings.options.num_ctx ?? undefined, | 							num_ctx: $settings?.options?.num_ctx ?? undefined, | ||||||
| 							frequency_penalty: $settings.options.repeat_penalty ?? undefined, | 							frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, | ||||||
| 							max_tokens: $settings.options.num_predict ?? undefined | 							max_tokens: $settings?.options?.num_predict ?? undefined | ||||||
| 						}) | 						}) | ||||||
| 					} | 					} | ||||||
| 				).catch((err) => { | 				).catch((err) => { | ||||||
|  |  | ||||||
|  | @ -44,7 +44,7 @@ | ||||||
| 
 | 
 | ||||||
| 		const url = 'https://ollamahub.com'; | 		const url = 'https://ollamahub.com'; | ||||||
| 
 | 
 | ||||||
| 		const tab = await window.open(`${url}/create`, '_blank'); | 		const tab = await window.open(`${url}/modelfiles/create`, '_blank'); | ||||||
| 		window.addEventListener( | 		window.addEventListener( | ||||||
| 			'message', | 			'message', | ||||||
| 			(event) => { | 			(event) => { | ||||||
|  | @ -254,6 +254,30 @@ | ||||||
| 							</svg> | 							</svg> | ||||||
| 						</div> | 						</div> | ||||||
| 					</button> | 					</button> | ||||||
|  | 
 | ||||||
|  | 					<button | ||||||
|  | 						class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex" | ||||||
|  | 						on:click={async () => { | ||||||
|  | 							saveModelfiles($modelfiles); | ||||||
|  | 						}} | ||||||
|  | 					> | ||||||
|  | 						<div class=" self-center mr-2 font-medium">Export Modelfiles</div> | ||||||
|  | 
 | ||||||
|  | 						<div class=" self-center"> | ||||||
|  | 							<svg | ||||||
|  | 								xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 								viewBox="0 0 16 16" | ||||||
|  | 								fill="currentColor" | ||||||
|  | 								class="w-3.5 h-3.5" | ||||||
|  | 							> | ||||||
|  | 								<path | ||||||
|  | 									fill-rule="evenodd" | ||||||
|  | 									d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" | ||||||
|  | 									clip-rule="evenodd" | ||||||
|  | 								/> | ||||||
|  | 							</svg> | ||||||
|  | 						</div> | ||||||
|  | 					</button> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				{#if localModelfiles.length > 0} | 				{#if localModelfiles.length > 0} | ||||||
|  |  | ||||||
							
								
								
									
										309
									
								
								src/routes/(app)/prompts/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										309
									
								
								src/routes/(app)/prompts/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,309 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 	import toast from 'svelte-french-toast'; | ||||||
|  | 	import fileSaver from 'file-saver'; | ||||||
|  | 	const { saveAs } = fileSaver; | ||||||
|  | 
 | ||||||
|  | 	import { onMount } from 'svelte'; | ||||||
|  | 	import { prompts } from '$lib/stores'; | ||||||
|  | 	import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts'; | ||||||
|  | 	import { error } from '@sveltejs/kit'; | ||||||
|  | 
 | ||||||
|  | 	let importFiles = ''; | ||||||
|  | 	let query = ''; | ||||||
|  | 
 | ||||||
|  | 	const sharePrompt = async (prompt) => { | ||||||
|  | 		toast.success('Redirecting you to OllamaHub'); | ||||||
|  | 
 | ||||||
|  | 		const url = 'https://ollamahub.com'; | ||||||
|  | 
 | ||||||
|  | 		const tab = await window.open(`${url}/prompts/create`, '_blank'); | ||||||
|  | 		window.addEventListener( | ||||||
|  | 			'message', | ||||||
|  | 			(event) => { | ||||||
|  | 				if (event.origin !== url) return; | ||||||
|  | 				if (event.data === 'loaded') { | ||||||
|  | 					tab.postMessage(JSON.stringify(prompt), '*'); | ||||||
|  | 				} | ||||||
|  | 			}, | ||||||
|  | 			false | ||||||
|  | 		); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const deletePrompt = async (command) => { | ||||||
|  | 		await deletePromptByCommand(localStorage.token, command); | ||||||
|  | 		await prompts.set(await getPrompts(localStorage.token)); | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="min-h-screen w-full flex justify-center dark:text-white"> | ||||||
|  | 	<div class=" py-2.5 flex flex-col justify-between w-full"> | ||||||
|  | 		<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> | ||||||
|  | 			<div class="mb-6 flex justify-between items-center"> | ||||||
|  | 				<div class=" text-2xl font-semibold self-center">My Prompts</div> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div class=" flex w-full space-x-2"> | ||||||
|  | 				<div class="flex flex-1"> | ||||||
|  | 					<div class=" self-center ml-1 mr-3"> | ||||||
|  | 						<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="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" | ||||||
|  | 								clip-rule="evenodd" | ||||||
|  | 							/> | ||||||
|  | 						</svg> | ||||||
|  | 					</div> | ||||||
|  | 					<input | ||||||
|  | 						class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" | ||||||
|  | 						bind:value={query} | ||||||
|  | 						placeholder="Search Prompt" | ||||||
|  | 					/> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div> | ||||||
|  | 					<a | ||||||
|  | 						class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1" | ||||||
|  | 						href="/prompts/create" | ||||||
|  | 					> | ||||||
|  | 						<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> | ||||||
|  | 					</a> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			{#if $prompts.length === 0} | ||||||
|  | 				<div /> | ||||||
|  | 			{:else} | ||||||
|  | 				{#each $prompts.filter((p) => query === '' || p.command.includes(query)) as prompt} | ||||||
|  | 					<hr class=" dark:border-gray-700 my-2.5" /> | ||||||
|  | 					<div class=" flex space-x-4 cursor-pointer w-full mb-3"> | ||||||
|  | 						<div class=" flex flex-1 space-x-4 cursor-pointer w-full"> | ||||||
|  | 							<a href={`/prompts/edit?command=${encodeURIComponent(prompt.command)}`}> | ||||||
|  | 								<div class=" flex-1 self-center pl-5"> | ||||||
|  | 									<div class=" font-bold">{prompt.command}</div> | ||||||
|  | 									<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> | ||||||
|  | 										{prompt.title} | ||||||
|  | 									</div> | ||||||
|  | 								</div> | ||||||
|  | 							</a> | ||||||
|  | 						</div> | ||||||
|  | 						<div class="flex flex-row space-x-1 self-center"> | ||||||
|  | 							<a | ||||||
|  | 								class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl" | ||||||
|  | 								type="button" | ||||||
|  | 								href={`/prompts/edit?command=${encodeURIComponent(prompt.command)}`} | ||||||
|  | 							> | ||||||
|  | 								<svg | ||||||
|  | 									xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 									fill="none" | ||||||
|  | 									viewBox="0 0 24 24" | ||||||
|  | 									stroke-width="1.5" | ||||||
|  | 									stroke="currentColor" | ||||||
|  | 									class="w-4 h-4" | ||||||
|  | 								> | ||||||
|  | 									<path | ||||||
|  | 										stroke-linecap="round" | ||||||
|  | 										stroke-linejoin="round" | ||||||
|  | 										d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" | ||||||
|  | 									/> | ||||||
|  | 								</svg> | ||||||
|  | 							</a> | ||||||
|  | 
 | ||||||
|  | 							<button | ||||||
|  | 								class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl" | ||||||
|  | 								type="button" | ||||||
|  | 								on:click={() => { | ||||||
|  | 									sharePrompt(prompt); | ||||||
|  | 								}} | ||||||
|  | 							> | ||||||
|  | 								<svg | ||||||
|  | 									xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 									fill="none" | ||||||
|  | 									viewBox="0 0 24 24" | ||||||
|  | 									stroke-width="1.5" | ||||||
|  | 									stroke="currentColor" | ||||||
|  | 									class="w-4 h-4" | ||||||
|  | 								> | ||||||
|  | 									<path | ||||||
|  | 										stroke-linecap="round" | ||||||
|  | 										stroke-linejoin="round" | ||||||
|  | 										d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" | ||||||
|  | 									/> | ||||||
|  | 								</svg> | ||||||
|  | 							</button> | ||||||
|  | 
 | ||||||
|  | 							<button | ||||||
|  | 								class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl" | ||||||
|  | 								type="button" | ||||||
|  | 								on:click={() => { | ||||||
|  | 									deletePrompt(prompt.command); | ||||||
|  | 								}} | ||||||
|  | 							> | ||||||
|  | 								<svg | ||||||
|  | 									xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 									fill="none" | ||||||
|  | 									viewBox="0 0 24 24" | ||||||
|  | 									stroke-width="1.5" | ||||||
|  | 									stroke="currentColor" | ||||||
|  | 									class="w-4 h-4" | ||||||
|  | 								> | ||||||
|  | 									<path | ||||||
|  | 										stroke-linecap="round" | ||||||
|  | 										stroke-linejoin="round" | ||||||
|  | 										d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" | ||||||
|  | 									/> | ||||||
|  | 								</svg> | ||||||
|  | 							</button> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				{/each} | ||||||
|  | 			{/if} | ||||||
|  | 
 | ||||||
|  | 			<hr class=" dark:border-gray-700 my-2.5" /> | ||||||
|  | 
 | ||||||
|  | 			<div class=" flex justify-between w-full mb-3"> | ||||||
|  | 				<div class="flex space-x-2"> | ||||||
|  | 					<input | ||||||
|  | 						id="prompts-import-input" | ||||||
|  | 						bind:files={importFiles} | ||||||
|  | 						type="file" | ||||||
|  | 						accept=".json" | ||||||
|  | 						hidden | ||||||
|  | 						on:change={() => { | ||||||
|  | 							console.log(importFiles); | ||||||
|  | 
 | ||||||
|  | 							const reader = new FileReader(); | ||||||
|  | 							reader.onload = async (event) => { | ||||||
|  | 								const savedPrompts = JSON.parse(event.target.result); | ||||||
|  | 								console.log(savedPrompts); | ||||||
|  | 
 | ||||||
|  | 								for (const prompt of savedPrompts) { | ||||||
|  | 									await createNewPrompt( | ||||||
|  | 										localStorage.token, | ||||||
|  | 										prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command, | ||||||
|  | 										prompt.title, | ||||||
|  | 										prompt.content | ||||||
|  | 									).catch((error) => { | ||||||
|  | 										toast.error(error); | ||||||
|  | 										return null; | ||||||
|  | 									}); | ||||||
|  | 								} | ||||||
|  | 
 | ||||||
|  | 								await prompts.set(await getPrompts(localStorage.token)); | ||||||
|  | 							}; | ||||||
|  | 
 | ||||||
|  | 							reader.readAsText(importFiles[0]); | ||||||
|  | 						}} | ||||||
|  | 					/> | ||||||
|  | 
 | ||||||
|  | 					<button | ||||||
|  | 						class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex" | ||||||
|  | 						on:click={async () => { | ||||||
|  | 							document.getElementById('prompts-import-input')?.click(); | ||||||
|  | 						}} | ||||||
|  | 					> | ||||||
|  | 						<div class=" self-center mr-2 font-medium">Import Prompts</div> | ||||||
|  | 
 | ||||||
|  | 						<div class=" self-center"> | ||||||
|  | 							<svg | ||||||
|  | 								xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 								viewBox="0 0 16 16" | ||||||
|  | 								fill="currentColor" | ||||||
|  | 								class="w-4 h-4" | ||||||
|  | 							> | ||||||
|  | 								<path | ||||||
|  | 									fill-rule="evenodd" | ||||||
|  | 									d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z" | ||||||
|  | 									clip-rule="evenodd" | ||||||
|  | 								/> | ||||||
|  | 							</svg> | ||||||
|  | 						</div> | ||||||
|  | 					</button> | ||||||
|  | 
 | ||||||
|  | 					<button | ||||||
|  | 						class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex" | ||||||
|  | 						on:click={async () => { | ||||||
|  | 							// document.getElementById('modelfiles-import-input')?.click(); | ||||||
|  | 							let blob = new Blob([JSON.stringify($prompts)], { | ||||||
|  | 								type: 'application/json' | ||||||
|  | 							}); | ||||||
|  | 							saveAs(blob, `prompts-export-${Date.now()}.json`); | ||||||
|  | 						}} | ||||||
|  | 					> | ||||||
|  | 						<div class=" self-center mr-2 font-medium">Export Prompts</div> | ||||||
|  | 
 | ||||||
|  | 						<div class=" self-center"> | ||||||
|  | 							<svg | ||||||
|  | 								xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 								viewBox="0 0 16 16" | ||||||
|  | 								fill="currentColor" | ||||||
|  | 								class="w-4 h-4" | ||||||
|  | 							> | ||||||
|  | 								<path | ||||||
|  | 									fill-rule="evenodd" | ||||||
|  | 									d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" | ||||||
|  | 									clip-rule="evenodd" | ||||||
|  | 								/> | ||||||
|  | 							</svg> | ||||||
|  | 						</div> | ||||||
|  | 					</button> | ||||||
|  | 
 | ||||||
|  | 					<!-- <button | ||||||
|  | 						on:click={() => { | ||||||
|  | 							loadDefaultPrompts(); | ||||||
|  | 						}} | ||||||
|  | 					> | ||||||
|  | 						dd | ||||||
|  | 					</button> --> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div class=" my-16"> | ||||||
|  | 				<div class=" text-2xl font-semibold mb-6">Made by OllamaHub Community</div> | ||||||
|  | 
 | ||||||
|  | 				<a | ||||||
|  | 					class=" flex space-x-4 cursor-pointer w-full mb-3" | ||||||
|  | 					href="https://ollamahub.com/?type=prompts" | ||||||
|  | 					target="_blank" | ||||||
|  | 				> | ||||||
|  | 					<div class=" self-center w-10"> | ||||||
|  | 						<div | ||||||
|  | 							class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" | ||||||
|  | 						> | ||||||
|  | 							<svg | ||||||
|  | 								xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 								viewBox="0 0 24 24" | ||||||
|  | 								fill="currentColor" | ||||||
|  | 								class="w-6" | ||||||
|  | 							> | ||||||
|  | 								<path | ||||||
|  | 									fill-rule="evenodd" | ||||||
|  | 									d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" | ||||||
|  | 									clip-rule="evenodd" | ||||||
|  | 								/> | ||||||
|  | 							</svg> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class=" self-center"> | ||||||
|  | 						<div class=" font-bold">Discover a prompt</div> | ||||||
|  | 						<div class=" text-sm">Discover, download, and explore custom Prompts</div> | ||||||
|  | 					</div> | ||||||
|  | 				</a> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
							
								
								
									
										222
									
								
								src/routes/(app)/prompts/create/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								src/routes/(app)/prompts/create/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,222 @@ | ||||||
|  | <script> | ||||||
|  | 	import toast from 'svelte-french-toast'; | ||||||
|  | 
 | ||||||
|  | 	import { goto } from '$app/navigation'; | ||||||
|  | 	import { prompts } from '$lib/stores'; | ||||||
|  | 	import { onMount, tick } from 'svelte'; | ||||||
|  | 
 | ||||||
|  | 	import { createNewPrompt, getPrompts } from '$lib/apis/prompts'; | ||||||
|  | 
 | ||||||
|  | 	let loading = false; | ||||||
|  | 
 | ||||||
|  | 	// /////////// | ||||||
|  | 	// Prompt | ||||||
|  | 	// /////////// | ||||||
|  | 
 | ||||||
|  | 	let title = ''; | ||||||
|  | 	let command = ''; | ||||||
|  | 	let content = ''; | ||||||
|  | 
 | ||||||
|  | 	$: command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : ''; | ||||||
|  | 
 | ||||||
|  | 	const submitHandler = async () => { | ||||||
|  | 		loading = true; | ||||||
|  | 
 | ||||||
|  | 		if (validateCommandString(command)) { | ||||||
|  | 			const prompt = await createNewPrompt(localStorage.token, command, title, content).catch( | ||||||
|  | 				(error) => { | ||||||
|  | 					toast.error(error); | ||||||
|  | 
 | ||||||
|  | 					return null; | ||||||
|  | 				} | ||||||
|  | 			); | ||||||
|  | 
 | ||||||
|  | 			if (prompt) { | ||||||
|  | 				await prompts.set(await getPrompts(localStorage.token)); | ||||||
|  | 				await goto('/prompts'); | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			toast.error('Only alphanumeric characters and hyphens are allowed in the command string.'); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		loading = false; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const validateCommandString = (inputString) => { | ||||||
|  | 		// Regular expression to match only alphanumeric characters and hyphen | ||||||
|  | 		const regex = /^[a-zA-Z0-9-]+$/; | ||||||
|  | 
 | ||||||
|  | 		// Test the input string against the regular expression | ||||||
|  | 		return regex.test(inputString); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	onMount(() => { | ||||||
|  | 		window.addEventListener('message', async (event) => { | ||||||
|  | 			if ( | ||||||
|  | 				!['https://ollamahub.com', 'https://www.ollamahub.com', 'http://localhost:5173'].includes( | ||||||
|  | 					event.origin | ||||||
|  | 				) | ||||||
|  | 			) | ||||||
|  | 				return; | ||||||
|  | 			const prompt = JSON.parse(event.data); | ||||||
|  | 			console.log(prompt); | ||||||
|  | 
 | ||||||
|  | 			title = prompt.title; | ||||||
|  | 			await tick(); | ||||||
|  | 			content = prompt.content; | ||||||
|  | 			command = prompt.command; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (window.opener ?? false) { | ||||||
|  | 			window.opener.postMessage('loaded', '*'); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="min-h-screen w-full flex justify-center dark:text-white"> | ||||||
|  | 	<div class=" py-2.5 flex flex-col justify-between w-full"> | ||||||
|  | 		<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> | ||||||
|  | 			<div class=" text-2xl font-semibold mb-6">My Prompts</div> | ||||||
|  | 
 | ||||||
|  | 			<button | ||||||
|  | 				class="flex space-x-1" | ||||||
|  | 				on:click={() => { | ||||||
|  | 					history.back(); | ||||||
|  | 				}} | ||||||
|  | 			> | ||||||
|  | 				<div class=" self-center"> | ||||||
|  | 					<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="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" | ||||||
|  | 							clip-rule="evenodd" | ||||||
|  | 						/> | ||||||
|  | 					</svg> | ||||||
|  | 				</div> | ||||||
|  | 				<div class=" self-center font-medium text-sm">Back</div> | ||||||
|  | 			</button> | ||||||
|  | 			<hr class="my-3 dark:border-gray-700" /> | ||||||
|  | 
 | ||||||
|  | 			<form | ||||||
|  | 				class="flex flex-col" | ||||||
|  | 				on:submit|preventDefault={() => { | ||||||
|  | 					submitHandler(); | ||||||
|  | 				}} | ||||||
|  | 			> | ||||||
|  | 				<div class="my-2"> | ||||||
|  | 					<div class=" text-sm font-semibold mb-2">Title*</div> | ||||||
|  | 
 | ||||||
|  | 					<div> | ||||||
|  | 						<input | ||||||
|  | 							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" | ||||||
|  | 							placeholder="Add a short title for this prompt" | ||||||
|  | 							bind:value={title} | ||||||
|  | 							required | ||||||
|  | 						/> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="my-2"> | ||||||
|  | 					<div class=" text-sm font-semibold mb-2">Command*</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="flex items-center mb-1"> | ||||||
|  | 						<div | ||||||
|  | 							class="bg-gray-200 dark:bg-gray-600 font-bold px-3 py-1 border border-r-0 dark:border-gray-600 rounded-l-lg" | ||||||
|  | 						> | ||||||
|  | 							/ | ||||||
|  | 						</div> | ||||||
|  | 						<input | ||||||
|  | 							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-r-lg" | ||||||
|  | 							placeholder="short-summary" | ||||||
|  | 							bind:value={command} | ||||||
|  | 							required | ||||||
|  | 						/> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="text-xs text-gray-400 dark:text-gray-500"> | ||||||
|  | 						Only <span class=" text-gray-600 dark:text-gray-300 font-medium" | ||||||
|  | 							>alphanumeric characters and hyphens</span | ||||||
|  | 						> | ||||||
|  | 						are allowed; Activate this command by typing "<span | ||||||
|  | 							class=" text-gray-600 dark:text-gray-300 font-medium" | ||||||
|  | 						> | ||||||
|  | 							/{command} | ||||||
|  | 						</span>" to chat input. | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="my-2"> | ||||||
|  | 					<div class="flex w-full justify-between"> | ||||||
|  | 						<div class=" self-center text-sm font-semibold">Prompt Content*</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="mt-2"> | ||||||
|  | 						<div> | ||||||
|  | 							<textarea | ||||||
|  | 								class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" | ||||||
|  | 								placeholder={`Write a summary in 50 words that summarizes [topic or keyword].`} | ||||||
|  | 								rows="6" | ||||||
|  | 								bind:value={content} | ||||||
|  | 								required | ||||||
|  | 							/> | ||||||
|  | 						</div> | ||||||
|  | 
 | ||||||
|  | 						<div class="text-xs text-gray-400 dark:text-gray-500"> | ||||||
|  | 							Format your variables using square brackets like this: <span | ||||||
|  | 								class=" text-gray-600 dark:text-gray-300 font-medium">[variable]</span | ||||||
|  | 							> | ||||||
|  | 							. Make sure to enclose them with | ||||||
|  | 							<span class=" text-gray-600 dark:text-gray-300 font-medium">'['</span> | ||||||
|  | 							and <span class=" text-gray-600 dark:text-gray-300 font-medium">']'</span> . | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="my-2 flex justify-end"> | ||||||
|  | 					<button | ||||||
|  | 						class=" text-sm px-3 py-2 transition rounded-xl {loading | ||||||
|  | 							? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800' | ||||||
|  | 							: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex" | ||||||
|  | 						type="submit" | ||||||
|  | 						disabled={loading} | ||||||
|  | 					> | ||||||
|  | 						<div class=" self-center font-medium">Save & Create</div> | ||||||
|  | 
 | ||||||
|  | 						{#if loading} | ||||||
|  | 							<div class="ml-1.5 self-center"> | ||||||
|  | 								<svg | ||||||
|  | 									class=" w-4 h-4" | ||||||
|  | 									viewBox="0 0 24 24" | ||||||
|  | 									fill="currentColor" | ||||||
|  | 									xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 									><style> | ||||||
|  | 										.spinner_ajPY { | ||||||
|  | 											transform-origin: center; | ||||||
|  | 											animation: spinner_AtaB 0.75s infinite linear; | ||||||
|  | 										} | ||||||
|  | 										@keyframes spinner_AtaB { | ||||||
|  | 											100% { | ||||||
|  | 												transform: rotate(360deg); | ||||||
|  | 											} | ||||||
|  | 										} | ||||||
|  | 									</style><path | ||||||
|  | 										d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||||
|  | 										opacity=".25" | ||||||
|  | 									/><path | ||||||
|  | 										d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||||
|  | 										class="spinner_ajPY" | ||||||
|  | 									/></svg | ||||||
|  | 								> | ||||||
|  | 							</div> | ||||||
|  | 						{/if} | ||||||
|  | 					</button> | ||||||
|  | 				</div> | ||||||
|  | 			</form> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
							
								
								
									
										221
									
								
								src/routes/(app)/prompts/edit/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								src/routes/(app)/prompts/edit/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,221 @@ | ||||||
|  | <script> | ||||||
|  | 	import toast from 'svelte-french-toast'; | ||||||
|  | 
 | ||||||
|  | 	import { goto } from '$app/navigation'; | ||||||
|  | 	import { prompts } from '$lib/stores'; | ||||||
|  | 	import { onMount, tick } from 'svelte'; | ||||||
|  | 
 | ||||||
|  | 	import { getPrompts, updatePromptByCommand } from '$lib/apis/prompts'; | ||||||
|  | 	import { page } from '$app/stores'; | ||||||
|  | 
 | ||||||
|  | 	let loading = false; | ||||||
|  | 
 | ||||||
|  | 	// /////////// | ||||||
|  | 	// Prompt | ||||||
|  | 	// /////////// | ||||||
|  | 
 | ||||||
|  | 	let title = ''; | ||||||
|  | 	let command = ''; | ||||||
|  | 	let content = ''; | ||||||
|  | 
 | ||||||
|  | 	const updateHandler = async () => { | ||||||
|  | 		loading = true; | ||||||
|  | 
 | ||||||
|  | 		if (validateCommandString(command)) { | ||||||
|  | 			const prompt = await updatePromptByCommand(localStorage.token, command, title, content).catch( | ||||||
|  | 				(error) => { | ||||||
|  | 					toast.error(error); | ||||||
|  | 					return null; | ||||||
|  | 				} | ||||||
|  | 			); | ||||||
|  | 
 | ||||||
|  | 			if (prompt) { | ||||||
|  | 				await prompts.set(await getPrompts(localStorage.token)); | ||||||
|  | 				await goto('/prompts'); | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			toast.error('Only alphanumeric characters and hyphens are allowed in the command string.'); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		loading = false; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const validateCommandString = (inputString) => { | ||||||
|  | 		// Regular expression to match only alphanumeric characters and hyphen | ||||||
|  | 		const regex = /^[a-zA-Z0-9-]+$/; | ||||||
|  | 
 | ||||||
|  | 		// Test the input string against the regular expression | ||||||
|  | 		return regex.test(inputString); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	onMount(async () => { | ||||||
|  | 		command = $page.url.searchParams.get('command'); | ||||||
|  | 		if (command) { | ||||||
|  | 			const prompt = $prompts.filter((prompt) => prompt.command === command).at(0); | ||||||
|  | 
 | ||||||
|  | 			if (prompt) { | ||||||
|  | 				console.log(prompt); | ||||||
|  | 
 | ||||||
|  | 				console.log(prompt.command); | ||||||
|  | 
 | ||||||
|  | 				title = prompt.title; | ||||||
|  | 				await tick(); | ||||||
|  | 				command = prompt.command.slice(1); | ||||||
|  | 				content = prompt.content; | ||||||
|  | 			} else { | ||||||
|  | 				goto('/prompts'); | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			goto('/prompts'); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="min-h-screen w-full flex justify-center dark:text-white"> | ||||||
|  | 	<div class=" py-2.5 flex flex-col justify-between w-full"> | ||||||
|  | 		<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> | ||||||
|  | 			<div class=" text-2xl font-semibold mb-6">My Prompts</div> | ||||||
|  | 
 | ||||||
|  | 			<button | ||||||
|  | 				class="flex space-x-1" | ||||||
|  | 				on:click={() => { | ||||||
|  | 					history.back(); | ||||||
|  | 				}} | ||||||
|  | 			> | ||||||
|  | 				<div class=" self-center"> | ||||||
|  | 					<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="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" | ||||||
|  | 							clip-rule="evenodd" | ||||||
|  | 						/> | ||||||
|  | 					</svg> | ||||||
|  | 				</div> | ||||||
|  | 				<div class=" self-center font-medium text-sm">Back</div> | ||||||
|  | 			</button> | ||||||
|  | 			<hr class="my-3 dark:border-gray-700" /> | ||||||
|  | 
 | ||||||
|  | 			<form | ||||||
|  | 				class="flex flex-col" | ||||||
|  | 				on:submit|preventDefault={() => { | ||||||
|  | 					updateHandler(); | ||||||
|  | 				}} | ||||||
|  | 			> | ||||||
|  | 				<div class="my-2"> | ||||||
|  | 					<div class=" text-sm font-semibold mb-2">Title*</div> | ||||||
|  | 
 | ||||||
|  | 					<div> | ||||||
|  | 						<input | ||||||
|  | 							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" | ||||||
|  | 							placeholder="Add a short title for this prompt" | ||||||
|  | 							bind:value={title} | ||||||
|  | 							required | ||||||
|  | 						/> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="my-2"> | ||||||
|  | 					<div class=" text-sm font-semibold mb-2">Command*</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="flex items-center mb-1"> | ||||||
|  | 						<div | ||||||
|  | 							class="bg-gray-200 dark:bg-gray-600 font-bold px-3 py-1 border border-r-0 dark:border-gray-600 rounded-l-lg" | ||||||
|  | 						> | ||||||
|  | 							/ | ||||||
|  | 						</div> | ||||||
|  | 						<input | ||||||
|  | 							class="px-3 py-1.5 text-sm w-full bg-transparent border disabled:text-gray-500 dark:border-gray-600 outline-none rounded-r-lg" | ||||||
|  | 							placeholder="short-summary" | ||||||
|  | 							bind:value={command} | ||||||
|  | 							disabled | ||||||
|  | 							required | ||||||
|  | 						/> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="text-xs text-gray-400 dark:text-gray-500"> | ||||||
|  | 						Only <span class=" text-gray-600 dark:text-gray-300 font-medium" | ||||||
|  | 							>alphanumeric characters and hyphens</span | ||||||
|  | 						> | ||||||
|  | 						are allowed; Activate this command by typing "<span | ||||||
|  | 							class=" text-gray-600 dark:text-gray-300 font-medium" | ||||||
|  | 						> | ||||||
|  | 							/{command} | ||||||
|  | 						</span>" to chat input. | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="my-2"> | ||||||
|  | 					<div class="flex w-full justify-between"> | ||||||
|  | 						<div class=" self-center text-sm font-semibold">Prompt Content*</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					<div class="mt-2"> | ||||||
|  | 						<div> | ||||||
|  | 							<textarea | ||||||
|  | 								class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" | ||||||
|  | 								placeholder={`Write a summary in 50 words that summarizes [topic or keyword].`} | ||||||
|  | 								rows="6" | ||||||
|  | 								bind:value={content} | ||||||
|  | 								required | ||||||
|  | 							/> | ||||||
|  | 						</div> | ||||||
|  | 
 | ||||||
|  | 						<div class="text-xs text-gray-400 dark:text-gray-500"> | ||||||
|  | 							Format your variables using square brackets like this: <span | ||||||
|  | 								class=" text-gray-600 dark:text-gray-300 font-medium">[variable]</span | ||||||
|  | 							> | ||||||
|  | 							. Make sure to enclose them with | ||||||
|  | 							<span class=" text-gray-600 dark:text-gray-300 font-medium">'['</span> | ||||||
|  | 							and <span class=" text-gray-600 dark:text-gray-300 font-medium">']'</span> . | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="my-2 flex justify-end"> | ||||||
|  | 					<button | ||||||
|  | 						class=" text-sm px-3 py-2 transition rounded-xl {loading | ||||||
|  | 							? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800' | ||||||
|  | 							: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex" | ||||||
|  | 						type="submit" | ||||||
|  | 						disabled={loading} | ||||||
|  | 					> | ||||||
|  | 						<div class=" self-center font-medium">Save & Update</div> | ||||||
|  | 
 | ||||||
|  | 						{#if loading} | ||||||
|  | 							<div class="ml-1.5 self-center"> | ||||||
|  | 								<svg | ||||||
|  | 									class=" w-4 h-4" | ||||||
|  | 									viewBox="0 0 24 24" | ||||||
|  | 									fill="currentColor" | ||||||
|  | 									xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 									><style> | ||||||
|  | 										.spinner_ajPY { | ||||||
|  | 											transform-origin: center; | ||||||
|  | 											animation: spinner_AtaB 0.75s infinite linear; | ||||||
|  | 										} | ||||||
|  | 										@keyframes spinner_AtaB { | ||||||
|  | 											100% { | ||||||
|  | 												transform: rotate(360deg); | ||||||
|  | 											} | ||||||
|  | 										} | ||||||
|  | 									</style><path | ||||||
|  | 										d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||||
|  | 										opacity=".25" | ||||||
|  | 									/><path | ||||||
|  | 										d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||||
|  | 										class="spinner_ajPY" | ||||||
|  | 									/></svg | ||||||
|  | 								> | ||||||
|  | 							</div> | ||||||
|  | 						{/if} | ||||||
|  | 					</button> | ||||||
|  | 				</div> | ||||||
|  | 			</form> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | @ -57,6 +57,7 @@ | ||||||
| 	<title>Ollama</title> | 	<title>Ollama</title> | ||||||
| 
 | 
 | ||||||
| 	<link rel="stylesheet" type="text/css" href="/themes/rosepine.css" /> | 	<link rel="stylesheet" type="text/css" href="/themes/rosepine.css" /> | ||||||
|  | 	<link rel="stylesheet" type="text/css" href="/themes/rosepine-dawn.css" /> | ||||||
| </svelte:head> | </svelte:head> | ||||||
| 
 | 
 | ||||||
| {#if loaded} | {#if loaded} | ||||||
|  |  | ||||||
							
								
								
									
										140
									
								
								static/themes/rosepine-dawn.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								static/themes/rosepine-dawn.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,140 @@ | ||||||
|  | .rose-pine-dawn * { | ||||||
|  | 	color: #575279 !important; | ||||||
|  | 	stroke: #d7827e !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .app > * { | ||||||
|  | 	background-color: #faf4ed !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn #nav { | ||||||
|  | 	background-color: #fffaf3; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .py-2\.5.my-auto.flex.flex-col.justify-between.h-screen { | ||||||
|  | 	background: #f2e9e1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .bg-white.dark\:bg-gray-800 { | ||||||
|  | 	background: #f2e9e1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .w-4.h-4 { | ||||||
|  | 	fill: #ebbcba; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn #chat-textarea { | ||||||
|  | 	background: #cecacd; | ||||||
|  | 	margin: 0.3rem; | ||||||
|  | 	padding: 0.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .bg-gradient-to-t.from-white.dark\:from-gray-800.from-40\%.pb-2 { | ||||||
|  | 	background: #f2e9e1 !important; | ||||||
|  | 	padding-top: 0.6rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn | ||||||
|  | 	.text-white.bg-gray-100.dark\:text-gray-800.dark\:bg-gray-600.disabled.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { | ||||||
|  | 	background-color: #cecacd; | ||||||
|  | 	transition: background-color 0.2s ease-out linear; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn | ||||||
|  | 	.bg-black.text-white.hover\:bg-gray-900.dark\:bg-white.dark\:text-black.dark\:hover\:bg-gray-100.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { | ||||||
|  | 	background-color: #286983; | ||||||
|  | 	transition: background-color 0.2s ease-out linear; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn | ||||||
|  | 	.bg-black.text-white.hover\:bg-gray-900.dark\:bg-white.dark\:text-black.dark\:hover\:bg-gray-100.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center | ||||||
|  | 	> * { | ||||||
|  | 	fill: #56949f !important; | ||||||
|  | 	transition: fill 0.2s ease-out linear; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn | ||||||
|  | 	.w-full.flex.justify-between.rounded-md.px-3.py-2.hover\:bg-gray-900.bg-gray-900.transition.whitespace-nowrap.text-ellipsis { | ||||||
|  | 	background-color: #56526e; | ||||||
|  | 	font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .hover\:bg-gray-900:hover { | ||||||
|  | 	--tw-bg-opacity: 1; | ||||||
|  | 	background-color: rgb(152 147 165 / var(--tw-bg-opacity)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .text-xs.text-gray-700.uppercase.bg-gray-50.dark\:bg-gray-700.dark\:text-gray-400 { | ||||||
|  | 	background-color: #403d52; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .scrollbar-hidden.relative.overflow-x-auto.whitespace-nowrap.svelte-3g4avz { | ||||||
|  | 	border-radius: 16px 16px 0 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .base.enter.svelte-ug60r4 { | ||||||
|  | 	background-color: #286983; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .message.svelte-1nauejd { | ||||||
|  | 	color: #e0def4 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn #dropdownDots { | ||||||
|  | 	background-color: #dfdad9; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .flex.py-2\.5.px-3\.5.w-full.hover\:bg-gray-800.transition:hover { | ||||||
|  | 	background: #cecacd; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn #dropdownDots { | ||||||
|  | 	background-color: #dfdad9; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .flex.py-2\.5.px-3\.5.w-full.hover\:bg-gray-800.transition:hover { | ||||||
|  | 	background: #cecacd; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn | ||||||
|  | 	.m-auto.rounded-xl.max-w-full.w-\[40rem\].mx-2.bg-gray-50.dark\:bg-gray-900.shadow-3xl { | ||||||
|  | 	background-color: #f2e9e1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn | ||||||
|  | 	.w-full.rounded.p-4.text-sm.dark\:text-gray-300.dark\:bg-gray-800.outline-none.resize-none { | ||||||
|  | 	background-color: #cecacd; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn | ||||||
|  | 	.w-full.rounded.py-2.px-4.text-sm.dark\:text-gray-300.dark\:bg-gray-800.outline-none.svelte-1vx7r9s { | ||||||
|  | 	background-color: #cecacd; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn | ||||||
|  | 	.px-2\.5.py-2\.5.min-w-fit.rounded-lg.flex-1.md\:flex-none.flex.text-right.transition.bg-gray-200.dark\:bg-gray-700 { | ||||||
|  | 	background-color: #dfdad9; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn | ||||||
|  | 	.px-2\.5.py-2\.5.min-w-fit.rounded-lg.flex-1.md\:flex-none.flex.text-right.transition.hover\:bg-gray-300.dark\:hover\:bg-gray-800:hover { | ||||||
|  | 	background-color: #cecacd; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .px-4.py-2.bg-emerald-600.hover\:bg-emerald-700.text-gray-100.transition.rounded { | ||||||
|  | 	background-color: #56949f; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn #chat-search > * { | ||||||
|  | 	background-color: #dfdad9 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .svelte-1ee93ns { | ||||||
|  | 	--primary: #b4637a !important; | ||||||
|  | 	--secondary: #fffaf3 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine-dawn .svelte-11kvm4p { | ||||||
|  | 	--primary: #56949f !important; | ||||||
|  | 	--secondary: #fffaf3 !important; | ||||||
|  | } | ||||||
|  | @ -37,13 +37,13 @@ | ||||||
| .rose-pine | .rose-pine | ||||||
| 	.text-white.bg-gray-100.dark\:text-gray-800.dark\:bg-gray-600.disabled.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { | 	.text-white.bg-gray-100.dark\:text-gray-800.dark\:bg-gray-600.disabled.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { | ||||||
| 	background-color: #6e6a86; | 	background-color: #6e6a86; | ||||||
| 	transition: background 0.2s ease-out linear; | 	transition: background-color 0.2s ease-out linear; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .rose-pine | .rose-pine | ||||||
| 	.bg-black.text-white.hover\:bg-gray-900.dark\:bg-white.dark\:text-black.dark\:hover\:bg-gray-100.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { | 	.bg-black.text-white.hover\:bg-gray-900.dark\:bg-white.dark\:text-black.dark\:hover\:bg-gray-100.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { | ||||||
| 	background-color: #286983; | 	background-color: #286983; | ||||||
| 	transition: background 0.2s ease-out linear; | 	transition: background-color 0.2s ease-out linear; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .rose-pine | .rose-pine | ||||||
|  | @ -80,6 +80,46 @@ | ||||||
| 	color: #e0def4 !important; | 	color: #e0def4 !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .rose-pine #dropdownDots { | ||||||
|  | 	background-color: #403d52; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine .flex.py-2\.5.px-3\.5.w-full.hover\:bg-gray-800.transition:hover { | ||||||
|  | 	background: #524f67; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine .m-auto.rounded-xl.max-w-full.w-\[40rem\].mx-2.bg-gray-50.dark\:bg-gray-900.shadow-3xl { | ||||||
|  | 	background-color: #26233a; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine | ||||||
|  | 	.w-full.rounded.p-4.text-sm.dark\:text-gray-300.dark\:bg-gray-800.outline-none.resize-none { | ||||||
|  | 	background-color: #524f67; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine | ||||||
|  | 	.w-full.rounded.py-2.px-4.text-sm.dark\:text-gray-300.dark\:bg-gray-800.outline-none.svelte-1vx7r9s { | ||||||
|  | 	background-color: #524f67; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine | ||||||
|  | 	.px-2\.5.py-2\.5.min-w-fit.rounded-lg.flex-1.md\:flex-none.flex.text-right.transition.bg-gray-200.dark\:bg-gray-700 { | ||||||
|  | 	background-color: #403d52; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine | ||||||
|  | 	.px-2\.5.py-2\.5.min-w-fit.rounded-lg.flex-1.md\:flex-none.flex.text-right.transition.hover\:bg-gray-300.dark\:hover\:bg-gray-800:hover { | ||||||
|  | 	background-color: #524f67; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine .px-4.py-2.bg-emerald-600.hover\:bg-emerald-700.text-gray-100.transition.rounded { | ||||||
|  | 	background-color: #31748f; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .rose-pine #chat-search > * { | ||||||
|  | 	background-color: #403d52 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .rose-pine .svelte-1ee93ns { | .rose-pine .svelte-1ee93ns { | ||||||
| 	--primary: #eb6f92 !important; | 	--primary: #eb6f92 !important; | ||||||
| 	--secondary: #e0def4 !important; | 	--secondary: #e0def4 !important; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek