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