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 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…
Reference in a new issue