"vscode:/vscode.git/clone" did not exist on "ca95556c76b55eede4f3cd2591219a250fd71bd5"
Unverified Commit 1fb6b13a authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge branch 'main' into dev

parents 34bd7429 3c43737e
...@@ -23,7 +23,6 @@ ARG OLLAMA_API_BASE_URL='/ollama/api' ...@@ -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): ...@@ -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): ...@@ -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": ...@@ -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 ...@@ -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)
#################################### ####################################
......
...@@ -17,6 +17,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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,8 +873,11 @@ ...@@ -802,8 +873,11 @@
> >
<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>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Title Auto Generation</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -821,11 +895,9 @@ ...@@ -821,11 +895,9 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700" />
<div> <div>
<div class=" py-1 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-sm font-medium">Voice Input Auto-Send</div> <div class=" self-center text-xs font-medium">Voice Input Auto-Send</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -843,6 +915,29 @@ ...@@ -843,6 +915,29 @@
</div> </div>
</div> </div>
<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>
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium"> <div class=" mb-2.5 text-sm font-medium">
...@@ -1029,6 +1124,17 @@ ...@@ -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,19 +2,47 @@ ...@@ -2,19 +2,47 @@
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">
<div class="pr-2">
<button <button
class=" cursor-pointer p-1 flex dark:hover:bg-gray-700 rounded-lg transition" class=" cursor-pointer p-1 flex dark:hover:bg-gray-700 rounded-lg transition"
on:click={async () => { on:click={async () => {
...@@ -40,13 +68,36 @@ ...@@ -40,13 +68,36 @@
</div> </div>
</button> </button>
</div> </div>
<div <div class=" flex-1 self-center font-medium text-ellipsis whitespace-nowrap overflow-hidden">
class=" flex-1 self-center font-medium overflow-hidden text-ellipsis whitespace-nowrap w-[80vw] pr-4"
>
{title != '' ? title : 'Ollama Web UI'} {title != '' ? title : 'Ollama Web UI'}
</div> </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> </div>
{/if}
</div> </div>
</nav>
</div> </div>
</div> </nav>
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) => { ...@@ -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 @@ ...@@ -6,6 +6,7 @@
import { import {
config, config,
info,
user, user,
showSettings, showSettings,
settings, settings,
...@@ -21,6 +22,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 () => {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment