diff --git a/Dockerfile b/Dockerfile index 1e8361fd..9521c600 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index 98048ca5..a2a09fc7 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -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__": diff --git a/backend/config.py b/backend/config.py index 14ad30e4..1dabe48a 100644 --- a/backend/config.py +++ b/backend/config.py @@ -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) #################################### diff --git a/package-lock.json b/package-lock.json index 7003749c..bcfc7253 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5b1a89ec..f90b99ad 100644 --- a/package.json +++ b/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index bc3a6a9d..172485ca 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -161,7 +161,7 @@
{#each files as file, fileIdx}
- input + input
+ {#if message.info} + + {/if} + {#if messageIdx + 1 === messages.length}
+ +
+
+
Notification
+ + +
+

@@ -802,44 +873,68 @@ >
-
-
Title Auto Generation
+
WebUI Add-ons
- +
+
+
Title Auto Generation
+ + +
-
-
+
+
+
Voice Input Auto-Send
-
-
-
Voice Input Auto-Send
+ +
+
- +
+
+
+ Response AutoCopy to Clipboard +
+ + +
@@ -1029,6 +1124,17 @@
+
+
Ollama Version
+
+
+ {$info?.ollama?.version ?? 'N/A'} +
+
+
+ +
+ + diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 342e6494..9962b4ac 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -1,6 +1,7 @@ import { writable } from 'svelte/store'; // Backend +export const info = writable({}); export const config = writable(undefined); export const user = writable(undefined); diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 22096994..2d9f1b31 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -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); + } + ); +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index e837741e..4f477423 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -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; }); {#if loaded} -
+
+ {#if ($info?.ollama?.version ?? '0').localeCompare( requiredOllamaVersion, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' } ) < 0} +
+
+
+
+ Ollama Update Required +
+ +
+ Oops! It seems like your Ollama needs a little attention. + We encountered a connection issue or noticed that you're running an outdated version. Please + update to + {requiredOllamaVersion} or above. +
+ +
+ + + +
+
+
+
+ {/if} +
diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 7c0c87b4..d0b83b80 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -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 @@ }} /> - + 0} />
diff --git a/src/routes/(app)/c/[id]/+page.svelte b/src/routes/(app)/c/[id]/+page.svelte index f108ba22..bf7207fb 100644 --- a/src/routes/(app)/c/[id]/+page.svelte +++ b/src/routes/(app)/c/[id]/+page.svelte @@ -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} - + 0} />
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 19926553..7479f559 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,7 +7,7 @@ import '../app.css'; import '../tailwind.css'; - + import 'tippy.js/dist/tippy.css'; let loaded = false; onMount(async () => {