forked from open-webui/open-webui
		
	feat: gguf file upload status
This commit is contained in:
		
							parent
							
								
									0fe7d89f0b
								
							
						
					
					
						commit
						232401a042
					
				
					 3 changed files with 256 additions and 111 deletions
				
			
		|  | @ -26,17 +26,20 @@ from urllib.parse import urlparse | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def parse_huggingface_url(hf_url): | def parse_huggingface_url(hf_url): | ||||||
|     # Parse the URL |     try: | ||||||
|     parsed_url = urlparse(hf_url) |         # Parse the URL | ||||||
|  |         parsed_url = urlparse(hf_url) | ||||||
| 
 | 
 | ||||||
|     # Get the path and split it into components |         # Get the path and split it into components | ||||||
|     path_components = parsed_url.path.split("/") |         path_components = parsed_url.path.split("/") | ||||||
| 
 | 
 | ||||||
|     # Extract the desired output |         # Extract the desired output | ||||||
|     user_repo = "/".join(path_components[1:3]) |         user_repo = "/".join(path_components[1:3]) | ||||||
|     model_file = path_components[-1] |         model_file = path_components[-1] | ||||||
| 
 | 
 | ||||||
|     return [user_repo, model_file] |         return model_file | ||||||
|  |     except ValueError: | ||||||
|  |         return None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async def download_file_stream(url, file_path, chunk_size=1024 * 1024): | async def download_file_stream(url, file_path, chunk_size=1024 * 1024): | ||||||
|  | @ -49,7 +52,7 @@ async def download_file_stream(url, file_path, chunk_size=1024 * 1024): | ||||||
| 
 | 
 | ||||||
|     headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {} |     headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {} | ||||||
| 
 | 
 | ||||||
|     timeout = aiohttp.ClientTimeout(total=60)  # Set the timeout |     timeout = aiohttp.ClientTimeout(total=600)  # Set the timeout | ||||||
| 
 | 
 | ||||||
|     async with aiohttp.ClientSession(timeout=timeout) as session: |     async with aiohttp.ClientSession(timeout=timeout) as session: | ||||||
|         async with session.get(url, headers=headers) as response: |         async with session.get(url, headers=headers) as response: | ||||||
|  | @ -62,7 +65,7 @@ async def download_file_stream(url, file_path, chunk_size=1024 * 1024): | ||||||
| 
 | 
 | ||||||
|                     done = current_size == total_size |                     done = current_size == total_size | ||||||
|                     progress = round((current_size / total_size) * 100, 2) |                     progress = round((current_size / total_size) * 100, 2) | ||||||
|                     yield f'data: {{"progress": {progress}, "current": {current_size}, "total": {total_size}}}\n\n' |                     yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' | ||||||
| 
 | 
 | ||||||
|                 if done: |                 if done: | ||||||
|                     file.seek(0) |                     file.seek(0) | ||||||
|  | @ -76,6 +79,7 @@ async def download_file_stream(url, file_path, chunk_size=1024 * 1024): | ||||||
|                         res = { |                         res = { | ||||||
|                             "done": done, |                             "done": done, | ||||||
|                             "blob": f"sha256:{hashed}", |                             "blob": f"sha256:{hashed}", | ||||||
|  |                             "name": file.name, | ||||||
|                         } |                         } | ||||||
|                         os.remove(file_path) |                         os.remove(file_path) | ||||||
| 
 | 
 | ||||||
|  | @ -86,16 +90,20 @@ async def download_file_stream(url, file_path, chunk_size=1024 * 1024): | ||||||
| 
 | 
 | ||||||
| @router.get("/download") | @router.get("/download") | ||||||
| async def download( | async def download( | ||||||
|     url: str = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf", |     url: str, | ||||||
| ): | ): | ||||||
|     user_repo, model_file = parse_huggingface_url(url) |     # url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf" | ||||||
|  |     model_file = parse_huggingface_url(url) | ||||||
| 
 | 
 | ||||||
|     os.makedirs("./uploads", exist_ok=True) |     if model_file: | ||||||
|     file_path = os.path.join("./uploads", f"{model_file}") |         os.makedirs("./uploads", exist_ok=True) | ||||||
|  |         file_path = os.path.join("./uploads", f"{model_file}") | ||||||
| 
 | 
 | ||||||
|     return StreamingResponse( |         return StreamingResponse( | ||||||
|         download_file_stream(url, file_path), media_type="text/event-stream" |             download_file_stream(url, file_path), media_type="text/event-stream" | ||||||
|     ) |         ) | ||||||
|  |     else: | ||||||
|  |         return None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @router.post("/upload") | @router.post("/upload") | ||||||
|  | @ -118,10 +126,12 @@ async def upload(file: UploadFile = File(...)): | ||||||
|                     f.write(chunk) |                     f.write(chunk) | ||||||
|                     total += len(chunk) |                     total += len(chunk) | ||||||
|                     done = total_size == total |                     done = total_size == total | ||||||
|  |                     progress = round((total / total_size) * 100, 2) | ||||||
| 
 | 
 | ||||||
|                     res = { |                     res = { | ||||||
|  |                         "progress": progress, | ||||||
|                         "total": total_size, |                         "total": total_size, | ||||||
|                         "uploaded": total, |                         "completed": total, | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     yield f"data: {json.dumps(res)}\n\n" |                     yield f"data: {json.dumps(res)}\n\n" | ||||||
|  | @ -138,6 +148,7 @@ async def upload(file: UploadFile = File(...)): | ||||||
|                         res = { |                         res = { | ||||||
|                             "done": done, |                             "done": done, | ||||||
|                             "blob": f"sha256:{hashed}", |                             "blob": f"sha256:{hashed}", | ||||||
|  |                             "name": file.filename, | ||||||
|                         } |                         } | ||||||
|                         os.remove(file_path) |                         os.remove(file_path) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ passlib[bcrypt] | ||||||
| uuid | uuid | ||||||
| 
 | 
 | ||||||
| requests | requests | ||||||
|  | aiohttp | ||||||
| pymongo | pymongo | ||||||
| bcrypt | bcrypt | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -50,15 +50,21 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	// Models | 	// Models | ||||||
| 	let modelTag = ''; | 	let modelTransferring = false; | ||||||
| 	let modelInputFile = ''; |  | ||||||
| 	let modelInputFileBlob = ''; |  | ||||||
| 	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`; |  | ||||||
| 
 | 
 | ||||||
| 	let deleteModelTag = ''; | 	let modelTag = ''; | ||||||
| 	let digest = ''; | 	let digest = ''; | ||||||
| 	let pullProgress = null; | 	let pullProgress = null; | ||||||
| 
 | 
 | ||||||
|  | 	let modelUploadMode = 'file'; | ||||||
|  | 	let modelInputFile = ''; | ||||||
|  | 	let modelFileUrl = ''; | ||||||
|  | 	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`; | ||||||
|  | 	let modelFileDigest = ''; | ||||||
|  | 	let uploadProgress = null; | ||||||
|  | 
 | ||||||
|  | 	let deleteModelTag = ''; | ||||||
|  | 
 | ||||||
| 	// Addons | 	// Addons | ||||||
| 	let titleAutoGenerate = true; | 	let titleAutoGenerate = true; | ||||||
| 	let speechAutoSend = false; | 	let speechAutoSend = false; | ||||||
|  | @ -162,6 +168,7 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const pullModelHandler = async () => { | 	const pullModelHandler = async () => { | ||||||
|  | 		modelTransferring = true; | ||||||
| 		const res = await fetch(`${API_BASE_URL}/pull`, { | 		const res = await fetch(`${API_BASE_URL}/pull`, { | ||||||
| 			method: 'POST', | 			method: 'POST', | ||||||
| 			headers: { | 			headers: { | ||||||
|  | @ -227,6 +234,8 @@ | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		modelTag = ''; | 		modelTag = ''; | ||||||
|  | 		modelTransferring = false; | ||||||
|  | 
 | ||||||
| 		models.set(await getModels()); | 		models.set(await getModels()); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | @ -266,26 +275,42 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const uploadModelHandler = async () => { | 	const uploadModelHandler = async () => { | ||||||
| 		const file = modelInputFile[0]; | 		modelTransferring = true; | ||||||
| 		const formData = new FormData(); |  | ||||||
| 		formData.append('file', file); |  | ||||||
| 
 |  | ||||||
| 		let uploaded = false; | 		let uploaded = false; | ||||||
|  | 		let fileResponse = null; | ||||||
|  | 		let name = ''; | ||||||
| 
 | 
 | ||||||
| 		const res = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, { | 		if (modelUploadMode === 'file') { | ||||||
| 			method: 'POST', | 			const file = modelInputFile[0]; | ||||||
| 			headers: { | 			const formData = new FormData(); | ||||||
| 				...($settings.authHeader && { Authorization: $settings.authHeader }), | 			formData.append('file', file); | ||||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) |  | ||||||
| 			}, |  | ||||||
| 			body: formData |  | ||||||
| 		}).catch((error) => { |  | ||||||
| 			console.log(error); |  | ||||||
| 			return null; |  | ||||||
| 		}); |  | ||||||
| 
 | 
 | ||||||
| 		if (res && res.ok) { | 			fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, { | ||||||
| 			const reader = res.body | 				method: 'POST', | ||||||
|  | 				headers: { | ||||||
|  | 					...($settings.authHeader && { Authorization: $settings.authHeader }), | ||||||
|  | 					...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||||
|  | 				}, | ||||||
|  | 				body: formData | ||||||
|  | 			}).catch((error) => { | ||||||
|  | 				console.log(error); | ||||||
|  | 				return null; | ||||||
|  | 			}); | ||||||
|  | 		} else { | ||||||
|  | 			fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/download?url=${modelFileUrl}`, { | ||||||
|  | 				method: 'GET', | ||||||
|  | 				headers: { | ||||||
|  | 					...($settings.authHeader && { Authorization: $settings.authHeader }), | ||||||
|  | 					...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||||
|  | 				} | ||||||
|  | 			}).catch((error) => { | ||||||
|  | 				console.log(error); | ||||||
|  | 				return null; | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (fileResponse && fileResponse.ok) { | ||||||
|  | 			const reader = fileResponse.body | ||||||
| 				.pipeThrough(new TextDecoderStream()) | 				.pipeThrough(new TextDecoderStream()) | ||||||
| 				.pipeThrough(splitStream('\n')) | 				.pipeThrough(splitStream('\n')) | ||||||
| 				.getReader(); | 				.getReader(); | ||||||
|  | @ -300,14 +325,18 @@ | ||||||
| 					for (const line of lines) { | 					for (const line of lines) { | ||||||
| 						if (line !== '') { | 						if (line !== '') { | ||||||
| 							let data = JSON.parse(line.replace(/^data: /, '')); | 							let data = JSON.parse(line.replace(/^data: /, '')); | ||||||
| 							console.log(data); | 
 | ||||||
|  | 							if (data.progress) { | ||||||
|  | 								uploadProgress = data.progress; | ||||||
|  | 							} | ||||||
| 
 | 
 | ||||||
| 							if (data.error) { | 							if (data.error) { | ||||||
| 								throw data.error; | 								throw data.error; | ||||||
| 							} | 							} | ||||||
| 
 | 
 | ||||||
| 							if (data.done) { | 							if (data.done) { | ||||||
| 								modelInputFileBlob = data.blob; | 								modelFileDigest = data.blob; | ||||||
|  | 								name = data.name; | ||||||
| 								uploaded = true; | 								uploaded = true; | ||||||
| 							} | 							} | ||||||
| 						} | 						} | ||||||
|  | @ -327,8 +356,8 @@ | ||||||
| 					...($user && { Authorization: `Bearer ${localStorage.token}` }) | 					...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||||
| 				}, | 				}, | ||||||
| 				body: JSON.stringify({ | 				body: JSON.stringify({ | ||||||
| 					name: `${file.name}:latest`, | 					name: `${name}:latest`, | ||||||
| 					modelfile: `FROM @${modelInputFileBlob}\n${modelFileContent}` | 					modelfile: `FROM @${modelFileDigest}\n${modelFileContent}` | ||||||
| 				}) | 				}) | ||||||
| 			}).catch((err) => { | 			}).catch((err) => { | ||||||
| 				console.log(err); | 				console.log(err); | ||||||
|  | @ -390,7 +419,9 @@ | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		modelTag = ''; | 		modelFileUrl = ''; | ||||||
|  | 		modelInputFile = ''; | ||||||
|  | 		modelTransferring = false; | ||||||
| 		models.set(await getModels()); | 		models.set(await getModels()); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | @ -977,24 +1008,53 @@ | ||||||
| 										/> | 										/> | ||||||
| 									</div> | 									</div> | ||||||
| 									<button | 									<button | ||||||
| 										class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 rounded transition" | 										class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition" | ||||||
| 										on:click={() => { | 										on:click={() => { | ||||||
| 											pullModelHandler(); | 											pullModelHandler(); | ||||||
| 										}} | 										}} | ||||||
|  | 										disabled={modelTransferring} | ||||||
| 									> | 									> | ||||||
| 										<svg | 										{#if modelTransferring} | ||||||
| 											xmlns="http://www.w3.org/2000/svg" | 											<div class="self-center"> | ||||||
| 											viewBox="0 0 16 16" | 												<svg | ||||||
| 											fill="currentColor" | 													class=" w-4 h-4" | ||||||
| 											class="w-4 h-4" | 													viewBox="0 0 24 24" | ||||||
| 										> | 													fill="currentColor" | ||||||
| 											<path | 													xmlns="http://www.w3.org/2000/svg" | ||||||
| 												d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" | 													><style> | ||||||
| 											/> | 														.spinner_ajPY { | ||||||
| 											<path | 															transform-origin: center; | ||||||
| 												d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" | 															animation: spinner_AtaB 0.75s infinite linear; | ||||||
| 											/> | 														} | ||||||
| 										</svg> | 														@keyframes spinner_AtaB { | ||||||
|  | 															100% { | ||||||
|  | 																transform: rotate(360deg); | ||||||
|  | 															} | ||||||
|  | 														} | ||||||
|  | 													</style><path | ||||||
|  | 														d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||||
|  | 														opacity=".25" | ||||||
|  | 													/><path | ||||||
|  | 														d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||||
|  | 														class="spinner_ajPY" | ||||||
|  | 													/></svg | ||||||
|  | 												> | ||||||
|  | 											</div> | ||||||
|  | 										{:else} | ||||||
|  | 											<svg | ||||||
|  | 												xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 												viewBox="0 0 16 16" | ||||||
|  | 												fill="currentColor" | ||||||
|  | 												class="w-4 h-4" | ||||||
|  | 											> | ||||||
|  | 												<path | ||||||
|  | 													d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" | ||||||
|  | 												/> | ||||||
|  | 												<path | ||||||
|  | 													d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" | ||||||
|  | 												/> | ||||||
|  | 											</svg> | ||||||
|  | 										{/if} | ||||||
| 									</button> | 									</button> | ||||||
| 								</div> | 								</div> | ||||||
| 
 | 
 | ||||||
|  | @ -1025,67 +1085,140 @@ | ||||||
| 							</div> | 							</div> | ||||||
| 							<hr class=" dark:border-gray-700" /> | 							<hr class=" dark:border-gray-700" /> | ||||||
| 
 | 
 | ||||||
| 							<div> | 							<form | ||||||
| 								<div class=" mb-2.5 text-sm font-medium">Upload a GGUF model</div> | 								on:submit|preventDefault={() => { | ||||||
| 								<div class="flex w-full mb-1.5"> | 									uploadModelHandler(); | ||||||
| 									<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"> | 								}} | ||||||
| 										<input | 							> | ||||||
| 											id="model-upload-input" | 								<div class=" mb-2 flex w-full justify-between"> | ||||||
| 											type="file" | 									<div class="  text-sm font-medium">Upload a GGUF model</div> | ||||||
| 											bind:files={modelInputFile} |  | ||||||
| 											on:change={() => { |  | ||||||
| 												console.log(modelInputFile); |  | ||||||
| 											}} |  | ||||||
| 											hidden |  | ||||||
| 										/> |  | ||||||
| 
 | 
 | ||||||
| 										<button | 									<button | ||||||
| 											type="button" | 										class="p-1 px-3 text-xs flex rounded transition" | ||||||
| 											class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800" | 										on:click={() => { | ||||||
| 											on:click={() => { | 											if (modelUploadMode === 'file') { | ||||||
| 												document.getElementById('model-upload-input').click(); | 												modelUploadMode = 'url'; | ||||||
| 											}} | 											} else { | ||||||
| 										> | 												modelUploadMode = 'file'; | ||||||
| 											{#if modelInputFile && modelInputFile.length > 0} | 											} | ||||||
| 												{modelInputFile[0].name} | 										}} | ||||||
| 											{:else} | 										type="button" | ||||||
| 												Click here to select | 									> | ||||||
| 											{/if} | 										{#if modelUploadMode === 'file'} | ||||||
| 										</button> | 											<span class="ml-2 self-center">File Mode</span> | ||||||
|  | 										{:else} | ||||||
|  | 											<span class="ml-2 self-center">URL Mode</span> | ||||||
|  | 										{/if} | ||||||
|  | 									</button> | ||||||
|  | 								</div> | ||||||
|  | 
 | ||||||
|  | 								<div class="flex w-full mb-1.5"> | ||||||
|  | 									<div class="flex flex-col w-full"> | ||||||
|  | 										{#if modelUploadMode === 'file'} | ||||||
|  | 											<div | ||||||
|  | 												class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}" | ||||||
|  | 											> | ||||||
|  | 												<input | ||||||
|  | 													id="model-upload-input" | ||||||
|  | 													type="file" | ||||||
|  | 													bind:files={modelInputFile} | ||||||
|  | 													on:change={() => { | ||||||
|  | 														console.log(modelInputFile); | ||||||
|  | 													}} | ||||||
|  | 													accept=".gguf" | ||||||
|  | 													required | ||||||
|  | 													hidden | ||||||
|  | 												/> | ||||||
|  | 
 | ||||||
|  | 												<button | ||||||
|  | 													type="button" | ||||||
|  | 													class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800" | ||||||
|  | 													on:click={() => { | ||||||
|  | 														document.getElementById('model-upload-input').click(); | ||||||
|  | 													}} | ||||||
|  | 												> | ||||||
|  | 													{#if modelInputFile && modelInputFile.length > 0} | ||||||
|  | 														{modelInputFile[0].name} | ||||||
|  | 													{:else} | ||||||
|  | 														Click here to select | ||||||
|  | 													{/if} | ||||||
|  | 												</button> | ||||||
|  | 											</div> | ||||||
|  | 										{:else} | ||||||
|  | 											<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}"> | ||||||
|  | 												<input | ||||||
|  | 													class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800 outline-none {modelFileUrl !== | ||||||
|  | 													'' | ||||||
|  | 														? 'mr-2' | ||||||
|  | 														: ''}" | ||||||
|  | 													type="url" | ||||||
|  | 													required | ||||||
|  | 													bind:value={modelFileUrl} | ||||||
|  | 													placeholder="Type HuggingFace Resolve (Download) URL" | ||||||
|  | 												/> | ||||||
|  | 											</div> | ||||||
|  | 										{/if} | ||||||
| 									</div> | 									</div> | ||||||
| 
 | 
 | ||||||
| 									{#if modelInputFile && modelInputFile.length > 0} | 									{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} | ||||||
| 										<button | 										<button | ||||||
| 											class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 rounded transition" | 											class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition" | ||||||
| 											on:click={() => { | 											type="submit" | ||||||
| 												uploadModelHandler(); | 											disabled={modelTransferring} | ||||||
| 											}} |  | ||||||
| 										> | 										> | ||||||
| 											<svg | 											{#if modelTransferring} | ||||||
| 												xmlns="http://www.w3.org/2000/svg" | 												<div class="self-center"> | ||||||
| 												viewBox="0 0 16 16" | 													<svg | ||||||
| 												fill="currentColor" | 														class=" w-4 h-4" | ||||||
| 												class="w-4 h-4" | 														viewBox="0 0 24 24" | ||||||
| 											> | 														fill="currentColor" | ||||||
| 												<path | 														xmlns="http://www.w3.org/2000/svg" | ||||||
| 													d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" | 														><style> | ||||||
| 												/> | 															.spinner_ajPY { | ||||||
| 												<path | 																transform-origin: center; | ||||||
| 													d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" | 																animation: spinner_AtaB 0.75s infinite linear; | ||||||
| 												/> | 															} | ||||||
| 											</svg> | 															@keyframes spinner_AtaB { | ||||||
|  | 																100% { | ||||||
|  | 																	transform: rotate(360deg); | ||||||
|  | 																} | ||||||
|  | 															} | ||||||
|  | 														</style><path | ||||||
|  | 															d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||||
|  | 															opacity=".25" | ||||||
|  | 														/><path | ||||||
|  | 															d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||||
|  | 															class="spinner_ajPY" | ||||||
|  | 														/></svg | ||||||
|  | 													> | ||||||
|  | 												</div> | ||||||
|  | 											{:else} | ||||||
|  | 												<svg | ||||||
|  | 													xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 													viewBox="0 0 16 16" | ||||||
|  | 													fill="currentColor" | ||||||
|  | 													class="w-4 h-4" | ||||||
|  | 												> | ||||||
|  | 													<path | ||||||
|  | 														d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" | ||||||
|  | 													/> | ||||||
|  | 													<path | ||||||
|  | 														d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" | ||||||
|  | 													/> | ||||||
|  | 												</svg> | ||||||
|  | 											{/if} | ||||||
| 										</button> | 										</button> | ||||||
| 									{/if} | 									{/if} | ||||||
| 								</div> | 								</div> | ||||||
| 
 | 
 | ||||||
| 								{#if modelInputFile && modelInputFile.length > 0} | 								{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} | ||||||
| 									<div> | 									<div> | ||||||
| 										<div> | 										<div> | ||||||
| 											<div class=" my-2.5 text-sm font-medium">Modelfile Content</div> | 											<div class=" my-2.5 text-sm font-medium">Modelfile Content</div> | ||||||
| 											<textarea | 											<textarea | ||||||
| 												bind:value={modelFileContent} | 												bind:value={modelFileContent} | ||||||
| 												class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" | 												class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" | ||||||
| 												rows="6" | 												rows="8" | ||||||
| 											/> | 											/> | ||||||
| 										</div> | 										</div> | ||||||
| 									</div> | 									</div> | ||||||
|  | @ -1098,23 +1231,23 @@ | ||||||
| 									> | 									> | ||||||
| 								</div> | 								</div> | ||||||
| 
 | 
 | ||||||
| 								{#if pullProgress !== null} | 								{#if uploadProgress !== null} | ||||||
| 									<div class="mt-2"> | 									<div class="mt-2"> | ||||||
| 										<div class=" mb-2 text-xs">Pull Progress</div> | 										<div class=" mb-2 text-xs">Upload Progress</div> | ||||||
| 										<div class="w-full rounded-full dark:bg-gray-800"> | 										<div class="w-full rounded-full dark:bg-gray-800"> | ||||||
| 											<div | 											<div | ||||||
| 												class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full" | 												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)}%" | 												style="width: {Math.max(15, uploadProgress ?? 0)}%" | ||||||
| 											> | 											> | ||||||
| 												{pullProgress ?? 0}% | 												{uploadProgress ?? 0}% | ||||||
| 											</div> | 											</div> | ||||||
| 										</div> | 										</div> | ||||||
| 										<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> | 										<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> | ||||||
| 											{digest} | 											{modelFileDigest} | ||||||
| 										</div> | 										</div> | ||||||
| 									</div> | 									</div> | ||||||
| 								{/if} | 								{/if} | ||||||
| 							</div> | 							</form> | ||||||
| 							<hr class=" dark:border-gray-700" /> | 							<hr class=" dark:border-gray-700" /> | ||||||
| 
 | 
 | ||||||
| 							<div> | 							<div> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek