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

Merge branch 'main' into dev

parents 34bd7429 3c43737e
......@@ -23,7 +23,6 @@ ARG OLLAMA_API_BASE_URL='/ollama/api'
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)
####################################
......
......@@ -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",
......
......@@ -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,8 +873,11 @@
>
<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>
<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"
......@@ -821,11 +895,9 @@
</div>
</div>
<hr class=" dark:border-gray-700" />
<div>
<div class=" py-1 flex w-full justify-between">
<div class=" self-center text-sm font-medium">Voice Input Auto-Send</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">Voice Input Auto-Send</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
......@@ -843,6 +915,29 @@
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
Response AutoCopy to Clipboard
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleResponseAutoCopy();
}}
type="button"
>
{#if responseAutoCopy === true}
<span class="ml-2 self-center">On</span>
{:else}
<span class="ml-2 self-center">Off</span>
{/if}
</button>
</div>
</div>
</div>
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">
......@@ -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,19 +2,47 @@
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">
<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 () => {
......@@ -40,13 +68,36 @@
</div>
</button>
</div>
<div
class=" flex-1 self-center font-medium overflow-hidden text-ellipsis whitespace-nowrap w-[80vw] pr-4"
>
<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>
</nav>
</div>
</div>
</nav>
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 () => {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment