forked from open-webui/open-webui
		
	Merge pull request #215 from ollama-webui/share-chat
WIP: chat enhancements
This commit is contained in:
		
						commit
						3c43737ea6
					
				
					 15 changed files with 654 additions and 109 deletions
				
			
		|  | @ -23,7 +23,6 @@ ARG OLLAMA_API_BASE_URL='/ollama/api' | |||
| ENV ENV=prod | ||||
| ENV OLLAMA_API_BASE_URL $OLLAMA_API_BASE_URL | ||||
| ENV WEBUI_AUTH "" | ||||
| ENV WEBUI_DB_URL "" | ||||
| ENV WEBUI_JWT_SECRET_KEY "SECRET_KEY" | ||||
| 
 | ||||
| WORKDIR /app | ||||
|  |  | |||
|  | @ -59,9 +59,11 @@ def proxy(path): | |||
|     else: | ||||
|         pass | ||||
| 
 | ||||
|     r = None | ||||
| 
 | ||||
|     try: | ||||
|         # Make a request to the target server | ||||
|         target_response = requests.request( | ||||
|         r = requests.request( | ||||
|             method=request.method, | ||||
|             url=target_url, | ||||
|             data=data, | ||||
|  | @ -69,22 +71,37 @@ def proxy(path): | |||
|             stream=True,  # Enable streaming for server-sent events | ||||
|         ) | ||||
| 
 | ||||
|         target_response.raise_for_status() | ||||
|         r.raise_for_status() | ||||
| 
 | ||||
|         # Proxy the target server's response to the client | ||||
|         def generate(): | ||||
|             for chunk in target_response.iter_content(chunk_size=8192): | ||||
|             for chunk in r.iter_content(chunk_size=8192): | ||||
|                 yield chunk | ||||
| 
 | ||||
|         response = Response(generate(), status=target_response.status_code) | ||||
|         response = Response(generate(), status=r.status_code) | ||||
| 
 | ||||
|         # Copy headers from the target server's response to the client's response | ||||
|         for key, value in target_response.headers.items(): | ||||
|         for key, value in r.headers.items(): | ||||
|             response.headers[key] = value | ||||
| 
 | ||||
|         return response | ||||
|     except Exception as e: | ||||
|         return jsonify({"detail": "Server Connection Error", "message": str(e)}), 400 | ||||
|         error_detail = "Ollama WebUI: Server Connection Error" | ||||
|         if r != None: | ||||
|             res = r.json() | ||||
|             if "error" in res: | ||||
|                 error_detail = f"Ollama: {res['error']}" | ||||
|             print(res) | ||||
| 
 | ||||
|         return ( | ||||
|             jsonify( | ||||
|                 { | ||||
|                     "detail": error_detail, | ||||
|                     "message": str(e), | ||||
|                 } | ||||
|             ), | ||||
|             400, | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ if ENV == "prod": | |||
| # WEBUI_VERSION | ||||
| #################################### | ||||
| 
 | ||||
| WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.21") | ||||
| WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.33") | ||||
| 
 | ||||
| #################################### | ||||
| # WEBUI_AUTH | ||||
|  | @ -41,7 +41,7 @@ WEBUI_AUTH = True if os.environ.get("WEBUI_AUTH", "FALSE") == "TRUE" else False | |||
| 
 | ||||
| 
 | ||||
| #################################### | ||||
| # WEBUI_DB | ||||
| # WEBUI_DB (Deprecated, Should be removed) | ||||
| #################################### | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										43
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										43
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -17,6 +17,7 @@ | |||
| 				"katex": "^0.16.9", | ||||
| 				"marked": "^9.1.0", | ||||
| 				"svelte-french-toast": "^1.2.0", | ||||
| 				"tippy.js": "^6.3.7", | ||||
| 				"uuid": "^9.0.1" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
|  | @ -584,6 +585,15 @@ | |||
| 			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", | ||||
| 			"integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==" | ||||
| 		}, | ||||
| 		"node_modules/@popperjs/core": { | ||||
| 			"version": "2.11.8", | ||||
| 			"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", | ||||
| 			"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", | ||||
| 			"funding": { | ||||
| 				"type": "opencollective", | ||||
| 				"url": "https://opencollective.com/popperjs" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@rollup/plugin-commonjs": { | ||||
| 			"version": "25.0.5", | ||||
| 			"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.5.tgz", | ||||
|  | @ -3994,6 +4004,14 @@ | |||
| 				"globrex": "^0.1.2" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/tippy.js": { | ||||
| 			"version": "6.3.7", | ||||
| 			"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", | ||||
| 			"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", | ||||
| 			"dependencies": { | ||||
| 				"@popperjs/core": "^2.9.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/to-regex-range": { | ||||
| 			"version": "5.0.1", | ||||
| 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | ||||
|  | @ -4160,9 +4178,9 @@ | |||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/vite": { | ||||
| 			"version": "4.4.11", | ||||
| 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz", | ||||
| 			"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==", | ||||
| 			"version": "4.5.1", | ||||
| 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", | ||||
| 			"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", | ||||
| 			"dependencies": { | ||||
| 				"esbuild": "^0.18.10", | ||||
| 				"postcss": "^8.4.27", | ||||
|  | @ -4570,6 +4588,11 @@ | |||
| 			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", | ||||
| 			"integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==" | ||||
| 		}, | ||||
| 		"@popperjs/core": { | ||||
| 			"version": "2.11.8", | ||||
| 			"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", | ||||
| 			"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" | ||||
| 		}, | ||||
| 		"@rollup/plugin-commonjs": { | ||||
| 			"version": "25.0.5", | ||||
| 			"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.5.tgz", | ||||
|  | @ -6885,6 +6908,14 @@ | |||
| 				"globrex": "^0.1.2" | ||||
| 			} | ||||
| 		}, | ||||
| 		"tippy.js": { | ||||
| 			"version": "6.3.7", | ||||
| 			"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", | ||||
| 			"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", | ||||
| 			"requires": { | ||||
| 				"@popperjs/core": "^2.9.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"to-regex-range": { | ||||
| 			"version": "5.0.1", | ||||
| 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | ||||
|  | @ -6991,9 +7022,9 @@ | |||
| 			"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" | ||||
| 		}, | ||||
| 		"vite": { | ||||
| 			"version": "4.4.11", | ||||
| 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz", | ||||
| 			"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==", | ||||
| 			"version": "4.5.1", | ||||
| 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", | ||||
| 			"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", | ||||
| 			"requires": { | ||||
| 				"esbuild": "^0.18.10", | ||||
| 				"fsevents": "~2.3.2", | ||||
|  |  | |||
|  | @ -48,6 +48,7 @@ | |||
| 		"katex": "^0.16.9", | ||||
| 		"marked": "^9.1.0", | ||||
| 		"svelte-french-toast": "^1.2.0", | ||||
| 		"tippy.js": "^6.3.7", | ||||
| 		"uuid": "^9.0.1" | ||||
| 	} | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -161,7 +161,7 @@ | |||
| 						<div class="ml-2 mt-2 mb-1 flex space-x-2"> | ||||
| 							{#each files as file, fileIdx} | ||||
| 								<div class=" relative group"> | ||||
| 									<img src={file.url} alt="input" class=" h-16 w-16 rounded-xl bg-cover" /> | ||||
| 									<img src={file.url} alt="input" class=" h-16 w-16 rounded-xl object-cover" /> | ||||
| 
 | ||||
| 									<div class=" absolute -top-1 -right-1"> | ||||
| 										<button | ||||
|  | @ -235,6 +235,30 @@ | |||
| 								e.target.style.height = ''; | ||||
| 								e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; | ||||
| 							}} | ||||
| 							on:paste={(e) => { | ||||
| 								const clipboardData = e.clipboardData || window.clipboardData; | ||||
| 
 | ||||
| 								if (clipboardData && clipboardData.items) { | ||||
| 									for (const item of clipboardData.items) { | ||||
| 										if (item.type.indexOf('image') !== -1) { | ||||
| 											const blob = item.getAsFile(); | ||||
| 											const reader = new FileReader(); | ||||
| 
 | ||||
| 											reader.onload = function (e) { | ||||
| 												files = [ | ||||
| 													...files, | ||||
| 													{ | ||||
| 														type: 'image', | ||||
| 														url: `${e.target.result}` | ||||
| 													} | ||||
| 												]; | ||||
| 											}; | ||||
| 
 | ||||
| 											reader.readAsDataURL(blob); | ||||
| 										} | ||||
| 									} | ||||
| 								} | ||||
| 							}} | ||||
| 						/> | ||||
| 
 | ||||
| 						<div class="self-end mb-2 flex space-x-0.5 mr-2"> | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
| 	import { marked } from 'marked'; | ||||
| 
 | ||||
| 	import { v4 as uuidv4 } from 'uuid'; | ||||
| 	import tippy from 'tippy.js'; | ||||
| 	import hljs from 'highlight.js'; | ||||
| 	import 'highlight.js/styles/github-dark.min.css'; | ||||
| 	import auto_render from 'katex/dist/contrib/auto-render.mjs'; | ||||
|  | @ -29,6 +30,35 @@ | |||
| 			renderLatex(); | ||||
| 			hljs.highlightAll(); | ||||
| 			createCopyCodeBlockButton(); | ||||
| 
 | ||||
| 			for (const message of messages) { | ||||
| 				if (message.info) { | ||||
| 					tippy(`#info-${message.id}`, { | ||||
| 						content: `<span class="text-xs">token/s: ${ | ||||
| 							`${ | ||||
| 								Math.round( | ||||
| 									((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100 | ||||
| 								) / 100 | ||||
| 							} tokens` ?? 'N/A' | ||||
| 						}<br/> | ||||
| 						total_duration: ${ | ||||
| 							Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A' | ||||
| 						}ms<br/> | ||||
| 						load_duration: ${ | ||||
| 							Math.round(((message.info.load_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A' | ||||
| 						}ms<br/> | ||||
| 						prompt_eval_count: ${message.info.prompt_eval_count ?? 'N/A'}<br/> | ||||
| 						prompt_eval_duration: ${ | ||||
| 							Math.round(((message.info.prompt_eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A' | ||||
| 						}ms<br/> | ||||
| 						eval_count: ${message.info.eval_count ?? 'N/A'}<br/> | ||||
| 						eval_duration: ${ | ||||
| 							Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A' | ||||
| 						}ms</span>`, | ||||
| 						allowHTML: true | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 		})(); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -861,6 +891,33 @@ | |||
| 															</svg> | ||||
| 														</button> | ||||
| 
 | ||||
| 														{#if message.info} | ||||
| 															<button | ||||
| 																class=" {messageIdx + 1 === messages.length | ||||
| 																	? 'visible' | ||||
| 																	: 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition whitespace-pre-wrap" | ||||
| 																on:click={() => { | ||||
| 																	console.log(message); | ||||
| 																}} | ||||
| 																id="info-{message.id}" | ||||
| 															> | ||||
| 																<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="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" | ||||
| 																	/> | ||||
| 																</svg> | ||||
| 															</button> | ||||
| 														{/if} | ||||
| 
 | ||||
| 														{#if messageIdx + 1 === messages.length} | ||||
| 															<button | ||||
| 																type="button" | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
| 	import { WEB_UI_VERSION, OLLAMA_API_BASE_URL } from '$lib/constants'; | ||||
| 	import toast from 'svelte-french-toast'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { config, models, settings, user } from '$lib/stores'; | ||||
| 	import { config, info, models, settings, user } from '$lib/stores'; | ||||
| 	import { splitStream, getGravatarURL } from '$lib/utils'; | ||||
| 	import Advanced from './Settings/Advanced.svelte'; | ||||
| 
 | ||||
|  | @ -22,6 +22,7 @@ | |||
| 	// General | ||||
| 	let API_BASE_URL = OLLAMA_API_BASE_URL; | ||||
| 	let theme = 'dark'; | ||||
| 	let notificationEnabled = false; | ||||
| 	let system = ''; | ||||
| 
 | ||||
| 	// Advanced | ||||
|  | @ -51,6 +52,8 @@ | |||
| 	// Addons | ||||
| 	let titleAutoGenerate = true; | ||||
| 	let speechAutoSend = false; | ||||
| 	let responseAutoCopy = false; | ||||
| 
 | ||||
| 	let gravatarEmail = ''; | ||||
| 	let OPENAI_API_KEY = ''; | ||||
| 
 | ||||
|  | @ -108,6 +111,41 @@ | |||
| 		saveSettings({ titleAutoGenerate: titleAutoGenerate }); | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleNotification = async () => { | ||||
| 		const permission = await Notification.requestPermission(); | ||||
| 
 | ||||
| 		if (permission === 'granted') { | ||||
| 			notificationEnabled = !notificationEnabled; | ||||
| 			saveSettings({ notificationEnabled: notificationEnabled }); | ||||
| 		} else { | ||||
| 			toast.error( | ||||
| 				'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.' | ||||
| 			); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleResponseAutoCopy = async () => { | ||||
| 		const permission = await navigator.clipboard | ||||
| 			.readText() | ||||
| 			.then(() => { | ||||
| 				return 'granted'; | ||||
| 			}) | ||||
| 			.catch(() => { | ||||
| 				return ''; | ||||
| 			}); | ||||
| 
 | ||||
| 		console.log(permission); | ||||
| 
 | ||||
| 		if (permission === 'granted') { | ||||
| 			responseAutoCopy = !responseAutoCopy; | ||||
| 			saveSettings({ responseAutoCopy: responseAutoCopy }); | ||||
| 		} else { | ||||
| 			toast.error( | ||||
| 				'Clipboard write permission denied. Please check your browser settings to grant the necessary access.' | ||||
| 			); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleAuthHeader = async () => { | ||||
| 		authEnabled = !authEnabled; | ||||
| 	}; | ||||
|  | @ -153,6 +191,13 @@ | |||
| 						if (data.status) { | ||||
| 							if (!data.digest) { | ||||
| 								toast.success(data.status); | ||||
| 
 | ||||
| 								if (data.status === 'success') { | ||||
| 									const notification = new Notification(`Ollama`, { | ||||
| 										body: `Model '${modelTag}' has been successfully downloaded.`, | ||||
| 										icon: '/favicon.png' | ||||
| 									}); | ||||
| 								} | ||||
| 							} else { | ||||
| 								digest = data.digest; | ||||
| 								if (data.completed) { | ||||
|  | @ -297,6 +342,8 @@ | |||
| 		console.log(settings); | ||||
| 
 | ||||
| 		theme = localStorage.theme ?? 'dark'; | ||||
| 		notificationEnabled = settings.notificationEnabled ?? false; | ||||
| 
 | ||||
| 		API_BASE_URL = settings.API_BASE_URL ?? OLLAMA_API_BASE_URL; | ||||
| 		system = settings.system ?? ''; | ||||
| 
 | ||||
|  | @ -312,6 +359,8 @@ | |||
| 
 | ||||
| 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | ||||
| 		speechAutoSend = settings.speechAutoSend ?? false; | ||||
| 		responseAutoCopy = settings.responseAutoCopy ?? false; | ||||
| 
 | ||||
| 		gravatarEmail = settings.gravatarEmail ?? ''; | ||||
| 		OPENAI_API_KEY = settings.OPENAI_API_KEY ?? ''; | ||||
| 
 | ||||
|  | @ -509,8 +558,10 @@ | |||
| 				{#if selectedTab === 'general'} | ||||
| 					<div class="flex flex-col space-y-3"> | ||||
| 						<div> | ||||
| 							<div class=" py-1 flex w-full justify-between"> | ||||
| 								<div class=" self-center text-sm font-medium">Theme</div> | ||||
| 							<div class=" mb-1 text-sm font-medium">WebUI Settings</div> | ||||
| 
 | ||||
| 							<div class=" py-0.5 flex w-full justify-between"> | ||||
| 								<div class=" self-center text-xs font-medium">Theme</div> | ||||
| 
 | ||||
| 								<button | ||||
| 									class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -548,6 +599,26 @@ | |||
| 									{/if} | ||||
| 								</button> | ||||
| 							</div> | ||||
| 
 | ||||
| 							<div> | ||||
| 								<div class=" py-0.5 flex w-full justify-between"> | ||||
| 									<div class=" self-center text-xs font-medium">Notification</div> | ||||
| 
 | ||||
| 									<button | ||||
| 										class="p-1 px-3 text-xs flex rounded transition" | ||||
| 										on:click={() => { | ||||
| 											toggleNotification(); | ||||
| 										}} | ||||
| 										type="button" | ||||
| 									> | ||||
| 										{#if notificationEnabled === true} | ||||
| 											<span class="ml-2 self-center">On</span> | ||||
| 										{:else} | ||||
| 											<span class="ml-2 self-center">Off</span> | ||||
| 										{/if} | ||||
| 									</button> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 
 | ||||
| 						<hr class=" dark:border-gray-700" /> | ||||
|  | @ -802,44 +873,68 @@ | |||
| 					> | ||||
| 						<div class=" space-y-3"> | ||||
| 							<div> | ||||
| 								<div class=" py-1 flex w-full justify-between"> | ||||
| 									<div class=" self-center text-sm font-medium">Title Auto Generation</div> | ||||
| 								<div class=" mb-1 text-sm font-medium">WebUI Add-ons</div> | ||||
| 
 | ||||
| 									<button | ||||
| 										class="p-1 px-3 text-xs flex rounded transition" | ||||
| 										on:click={() => { | ||||
| 											toggleTitleAutoGenerate(); | ||||
| 										}} | ||||
| 										type="button" | ||||
| 									> | ||||
| 										{#if titleAutoGenerate === true} | ||||
| 											<span class="ml-2 self-center">On</span> | ||||
| 										{:else} | ||||
| 											<span class="ml-2 self-center">Off</span> | ||||
| 										{/if} | ||||
| 									</button> | ||||
| 								<div> | ||||
| 									<div class=" py-0.5 flex w-full justify-between"> | ||||
| 										<div class=" self-center text-xs font-medium">Title Auto Generation</div> | ||||
| 
 | ||||
| 										<button | ||||
| 											class="p-1 px-3 text-xs flex rounded transition" | ||||
| 											on:click={() => { | ||||
| 												toggleTitleAutoGenerate(); | ||||
| 											}} | ||||
| 											type="button" | ||||
| 										> | ||||
| 											{#if titleAutoGenerate === true} | ||||
| 												<span class="ml-2 self-center">On</span> | ||||
| 											{:else} | ||||
| 												<span class="ml-2 self-center">Off</span> | ||||
| 											{/if} | ||||
| 										</button> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 
 | ||||
| 							<hr class=" dark:border-gray-700" /> | ||||
| 								<div> | ||||
| 									<div class=" py-0.5 flex w-full justify-between"> | ||||
| 										<div class=" self-center text-xs font-medium">Voice Input Auto-Send</div> | ||||
| 
 | ||||
| 							<div> | ||||
| 								<div class=" py-1 flex w-full justify-between"> | ||||
| 									<div class=" self-center text-sm font-medium">Voice Input Auto-Send</div> | ||||
| 										<button | ||||
| 											class="p-1 px-3 text-xs flex rounded transition" | ||||
| 											on:click={() => { | ||||
| 												toggleSpeechAutoSend(); | ||||
| 											}} | ||||
| 											type="button" | ||||
| 										> | ||||
| 											{#if speechAutoSend === true} | ||||
| 												<span class="ml-2 self-center">On</span> | ||||
| 											{:else} | ||||
| 												<span class="ml-2 self-center">Off</span> | ||||
| 											{/if} | ||||
| 										</button> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 
 | ||||
| 									<button | ||||
| 										class="p-1 px-3 text-xs flex rounded transition" | ||||
| 										on:click={() => { | ||||
| 											toggleSpeechAutoSend(); | ||||
| 										}} | ||||
| 										type="button" | ||||
| 									> | ||||
| 										{#if speechAutoSend === true} | ||||
| 											<span class="ml-2 self-center">On</span> | ||||
| 										{:else} | ||||
| 											<span class="ml-2 self-center">Off</span> | ||||
| 										{/if} | ||||
| 									</button> | ||||
| 								<div> | ||||
| 									<div class=" py-0.5 flex w-full justify-between"> | ||||
| 										<div class=" self-center text-xs font-medium"> | ||||
| 											Response AutoCopy to Clipboard | ||||
| 										</div> | ||||
| 
 | ||||
| 										<button | ||||
| 											class="p-1 px-3 text-xs flex rounded transition" | ||||
| 											on:click={() => { | ||||
| 												toggleResponseAutoCopy(); | ||||
| 											}} | ||||
| 											type="button" | ||||
| 										> | ||||
| 											{#if responseAutoCopy === true} | ||||
| 												<span class="ml-2 self-center">On</span> | ||||
| 											{:else} | ||||
| 												<span class="ml-2 self-center">Off</span> | ||||
| 											{/if} | ||||
| 										</button> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 
 | ||||
|  | @ -1029,6 +1124,17 @@ | |||
| 
 | ||||
| 							<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 							<div> | ||||
| 								<div class=" mb-2.5 text-sm font-medium">Ollama Version</div> | ||||
| 								<div class="flex w-full"> | ||||
| 									<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> | ||||
| 										{$info?.ollama?.version ?? 'N/A'} | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 
 | ||||
| 							<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 							<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 								Created by <a | ||||
| 									class=" text-gray-500 dark:text-gray-300 font-medium" | ||||
|  |  | |||
|  | @ -2,51 +2,102 @@ | |||
| 	import { v4 as uuidv4 } from 'uuid'; | ||||
| 
 | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { chatId } from '$lib/stores'; | ||||
| 	import { chatId, db, modelfiles } from '$lib/stores'; | ||||
| 	import toast from 'svelte-french-toast'; | ||||
| 
 | ||||
| 	export let title: string = 'Ollama Web UI'; | ||||
| 	export let shareEnabled: boolean = false; | ||||
| 
 | ||||
| 	const shareChat = async () => { | ||||
| 		const chat = await $db.getChatById($chatId); | ||||
| 		console.log('share', chat); | ||||
| 		toast.success('Redirecting you to OllamaHub'); | ||||
| 
 | ||||
| 		const url = 'https://ollamahub.com'; | ||||
| 		// const url = 'http://localhost:5173'; | ||||
| 
 | ||||
| 		const tab = await window.open(`${url}/chats/upload`, '_blank'); | ||||
| 		window.addEventListener( | ||||
| 			'message', | ||||
| 			(event) => { | ||||
| 				if (event.origin !== url) return; | ||||
| 				if (event.data === 'loaded') { | ||||
| 					tab.postMessage( | ||||
| 						JSON.stringify({ | ||||
| 							chat: chat, | ||||
| 							modelfiles: $modelfiles.filter((modelfile) => chat.models.includes(modelfile.tagName)) | ||||
| 						}), | ||||
| 						'*' | ||||
| 					); | ||||
| 				} | ||||
| 			}, | ||||
| 			false | ||||
| 		); | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <div | ||||
| 	class=" fixed top-0 flex flex-row justify-center bg-white/95 dark:bg-gray-800/90 dark:text-gray-200 backdrop-blur-xl w-full z-30" | ||||
| <nav | ||||
| 	id="nav" | ||||
| 	class=" fixed py-2.5 top-0 flex flex-row justify-center bg-white/95 dark:bg-gray-800/90 dark:text-gray-200 backdrop-blur-xl w-screen z-30" | ||||
| > | ||||
| 	<div class="basis-full"> | ||||
| 		<nav class="py-3" id="nav"> | ||||
| 			<div class=" flex max-w-3xl mx-auto px-3"> | ||||
| 				<div class="flex w-full max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> | ||||
| 					<div class="pr-2"> | ||||
| 						<button | ||||
| 							class=" cursor-pointer p-1 flex dark:hover:bg-gray-700 rounded-lg transition" | ||||
| 							on:click={async () => { | ||||
| 								console.log('newChat'); | ||||
| 								goto('/'); | ||||
| 								await chatId.set(uuidv4()); | ||||
| 							}} | ||||
| 	<div class=" flex max-w-3xl w-full mx-auto px-3"> | ||||
| 		<div class="flex w-full max-w-full"> | ||||
| 			<div class="pr-2 self-center"> | ||||
| 				<button | ||||
| 					class=" cursor-pointer p-1 flex dark:hover:bg-gray-700 rounded-lg transition" | ||||
| 					on:click={async () => { | ||||
| 						console.log('newChat'); | ||||
| 						goto('/'); | ||||
| 						await chatId.set(uuidv4()); | ||||
| 					}} | ||||
| 				> | ||||
| 					<div class=" m-auto self-center"> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 20 20" | ||||
| 							fill="currentColor" | ||||
| 							class="w-5 h-5" | ||||
| 						> | ||||
| 							<div class=" m-auto self-center"> | ||||
| 								<svg | ||||
| 									xmlns="http://www.w3.org/2000/svg" | ||||
| 									viewBox="0 0 20 20" | ||||
| 									fill="currentColor" | ||||
| 									class="w-5 h-5" | ||||
| 								> | ||||
| 									<path | ||||
| 										d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" | ||||
| 									/> | ||||
| 									<path | ||||
| 										d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" | ||||
| 									/> | ||||
| 								</svg> | ||||
| 							</div> | ||||
| 						</button> | ||||
| 							<path | ||||
| 								d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" | ||||
| 							/> | ||||
| 							<path | ||||
| 								d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div | ||||
| 						class=" flex-1 self-center font-medium overflow-hidden text-ellipsis whitespace-nowrap w-[80vw] pr-4" | ||||
| 					> | ||||
| 						{title != '' ? title : 'Ollama Web UI'} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</nav> | ||||
| 			<div class=" flex-1 self-center font-medium text-ellipsis whitespace-nowrap overflow-hidden"> | ||||
| 				{title != '' ? title : 'Ollama Web UI'} | ||||
| 			</div> | ||||
| 
 | ||||
| 			{#if shareEnabled} | ||||
| 				<div class="pl-2"> | ||||
| 					<button | ||||
| 						class=" cursor-pointer p-2 flex dark:hover:bg-gray-700 rounded-lg transition border dark:border-gray-600" | ||||
| 						on:click={async () => { | ||||
| 							shareChat(); | ||||
| 						}} | ||||
| 					> | ||||
| 						<div class=" m-auto self-center"> | ||||
| 							<svg | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								viewBox="0 0 20 20" | ||||
| 								fill="currentColor" | ||||
| 								class="w-4 h-4" | ||||
| 							> | ||||
| 								<path | ||||
| 									d="M9.25 13.25a.75.75 0 001.5 0V4.636l2.955 3.129a.75.75 0 001.09-1.03l-4.25-4.5a.75.75 0 00-1.09 0l-4.25 4.5a.75.75 0 101.09 1.03L9.25 4.636v8.614z" | ||||
| 								/> | ||||
| 								<path | ||||
| 									d="M3.5 12.75a.75.75 0 00-1.5 0v2.5A2.75 2.75 0 004.75 18h10.5A2.75 2.75 0 0018 15.25v-2.5a.75.75 0 00-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5z" | ||||
| 								/> | ||||
| 							</svg> | ||||
| 						</div> | ||||
| 					</button> | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </nav> | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { writable } from 'svelte/store'; | ||||
| 
 | ||||
| // Backend
 | ||||
| export const info = writable({}); | ||||
| export const config = writable(undefined); | ||||
| export const user = writable(undefined); | ||||
| 
 | ||||
|  |  | |||
|  | @ -65,3 +65,38 @@ export const getGravatarURL = (email) => { | |||
| 	// Grab the actual image URL
 | ||||
| 	return `https://www.gravatar.com/avatar/${hash}`; | ||||
| }; | ||||
| 
 | ||||
| const copyToClipboard = (text) => { | ||||
| 	if (!navigator.clipboard) { | ||||
| 		var textArea = document.createElement('textarea'); | ||||
| 		textArea.value = text; | ||||
| 
 | ||||
| 		// Avoid scrolling to bottom
 | ||||
| 		textArea.style.top = '0'; | ||||
| 		textArea.style.left = '0'; | ||||
| 		textArea.style.position = 'fixed'; | ||||
| 
 | ||||
| 		document.body.appendChild(textArea); | ||||
| 		textArea.focus(); | ||||
| 		textArea.select(); | ||||
| 
 | ||||
| 		try { | ||||
| 			var successful = document.execCommand('copy'); | ||||
| 			var msg = successful ? 'successful' : 'unsuccessful'; | ||||
| 			console.log('Fallback: Copying text command was ' + msg); | ||||
| 		} catch (err) { | ||||
| 			console.error('Fallback: Oops, unable to copy', err); | ||||
| 		} | ||||
| 
 | ||||
| 		document.body.removeChild(textArea); | ||||
| 		return; | ||||
| 	} | ||||
| 	navigator.clipboard.writeText(text).then( | ||||
| 		function () { | ||||
| 			console.log('Async: Copying to clipboard was successful!'); | ||||
| 		}, | ||||
| 		function (err) { | ||||
| 			console.error('Async: Could not copy text: ', err); | ||||
| 		} | ||||
| 	); | ||||
| }; | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| 
 | ||||
| 	import { | ||||
| 		config, | ||||
| 		info, | ||||
| 		user, | ||||
| 		showSettings, | ||||
| 		settings, | ||||
|  | @ -21,6 +22,7 @@ | |||
| 	import toast from 'svelte-french-toast'; | ||||
| 	import { OLLAMA_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants'; | ||||
| 
 | ||||
| 	let requiredOllamaVersion = '0.1.16'; | ||||
| 	let loaded = false; | ||||
| 
 | ||||
| 	const getModels = async () => { | ||||
|  | @ -160,33 +162,116 @@ | |||
| 		}; | ||||
| 	}; | ||||
| 
 | ||||
| 	const getOllamaVersion = async () => { | ||||
| 		const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/version`, { | ||||
| 			method: 'GET', | ||||
| 			headers: { | ||||
| 				Accept: 'application/json', | ||||
| 				'Content-Type': 'application/json', | ||||
| 				...($settings.authHeader && { Authorization: $settings.authHeader }), | ||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 			} | ||||
| 		}) | ||||
| 			.then(async (res) => { | ||||
| 				if (!res.ok) throw await res.json(); | ||||
| 				return res.json(); | ||||
| 			}) | ||||
| 			.catch((error) => { | ||||
| 				console.log(error); | ||||
| 				if ('detail' in error) { | ||||
| 					toast.error(error.detail); | ||||
| 				} else { | ||||
| 					toast.error('Server connection failed'); | ||||
| 				} | ||||
| 				return null; | ||||
| 			}); | ||||
| 
 | ||||
| 		console.log(res); | ||||
| 
 | ||||
| 		return res?.version ?? '0'; | ||||
| 	}; | ||||
| 
 | ||||
| 	const setOllamaVersion = async (ollamaVersion) => { | ||||
| 		await info.set({ ...$info, ollama: { version: ollamaVersion } }); | ||||
| 
 | ||||
| 		if ( | ||||
| 			ollamaVersion.localeCompare(requiredOllamaVersion, undefined, { | ||||
| 				numeric: true, | ||||
| 				sensitivity: 'case', | ||||
| 				caseFirst: 'upper' | ||||
| 			}) < 0 | ||||
| 		) { | ||||
| 			toast.error(`Ollama Version: ${ollamaVersion}`); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		if ($config && $config.auth && $user === undefined) { | ||||
| 			await goto('/auth'); | ||||
| 		} | ||||
| 
 | ||||
| 		await settings.set(JSON.parse(localStorage.getItem('settings') ?? JSON.stringify($settings))); | ||||
| 		await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); | ||||
| 
 | ||||
| 		let _models = await getModels(); | ||||
| 		await models.set(_models); | ||||
| 		let _db = await getDB(); | ||||
| 		await db.set(_db); | ||||
| 
 | ||||
| 		await modelfiles.set( | ||||
| 			JSON.parse(localStorage.getItem('modelfiles') ?? JSON.stringify($modelfiles)) | ||||
| 		); | ||||
| 		await models.set(await getModels()); | ||||
| 		await modelfiles.set(JSON.parse(localStorage.getItem('modelfiles') ?? '[]')); | ||||
| 
 | ||||
| 		modelfiles.subscribe(async () => { | ||||
| 			await models.set(await getModels()); | ||||
| 		}); | ||||
| 
 | ||||
| 		let _db = await getDB(); | ||||
| 		await db.set(_db); | ||||
| 
 | ||||
| 		await setOllamaVersion(await getOllamaVersion()); | ||||
| 
 | ||||
| 		await tick(); | ||||
| 		loaded = true; | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| {#if loaded} | ||||
| 	<div class="app"> | ||||
| 	<div class="app relative"> | ||||
| 		{#if ($info?.ollama?.version ?? '0').localeCompare( requiredOllamaVersion, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' } ) < 0} | ||||
| 			<div class="absolute w-full h-full flex z-50"> | ||||
| 				<div | ||||
| 					class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-900/60 flex justify-center" | ||||
| 				> | ||||
| 					<div class="m-auto pb-44"> | ||||
| 						<div class="text-center dark:text-white text-2xl font-medium z-50"> | ||||
| 							Ollama Update Required | ||||
| 						</div> | ||||
| 
 | ||||
| 						<div class=" mt-4 text-center max-w-md text-sm dark:text-gray-200"> | ||||
| 							Oops! It seems like your Ollama needs a little attention. <br | ||||
| 								class=" hidden sm:flex" | ||||
| 							/> | ||||
| 							We encountered a connection issue or noticed that you're running an outdated version. Please | ||||
| 							update to | ||||
| 							<span class=" dark:text-white font-medium">{requiredOllamaVersion} or above</span>. | ||||
| 						</div> | ||||
| 
 | ||||
| 						<div class=" mt-6 mx-auto relative group w-fit"> | ||||
| 							<button | ||||
| 								class="relative z-20 flex px-5 py-2 rounded-full bg-gray-100 hover:bg-gray-200 transition font-medium text-sm" | ||||
| 								on:click={async () => { | ||||
| 									await setOllamaVersion(await getOllamaVersion()); | ||||
| 								}} | ||||
| 							> | ||||
| 								Check Again | ||||
| 							</button> | ||||
| 
 | ||||
| 							<button | ||||
| 								class="text-xs text-center w-full mt-2 text-gray-400 underline" | ||||
| 								on:click={async () => { | ||||
| 									await setOllamaVersion(requiredOllamaVersion); | ||||
| 								}}>Close</button | ||||
| 							> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 
 | ||||
| 		<div | ||||
| 			class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-800 min-h-screen overflow-auto flex flex-row" | ||||
| 		> | ||||
|  |  | |||
|  | @ -84,11 +84,45 @@ | |||
| 		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 		console.log(_settings); | ||||
| 		settings.set({ | ||||
| 			...$settings, | ||||
| 			..._settings | ||||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
| 	const copyToClipboard = (text) => { | ||||
| 		if (!navigator.clipboard) { | ||||
| 			var textArea = document.createElement('textarea'); | ||||
| 			textArea.value = text; | ||||
| 
 | ||||
| 			// Avoid scrolling to bottom | ||||
| 			textArea.style.top = '0'; | ||||
| 			textArea.style.left = '0'; | ||||
| 			textArea.style.position = 'fixed'; | ||||
| 
 | ||||
| 			document.body.appendChild(textArea); | ||||
| 			textArea.focus(); | ||||
| 			textArea.select(); | ||||
| 
 | ||||
| 			try { | ||||
| 				var successful = document.execCommand('copy'); | ||||
| 				var msg = successful ? 'successful' : 'unsuccessful'; | ||||
| 				console.log('Fallback: Copying text command was ' + msg); | ||||
| 			} catch (err) { | ||||
| 				console.error('Fallback: Oops, unable to copy', err); | ||||
| 			} | ||||
| 
 | ||||
| 			document.body.removeChild(textArea); | ||||
| 			return; | ||||
| 		} | ||||
| 		navigator.clipboard.writeText(text).then( | ||||
| 			function () { | ||||
| 				console.log('Async: Copying to clipboard was successful!'); | ||||
| 			}, | ||||
| 			function (err) { | ||||
| 				console.error('Async: Could not copy text: ', err); | ||||
| 			} | ||||
| 		); | ||||
| 	}; | ||||
| 
 | ||||
| 	////////////////////////// | ||||
| 	// Ollama functions | ||||
| 	////////////////////////// | ||||
|  | @ -213,12 +247,34 @@ | |||
| 								responseMessage.context = data.context ?? null; | ||||
| 								responseMessage.info = { | ||||
| 									total_duration: data.total_duration, | ||||
| 									load_duration: data.load_duration, | ||||
| 									sample_count: data.sample_count, | ||||
| 									sample_duration: data.sample_duration, | ||||
| 									prompt_eval_count: data.prompt_eval_count, | ||||
| 									prompt_eval_duration: data.prompt_eval_duration, | ||||
| 									eval_count: data.eval_count, | ||||
| 									eval_duration: data.eval_duration | ||||
| 								}; | ||||
| 								messages = messages; | ||||
| 
 | ||||
| 								if ($settings.notificationEnabled && !document.hasFocus()) { | ||||
| 									const notification = new Notification( | ||||
| 										selectedModelfile | ||||
| 											? `${ | ||||
| 													selectedModelfile.title.charAt(0).toUpperCase() + | ||||
| 													selectedModelfile.title.slice(1) | ||||
| 											  }` | ||||
| 											: `Ollama - ${model}`, | ||||
| 										{ | ||||
| 											body: responseMessage.content, | ||||
| 											icon: selectedModelfile?.imageUrl ?? '/favicon.png' | ||||
| 										} | ||||
| 									); | ||||
| 								} | ||||
| 
 | ||||
| 								if ($settings.responseAutoCopy) { | ||||
| 									copyToClipboard(responseMessage.content); | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
|  | @ -423,6 +479,18 @@ | |||
| 				stopResponseFlag = false; | ||||
| 
 | ||||
| 				await tick(); | ||||
| 
 | ||||
| 				if ($settings.notificationEnabled && !document.hasFocus()) { | ||||
| 					const notification = new Notification(`OpenAI ${model}`, { | ||||
| 						body: responseMessage.content, | ||||
| 						icon: '/favicon.png' | ||||
| 					}); | ||||
| 				} | ||||
| 
 | ||||
| 				if ($settings.responseAutoCopy) { | ||||
| 					copyToClipboard(responseMessage.content); | ||||
| 				} | ||||
| 
 | ||||
| 				if (autoScroll) { | ||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | ||||
| 				} | ||||
|  | @ -566,7 +634,7 @@ | |||
| 	}} | ||||
| /> | ||||
| 
 | ||||
| <Navbar {title} /> | ||||
| <Navbar {title} shareEnabled={messages.length > 0} /> | ||||
| <div class="min-h-screen w-full flex justify-center"> | ||||
| 	<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 mt-10"> | ||||
|  |  | |||
|  | @ -82,10 +82,11 @@ | |||
| 					: convertMessagesToHistory(chat.messages); | ||||
| 			title = chat.title; | ||||
| 
 | ||||
| 			let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 			await settings.set({ | ||||
| 				...$settings, | ||||
| 				system: chat.system ?? $settings.system, | ||||
| 				options: chat.options ?? $settings.options | ||||
| 				..._settings, | ||||
| 				system: chat.system ?? _settings.system, | ||||
| 				options: chat.options ?? _settings.options | ||||
| 			}); | ||||
| 			autoScroll = true; | ||||
| 
 | ||||
|  | @ -101,6 +102,41 @@ | |||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const copyToClipboard = (text) => { | ||||
| 		if (!navigator.clipboard) { | ||||
| 			var textArea = document.createElement('textarea'); | ||||
| 			textArea.value = text; | ||||
| 
 | ||||
| 			// Avoid scrolling to bottom | ||||
| 			textArea.style.top = '0'; | ||||
| 			textArea.style.left = '0'; | ||||
| 			textArea.style.position = 'fixed'; | ||||
| 
 | ||||
| 			document.body.appendChild(textArea); | ||||
| 			textArea.focus(); | ||||
| 			textArea.select(); | ||||
| 
 | ||||
| 			try { | ||||
| 				var successful = document.execCommand('copy'); | ||||
| 				var msg = successful ? 'successful' : 'unsuccessful'; | ||||
| 				console.log('Fallback: Copying text command was ' + msg); | ||||
| 			} catch (err) { | ||||
| 				console.error('Fallback: Oops, unable to copy', err); | ||||
| 			} | ||||
| 
 | ||||
| 			document.body.removeChild(textArea); | ||||
| 			return; | ||||
| 		} | ||||
| 		navigator.clipboard.writeText(text).then( | ||||
| 			function () { | ||||
| 				console.log('Async: Copying to clipboard was successful!'); | ||||
| 			}, | ||||
| 			function (err) { | ||||
| 				console.error('Async: Could not copy text: ', err); | ||||
| 			} | ||||
| 		); | ||||
| 	}; | ||||
| 
 | ||||
| 	////////////////////////// | ||||
| 	// Ollama functions | ||||
| 	////////////////////////// | ||||
|  | @ -225,12 +261,34 @@ | |||
| 								responseMessage.context = data.context ?? null; | ||||
| 								responseMessage.info = { | ||||
| 									total_duration: data.total_duration, | ||||
| 									load_duration: data.load_duration, | ||||
| 									sample_count: data.sample_count, | ||||
| 									sample_duration: data.sample_duration, | ||||
| 									prompt_eval_count: data.prompt_eval_count, | ||||
| 									prompt_eval_duration: data.prompt_eval_duration, | ||||
| 									eval_count: data.eval_count, | ||||
| 									eval_duration: data.eval_duration | ||||
| 								}; | ||||
| 								messages = messages; | ||||
| 
 | ||||
| 								if ($settings.notificationEnabled && !document.hasFocus()) { | ||||
| 									const notification = new Notification( | ||||
| 										selectedModelfile | ||||
| 											? `${ | ||||
| 													selectedModelfile.title.charAt(0).toUpperCase() + | ||||
| 													selectedModelfile.title.slice(1) | ||||
| 											  }` | ||||
| 											: `Ollama - ${model}`, | ||||
| 										{ | ||||
| 											body: responseMessage.content, | ||||
| 											icon: selectedModelfile?.imageUrl ?? '/favicon.png' | ||||
| 										} | ||||
| 									); | ||||
| 								} | ||||
| 
 | ||||
| 								if ($settings.responseAutoCopy) { | ||||
| 									copyToClipboard(responseMessage.content); | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
|  | @ -435,6 +493,18 @@ | |||
| 				stopResponseFlag = false; | ||||
| 
 | ||||
| 				await tick(); | ||||
| 
 | ||||
| 				if ($settings.notificationEnabled && !document.hasFocus()) { | ||||
| 					const notification = new Notification(`OpenAI ${model}`, { | ||||
| 						body: responseMessage.content, | ||||
| 						icon: '/favicon.png' | ||||
| 					}); | ||||
| 				} | ||||
| 
 | ||||
| 				if ($settings.responseAutoCopy) { | ||||
| 					copyToClipboard(responseMessage.content); | ||||
| 				} | ||||
| 
 | ||||
| 				if (autoScroll) { | ||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | ||||
| 				} | ||||
|  | @ -579,7 +649,7 @@ | |||
| /> | ||||
| 
 | ||||
| {#if loaded} | ||||
| 	<Navbar {title} /> | ||||
| 	<Navbar {title} shareEnabled={messages.length > 0} /> | ||||
| 	<div class="min-h-screen w-full flex justify-center"> | ||||
| 		<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 mt-10"> | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
| 
 | ||||
| 	import '../app.css'; | ||||
| 	import '../tailwind.css'; | ||||
| 
 | ||||
| 	import 'tippy.js/dist/tippy.css'; | ||||
| 	let loaded = false; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek