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/ | ||||
| eggs/ | ||||
| .eggs/ | ||||
| lib/ | ||||
| lib64/ | ||||
| parts/ | ||||
| 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. | ||||
| 
 | ||||
| - 📜 **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. | ||||
| 
 | ||||
| - ⬆️ **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.routing import APIRoute | ||||
| 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 | ||||
| 
 | ||||
| app = FastAPI() | ||||
|  | @ -9,6 +9,7 @@ app = FastAPI() | |||
| origins = ["*"] | ||||
| 
 | ||||
| app.state.ENABLE_SIGNUP = True | ||||
| app.state.DEFAULT_MODELS = None | ||||
| 
 | ||||
| app.add_middleware( | ||||
|     CORSMiddleware, | ||||
|  | @ -19,13 +20,21 @@ app.add_middleware( | |||
| ) | ||||
| 
 | ||||
| app.include_router(auths.router, prefix="/auths", tags=["auths"]) | ||||
| 
 | ||||
| app.include_router(users.router, prefix="/users", tags=["users"]) | ||||
| app.include_router(chats.router, prefix="/chats", tags=["chats"]) | ||||
| app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"]) | ||||
| app.include_router(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.get("/") | ||||
| 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 | ||||
| 
 | ||||
| #################### | ||||
| # 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 uuid | ||||
| 
 | ||||
| 
 | ||||
| from apps.web.models.auths import ( | ||||
|     SigninForm, | ||||
|     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.misc import get_gravatar_url | ||||
| from utils.misc import get_gravatar_url, validate_email_format | ||||
| from constants import ERROR_MESSAGES | ||||
| 
 | ||||
| 
 | ||||
|  | @ -95,6 +96,7 @@ async def signin(form_data: SigninForm): | |||
| @router.post("/signup", response_model=SigninResponse) | ||||
| async def signup(request: Request, form_data: SignupForm): | ||||
|     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()): | ||||
|                 try: | ||||
|                     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, | ||||
|                         } | ||||
|                     else: | ||||
|                     raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) | ||||
|                         raise HTTPException( | ||||
|                             500, detail=ERROR_MESSAGES.CREATE_USER_ERROR | ||||
|                         ) | ||||
|                 except Exception as err: | ||||
|                     raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) | ||||
|             else: | ||||
|                 raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) | ||||
|         else: | ||||
|             raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) | ||||
|     else: | ||||
|         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 = ( | ||||
|         "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 = ( | ||||
|         "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_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 = ( | ||||
|         "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 :/" | ||||
|     USER_NOT_FOUND = "We could not find what you're looking for :/" | ||||
| 
 | ||||
|     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 '*' | ||||
|  | @ -1,4 +1,5 @@ | |||
| import hashlib | ||||
| import re | ||||
| 
 | ||||
| 
 | ||||
| def get_gravatar_url(email): | ||||
|  | @ -21,3 +22,9 @@ def calculate_sha256(file): | |||
|     for chunk in iter(lambda: file.read(8192), b""): | ||||
|         sha256.update(chunk) | ||||
|     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) | ||||
| 			) { | ||||
| 				document.documentElement.classList.add('light'); | ||||
| 			} else if (localStorage.theme === 'dark') { | ||||
| 				document.documentElement.classList.add('dark'); | ||||
| 			} else if (localStorage.theme) { | ||||
| 				localStorage.theme.split(' ').forEach((e) => { | ||||
| 					document.documentElement.classList.add(e); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				document.documentElement.classList.add('dark'); | ||||
| 				document.documentElement.classList.add(localStorage.theme); | ||||
| 			} | ||||
| 		</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"> | ||||
| 	import { settings } from '$lib/stores'; | ||||
| 	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 { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	export let submitPrompt: Function; | ||||
| 	export let stopResponse: Function; | ||||
|  | @ -11,6 +14,8 @@ | |||
| 	export let autoScroll = true; | ||||
| 
 | ||||
| 	let filesInputElement; | ||||
| 	let promptsElement; | ||||
| 
 | ||||
| 	let inputFiles; | ||||
| 	let dragged = false; | ||||
| 
 | ||||
|  | @ -154,12 +159,8 @@ | |||
| 
 | ||||
| <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"> | ||||
| 		{#if messages.length == 0 && suggestionPrompts.length !== 0} | ||||
| 			<div class="max-w-3xl w-full"> | ||||
| 				<Suggestions {suggestionPrompts} {submitPrompt} /> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 
 | ||||
| 		<div class="flex flex-col max-w-3xl w-full"> | ||||
| 			<div> | ||||
| 				{#if autoScroll === false && messages.length > 0} | ||||
| 					<div class=" flex justify-center mb-4"> | ||||
| 						<button | ||||
|  | @ -185,6 +186,16 @@ | |||
| 					</div> | ||||
| 				{/if} | ||||
| 			</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="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"> | ||||
|  | @ -287,7 +298,7 @@ | |||
| 							id="chat-textarea" | ||||
| 							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'} | ||||
| 							bind:value={prompt} | ||||
| 							on:keypress={(e) => { | ||||
|  | @ -298,7 +309,7 @@ | |||
| 									submitPrompt(prompt); | ||||
| 								} | ||||
| 							}} | ||||
| 							on:keydown={(e) => { | ||||
| 							on:keydown={async (e) => { | ||||
| 								if (prompt === '' && e.key == 'ArrowUp') { | ||||
| 									e.preventDefault(); | ||||
| 
 | ||||
|  | @ -315,6 +326,61 @@ | |||
| 									userMessageElement.scrollIntoView({ block: 'center' }); | ||||
| 									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" | ||||
| 							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"> | ||||
| 	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'; | ||||
| 
 | ||||
| 	export let selectedModels = ['']; | ||||
| 	export let disabled = false; | ||||
| 
 | ||||
| 	const saveDefaultModel = () => { | ||||
| 	const saveDefaultModel = async () => { | ||||
| 		const hasEmptyModel = selectedModels.filter((it) => it === ''); | ||||
| 		if (hasEmptyModel.length) { | ||||
| 			toast.error('Choose a model before saving...'); | ||||
|  | @ -13,8 +15,19 @@ | |||
| 		} | ||||
| 		settings.set({ ...$settings, models: selectedModels }); | ||||
| 		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'); | ||||
| 	}; | ||||
| 
 | ||||
| 	$: if (selectedModels.length > 0 && $models.length > 0) { | ||||
| 		selectedModels = selectedModels.map((model) => | ||||
| 			$models.map((m) => m.name).includes(model) ? model : '' | ||||
| 		); | ||||
| 	} | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col my-2"> | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ | |||
| 
 | ||||
| 	// General | ||||
| 	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 notificationEnabled = false; | ||||
| 	let system = ''; | ||||
|  | @ -74,17 +74,20 @@ | |||
| 
 | ||||
| 	let deleteModelTag = ''; | ||||
| 
 | ||||
| 	// External | ||||
| 
 | ||||
| 	let OPENAI_API_KEY = ''; | ||||
| 	let OPENAI_API_BASE_URL = ''; | ||||
| 
 | ||||
| 	// Addons | ||||
| 	let titleAutoGenerate = true; | ||||
| 	let speechAutoSend = false; | ||||
| 	let responseAutoCopy = false; | ||||
| 
 | ||||
| 	let gravatarEmail = ''; | ||||
| 	let OPENAI_API_KEY = ''; | ||||
| 	let OPENAI_API_BASE_URL = ''; | ||||
| 	let titleAutoGenerateModel = ''; | ||||
| 
 | ||||
| 	// Chats | ||||
| 
 | ||||
| 	let importFiles; | ||||
| 	let showDeleteConfirm = false; | ||||
| 
 | ||||
|  | @ -656,13 +659,14 @@ | |||
| 		options = { ...options, ...settings.options }; | ||||
| 		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; | ||||
| 		speechAutoSend = settings.speechAutoSend ?? false; | ||||
| 		responseAutoCopy = settings.responseAutoCopy ?? false; | ||||
| 
 | ||||
| 		titleAutoGenerateModel = settings.titleAutoGenerateModel ?? ''; | ||||
| 		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; | ||||
| 		if (authEnabled) { | ||||
|  | @ -757,6 +761,7 @@ | |||
| 					<div class=" self-center">Advanced</div> | ||||
| 				</button> | ||||
| 
 | ||||
| 				{#if $user?.role === 'admin'} | ||||
| 					<button | ||||
| 						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | ||||
| 						'models' | ||||
|  | @ -782,6 +787,7 @@ | |||
| 						</div> | ||||
| 						<div class=" self-center">Models</div> | ||||
| 					</button> | ||||
| 				{/if} | ||||
| 
 | ||||
| 				<button | ||||
| 					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 | ||||
| 												.filter((e) => e !== theme) | ||||
| 												.forEach((e) => { | ||||
| 													e.split(' ').forEach((e) => { | ||||
| 														document.documentElement.classList.remove(e); | ||||
| 													}); | ||||
| 												}); | ||||
| 
 | ||||
| 											document.documentElement.classList.add(theme); | ||||
| 
 | ||||
| 											if (theme === 'rose-pine') { | ||||
| 												document.documentElement.classList.add('dark'); | ||||
| 											} | ||||
| 											theme.split(' ').forEach((e) => { | ||||
| 												document.documentElement.classList.add(e); | ||||
| 											}); | ||||
| 
 | ||||
| 											console.log(theme); | ||||
| 										}} | ||||
| 									> | ||||
| 										<option value="dark">Dark</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> | ||||
| 								</div> | ||||
| 							</div> | ||||
|  | @ -1547,10 +1554,6 @@ | |||
| 					<form | ||||
| 						class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||
| 						on:submit|preventDefault={() => { | ||||
| 							saveSettings({ | ||||
| 								gravatarEmail: gravatarEmail !== '' ? gravatarEmail : undefined, | ||||
| 								gravatarUrl: gravatarEmail !== '' ? getGravatarURL(gravatarEmail) : undefined | ||||
| 							}); | ||||
| 							show = false; | ||||
| 						}} | ||||
| 					> | ||||
|  | @ -1560,7 +1563,7 @@ | |||
| 
 | ||||
| 								<div> | ||||
| 									<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 | ||||
| 											class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -1622,6 +1625,54 @@ | |||
| 							</div> | ||||
| 
 | ||||
| 							<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 class=" mb-2.5 text-sm font-medium"> | ||||
| 									Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span> | ||||
|  | @ -1644,7 +1695,7 @@ | |||
| 										target="_blank">Gravatar.</a | ||||
| 									> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 							</div> --> | ||||
| 						</div> | ||||
| 
 | ||||
| 						<div class="flex justify-end pt-3 text-sm font-medium"> | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 		</div> | ||||
| 
 | ||||
| 		{#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 | ||||
| 					class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition" | ||||
| 					on:click={async () => { | ||||
|  | @ -129,10 +129,38 @@ | |||
| 					</div> | ||||
| 				</button> | ||||
| 			</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} | ||||
| 
 | ||||
| 		<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"> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
|  |  | |||
|  | @ -6,10 +6,13 @@ export const user = writable(undefined); | |||
| 
 | ||||
| // Frontend
 | ||||
| export const theme = writable('dark'); | ||||
| 
 | ||||
| export const chatId = writable(''); | ||||
| 
 | ||||
| export const chats = writable([]); | ||||
| export const models = writable([]); | ||||
| export const modelfiles = writable([]); | ||||
| export const prompts = writable([]); | ||||
| 
 | ||||
| export const settings = writable({}); | ||||
| export const showSettings = writable(false); | ||||
|  |  | |||
|  | @ -111,3 +111,19 @@ export const checkVersion = (required, current) => { | |||
| 				caseFirst: 'upper' | ||||
| 		  }) < 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 { getModelfiles } from '$lib/apis/modelfiles'; | ||||
| 	import { getPrompts } from '$lib/apis/prompts'; | ||||
| 
 | ||||
| 	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 SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | ||||
|  | @ -101,6 +102,9 @@ | |||
| 			console.log(); | ||||
| 			await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); | ||||
| 			await modelfiles.set(await getModelfiles(localStorage.token)); | ||||
| 
 | ||||
| 			await prompts.set(await getPrompts(localStorage.token)); | ||||
| 
 | ||||
| 			console.log($modelfiles); | ||||
| 
 | ||||
| 			modelfiles.subscribe(async () => { | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
| 	import { goto } from '$app/navigation'; | ||||
| 	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 { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; | ||||
|  | @ -90,9 +90,18 @@ | |||
| 			messages: {}, | ||||
| 			currentId: null | ||||
| 		}; | ||||
| 		selectedModels = $page.url.searchParams.get('models') | ||||
| 			? $page.url.searchParams.get('models')?.split(',') | ||||
| 			: $settings.models ?? ['']; | ||||
| 
 | ||||
| 		console.log($config); | ||||
| 
 | ||||
| 		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') ?? '{}'); | ||||
| 		settings.set({ | ||||
|  | @ -109,10 +118,14 @@ | |||
| 		await Promise.all( | ||||
| 			selectedModels.map(async (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); | ||||
| 				} else { | ||||
| 				} else if (modelTag) { | ||||
| 					await sendPromptOllama(model, prompt, parentId, _chatId); | ||||
| 				} else { | ||||
| 					toast.error(`Model ${model} not found`); | ||||
| 				} | ||||
| 			}) | ||||
| 		); | ||||
|  | @ -379,13 +392,13 @@ | |||
| 										  } | ||||
| 										: { content: message.content }) | ||||
| 								})), | ||||
| 							seed: $settings.options.seed ?? undefined, | ||||
| 							stop: $settings.options.stop ?? undefined, | ||||
| 							temperature: $settings.options.temperature ?? undefined, | ||||
| 							top_p: $settings.options.top_p ?? undefined, | ||||
| 							num_ctx: $settings.options.num_ctx ?? undefined, | ||||
| 							frequency_penalty: $settings.options.repeat_penalty ?? undefined, | ||||
| 							max_tokens: $settings.options.num_predict ?? undefined | ||||
| 							seed: $settings?.options?.seed ?? undefined, | ||||
| 							stop: $settings?.options?.stop ?? undefined, | ||||
| 							temperature: $settings?.options?.temperature ?? undefined, | ||||
| 							top_p: $settings?.options?.top_p ?? undefined, | ||||
| 							num_ctx: $settings?.options?.num_ctx ?? undefined, | ||||
| 							frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, | ||||
| 							max_tokens: $settings?.options?.num_predict ?? undefined | ||||
| 						}) | ||||
| 					} | ||||
| 				).catch((err) => { | ||||
|  | @ -584,7 +597,7 @@ | |||
| 			const title = await generateTitle( | ||||
| 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, | ||||
| 				localStorage.token, | ||||
| 				selectedModels[0], | ||||
| 				$settings?.titleAutoGenerateModel ?? selectedModels[0], | ||||
| 				userPrompt | ||||
| 			); | ||||
| 
 | ||||
|  |  | |||
|  | @ -136,17 +136,20 @@ | |||
| 		await Promise.all( | ||||
| 			selectedModels.map(async (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); | ||||
| 				} else { | ||||
| 				} else if (modelTag) { | ||||
| 					await sendPromptOllama(model, prompt, parentId, _chatId); | ||||
| 				} else { | ||||
| 					toast.error(`Model ${model} not found`); | ||||
| 				} | ||||
| 			}) | ||||
| 		); | ||||
| 
 | ||||
| 		await chats.set(await getChatList(localStorage.token)); | ||||
| 	}; | ||||
| 
 | ||||
| 	const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => { | ||||
| 		// Create response message | ||||
| 		let responseMessageId = uuidv4(); | ||||
|  | @ -406,13 +409,13 @@ | |||
| 										  } | ||||
| 										: { content: message.content }) | ||||
| 								})), | ||||
| 							seed: $settings.options.seed ?? undefined, | ||||
| 							stop: $settings.options.stop ?? undefined, | ||||
| 							temperature: $settings.options.temperature ?? undefined, | ||||
| 							top_p: $settings.options.top_p ?? undefined, | ||||
| 							num_ctx: $settings.options.num_ctx ?? undefined, | ||||
| 							frequency_penalty: $settings.options.repeat_penalty ?? undefined, | ||||
| 							max_tokens: $settings.options.num_predict ?? undefined | ||||
| 							seed: $settings?.options?.seed ?? undefined, | ||||
| 							stop: $settings?.options?.stop ?? undefined, | ||||
| 							temperature: $settings?.options?.temperature ?? undefined, | ||||
| 							top_p: $settings?.options?.top_p ?? undefined, | ||||
| 							num_ctx: $settings?.options?.num_ctx ?? undefined, | ||||
| 							frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, | ||||
| 							max_tokens: $settings?.options?.num_predict ?? undefined | ||||
| 						}) | ||||
| 					} | ||||
| 				).catch((err) => { | ||||
|  |  | |||
|  | @ -44,7 +44,7 @@ | |||
| 
 | ||||
| 		const url = 'https://ollamahub.com'; | ||||
| 
 | ||||
| 		const tab = await window.open(`${url}/create`, '_blank'); | ||||
| 		const tab = await window.open(`${url}/modelfiles/create`, '_blank'); | ||||
| 		window.addEventListener( | ||||
| 			'message', | ||||
| 			(event) => { | ||||
|  | @ -254,6 +254,30 @@ | |||
| 							</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 () => { | ||||
| 							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> | ||||
| 
 | ||||
| 				{#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> | ||||
| 
 | ||||
| 	<link rel="stylesheet" type="text/css" href="/themes/rosepine.css" /> | ||||
| 	<link rel="stylesheet" type="text/css" href="/themes/rosepine-dawn.css" /> | ||||
| </svelte:head> | ||||
| 
 | ||||
| {#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 | ||||
| 	.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; | ||||
| 	transition: background 0.2s ease-out linear; | ||||
| 	transition: background-color 0.2s ease-out linear; | ||||
| } | ||||
| 
 | ||||
| .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 { | ||||
| 	background-color: #286983; | ||||
| 	transition: background 0.2s ease-out linear; | ||||
| 	transition: background-color 0.2s ease-out linear; | ||||
| } | ||||
| 
 | ||||
| .rose-pine | ||||
|  | @ -80,6 +80,46 @@ | |||
| 	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 { | ||||
| 	--primary: #eb6f92 !important; | ||||
| 	--secondary: #e0def4 !important; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek