forked from open-webui/open-webui
		
	feat: prompt crud
This commit is contained in:
		
							parent
							
								
									69ff596045
								
							
						
					
					
						commit
						7fc1d7c2c7
					
				
					 7 changed files with 167 additions and 451 deletions
				
			
		|  | @ -37,14 +37,21 @@ async def create_new_prompt(form_data: PromptForm, user=Depends(get_current_user | ||||||
|             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     prompt = Prompts.insert_new_prompt(user.id, form_data) |     prompt = Prompts.get_prompt_by_command(form_data.command) | ||||||
|  |     if prompt == None: | ||||||
|  |         prompt = Prompts.insert_new_prompt(user.id, form_data) | ||||||
| 
 | 
 | ||||||
|     if prompt: |         if prompt: | ||||||
|         return prompt |             return prompt | ||||||
|  |         else: | ||||||
|  |             raise HTTPException( | ||||||
|  |                 status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|  |                 detail=ERROR_MESSAGES.DEFAULT(), | ||||||
|  |             ) | ||||||
|     else: |     else: | ||||||
|         raise HTTPException( |         raise HTTPException( | ||||||
|             status_code=status.HTTP_401_UNAUTHORIZED, |             status_code=status.HTTP_400_BAD_REQUEST, | ||||||
|             detail=ERROR_MESSAGES.DEFAULT(), |             detail=ERROR_MESSAGES.COMMAND_TAKEN, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -55,7 +62,7 @@ async def create_new_prompt(form_data: PromptForm, user=Depends(get_current_user | ||||||
| 
 | 
 | ||||||
| @router.get("/{command}", response_model=Optional[PromptModel]) | @router.get("/{command}", response_model=Optional[PromptModel]) | ||||||
| async def get_prompt_by_command(command: str, user=Depends(get_current_user)): | async def get_prompt_by_command(command: str, user=Depends(get_current_user)): | ||||||
|     prompt = Prompts.get_prompt_by_command(command) |     prompt = Prompts.get_prompt_by_command(f"/{command}") | ||||||
| 
 | 
 | ||||||
|     if prompt: |     if prompt: | ||||||
|         return prompt |         return prompt | ||||||
|  | @ -81,7 +88,7 @@ async def update_prompt_by_command( | ||||||
|             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     prompt = Prompts.update_prompt_by_command(command, form_data) |     prompt = Prompts.update_prompt_by_command(f"/{command}", form_data) | ||||||
|     if prompt: |     if prompt: | ||||||
|         return prompt |         return prompt | ||||||
|     else: |     else: | ||||||
|  | @ -104,5 +111,5 @@ async def delete_prompt_by_command(command: str, user=Depends(get_current_user)) | ||||||
|             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, |             detail=ERROR_MESSAGES.ACCESS_PROHIBITED, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     result = Prompts.delete_prompt_by_command(command) |     result = Prompts.delete_prompt_by_command(f"/{command}") | ||||||
|     return result |     return result | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     USERNAME_TAKEN = ( |     USERNAME_TAKEN = ( | ||||||
|         "Uh-oh! This username is already registered. Please choose another username." |         "Uh-oh! This username is already registered. Please choose another username." | ||||||
|     ) |     ) | ||||||
|  |     COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string." | ||||||
|     INVALID_TOKEN = ( |     INVALID_TOKEN = ( | ||||||
|         "Your session has expired or the token is invalid. Please sign in again." |         "Your session has expired or the token is invalid. Please sign in again." | ||||||
|     ) |     ) | ||||||
|  | @ -32,5 +33,4 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     ) |     ) | ||||||
|     NOT_FOUND = "We could not find what you're looking for :/" |     NOT_FOUND = "We could not find what you're looking for :/" | ||||||
|     USER_NOT_FOUND = "We could not find what you're looking for :/" |     USER_NOT_FOUND = "We could not find what you're looking for :/" | ||||||
| 
 |  | ||||||
|     MALICIOUS = "Unusual activities detected, please try again in a few minutes." |     MALICIOUS = "Unusual activities detected, please try again in a few minutes." | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ export const createNewPrompt = async ( | ||||||
| 			authorization: `Bearer ${token}` | 			authorization: `Bearer ${token}` | ||||||
| 		}, | 		}, | ||||||
| 		body: JSON.stringify({ | 		body: JSON.stringify({ | ||||||
| 			command: command, | 			command: `/${command}`, | ||||||
| 			title: title, | 			title: title, | ||||||
| 			content: content | 			content: content | ||||||
| 		}) | 		}) | ||||||
|  | @ -57,7 +57,7 @@ export const getPrompts = async (token: string = '') => { | ||||||
| 			return json; | 			return json; | ||||||
| 		}) | 		}) | ||||||
| 		.catch((err) => { | 		.catch((err) => { | ||||||
| 			error = err; | 			error = err.detail; | ||||||
| 			console.log(err); | 			console.log(err); | ||||||
| 			return null; | 			return null; | ||||||
| 		}); | 		}); | ||||||
|  | @ -88,7 +88,7 @@ export const getPromptByCommand = async (token: string, command: string) => { | ||||||
| 			return json; | 			return json; | ||||||
| 		}) | 		}) | ||||||
| 		.catch((err) => { | 		.catch((err) => { | ||||||
| 			error = err; | 			error = err.detail; | ||||||
| 
 | 
 | ||||||
| 			console.log(err); | 			console.log(err); | ||||||
| 			return null; | 			return null; | ||||||
|  | @ -117,7 +117,7 @@ export const updatePromptByCommand = async ( | ||||||
| 			authorization: `Bearer ${token}` | 			authorization: `Bearer ${token}` | ||||||
| 		}, | 		}, | ||||||
| 		body: JSON.stringify({ | 		body: JSON.stringify({ | ||||||
| 			command: command, | 			command: `/${command}`, | ||||||
| 			title: title, | 			title: title, | ||||||
| 			content: content | 			content: content | ||||||
| 		}) | 		}) | ||||||
|  | @ -130,7 +130,7 @@ export const updatePromptByCommand = async ( | ||||||
| 			return json; | 			return json; | ||||||
| 		}) | 		}) | ||||||
| 		.catch((err) => { | 		.catch((err) => { | ||||||
| 			error = err; | 			error = err.detail; | ||||||
| 
 | 
 | ||||||
| 			console.log(err); | 			console.log(err); | ||||||
| 			return null; | 			return null; | ||||||
|  | @ -146,6 +146,8 @@ export const updatePromptByCommand = async ( | ||||||
| export const deletePromptByCommand = async (token: string, command: string) => { | export const deletePromptByCommand = async (token: string, command: string) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
|  | 	command = command.charAt(0) === '/' ? command.slice(1) : command; | ||||||
|  | 
 | ||||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}/delete`, { | 	const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}/delete`, { | ||||||
| 		method: 'DELETE', | 		method: 'DELETE', | ||||||
| 		headers: { | 		headers: { | ||||||
|  | @ -162,7 +164,7 @@ export const deletePromptByCommand = async (token: string, command: string) => { | ||||||
| 			return json; | 			return json; | ||||||
| 		}) | 		}) | ||||||
| 		.catch((err) => { | 		.catch((err) => { | ||||||
| 			error = err; | 			error = err.detail; | ||||||
| 
 | 
 | ||||||
| 			console.log(err); | 			console.log(err); | ||||||
| 			return null; | 			return null; | ||||||
|  |  | ||||||
|  | @ -53,8 +53,8 @@ | ||||||
| 				<div class=" text-lg font-medium mt-2">/</div> | 				<div class=" text-lg font-medium mt-2">/</div> | ||||||
| 			</div> | 			</div> | ||||||
| 
 | 
 | ||||||
| 			<div class="max-h-60 flex flex-col w-full"> | 			<div class="max-h-60 flex flex-col w-full rounded-r-lg"> | ||||||
| 				<div class=" overflow-y-auto bg-white p-2 rounded-r-lg space-y-0.5"> | 				<div class=" overflow-y-auto bg-white p-2 rounded-t-lg space-y-0.5"> | ||||||
| 					{#each filteredPromptCommands as command, commandIdx} | 					{#each filteredPromptCommands as command, commandIdx} | ||||||
| 						<button | 						<button | ||||||
| 							class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx | 							class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx | ||||||
|  | @ -80,7 +80,9 @@ | ||||||
| 					{/each} | 					{/each} | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<div class=" px-2 py-0.5 text-xs text-gray-600 flex items-center space-x-1"> | 				<div | ||||||
|  | 					class=" px-2 py-0.5 text-xs text-gray-600 bg-white rounded-b-lg flex items-center space-x-1" | ||||||
|  | 				> | ||||||
| 					<div> | 					<div> | ||||||
| 						<svg | 						<svg | ||||||
| 							xmlns="http://www.w3.org/2000/svg" | 							xmlns="http://www.w3.org/2000/svg" | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ | ||||||
| 
 | 
 | ||||||
| 	import { onMount } from 'svelte'; | 	import { onMount } from 'svelte'; | ||||||
| 	import { prompts } from '$lib/stores'; | 	import { prompts } from '$lib/stores'; | ||||||
|  | 	import { deletePromptByCommand, getPrompts } from '$lib/apis/prompts'; | ||||||
| 
 | 
 | ||||||
| 	let query = ''; | 	let query = ''; | ||||||
| 
 | 
 | ||||||
|  | @ -152,6 +153,10 @@ and mentions the following keywords | ||||||
| 		} | 		} | ||||||
| 	]; | 	]; | ||||||
| 
 | 
 | ||||||
|  | 	const deletePrompt = async (command) => { | ||||||
|  | 		await deletePromptByCommand(localStorage.token, command); | ||||||
|  | 		await prompts.set(await getPrompts(localStorage.token)); | ||||||
|  | 	}; | ||||||
| 	const loadDefaultPrompts = () => { | 	const loadDefaultPrompts = () => { | ||||||
| 		prompts.set(defaultPrompts); | 		prompts.set(defaultPrompts); | ||||||
| 	}; | 	}; | ||||||
|  | @ -213,12 +218,14 @@ and mentions the following keywords | ||||||
| 					<hr class=" dark:border-gray-700 my-2.5" /> | 					<hr class=" dark:border-gray-700 my-2.5" /> | ||||||
| 					<div class=" flex space-x-4 cursor-pointer w-full mb-3"> | 					<div class=" flex space-x-4 cursor-pointer w-full mb-3"> | ||||||
| 						<div class=" flex flex-1 space-x-4 cursor-pointer w-full"> | 						<div class=" flex flex-1 space-x-4 cursor-pointer w-full"> | ||||||
| 							<div class=" flex-1 self-center"> | 							<a href={`/prompts/edit?command=${encodeURIComponent(prompt.command)}`}> | ||||||
| 								<div class=" font-bold">{prompt.command}</div> | 								<div class=" flex-1 self-center"> | ||||||
| 								<div class=" text-sm overflow-hidden text-ellipsis line-clamp-1"> | 									<div class=" font-bold">{prompt.command}</div> | ||||||
| 									{prompt.title} | 									<div class=" text-sm overflow-hidden text-ellipsis line-clamp-1"> | ||||||
|  | 										{prompt.title} | ||||||
|  | 									</div> | ||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</a> | ||||||
| 						</div> | 						</div> | ||||||
| 						<div class="flex flex-row space-x-1 self-center"> | 						<div class="flex flex-row space-x-1 self-center"> | ||||||
| 							<a | 							<a | ||||||
|  | @ -269,7 +276,7 @@ and mentions the following keywords | ||||||
| 								class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl" | 								class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl" | ||||||
| 								type="button" | 								type="button" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
| 									// deleteModelfile(modelfile.tagName); | 									deletePrompt(prompt.command); | ||||||
| 								}} | 								}} | ||||||
| 							> | 							> | ||||||
| 								<svg | 								<svg | ||||||
|  |  | ||||||
|  | @ -1,12 +1,16 @@ | ||||||
| <script> | <script> | ||||||
| 	import Advanced from '$lib/components/chat/Settings/Advanced.svelte'; |  | ||||||
| 	import { onMount, tick } from 'svelte'; |  | ||||||
| 	import toast from 'svelte-french-toast'; | 	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; | 	let loading = false; | ||||||
| 
 | 
 | ||||||
| 	// /////////// | 	// /////////// | ||||||
| 	// Modelfile | 	// Prompt | ||||||
| 	// /////////// | 	// /////////// | ||||||
| 
 | 
 | ||||||
| 	let title = ''; | 	let title = ''; | ||||||
|  | @ -16,13 +20,26 @@ | ||||||
| 	$: command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : ''; | 	$: command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : ''; | ||||||
| 
 | 
 | ||||||
| 	const submitHandler = async () => { | 	const submitHandler = async () => { | ||||||
| 		console.log(validateCommandString(command)); | 		loading = true; | ||||||
|  | 
 | ||||||
| 		if (validateCommandString(command)) { | 		if (validateCommandString(command)) { | ||||||
| 			console.log('valid'); | 			const prompt = await createNewPrompt(localStorage.token, command, title, content).catch( | ||||||
| 			console.log('submit'); | 				(error) => { | ||||||
|  | 					toast.error(error); | ||||||
|  | 
 | ||||||
|  | 					return null; | ||||||
|  | 				} | ||||||
|  | 			); | ||||||
|  | 
 | ||||||
|  | 			if (prompt) { | ||||||
|  | 				await prompts.set(await getPrompts(localStorage.token)); | ||||||
|  | 				await goto('/prompts'); | ||||||
|  | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			toast.error('Only alphanumeric characters and hyphens are allowed in the command string.'); | 			toast.error('Only alphanumeric characters and hyphens are allowed in the command string.'); | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		loading = false; | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const validateCommandString = (inputString) => { | 	const validateCommandString = (inputString) => { | ||||||
|  | @ -41,33 +58,12 @@ | ||||||
| 				) | 				) | ||||||
| 			) | 			) | ||||||
| 				return; | 				return; | ||||||
| 			const modelfile = JSON.parse(event.data); | 			const prompt = JSON.parse(event.data); | ||||||
| 			console.log(modelfile); | 			console.log(prompt); | ||||||
| 
 | 
 | ||||||
| 			imageUrl = modelfile.imageUrl; | 			title = prompt.title; | ||||||
| 			title = modelfile.title; | 			content = prompt.content; | ||||||
| 			await tick(); | 			command = prompt.command; | ||||||
| 			tagName = `${modelfile.user.username === 'hub' ? '' : `hub/`}${modelfile.user.username}/${ |  | ||||||
| 				modelfile.tagName |  | ||||||
| 			}`; |  | ||||||
| 			desc = modelfile.desc; |  | ||||||
| 			content = modelfile.content; |  | ||||||
| 			suggestions = |  | ||||||
| 				modelfile.suggestionPrompts.length != 0 |  | ||||||
| 					? modelfile.suggestionPrompts |  | ||||||
| 					: [ |  | ||||||
| 							{ |  | ||||||
| 								content: '' |  | ||||||
| 							} |  | ||||||
| 					  ]; |  | ||||||
| 
 |  | ||||||
| 			modelfileCreator = { |  | ||||||
| 				username: modelfile.user.username, |  | ||||||
| 				name: modelfile.user.name |  | ||||||
| 			}; |  | ||||||
| 			for (const category of modelfile.categories) { |  | ||||||
| 				categories[category.toLowerCase()] = true; |  | ||||||
| 			} |  | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		if (window.opener ?? false) { | 		if (window.opener ?? false) { | ||||||
|  | @ -155,12 +151,10 @@ | ||||||
| 
 | 
 | ||||||
| 				<div class="my-2"> | 				<div class="my-2"> | ||||||
| 					<div class="flex w-full justify-between"> | 					<div class="flex w-full justify-between"> | ||||||
| 						<div class=" self-center text-sm font-semibold">Prompt</div> | 						<div class=" self-center text-sm font-semibold">Prompt Content*</div> | ||||||
| 					</div> | 					</div> | ||||||
| 
 | 
 | ||||||
| 					<div class="mt-2"> | 					<div class="mt-2"> | ||||||
| 						<div class=" text-xs font-semibold mb-2">Content*</div> |  | ||||||
| 
 |  | ||||||
| 						<div> | 						<div> | ||||||
| 							<textarea | 							<textarea | ||||||
| 								class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" | 								class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" | ||||||
|  | @ -174,7 +168,10 @@ | ||||||
| 						<div class="text-xs text-gray-400 dark:text-gray-500"> | 						<div class="text-xs text-gray-400 dark:text-gray-500"> | ||||||
| 							Format your variables using square brackets like this: <span | 							Format your variables using square brackets like this: <span | ||||||
| 								class=" text-gray-600 dark:text-gray-300 font-medium">[variable]</span | 								class=" text-gray-600 dark:text-gray-300 font-medium">[variable]</span | ||||||
| 							> . Remember to enclose them with '[' and ']'. | 							> | ||||||
|  | 							. 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> | ||||||
| 				</div> | 				</div> | ||||||
|  |  | ||||||
|  | @ -1,261 +1,80 @@ | ||||||
| <script> | <script> | ||||||
| 	import { v4 as uuidv4 } from 'uuid'; | 	import toast from 'svelte-french-toast'; | ||||||
| 	import { toast } from 'svelte-french-toast'; | 
 | ||||||
| 	import { goto } from '$app/navigation'; | 	import { goto } from '$app/navigation'; | ||||||
|  | 	import { prompts } from '$lib/stores'; | ||||||
|  | 	import { onMount, tick } from 'svelte'; | ||||||
| 
 | 
 | ||||||
| 	import { onMount } from 'svelte'; | 	import { getPrompts, updatePromptByCommand } from '$lib/apis/prompts'; | ||||||
| 	import { page } from '$app/stores'; | 	import { page } from '$app/stores'; | ||||||
| 
 | 
 | ||||||
| 	import { settings, user, config, modelfiles } from '$lib/stores'; |  | ||||||
| 
 |  | ||||||
| 	import { OLLAMA_API_BASE_URL } from '$lib/constants'; |  | ||||||
| 	import { splitStream } from '$lib/utils'; |  | ||||||
| 
 |  | ||||||
| 	import { createModel } from '$lib/apis/ollama'; |  | ||||||
| 	import { getModelfiles, updateModelfileByTagName } from '$lib/apis/modelfiles'; |  | ||||||
| 
 |  | ||||||
| 	import Advanced from '$lib/components/chat/Settings/Advanced.svelte'; |  | ||||||
| 
 |  | ||||||
| 	let loading = false; | 	let loading = false; | ||||||
| 
 | 
 | ||||||
| 	let filesInputElement; |  | ||||||
| 	let inputFiles; |  | ||||||
| 	let imageUrl = null; |  | ||||||
| 	let digest = ''; |  | ||||||
| 	let pullProgress = null; |  | ||||||
| 	let success = false; |  | ||||||
| 
 |  | ||||||
| 	let modelfile = null; |  | ||||||
| 	// /////////// | 	// /////////// | ||||||
| 	// Modelfile | 	// Prompt | ||||||
| 	// /////////// | 	// /////////// | ||||||
| 
 | 
 | ||||||
| 	let title = ''; | 	let title = ''; | ||||||
| 	let tagName = ''; | 	let command = ''; | ||||||
| 	let desc = ''; |  | ||||||
| 
 |  | ||||||
| 	// Raw Mode |  | ||||||
| 	let content = ''; | 	let content = ''; | ||||||
| 
 | 
 | ||||||
| 	let suggestions = [ |  | ||||||
| 		{ |  | ||||||
| 			content: '' |  | ||||||
| 		} |  | ||||||
| 	]; |  | ||||||
| 
 |  | ||||||
| 	let categories = { |  | ||||||
| 		character: false, |  | ||||||
| 		assistant: false, |  | ||||||
| 		writing: false, |  | ||||||
| 		productivity: false, |  | ||||||
| 		programming: false, |  | ||||||
| 		'data analysis': false, |  | ||||||
| 		lifestyle: false, |  | ||||||
| 		education: false, |  | ||||||
| 		business: false |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	onMount(() => { |  | ||||||
| 		tagName = $page.url.searchParams.get('tag'); |  | ||||||
| 
 |  | ||||||
| 		if (tagName) { |  | ||||||
| 			modelfile = $modelfiles.filter((modelfile) => modelfile.tagName === tagName)[0]; |  | ||||||
| 
 |  | ||||||
| 			console.log(modelfile); |  | ||||||
| 
 |  | ||||||
| 			imageUrl = modelfile.imageUrl; |  | ||||||
| 			title = modelfile.title; |  | ||||||
| 			desc = modelfile.desc; |  | ||||||
| 			content = modelfile.content; |  | ||||||
| 			suggestions = |  | ||||||
| 				modelfile.suggestionPrompts.length != 0 |  | ||||||
| 					? modelfile.suggestionPrompts |  | ||||||
| 					: [ |  | ||||||
| 							{ |  | ||||||
| 								content: '' |  | ||||||
| 							} |  | ||||||
| 					  ]; |  | ||||||
| 
 |  | ||||||
| 			for (const category of modelfile.categories) { |  | ||||||
| 				categories[category.toLowerCase()] = true; |  | ||||||
| 			} |  | ||||||
| 		} else { |  | ||||||
| 			goto('/modelfiles'); |  | ||||||
| 		} |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	const updateModelfile = async (modelfile) => { |  | ||||||
| 		await updateModelfileByTagName(localStorage.token, modelfile.tagName, modelfile); |  | ||||||
| 		await modelfiles.set(await getModelfiles(localStorage.token)); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const updateHandler = async () => { | 	const updateHandler = async () => { | ||||||
| 		loading = true; | 		loading = true; | ||||||
| 
 | 
 | ||||||
| 		if (Object.keys(categories).filter((category) => categories[category]).length == 0) { | 		if (validateCommandString(command)) { | ||||||
| 			toast.error( | 			const prompt = await updatePromptByCommand(localStorage.token, command, title, content).catch( | ||||||
| 				'Uh-oh! It looks like you missed selecting a category. Please choose one to complete your modelfile.' | 				(error) => { | ||||||
| 			); | 					toast.error(error); | ||||||
| 		} | 					return null; | ||||||
| 
 |  | ||||||
| 		if ( |  | ||||||
| 			title !== '' && |  | ||||||
| 			desc !== '' && |  | ||||||
| 			content !== '' && |  | ||||||
| 			Object.keys(categories).filter((category) => categories[category]).length > 0 |  | ||||||
| 		) { |  | ||||||
| 			const res = await createModel( |  | ||||||
| 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 				localStorage.token, |  | ||||||
| 				tagName, |  | ||||||
| 				content |  | ||||||
| 			); |  | ||||||
| 
 |  | ||||||
| 			if (res) { |  | ||||||
| 				const reader = res.body |  | ||||||
| 					.pipeThrough(new TextDecoderStream()) |  | ||||||
| 					.pipeThrough(splitStream('\n')) |  | ||||||
| 					.getReader(); |  | ||||||
| 
 |  | ||||||
| 				while (true) { |  | ||||||
| 					const { value, done } = await reader.read(); |  | ||||||
| 					if (done) break; |  | ||||||
| 
 |  | ||||||
| 					try { |  | ||||||
| 						let lines = value.split('\n'); |  | ||||||
| 
 |  | ||||||
| 						for (const line of lines) { |  | ||||||
| 							if (line !== '') { |  | ||||||
| 								console.log(line); |  | ||||||
| 								let data = JSON.parse(line); |  | ||||||
| 								console.log(data); |  | ||||||
| 
 |  | ||||||
| 								if (data.error) { |  | ||||||
| 									throw data.error; |  | ||||||
| 								} |  | ||||||
| 								if (data.detail) { |  | ||||||
| 									throw data.detail; |  | ||||||
| 								} |  | ||||||
| 
 |  | ||||||
| 								if (data.status) { |  | ||||||
| 									if ( |  | ||||||
| 										!data.digest && |  | ||||||
| 										!data.status.includes('writing') && |  | ||||||
| 										!data.status.includes('sha256') |  | ||||||
| 									) { |  | ||||||
| 										toast.success(data.status); |  | ||||||
| 
 |  | ||||||
| 										if (data.status === 'success') { |  | ||||||
| 											success = true; |  | ||||||
| 										} |  | ||||||
| 									} else { |  | ||||||
| 										if (data.digest) { |  | ||||||
| 											digest = data.digest; |  | ||||||
| 
 |  | ||||||
| 											if (data.completed) { |  | ||||||
| 												pullProgress = Math.round((data.completed / data.total) * 1000) / 10; |  | ||||||
| 											} else { |  | ||||||
| 												pullProgress = 100; |  | ||||||
| 											} |  | ||||||
| 										} |  | ||||||
| 									} |  | ||||||
| 								} |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} catch (error) { |  | ||||||
| 						console.log(error); |  | ||||||
| 						toast.error(error); |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
| 			} | 			); | ||||||
| 
 | 
 | ||||||
| 			if (success) { | 			if (prompt) { | ||||||
| 				await updateModelfile({ | 				await prompts.set(await getPrompts(localStorage.token)); | ||||||
| 					tagName: tagName, | 				await goto('/prompts'); | ||||||
| 					imageUrl: imageUrl, |  | ||||||
| 					title: title, |  | ||||||
| 					desc: desc, |  | ||||||
| 					content: content, |  | ||||||
| 					suggestionPrompts: suggestions.filter((prompt) => prompt.content !== ''), |  | ||||||
| 					categories: Object.keys(categories).filter((category) => categories[category]) |  | ||||||
| 				}); |  | ||||||
| 				await goto('/modelfiles'); |  | ||||||
| 			} | 			} | ||||||
|  | 		} else { | ||||||
|  | 			toast.error('Only alphanumeric characters and hyphens are allowed in the command string.'); | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
| 		loading = false; | 		loading = false; | ||||||
| 		success = 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> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="min-h-screen w-full flex justify-center dark:text-white"> | <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=" 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="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> | ||||||
| 			<input | 			<div class=" text-2xl font-semibold mb-6">My Prompts</div> | ||||||
| 				bind:this={filesInputElement} |  | ||||||
| 				bind:files={inputFiles} |  | ||||||
| 				type="file" |  | ||||||
| 				hidden |  | ||||||
| 				accept="image/*" |  | ||||||
| 				on:change={() => { |  | ||||||
| 					let reader = new FileReader(); |  | ||||||
| 					reader.onload = (event) => { |  | ||||||
| 						let originalImageUrl = `${event.target.result}`; |  | ||||||
| 
 |  | ||||||
| 						const img = new Image(); |  | ||||||
| 						img.src = originalImageUrl; |  | ||||||
| 
 |  | ||||||
| 						img.onload = function () { |  | ||||||
| 							const canvas = document.createElement('canvas'); |  | ||||||
| 							const ctx = canvas.getContext('2d'); |  | ||||||
| 
 |  | ||||||
| 							// Calculate the aspect ratio of the image |  | ||||||
| 							const aspectRatio = img.width / img.height; |  | ||||||
| 
 |  | ||||||
| 							// Calculate the new width and height to fit within 100x100 |  | ||||||
| 							let newWidth, newHeight; |  | ||||||
| 							if (aspectRatio > 1) { |  | ||||||
| 								newWidth = 100 * aspectRatio; |  | ||||||
| 								newHeight = 100; |  | ||||||
| 							} else { |  | ||||||
| 								newWidth = 100; |  | ||||||
| 								newHeight = 100 / aspectRatio; |  | ||||||
| 							} |  | ||||||
| 
 |  | ||||||
| 							// Set the canvas size |  | ||||||
| 							canvas.width = 100; |  | ||||||
| 							canvas.height = 100; |  | ||||||
| 
 |  | ||||||
| 							// Calculate the position to center the image |  | ||||||
| 							const offsetX = (100 - newWidth) / 2; |  | ||||||
| 							const offsetY = (100 - newHeight) / 2; |  | ||||||
| 
 |  | ||||||
| 							// Draw the image on the canvas |  | ||||||
| 							ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight); |  | ||||||
| 
 |  | ||||||
| 							// Get the base64 representation of the compressed image |  | ||||||
| 							const compressedSrc = canvas.toDataURL('image/jpeg'); |  | ||||||
| 
 |  | ||||||
| 							// Display the compressed image |  | ||||||
| 							imageUrl = compressedSrc; |  | ||||||
| 
 |  | ||||||
| 							inputFiles = null; |  | ||||||
| 						}; |  | ||||||
| 					}; |  | ||||||
| 
 |  | ||||||
| 					if ( |  | ||||||
| 						inputFiles && |  | ||||||
| 						inputFiles.length > 0 && |  | ||||||
| 						['image/gif', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type']) |  | ||||||
| 					) { |  | ||||||
| 						reader.readAsDataURL(inputFiles[0]); |  | ||||||
| 					} else { |  | ||||||
| 						console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`); |  | ||||||
| 						inputFiles = null; |  | ||||||
| 					} |  | ||||||
| 				}} |  | ||||||
| 			/> |  | ||||||
| 
 |  | ||||||
| 			<div class=" text-2xl font-semibold mb-6">My Modelfiles</div> |  | ||||||
| 
 | 
 | ||||||
| 			<button | 			<button | ||||||
| 				class="flex space-x-1" | 				class="flex space-x-1" | ||||||
|  | @ -287,193 +106,75 @@ | ||||||
| 					updateHandler(); | 					updateHandler(); | ||||||
| 				}} | 				}} | ||||||
| 			> | 			> | ||||||
| 				<div class="flex justify-center my-4"> |  | ||||||
| 					<div class="self-center"> |  | ||||||
| 						<button |  | ||||||
| 							class=" {imageUrl |  | ||||||
| 								? '' |  | ||||||
| 								: 'p-6'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200" |  | ||||||
| 							type="button" |  | ||||||
| 							on:click={() => { |  | ||||||
| 								filesInputElement.click(); |  | ||||||
| 							}} |  | ||||||
| 						> |  | ||||||
| 							{#if imageUrl} |  | ||||||
| 								<img |  | ||||||
| 									src={imageUrl} |  | ||||||
| 									alt="modelfile profile" |  | ||||||
| 									class=" rounded-full w-20 h-20 object-cover" |  | ||||||
| 								/> |  | ||||||
| 							{:else} |  | ||||||
| 								<svg |  | ||||||
| 									xmlns="http://www.w3.org/2000/svg" |  | ||||||
| 									viewBox="0 0 24 24" |  | ||||||
| 									fill="currentColor" |  | ||||||
| 									class="w-8" |  | ||||||
| 								> |  | ||||||
| 									<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> |  | ||||||
| 							{/if} |  | ||||||
| 						</button> |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 
 |  | ||||||
| 				<div class="my-2 flex space-x-2"> |  | ||||||
| 					<div class="flex-1"> |  | ||||||
| 						<div class=" text-sm font-semibold mb-2">Name*</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="Name your modelfile" |  | ||||||
| 								bind:value={title} |  | ||||||
| 								required |  | ||||||
| 							/> |  | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 
 |  | ||||||
| 					<div class="flex-1"> |  | ||||||
| 						<div class=" text-sm font-semibold mb-2">Model Tag Name*</div> |  | ||||||
| 
 |  | ||||||
| 						<div> |  | ||||||
| 							<input |  | ||||||
| 								class="px-3 py-1.5 text-sm w-full bg-transparent disabled:text-gray-500 border dark:border-gray-600 outline-none rounded-lg" |  | ||||||
| 								placeholder="Add a model tag name" |  | ||||||
| 								value={tagName} |  | ||||||
| 								disabled |  | ||||||
| 								required |  | ||||||
| 							/> |  | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 
 |  | ||||||
| 				<div class="my-2"> | 				<div class="my-2"> | ||||||
| 					<div class=" text-sm font-semibold mb-2">Description*</div> | 					<div class=" text-sm font-semibold mb-2">Title*</div> | ||||||
| 
 | 
 | ||||||
| 					<div> | 					<div> | ||||||
| 						<input | 						<input | ||||||
| 							class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" | 							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 description about what this modelfile does" | 							placeholder="Add a short title for this prompt" | ||||||
| 							bind:value={desc} | 							bind:value={title} | ||||||
| 							required | 							required | ||||||
| 						/> | 						/> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<div class="my-2"> | 				<div class="my-2"> | ||||||
| 					<div class="flex w-full justify-between"> | 					<div class=" text-sm font-semibold mb-2">Command*</div> | ||||||
| 						<div class=" self-center text-sm font-semibold">Modelfile</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> | ||||||
| 
 | 
 | ||||||
| 					<!-- <div class=" text-sm font-semibold mb-2"></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 class="mt-2"> | ||||||
| 						<div class=" text-xs font-semibold mb-2">Content*</div> |  | ||||||
| 
 |  | ||||||
| 						<div> | 						<div> | ||||||
| 							<textarea | 							<textarea | ||||||
| 								class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" | 								class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" | ||||||
| 								placeholder={`FROM llama2\nPARAMETER temperature 1\nSYSTEM """\nYou are Mario from Super Mario Bros, acting as an assistant.\n"""`} | 								placeholder={`Write a summary in 50 words that summarizes [topic or keyword].`} | ||||||
| 								rows="6" | 								rows="6" | ||||||
| 								bind:value={content} | 								bind:value={content} | ||||||
| 								required | 								required | ||||||
| 							/> | 							/> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 
 | 
 | ||||||
| 				<div class="my-2"> | 						<div class="text-xs text-gray-400 dark:text-gray-500"> | ||||||
| 					<div class="flex w-full justify-between mb-2"> | 							Format your variables using square brackets like this: <span | ||||||
| 						<div class=" self-center text-sm font-semibold">Prompt suggestions</div> | 								class=" text-gray-600 dark:text-gray-300 font-medium">[variable]</span | ||||||
| 
 |  | ||||||
| 						<button |  | ||||||
| 							class="p-1 px-3 text-xs flex rounded transition" |  | ||||||
| 							type="button" |  | ||||||
| 							on:click={() => { |  | ||||||
| 								if (suggestions.length === 0 || suggestions.at(-1).content !== '') { |  | ||||||
| 									suggestions = [...suggestions, { content: '' }]; |  | ||||||
| 								} |  | ||||||
| 							}} |  | ||||||
| 						> |  | ||||||
| 							<svg |  | ||||||
| 								xmlns="http://www.w3.org/2000/svg" |  | ||||||
| 								viewBox="0 0 20 20" |  | ||||||
| 								fill="currentColor" |  | ||||||
| 								class="w-4 h-4" |  | ||||||
| 							> | 							> | ||||||
| 								<path | 							. Make sure to enclose them with | ||||||
| 									d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" | 							<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> . | ||||||
| 							</svg> |  | ||||||
| 						</button> |  | ||||||
| 					</div> |  | ||||||
| 					<div class="flex flex-col space-y-1"> |  | ||||||
| 						{#each suggestions as prompt, promptIdx} |  | ||||||
| 							<div class=" flex border dark:border-gray-600 rounded-lg"> |  | ||||||
| 								<input |  | ||||||
| 									class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600" |  | ||||||
| 									placeholder="Write a prompt suggestion (e.g. Who are you?)" |  | ||||||
| 									bind:value={prompt.content} |  | ||||||
| 								/> |  | ||||||
| 
 |  | ||||||
| 								<button |  | ||||||
| 									class="px-2" |  | ||||||
| 									type="button" |  | ||||||
| 									on:click={() => { |  | ||||||
| 										suggestions.splice(promptIdx, 1); |  | ||||||
| 										suggestions = suggestions; |  | ||||||
| 									}} |  | ||||||
| 								> |  | ||||||
| 									<svg |  | ||||||
| 										xmlns="http://www.w3.org/2000/svg" |  | ||||||
| 										viewBox="0 0 20 20" |  | ||||||
| 										fill="currentColor" |  | ||||||
| 										class="w-4 h-4" |  | ||||||
| 									> |  | ||||||
| 										<path |  | ||||||
| 											d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" |  | ||||||
| 										/> |  | ||||||
| 									</svg> |  | ||||||
| 								</button> |  | ||||||
| 							</div> |  | ||||||
| 						{/each} |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 
 |  | ||||||
| 				<div class="my-2"> |  | ||||||
| 					<div class=" text-sm font-semibold mb-2">Categories</div> |  | ||||||
| 
 |  | ||||||
| 					<div class="grid grid-cols-4"> |  | ||||||
| 						{#each Object.keys(categories) as category} |  | ||||||
| 							<div class="flex space-x-2 text-sm"> |  | ||||||
| 								<input type="checkbox" bind:checked={categories[category]} /> |  | ||||||
| 
 |  | ||||||
| 								<div class=" capitalize">{category}</div> |  | ||||||
| 							</div> |  | ||||||
| 						{/each} |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 
 |  | ||||||
| 				{#if pullProgress !== null} |  | ||||||
| 					<div class="my-2"> |  | ||||||
| 						<div class=" text-sm font-semibold mb-2">Pull Progress</div> |  | ||||||
| 						<div class="w-full rounded-full dark:bg-gray-800"> |  | ||||||
| 							<div |  | ||||||
| 								class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full" |  | ||||||
| 								style="width: {Math.max(15, pullProgress ?? 0)}%" |  | ||||||
| 							> |  | ||||||
| 								{pullProgress ?? 0}% |  | ||||||
| 							</div> |  | ||||||
| 						</div> |  | ||||||
| 						<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> |  | ||||||
| 							{digest} |  | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				{/if} | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<div class="my-2 flex justify-end"> | 				<div class="my-2 flex justify-end"> | ||||||
| 					<button | 					<button | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek