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
				
			
		|  | @ -50,15 +50,21 @@ | |||
| 	}; | ||||
| 
 | ||||
| 	// Models | ||||
| 	let modelTag = ''; | ||||
| 	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 modelTransferring = false; | ||||
| 
 | ||||
| 	let deleteModelTag = ''; | ||||
| 	let modelTag = ''; | ||||
| 	let digest = ''; | ||||
| 	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 | ||||
| 	let titleAutoGenerate = true; | ||||
| 	let speechAutoSend = false; | ||||
|  | @ -162,6 +168,7 @@ | |||
| 	}; | ||||
| 
 | ||||
| 	const pullModelHandler = async () => { | ||||
| 		modelTransferring = true; | ||||
| 		const res = await fetch(`${API_BASE_URL}/pull`, { | ||||
| 			method: 'POST', | ||||
| 			headers: { | ||||
|  | @ -227,6 +234,8 @@ | |||
| 		} | ||||
| 
 | ||||
| 		modelTag = ''; | ||||
| 		modelTransferring = false; | ||||
| 
 | ||||
| 		models.set(await getModels()); | ||||
| 	}; | ||||
| 
 | ||||
|  | @ -266,26 +275,42 @@ | |||
| 	}; | ||||
| 
 | ||||
| 	const uploadModelHandler = async () => { | ||||
| 		const file = modelInputFile[0]; | ||||
| 		const formData = new FormData(); | ||||
| 		formData.append('file', file); | ||||
| 
 | ||||
| 		modelTransferring = true; | ||||
| 		let uploaded = false; | ||||
| 		let fileResponse = null; | ||||
| 		let name = ''; | ||||
| 
 | ||||
| 		const res = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, { | ||||
| 			method: 'POST', | ||||
| 			headers: { | ||||
| 				...($settings.authHeader && { Authorization: $settings.authHeader }), | ||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 			}, | ||||
| 			body: formData | ||||
| 		}).catch((error) => { | ||||
| 			console.log(error); | ||||
| 			return null; | ||||
| 		}); | ||||
| 		if (modelUploadMode === 'file') { | ||||
| 			const file = modelInputFile[0]; | ||||
| 			const formData = new FormData(); | ||||
| 			formData.append('file', file); | ||||
| 
 | ||||
| 		if (res && res.ok) { | ||||
| 			const reader = res.body | ||||
| 			fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, { | ||||
| 				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(splitStream('\n')) | ||||
| 				.getReader(); | ||||
|  | @ -300,14 +325,18 @@ | |||
| 					for (const line of lines) { | ||||
| 						if (line !== '') { | ||||
| 							let data = JSON.parse(line.replace(/^data: /, '')); | ||||
| 							console.log(data); | ||||
| 
 | ||||
| 							if (data.progress) { | ||||
| 								uploadProgress = data.progress; | ||||
| 							} | ||||
| 
 | ||||
| 							if (data.error) { | ||||
| 								throw data.error; | ||||
| 							} | ||||
| 
 | ||||
| 							if (data.done) { | ||||
| 								modelInputFileBlob = data.blob; | ||||
| 								modelFileDigest = data.blob; | ||||
| 								name = data.name; | ||||
| 								uploaded = true; | ||||
| 							} | ||||
| 						} | ||||
|  | @ -327,8 +356,8 @@ | |||
| 					...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 				}, | ||||
| 				body: JSON.stringify({ | ||||
| 					name: `${file.name}:latest`, | ||||
| 					modelfile: `FROM @${modelInputFileBlob}\n${modelFileContent}` | ||||
| 					name: `${name}:latest`, | ||||
| 					modelfile: `FROM @${modelFileDigest}\n${modelFileContent}` | ||||
| 				}) | ||||
| 			}).catch((err) => { | ||||
| 				console.log(err); | ||||
|  | @ -390,7 +419,9 @@ | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		modelTag = ''; | ||||
| 		modelFileUrl = ''; | ||||
| 		modelInputFile = ''; | ||||
| 		modelTransferring = false; | ||||
| 		models.set(await getModels()); | ||||
| 	}; | ||||
| 
 | ||||
|  | @ -977,24 +1008,53 @@ | |||
| 										/> | ||||
| 									</div> | ||||
| 									<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={() => { | ||||
| 											pullModelHandler(); | ||||
| 										}} | ||||
| 										disabled={modelTransferring} | ||||
| 									> | ||||
| 										<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 modelTransferring} | ||||
| 											<div class="self-center"> | ||||
| 												<svg | ||||
| 													class=" w-4 h-4" | ||||
| 													viewBox="0 0 24 24" | ||||
| 													fill="currentColor" | ||||
| 													xmlns="http://www.w3.org/2000/svg" | ||||
| 													><style> | ||||
| 														.spinner_ajPY { | ||||
| 															transform-origin: center; | ||||
| 															animation: spinner_AtaB 0.75s infinite linear; | ||||
| 														} | ||||
| 														@keyframes spinner_AtaB { | ||||
| 															100% { | ||||
| 																transform: rotate(360deg); | ||||
| 															} | ||||
| 														} | ||||
| 													</style><path | ||||
| 														d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||
| 														opacity=".25" | ||||
| 													/><path | ||||
| 														d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||
| 														class="spinner_ajPY" | ||||
| 													/></svg | ||||
| 												> | ||||
| 											</div> | ||||
| 										{:else} | ||||
| 											<svg | ||||
| 												xmlns="http://www.w3.org/2000/svg" | ||||
| 												viewBox="0 0 16 16" | ||||
| 												fill="currentColor" | ||||
| 												class="w-4 h-4" | ||||
| 											> | ||||
| 												<path | ||||
| 													d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" | ||||
| 												/> | ||||
| 												<path | ||||
| 													d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" | ||||
| 												/> | ||||
| 											</svg> | ||||
| 										{/if} | ||||
| 									</button> | ||||
| 								</div> | ||||
| 
 | ||||
|  | @ -1025,67 +1085,140 @@ | |||
| 							</div> | ||||
| 							<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 							<div> | ||||
| 								<div class=" mb-2.5 text-sm font-medium">Upload a GGUF model</div> | ||||
| 								<div class="flex w-full mb-1.5"> | ||||
| 									<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); | ||||
| 											}} | ||||
| 											hidden | ||||
| 										/> | ||||
| 							<form | ||||
| 								on:submit|preventDefault={() => { | ||||
| 									uploadModelHandler(); | ||||
| 								}} | ||||
| 							> | ||||
| 								<div class=" mb-2 flex w-full justify-between"> | ||||
| 									<div class="  text-sm font-medium">Upload a GGUF model</div> | ||||
| 
 | ||||
| 										<button | ||||
| 											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> | ||||
| 									<button | ||||
| 										class="p-1 px-3 text-xs flex rounded transition" | ||||
| 										on:click={() => { | ||||
| 											if (modelUploadMode === 'file') { | ||||
| 												modelUploadMode = 'url'; | ||||
| 											} else { | ||||
| 												modelUploadMode = 'file'; | ||||
| 											} | ||||
| 										}} | ||||
| 										type="button" | ||||
| 									> | ||||
| 										{#if modelUploadMode === 'file'} | ||||
| 											<span class="ml-2 self-center">File Mode</span> | ||||
| 										{:else} | ||||
| 											<span class="ml-2 self-center">URL Mode</span> | ||||
| 										{/if} | ||||
| 									</button> | ||||
| 								</div> | ||||
| 
 | ||||
| 								<div class="flex w-full mb-1.5"> | ||||
| 									<div class="flex flex-col w-full"> | ||||
| 										{#if modelUploadMode === 'file'} | ||||
| 											<div | ||||
| 												class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}" | ||||
| 											> | ||||
| 												<input | ||||
| 													id="model-upload-input" | ||||
| 													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> | ||||
| 
 | ||||
| 									{#if modelInputFile && modelInputFile.length > 0} | ||||
| 									{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} | ||||
| 										<button | ||||
| 											class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 rounded transition" | ||||
| 											on:click={() => { | ||||
| 												uploadModelHandler(); | ||||
| 											}} | ||||
| 											class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition" | ||||
| 											type="submit" | ||||
| 											disabled={modelTransferring} | ||||
| 										> | ||||
| 											<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 modelTransferring} | ||||
| 												<div class="self-center"> | ||||
| 													<svg | ||||
| 														class=" w-4 h-4" | ||||
| 														viewBox="0 0 24 24" | ||||
| 														fill="currentColor" | ||||
| 														xmlns="http://www.w3.org/2000/svg" | ||||
| 														><style> | ||||
| 															.spinner_ajPY { | ||||
| 																transform-origin: center; | ||||
| 																animation: spinner_AtaB 0.75s infinite linear; | ||||
| 															} | ||||
| 															@keyframes spinner_AtaB { | ||||
| 																100% { | ||||
| 																	transform: rotate(360deg); | ||||
| 																} | ||||
| 															} | ||||
| 														</style><path | ||||
| 															d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||
| 															opacity=".25" | ||||
| 														/><path | ||||
| 															d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||
| 															class="spinner_ajPY" | ||||
| 														/></svg | ||||
| 													> | ||||
| 												</div> | ||||
| 											{:else} | ||||
| 												<svg | ||||
| 													xmlns="http://www.w3.org/2000/svg" | ||||
| 													viewBox="0 0 16 16" | ||||
| 													fill="currentColor" | ||||
| 													class="w-4 h-4" | ||||
| 												> | ||||
| 													<path | ||||
| 														d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" | ||||
| 													/> | ||||
| 													<path | ||||
| 														d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" | ||||
| 													/> | ||||
| 												</svg> | ||||
| 											{/if} | ||||
| 										</button> | ||||
| 									{/if} | ||||
| 								</div> | ||||
| 
 | ||||
| 								{#if modelInputFile && modelInputFile.length > 0} | ||||
| 								{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} | ||||
| 									<div> | ||||
| 										<div> | ||||
| 											<div class=" my-2.5 text-sm font-medium">Modelfile Content</div> | ||||
| 											<textarea | ||||
| 												bind:value={modelFileContent} | ||||
| 												class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" | ||||
| 												rows="6" | ||||
| 												rows="8" | ||||
| 											/> | ||||
| 										</div> | ||||
| 									</div> | ||||
|  | @ -1098,23 +1231,23 @@ | |||
| 									> | ||||
| 								</div> | ||||
| 
 | ||||
| 								{#if pullProgress !== null} | ||||
| 								{#if uploadProgress !== null} | ||||
| 									<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="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 class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> | ||||
| 											{digest} | ||||
| 											{modelFileDigest} | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								{/if} | ||||
| 							</div> | ||||
| 							</form> | ||||
| 							<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 							<div> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek