forked from open-webui/open-webui
		
	Merge branch 'main' into dev
This commit is contained in:
		
						commit
						1fb6b13ab7
					
				
					 15 changed files with 654 additions and 109 deletions
				
			
		|  | @ -23,7 +23,6 @@ ARG OLLAMA_API_BASE_URL='/ollama/api' | ||||||
| ENV ENV=prod | ENV ENV=prod | ||||||
| ENV OLLAMA_API_BASE_URL $OLLAMA_API_BASE_URL | ENV OLLAMA_API_BASE_URL $OLLAMA_API_BASE_URL | ||||||
| ENV WEBUI_AUTH "" | ENV WEBUI_AUTH "" | ||||||
| ENV WEBUI_DB_URL "" |  | ||||||
| ENV WEBUI_JWT_SECRET_KEY "SECRET_KEY" | ENV WEBUI_JWT_SECRET_KEY "SECRET_KEY" | ||||||
| 
 | 
 | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
|  |  | ||||||
|  | @ -59,9 +59,11 @@ def proxy(path): | ||||||
|     else: |     else: | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|  |     r = None | ||||||
|  | 
 | ||||||
|     try: |     try: | ||||||
|         # Make a request to the target server |         # Make a request to the target server | ||||||
|         target_response = requests.request( |         r = requests.request( | ||||||
|             method=request.method, |             method=request.method, | ||||||
|             url=target_url, |             url=target_url, | ||||||
|             data=data, |             data=data, | ||||||
|  | @ -69,22 +71,37 @@ def proxy(path): | ||||||
|             stream=True,  # Enable streaming for server-sent events |             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 |         # Proxy the target server's response to the client | ||||||
|         def generate(): |         def generate(): | ||||||
|             for chunk in target_response.iter_content(chunk_size=8192): |             for chunk in r.iter_content(chunk_size=8192): | ||||||
|                 yield chunk |                 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 |         # 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 |             response.headers[key] = value | ||||||
| 
 | 
 | ||||||
|         return response |         return response | ||||||
|     except Exception as e: |     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__": | if __name__ == "__main__": | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ if ENV == "prod": | ||||||
| # WEBUI_VERSION | # 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 | # 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", | 				"katex": "^0.16.9", | ||||||
| 				"marked": "^9.1.0", | 				"marked": "^9.1.0", | ||||||
| 				"svelte-french-toast": "^1.2.0", | 				"svelte-french-toast": "^1.2.0", | ||||||
|  | 				"tippy.js": "^6.3.7", | ||||||
| 				"uuid": "^9.0.1" | 				"uuid": "^9.0.1" | ||||||
| 			}, | 			}, | ||||||
| 			"devDependencies": { | 			"devDependencies": { | ||||||
|  | @ -584,6 +585,15 @@ | ||||||
| 			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", | 			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", | ||||||
| 			"integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==" | 			"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": { | 		"node_modules/@rollup/plugin-commonjs": { | ||||||
| 			"version": "25.0.5", | 			"version": "25.0.5", | ||||||
| 			"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.5.tgz", | 			"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.5.tgz", | ||||||
|  | @ -3994,6 +4004,14 @@ | ||||||
| 				"globrex": "^0.1.2" | 				"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": { | 		"node_modules/to-regex-range": { | ||||||
| 			"version": "5.0.1", | 			"version": "5.0.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | ||||||
|  | @ -4160,9 +4178,9 @@ | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		"node_modules/vite": { | 		"node_modules/vite": { | ||||||
| 			"version": "4.4.11", | 			"version": "4.5.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz", | 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", | ||||||
| 			"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==", | 			"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", | ||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"esbuild": "^0.18.10", | 				"esbuild": "^0.18.10", | ||||||
| 				"postcss": "^8.4.27", | 				"postcss": "^8.4.27", | ||||||
|  | @ -4570,6 +4588,11 @@ | ||||||
| 			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", | 			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", | ||||||
| 			"integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==" | 			"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": { | 		"@rollup/plugin-commonjs": { | ||||||
| 			"version": "25.0.5", | 			"version": "25.0.5", | ||||||
| 			"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.5.tgz", | 			"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.5.tgz", | ||||||
|  | @ -6885,6 +6908,14 @@ | ||||||
| 				"globrex": "^0.1.2" | 				"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": { | 		"to-regex-range": { | ||||||
| 			"version": "5.0.1", | 			"version": "5.0.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | ||||||
|  | @ -6991,9 +7022,9 @@ | ||||||
| 			"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" | 			"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" | ||||||
| 		}, | 		}, | ||||||
| 		"vite": { | 		"vite": { | ||||||
| 			"version": "4.4.11", | 			"version": "4.5.1", | ||||||
| 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz", | 			"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", | ||||||
| 			"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==", | 			"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", | ||||||
| 			"requires": { | 			"requires": { | ||||||
| 				"esbuild": "^0.18.10", | 				"esbuild": "^0.18.10", | ||||||
| 				"fsevents": "~2.3.2", | 				"fsevents": "~2.3.2", | ||||||
|  |  | ||||||
|  | @ -48,6 +48,7 @@ | ||||||
| 		"katex": "^0.16.9", | 		"katex": "^0.16.9", | ||||||
| 		"marked": "^9.1.0", | 		"marked": "^9.1.0", | ||||||
| 		"svelte-french-toast": "^1.2.0", | 		"svelte-french-toast": "^1.2.0", | ||||||
|  | 		"tippy.js": "^6.3.7", | ||||||
| 		"uuid": "^9.0.1" | 		"uuid": "^9.0.1" | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -161,7 +161,7 @@ | ||||||
| 						<div class="ml-2 mt-2 mb-1 flex space-x-2"> | 						<div class="ml-2 mt-2 mb-1 flex space-x-2"> | ||||||
| 							{#each files as file, fileIdx} | 							{#each files as file, fileIdx} | ||||||
| 								<div class=" relative group"> | 								<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"> | 									<div class=" absolute -top-1 -right-1"> | ||||||
| 										<button | 										<button | ||||||
|  | @ -235,6 +235,30 @@ | ||||||
| 								e.target.style.height = ''; | 								e.target.style.height = ''; | ||||||
| 								e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; | 								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"> | 						<div class="self-end mb-2 flex space-x-0.5 mr-2"> | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
| 	import { marked } from 'marked'; | 	import { marked } from 'marked'; | ||||||
| 
 | 
 | ||||||
| 	import { v4 as uuidv4 } from 'uuid'; | 	import { v4 as uuidv4 } from 'uuid'; | ||||||
|  | 	import tippy from 'tippy.js'; | ||||||
| 	import hljs from 'highlight.js'; | 	import hljs from 'highlight.js'; | ||||||
| 	import 'highlight.js/styles/github-dark.min.css'; | 	import 'highlight.js/styles/github-dark.min.css'; | ||||||
| 	import auto_render from 'katex/dist/contrib/auto-render.mjs'; | 	import auto_render from 'katex/dist/contrib/auto-render.mjs'; | ||||||
|  | @ -29,6 +30,35 @@ | ||||||
| 			renderLatex(); | 			renderLatex(); | ||||||
| 			hljs.highlightAll(); | 			hljs.highlightAll(); | ||||||
| 			createCopyCodeBlockButton(); | 			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> | 															</svg> | ||||||
| 														</button> | 														</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} | 														{#if messageIdx + 1 === messages.length} | ||||||
| 															<button | 															<button | ||||||
| 																type="button" | 																type="button" | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 	import { WEB_UI_VERSION, OLLAMA_API_BASE_URL } from '$lib/constants'; | 	import { WEB_UI_VERSION, OLLAMA_API_BASE_URL } from '$lib/constants'; | ||||||
| 	import toast from 'svelte-french-toast'; | 	import toast from 'svelte-french-toast'; | ||||||
| 	import { onMount } from 'svelte'; | 	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 { splitStream, getGravatarURL } from '$lib/utils'; | ||||||
| 	import Advanced from './Settings/Advanced.svelte'; | 	import Advanced from './Settings/Advanced.svelte'; | ||||||
| 
 | 
 | ||||||
|  | @ -22,6 +22,7 @@ | ||||||
| 	// General | 	// General | ||||||
| 	let API_BASE_URL = OLLAMA_API_BASE_URL; | 	let API_BASE_URL = OLLAMA_API_BASE_URL; | ||||||
| 	let theme = 'dark'; | 	let theme = 'dark'; | ||||||
|  | 	let notificationEnabled = false; | ||||||
| 	let system = ''; | 	let system = ''; | ||||||
| 
 | 
 | ||||||
| 	// Advanced | 	// Advanced | ||||||
|  | @ -51,6 +52,8 @@ | ||||||
| 	// Addons | 	// Addons | ||||||
| 	let titleAutoGenerate = true; | 	let titleAutoGenerate = true; | ||||||
| 	let speechAutoSend = false; | 	let speechAutoSend = false; | ||||||
|  | 	let responseAutoCopy = false; | ||||||
|  | 
 | ||||||
| 	let gravatarEmail = ''; | 	let gravatarEmail = ''; | ||||||
| 	let OPENAI_API_KEY = ''; | 	let OPENAI_API_KEY = ''; | ||||||
| 
 | 
 | ||||||
|  | @ -108,6 +111,41 @@ | ||||||
| 		saveSettings({ titleAutoGenerate: titleAutoGenerate }); | 		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 () => { | 	const toggleAuthHeader = async () => { | ||||||
| 		authEnabled = !authEnabled; | 		authEnabled = !authEnabled; | ||||||
| 	}; | 	}; | ||||||
|  | @ -153,6 +191,13 @@ | ||||||
| 						if (data.status) { | 						if (data.status) { | ||||||
| 							if (!data.digest) { | 							if (!data.digest) { | ||||||
| 								toast.success(data.status); | 								toast.success(data.status); | ||||||
|  | 
 | ||||||
|  | 								if (data.status === 'success') { | ||||||
|  | 									const notification = new Notification(`Ollama`, { | ||||||
|  | 										body: `Model '${modelTag}' has been successfully downloaded.`, | ||||||
|  | 										icon: '/favicon.png' | ||||||
|  | 									}); | ||||||
|  | 								} | ||||||
| 							} else { | 							} else { | ||||||
| 								digest = data.digest; | 								digest = data.digest; | ||||||
| 								if (data.completed) { | 								if (data.completed) { | ||||||
|  | @ -297,6 +342,8 @@ | ||||||
| 		console.log(settings); | 		console.log(settings); | ||||||
| 
 | 
 | ||||||
| 		theme = localStorage.theme ?? 'dark'; | 		theme = localStorage.theme ?? 'dark'; | ||||||
|  | 		notificationEnabled = settings.notificationEnabled ?? false; | ||||||
|  | 
 | ||||||
| 		API_BASE_URL = settings.API_BASE_URL ?? OLLAMA_API_BASE_URL; | 		API_BASE_URL = settings.API_BASE_URL ?? OLLAMA_API_BASE_URL; | ||||||
| 		system = settings.system ?? ''; | 		system = settings.system ?? ''; | ||||||
| 
 | 
 | ||||||
|  | @ -312,6 +359,8 @@ | ||||||
| 
 | 
 | ||||||
| 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | ||||||
| 		speechAutoSend = settings.speechAutoSend ?? false; | 		speechAutoSend = settings.speechAutoSend ?? false; | ||||||
|  | 		responseAutoCopy = settings.responseAutoCopy ?? false; | ||||||
|  | 
 | ||||||
| 		gravatarEmail = settings.gravatarEmail ?? ''; | 		gravatarEmail = settings.gravatarEmail ?? ''; | ||||||
| 		OPENAI_API_KEY = settings.OPENAI_API_KEY ?? ''; | 		OPENAI_API_KEY = settings.OPENAI_API_KEY ?? ''; | ||||||
| 
 | 
 | ||||||
|  | @ -509,8 +558,10 @@ | ||||||
| 				{#if selectedTab === 'general'} | 				{#if selectedTab === 'general'} | ||||||
| 					<div class="flex flex-col space-y-3"> | 					<div class="flex flex-col space-y-3"> | ||||||
| 						<div> | 						<div> | ||||||
| 							<div class=" py-1 flex w-full justify-between"> | 							<div class=" mb-1 text-sm font-medium">WebUI Settings</div> | ||||||
| 								<div class=" self-center text-sm font-medium">Theme</div> | 
 | ||||||
|  | 							<div class=" py-0.5 flex w-full justify-between"> | ||||||
|  | 								<div class=" self-center text-xs font-medium">Theme</div> | ||||||
| 
 | 
 | ||||||
| 								<button | 								<button | ||||||
| 									class="p-1 px-3 text-xs flex rounded transition" | 									class="p-1 px-3 text-xs flex rounded transition" | ||||||
|  | @ -548,6 +599,26 @@ | ||||||
| 									{/if} | 									{/if} | ||||||
| 								</button> | 								</button> | ||||||
| 							</div> | 							</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> | 						</div> | ||||||
| 
 | 
 | ||||||
| 						<hr class=" dark:border-gray-700" /> | 						<hr class=" dark:border-gray-700" /> | ||||||
|  | @ -802,44 +873,68 @@ | ||||||
| 					> | 					> | ||||||
| 						<div class=" space-y-3"> | 						<div class=" space-y-3"> | ||||||
| 							<div> | 							<div> | ||||||
| 								<div class=" py-1 flex w-full justify-between"> | 								<div class=" mb-1 text-sm font-medium">WebUI Add-ons</div> | ||||||
| 									<div class=" self-center text-sm font-medium">Title Auto Generation</div> |  | ||||||
| 
 | 
 | ||||||
| 									<button | 								<div> | ||||||
| 										class="p-1 px-3 text-xs flex rounded transition" | 									<div class=" py-0.5 flex w-full justify-between"> | ||||||
| 										on:click={() => { | 										<div class=" self-center text-xs font-medium">Title Auto Generation</div> | ||||||
| 											toggleTitleAutoGenerate(); | 
 | ||||||
| 										}} | 										<button | ||||||
| 										type="button" | 											class="p-1 px-3 text-xs flex rounded transition" | ||||||
| 									> | 											on:click={() => { | ||||||
| 										{#if titleAutoGenerate === true} | 												toggleTitleAutoGenerate(); | ||||||
| 											<span class="ml-2 self-center">On</span> | 											}} | ||||||
| 										{:else} | 											type="button" | ||||||
| 											<span class="ml-2 self-center">Off</span> | 										> | ||||||
| 										{/if} | 											{#if titleAutoGenerate === true} | ||||||
| 									</button> | 												<span class="ml-2 self-center">On</span> | ||||||
|  | 											{:else} | ||||||
|  | 												<span class="ml-2 self-center">Off</span> | ||||||
|  | 											{/if} | ||||||
|  | 										</button> | ||||||
|  | 									</div> | ||||||
| 								</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> | 										<button | ||||||
| 								<div class=" py-1 flex w-full justify-between"> | 											class="p-1 px-3 text-xs flex rounded transition" | ||||||
| 									<div class=" self-center text-sm font-medium">Voice Input Auto-Send</div> | 											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 | 								<div> | ||||||
| 										class="p-1 px-3 text-xs flex rounded transition" | 									<div class=" py-0.5 flex w-full justify-between"> | ||||||
| 										on:click={() => { | 										<div class=" self-center text-xs font-medium"> | ||||||
| 											toggleSpeechAutoSend(); | 											Response AutoCopy to Clipboard | ||||||
| 										}} | 										</div> | ||||||
| 										type="button" | 
 | ||||||
| 									> | 										<button | ||||||
| 										{#if speechAutoSend === true} | 											class="p-1 px-3 text-xs flex rounded transition" | ||||||
| 											<span class="ml-2 self-center">On</span> | 											on:click={() => { | ||||||
| 										{:else} | 												toggleResponseAutoCopy(); | ||||||
| 											<span class="ml-2 self-center">Off</span> | 											}} | ||||||
| 										{/if} | 											type="button" | ||||||
| 									</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> | ||||||
| 							</div> | 							</div> | ||||||
| 
 | 
 | ||||||
|  | @ -1029,6 +1124,17 @@ | ||||||
| 
 | 
 | ||||||
| 							<hr class=" dark:border-gray-700" /> | 							<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"> | 							<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||||
| 								Created by <a | 								Created by <a | ||||||
| 									class=" text-gray-500 dark:text-gray-300 font-medium" | 									class=" text-gray-500 dark:text-gray-300 font-medium" | ||||||
|  |  | ||||||
|  | @ -2,51 +2,102 @@ | ||||||
| 	import { v4 as uuidv4 } from 'uuid'; | 	import { v4 as uuidv4 } from 'uuid'; | ||||||
| 
 | 
 | ||||||
| 	import { goto } from '$app/navigation'; | 	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 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> | </script> | ||||||
| 
 | 
 | ||||||
| <div | <nav | ||||||
| 	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" | 	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"> | 	<div class=" flex max-w-3xl w-full mx-auto px-3"> | ||||||
| 		<nav class="py-3" id="nav"> | 		<div class="flex w-full max-w-full"> | ||||||
| 			<div class=" flex max-w-3xl mx-auto px-3"> | 			<div class="pr-2 self-center"> | ||||||
| 				<div class="flex w-full max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> | 				<button | ||||||
| 					<div class="pr-2"> | 					class=" cursor-pointer p-1 flex dark:hover:bg-gray-700 rounded-lg transition" | ||||||
| 						<button | 					on:click={async () => { | ||||||
| 							class=" cursor-pointer p-1 flex dark:hover:bg-gray-700 rounded-lg transition" | 						console.log('newChat'); | ||||||
| 							on:click={async () => { | 						goto('/'); | ||||||
| 								console.log('newChat'); | 						await chatId.set(uuidv4()); | ||||||
| 								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"> | 							<path | ||||||
| 								<svg | 								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" | ||||||
| 									xmlns="http://www.w3.org/2000/svg" | 							/> | ||||||
| 									viewBox="0 0 20 20" | 							<path | ||||||
| 									fill="currentColor" | 								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" | ||||||
| 									class="w-5 h-5" | 							/> | ||||||
| 								> | 						</svg> | ||||||
| 									<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> |  | ||||||
| 					</div> | 					</div> | ||||||
| 					<div | 				</button> | ||||||
| 						class=" flex-1 self-center font-medium overflow-hidden text-ellipsis whitespace-nowrap w-[80vw] pr-4" |  | ||||||
| 					> |  | ||||||
| 						{title != '' ? title : 'Ollama Web UI'} |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 			</div> | 			</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> | ||||||
| </div> | </nav> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { writable } from 'svelte/store'; | import { writable } from 'svelte/store'; | ||||||
| 
 | 
 | ||||||
| // Backend
 | // Backend
 | ||||||
|  | export const info = writable({}); | ||||||
| export const config = writable(undefined); | export const config = writable(undefined); | ||||||
| export const user = writable(undefined); | export const user = writable(undefined); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -65,3 +65,38 @@ export const getGravatarURL = (email) => { | ||||||
| 	// Grab the actual image URL
 | 	// Grab the actual image URL
 | ||||||
| 	return `https://www.gravatar.com/avatar/${hash}`; | 	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 { | 	import { | ||||||
| 		config, | 		config, | ||||||
|  | 		info, | ||||||
| 		user, | 		user, | ||||||
| 		showSettings, | 		showSettings, | ||||||
| 		settings, | 		settings, | ||||||
|  | @ -21,6 +22,7 @@ | ||||||
| 	import toast from 'svelte-french-toast'; | 	import toast from 'svelte-french-toast'; | ||||||
| 	import { OLLAMA_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants'; | 	import { OLLAMA_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants'; | ||||||
| 
 | 
 | ||||||
|  | 	let requiredOllamaVersion = '0.1.16'; | ||||||
| 	let loaded = false; | 	let loaded = false; | ||||||
| 
 | 
 | ||||||
| 	const getModels = async () => { | 	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 () => { | 	onMount(async () => { | ||||||
| 		if ($config && $config.auth && $user === undefined) { | 		if ($config && $config.auth && $user === undefined) { | ||||||
| 			await goto('/auth'); | 			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(await getModels()); | ||||||
| 		await models.set(_models); | 		await modelfiles.set(JSON.parse(localStorage.getItem('modelfiles') ?? '[]')); | ||||||
| 		let _db = await getDB(); |  | ||||||
| 		await db.set(_db); |  | ||||||
| 
 |  | ||||||
| 		await modelfiles.set( |  | ||||||
| 			JSON.parse(localStorage.getItem('modelfiles') ?? JSON.stringify($modelfiles)) |  | ||||||
| 		); |  | ||||||
| 
 | 
 | ||||||
| 		modelfiles.subscribe(async () => { | 		modelfiles.subscribe(async () => { | ||||||
| 			await models.set(await getModels()); | 			await models.set(await getModels()); | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
|  | 		let _db = await getDB(); | ||||||
|  | 		await db.set(_db); | ||||||
|  | 
 | ||||||
|  | 		await setOllamaVersion(await getOllamaVersion()); | ||||||
|  | 
 | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 		loaded = true; | 		loaded = true; | ||||||
| 	}); | 	}); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if loaded} | {#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 | 		<div | ||||||
| 			class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-800 min-h-screen overflow-auto flex flex-row" | 			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') ?? '{}'); | 		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||||
| 		console.log(_settings); | 		console.log(_settings); | ||||||
| 		settings.set({ | 		settings.set({ | ||||||
| 			...$settings, |  | ||||||
| 			..._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 | 	// Ollama functions | ||||||
| 	////////////////////////// | 	////////////////////////// | ||||||
|  | @ -213,12 +247,34 @@ | ||||||
| 								responseMessage.context = data.context ?? null; | 								responseMessage.context = data.context ?? null; | ||||||
| 								responseMessage.info = { | 								responseMessage.info = { | ||||||
| 									total_duration: data.total_duration, | 									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_count: data.prompt_eval_count, | ||||||
| 									prompt_eval_duration: data.prompt_eval_duration, | 									prompt_eval_duration: data.prompt_eval_duration, | ||||||
| 									eval_count: data.eval_count, | 									eval_count: data.eval_count, | ||||||
| 									eval_duration: data.eval_duration | 									eval_duration: data.eval_duration | ||||||
| 								}; | 								}; | ||||||
| 								messages = messages; | 								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; | 				stopResponseFlag = false; | ||||||
| 
 | 
 | ||||||
| 				await tick(); | 				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) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					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="min-h-screen w-full flex justify-center"> | ||||||
| 	<div class=" py-2.5 flex flex-col justify-between w-full"> | 	<div class=" py-2.5 flex flex-col justify-between w-full"> | ||||||
| 		<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10"> | 		<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10"> | ||||||
|  |  | ||||||
|  | @ -82,10 +82,11 @@ | ||||||
| 					: convertMessagesToHistory(chat.messages); | 					: convertMessagesToHistory(chat.messages); | ||||||
| 			title = chat.title; | 			title = chat.title; | ||||||
| 
 | 
 | ||||||
|  | 			let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||||
| 			await settings.set({ | 			await settings.set({ | ||||||
| 				...$settings, | 				..._settings, | ||||||
| 				system: chat.system ?? $settings.system, | 				system: chat.system ?? _settings.system, | ||||||
| 				options: chat.options ?? $settings.options | 				options: chat.options ?? _settings.options | ||||||
| 			}); | 			}); | ||||||
| 			autoScroll = true; | 			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 | 	// Ollama functions | ||||||
| 	////////////////////////// | 	////////////////////////// | ||||||
|  | @ -225,12 +261,34 @@ | ||||||
| 								responseMessage.context = data.context ?? null; | 								responseMessage.context = data.context ?? null; | ||||||
| 								responseMessage.info = { | 								responseMessage.info = { | ||||||
| 									total_duration: data.total_duration, | 									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_count: data.prompt_eval_count, | ||||||
| 									prompt_eval_duration: data.prompt_eval_duration, | 									prompt_eval_duration: data.prompt_eval_duration, | ||||||
| 									eval_count: data.eval_count, | 									eval_count: data.eval_count, | ||||||
| 									eval_duration: data.eval_duration | 									eval_duration: data.eval_duration | ||||||
| 								}; | 								}; | ||||||
| 								messages = messages; | 								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; | 				stopResponseFlag = false; | ||||||
| 
 | 
 | ||||||
| 				await tick(); | 				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) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 				} | 				} | ||||||
|  | @ -579,7 +649,7 @@ | ||||||
| /> | /> | ||||||
| 
 | 
 | ||||||
| {#if loaded} | {#if loaded} | ||||||
| 	<Navbar {title} /> | 	<Navbar {title} shareEnabled={messages.length > 0} /> | ||||||
| 	<div class="min-h-screen w-full flex justify-center"> | 	<div class="min-h-screen w-full flex justify-center"> | ||||||
| 		<div class=" py-2.5 flex flex-col justify-between w-full"> | 		<div class=" py-2.5 flex flex-col justify-between w-full"> | ||||||
| 			<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10"> | 			<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10"> | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
| 
 | 
 | ||||||
| 	import '../app.css'; | 	import '../app.css'; | ||||||
| 	import '../tailwind.css'; | 	import '../tailwind.css'; | ||||||
| 
 | 	import 'tippy.js/dist/tippy.css'; | ||||||
| 	let loaded = false; | 	let loaded = false; | ||||||
| 
 | 
 | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek