Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
1fb6b13a
Unverified
Commit
1fb6b13a
authored
Dec 18, 2023
by
Timothy Jaeryang Baek
Committed by
GitHub
Dec 18, 2023
Browse files
Merge branch 'main' into dev
parents
34bd7429
3c43737e
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
653 additions
and
108 deletions
+653
-108
Dockerfile
Dockerfile
+0
-1
backend/apps/ollama/main.py
backend/apps/ollama/main.py
+23
-6
backend/config.py
backend/config.py
+2
-2
package-lock.json
package-lock.json
+37
-6
package.json
package.json
+2
-1
src/lib/components/chat/MessageInput.svelte
src/lib/components/chat/MessageInput.svelte
+25
-1
src/lib/components/chat/Messages.svelte
src/lib/components/chat/Messages.svelte
+57
-0
src/lib/components/chat/SettingsModal.svelte
src/lib/components/chat/SettingsModal.svelte
+142
-36
src/lib/components/layout/Navbar.svelte
src/lib/components/layout/Navbar.svelte
+89
-38
src/lib/stores/index.ts
src/lib/stores/index.ts
+1
-0
src/lib/utils/index.ts
src/lib/utils/index.ts
+35
-0
src/routes/(app)/+layout.svelte
src/routes/(app)/+layout.svelte
+95
-10
src/routes/(app)/+page.svelte
src/routes/(app)/+page.svelte
+70
-2
src/routes/(app)/c/[id]/+page.svelte
src/routes/(app)/c/[id]/+page.svelte
+74
-4
src/routes/+layout.svelte
src/routes/+layout.svelte
+1
-1
No files found.
Dockerfile
View file @
1fb6b13a
...
...
@@ -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
...
...
backend/apps/ollama/main.py
View file @
1fb6b13a
...
...
@@ -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__"
:
...
...
backend/config.py
View file @
1fb6b13a
...
...
@@ -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)
####################################
...
...
package-lock.json
View file @
1fb6b13a
...
...
@@ -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.1
1"
,
"resolved"
:
"https://registry.npmjs.org/vite/-/vite-4.
4.1
1.tgz"
,
"integrity"
:
"sha512-
ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5
A=="
,
"version"
:
"4.
5.
1"
,
"resolved"
:
"https://registry.npmjs.org/vite/-/vite-4.
5.
1.tgz"
,
"integrity"
:
"sha512-
AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQ
A=="
,
"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.1
1"
,
"resolved"
:
"https://registry.npmjs.org/vite/-/vite-4.
4.1
1.tgz"
,
"integrity"
:
"sha512-
ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5
A=="
,
"version"
:
"4.
5.
1"
,
"resolved"
:
"https://registry.npmjs.org/vite/-/vite-4.
5.
1.tgz"
,
"integrity"
:
"sha512-
AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQ
A=="
,
"requires"
:
{
"esbuild"
:
"^0.18.10"
,
"fsevents"
:
"~2.3.2"
,
...
...
package.json
View file @
1fb6b13a
...
...
@@ -48,6 +48,7 @@
"
katex
"
:
"
^0.16.9
"
,
"
marked
"
:
"
^9.1.0
"
,
"
svelte-french-toast
"
:
"
^1.2.0
"
,
"
tippy.js
"
:
"
^6.3.7
"
,
"
uuid
"
:
"
^9.0.1
"
}
}
\ No newline at end of file
}
src/lib/components/chat/MessageInput.svelte
View file @
1fb6b13a
...
...
@@ -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">
...
...
src/lib/components/chat/Messages.svelte
View file @
1fb6b13a
...
...
@@ -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"
...
...
src/lib/components/chat/SettingsModal.svelte
View file @
1fb6b13a
...
...
@@ -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"
...
...
src/lib/components/layout/Navbar.svelte
View file @
1fb6b13a
...
...
@@ -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"
</button>
</div>
<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();
}}
>
{title != '' ? title : 'Ollama Web UI'}
</div>
<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>
</
na
v>
{/if}
</
di
v>
</div>
</
di
v>
</
na
v>
src/lib/stores/index.ts
View file @
1fb6b13a
import
{
writable
}
from
'
svelte/store
'
;
// Backend
export
const
info
=
writable
({});
export
const
config
=
writable
(
undefined
);
export
const
user
=
writable
(
undefined
);
...
...
src/lib/utils/index.ts
View file @
1fb6b13a
...
...
@@ -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
);
}
);
};
src/routes/(app)/+layout.svelte
View file @
1fb6b13a
...
...
@@ -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)));
let _models = await getModels();
await models.set(_models);
let _db = await getDB();
await db.set(_db);
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
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"
>
...
...
src/routes/(app)/+page.svelte
View file @
1fb6b13a
...
...
@@ -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">
...
...
src/routes/(app)/c/[id]/+page.svelte
View file @
1fb6b13a
...
...
@@ -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">
...
...
src/routes/+layout.svelte
View file @
1fb6b13a
...
...
@@ -7,7 +7,7 @@
import '../app.css';
import '../tailwind.css';
import 'tippy.js/dist/tippy.css';
let loaded = false;
onMount(async () => {
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment