"examples/deepseek_nsa/vscode:/vscode.git/clone" did not exist on "557589ffd7af10f2740d4bbf5f4f0ce70305ea3c"
Unverified Commit 506a0613 authored by lainedfles's avatar lainedfles Committed by GitHub
Browse files

Merge branch 'dev' into embedding-model-fix-and-manual-update

parents ec530ac9 1a2971ae
services:
ollama:
devices:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
image: ollama/ollama:${OLLAMA_DOCKER_TAG-rocm}
environment:
- 'HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION-11.0.0}'
\ No newline at end of file
...@@ -8,7 +8,7 @@ services: ...@@ -8,7 +8,7 @@ services:
pull_policy: always pull_policy: always
tty: true tty: true
restart: unless-stopped restart: unless-stopped
image: ollama/ollama:latest image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest}
open-webui: open-webui:
build: build:
...@@ -16,7 +16,7 @@ services: ...@@ -16,7 +16,7 @@ services:
args: args:
OLLAMA_BASE_URL: '/ollama' OLLAMA_BASE_URL: '/ollama'
dockerfile: Dockerfile dockerfile: Dockerfile
image: ghcr.io/open-webui/open-webui:main image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
container_name: open-webui container_name: open-webui
volumes: volumes:
- open-webui:/app/backend/data - open-webui:/app/backend/data
......
...@@ -7,7 +7,7 @@ ollama ...@@ -7,7 +7,7 @@ ollama
{{- end -}} {{- end -}}
{{- define "ollama.url" -}} {{- define "ollama.url" -}}
{{- printf "http://%s.%s.svc.cluster.local:%d/api" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }} {{- printf "http://%s.%s.svc.cluster.local:%d/" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
{{- end }} {{- end }}
{{- define "chart.name" -}} {{- define "chart.name" -}}
......
...@@ -58,7 +58,12 @@ export const userSignIn = async (email: string, password: string) => { ...@@ -58,7 +58,12 @@ export const userSignIn = async (email: string, password: string) => {
return res; return res;
}; };
export const userSignUp = async (name: string, email: string, password: string) => { export const userSignUp = async (
name: string,
email: string,
password: string,
profile_image_url: string
) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, { const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
...@@ -69,7 +74,8 @@ export const userSignUp = async (name: string, email: string, password: string) ...@@ -69,7 +74,8 @@ export const userSignUp = async (name: string, email: string, password: string)
body: JSON.stringify({ body: JSON.stringify({
name: name, name: name,
email: email, email: email,
password: password password: password,
profile_image_url: profile_image_url
}) })
}) })
.then(async (res) => { .then(async (res) => {
......
...@@ -22,6 +22,32 @@ export const getGravatarUrl = async (email: string) => { ...@@ -22,6 +22,32 @@ export const getGravatarUrl = async (email: string) => {
return res; return res;
}; };
export const downloadChatAsPDF = async (chat: object) => {
let error = null;
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: chat.title,
messages: chat.messages
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.blob();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
return blob;
};
export const getHTMLFromMarkdown = async (md: string) => { export const getHTMLFromMarkdown = async (md: string) => {
let error = null; let error = null;
......
...@@ -295,6 +295,13 @@ ...@@ -295,6 +295,13 @@
const dropZone = document.querySelector('body'); const dropZone = document.querySelector('body');
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
console.log('Escape');
dragged = false;
}
};
const onDragOver = (e) => { const onDragOver = (e) => {
e.preventDefault(); e.preventDefault();
dragged = true; dragged = true;
...@@ -350,11 +357,15 @@ ...@@ -350,11 +357,15 @@
dragged = false; dragged = false;
}; };
window.addEventListener('keydown', handleKeyDown);
dropZone?.addEventListener('dragover', onDragOver); dropZone?.addEventListener('dragover', onDragOver);
dropZone?.addEventListener('drop', onDrop); dropZone?.addEventListener('drop', onDrop);
dropZone?.addEventListener('dragleave', onDragLeave); dropZone?.addEventListener('dragleave', onDragLeave);
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown);
dropZone?.removeEventListener('dragover', onDragOver); dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop); dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave); dropZone?.removeEventListener('dragleave', onDragLeave);
......
...@@ -107,12 +107,8 @@ ...@@ -107,12 +107,8 @@
await sendPrompt(userPrompt, userMessageId, chatId); await sendPrompt(userPrompt, userMessageId, chatId);
}; };
const confirmEditResponseMessage = async (messageId, content) => { const updateChatMessages = async () => {
history.messages[messageId].originalContent = history.messages[messageId].content;
history.messages[messageId].content = content;
await tick(); await tick();
await updateChatById(localStorage.token, chatId, { await updateChatById(localStorage.token, chatId, {
messages: messages, messages: messages,
history: history history: history
...@@ -121,15 +117,20 @@ ...@@ -121,15 +117,20 @@
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
}; };
const confirmEditResponseMessage = async (messageId, content) => {
history.messages[messageId].originalContent = history.messages[messageId].content;
history.messages[messageId].content = content;
await updateChatMessages();
};
const rateMessage = async (messageId, rating) => { const rateMessage = async (messageId, rating) => {
history.messages[messageId].rating = rating; history.messages[messageId].annotation = {
await tick(); ...history.messages[messageId].annotation,
await updateChatById(localStorage.token, chatId, { rating: rating
messages: messages, };
history: history
});
await chats.set(await getChatList(localStorage.token)); await updateChatMessages();
}; };
const showPreviousMessage = async (message) => { const showPreviousMessage = async (message) => {
...@@ -338,6 +339,7 @@ ...@@ -338,6 +339,7 @@
siblings={history.messages[message.parentId]?.childrenIds ?? []} siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length} isLastMessage={messageIdx + 1 === messages.length}
{readOnly} {readOnly}
{updateChatMessages}
{confirmEditResponseMessage} {confirmEditResponseMessage}
{showPreviousMessage} {showPreviousMessage}
{showNextMessage} {showNextMessage}
......
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher();
export let show = false;
export let message;
const LIKE_REASONS = [
`Accurate information`,
`Followed instructions perfectly`,
`Showcased creativity`,
`Positive attitude`,
`Attention to detail`,
`Thorough explanation`,
`Other`
];
const DISLIKE_REASONS = [
`Don't like the style`,
`Not factually correct`,
`Didn't fully follow instructions`,
`Refused when it shouldn't have`,
`Being Lazy`,
`Other`
];
let reasons = [];
let selectedReason = null;
let comment = '';
$: if (message.annotation.rating === 1) {
reasons = LIKE_REASONS;
} else if (message.annotation.rating === -1) {
reasons = DISLIKE_REASONS;
}
onMount(() => {
selectedReason = message.annotation.reason;
comment = message.annotation.comment;
});
const submitHandler = () => {
console.log('submitHandler');
message.annotation.reason = selectedReason;
message.annotation.comment = comment;
dispatch('submit');
toast.success('Thanks for your feedback!');
show = false;
};
</script>
<div class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850">
<div class="flex justify-between items-center">
<div class=" text-sm">Tell us more:</div>
<button
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
{#if reasons.length > 0}
<div class="flex flex-wrap gap-2 text-sm mt-2.5">
{#each reasons as reason}
<button
class="px-3.5 py-1 border dark:border-gray-850 dark:hover:bg-gray-850 {selectedReason ===
reason
? 'dark:bg-gray-800'
: ''} transition rounded-lg"
on:click={() => {
selectedReason = reason;
}}
>
{reason}
</button>
{/each}
</div>
{/if}
<div class="mt-2">
<textarea
bind:value={comment}
class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl"
placeholder="Feel free to add specific details"
rows="2"
/>
</div>
<div class="mt-2 flex justify-end">
<button
class=" bg-emerald-700 text-white text-sm font-medium rounded-lg px-3.5 py-1.5"
on:click={() => {
submitHandler();
}}
>
Submit
</button>
</div>
</div>
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
import Image from '$lib/components/common/Image.svelte'; import Image from '$lib/components/common/Image.svelte';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import RateComment from './RateComment.svelte';
export let modelfiles = []; export let modelfiles = [];
export let message; export let message;
...@@ -39,6 +40,7 @@ ...@@ -39,6 +40,7 @@
export let readOnly = false; export let readOnly = false;
export let updateChatMessages: Function;
export let confirmEditResponseMessage: Function; export let confirmEditResponseMessage: Function;
export let showPreviousMessage: Function; export let showPreviousMessage: Function;
export let showNextMessage: Function; export let showNextMessage: Function;
...@@ -60,6 +62,8 @@ ...@@ -60,6 +62,8 @@
let loadingSpeech = false; let loadingSpeech = false;
let generatingImage = false; let generatingImage = false;
let showRateComment = false;
$: tokens = marked.lexer(sanitizeResponseContent(message.content)); $: tokens = marked.lexer(sanitizeResponseContent(message.content));
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
...@@ -536,11 +540,13 @@ ...@@ -536,11 +540,13 @@
<button <button
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1 : 'invisible group-hover:visible'} p-1 rounded {message?.annotation
?.rating === 1
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition" : ''} dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
rateMessage(message.id, 1); rateMessage(message.id, 1);
showRateComment = true;
}} }}
> >
<svg <svg
...@@ -563,11 +569,13 @@ ...@@ -563,11 +569,13 @@
<button <button
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1 : 'invisible group-hover:visible'} p-1 rounded {message?.annotation
?.rating === -1
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition" : ''} dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
rateMessage(message.id, -1); rateMessage(message.id, -1);
showRateComment = true;
}} }}
> >
<svg <svg
...@@ -824,6 +832,16 @@ ...@@ -824,6 +832,16 @@
{/if} {/if}
</div> </div>
{/if} {/if}
{#if showRateComment}
<RateComment
bind:show={showRateComment}
bind:message
on:submit={() => {
updateChatMessages();
}}
/>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
import UpdatePassword from './Account/UpdatePassword.svelte'; import UpdatePassword from './Account/UpdatePassword.svelte';
import { getGravatarUrl } from '$lib/apis/utils'; import { getGravatarUrl } from '$lib/apis/utils';
import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import Plus from '$lib/components/icons/Plus.svelte'; import Plus from '$lib/components/icons/Plus.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
...@@ -18,6 +19,8 @@ ...@@ -18,6 +19,8 @@
let profileImageUrl = ''; let profileImageUrl = '';
let name = ''; let name = '';
let showAPIKeys = false;
let showJWTToken = false; let showJWTToken = false;
let JWTTokenCopied = false; let JWTTokenCopied = false;
...@@ -28,6 +31,12 @@ ...@@ -28,6 +31,12 @@
let profileImageInputElement: HTMLInputElement; let profileImageInputElement: HTMLInputElement;
const submitHandler = async () => { const submitHandler = async () => {
if (name !== $user.name) {
if (profileImageUrl === generateInitialsImage($user.name) || profileImageUrl === '') {
profileImageUrl = generateInitialsImage(name);
}
}
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch( const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
(error) => { (error) => {
toast.error(error); toast.error(error);
...@@ -125,59 +134,93 @@ ...@@ -125,59 +134,93 @@
}} }}
/> />
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div> <div class="space-y-1">
<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> -->
<div class="flex space-x-5">
<div class="flex flex-col">
<div class="self-center">
<button
class="relative rounded-full dark:bg-gray-700"
type="button"
on:click={() => {
profileImageInputElement.click();
}}
>
<img
src={profileImageUrl !== '' ? profileImageUrl : '/user.png'}
alt="profile"
class=" rounded-full w-16 h-16 object-cover"
/>
<div <div class="flex space-x-5">
class="absolute flex justify-center rounded-full bottom-0 left-0 right-0 top-0 h-full w-full overflow-hidden bg-gray-700 bg-fixed opacity-0 transition duration-300 ease-in-out hover:opacity-50" <div class="flex flex-col">
<div class="self-center mt-2">
<button
class="relative rounded-full dark:bg-gray-700"
type="button"
on:click={() => {
profileImageInputElement.click();
}}
> >
<div class="my-auto text-gray-100"> <img
<svg src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)}
xmlns="http://www.w3.org/2000/svg" alt="profile"
viewBox="0 0 20 20" class=" rounded-full size-16 object-cover"
fill="currentColor" />
class="w-5 h-5"
> <div
<path class="absolute flex justify-center rounded-full bottom-0 left-0 right-0 top-0 h-full w-full overflow-hidden bg-gray-700 bg-fixed opacity-0 transition duration-300 ease-in-out hover:opacity-50"
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" >
/> <div class="my-auto text-gray-100">
</svg> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
/>
</svg>
</div>
</div> </div>
</div> </button>
</button> </div>
</div>
<div class="flex-1 flex flex-col self-center gap-0.5">
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('Profile Image')}</div>
<div>
<button
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-full px-4 py-0.5 bg-gray-100 dark:bg-gray-850"
on:click={async () => {
if (canvasPixelTest()) {
profileImageUrl = generateInitialsImage(name);
} else {
toast.info(
$i18n.t(
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
),
{
duration: 1000 * 10
}
);
}
}}>{$i18n.t('Use Initials')}</button
>
<button
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-full px-4 py-0.5 bg-gray-100 dark:bg-gray-850"
on:click={async () => {
const url = await getGravatarUrl($user.email);
profileImageUrl = url;
}}>{$i18n.t('Use Gravatar')}</button
>
<button
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg px-2 py-1"
on:click={async () => {
profileImageUrl = '/user.png';
}}>{$i18n.t('Remove')}</button
>
</div>
</div> </div>
<button
class=" text-xs text-gray-600"
on:click={async () => {
const url = await getGravatarUrl($user.email);
profileImageUrl = url;
}}>{$i18n.t('Use Gravatar')}</button
>
</div> </div>
<div class="flex-1"> <div class="pt-0.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div> <div class=" mb-1 text-xs font-medium">{$i18n.t('Name')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text" type="text"
bind:value={name} bind:value={name}
required required
...@@ -187,133 +230,46 @@ ...@@ -187,133 +230,46 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-700 my-4" /> <div class="py-0.5">
<UpdatePassword /> <UpdatePassword />
</div>
<hr class=" dark:border-gray-700 my-4" /> <hr class=" dark:border-gray-700 my-4" />
<div class="flex flex-col gap-4"> <div class="flex justify-between items-center text-sm">
<div class="justify-between w-full"> <div class=" font-medium">{$i18n.t('API keys')}</div>
<div class="flex justify-between w-full"> <button
<div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div> class=" text-xs font-medium text-gray-500"
</div> type="button"
on:click={() => {
<div class="flex mt-2"> showAPIKeys = !showAPIKeys;
<div class="flex w-full"> }}>{showAPIKeys ? $i18n.t('Hide') : $i18n.t('Show')}</button
<input >
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none" </div>
type={showJWTToken ? 'text' : 'password'}
value={localStorage.token}
disabled
/>
<button {#if showAPIKeys}
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800" <div class="flex flex-col gap-4">
on:click={() => { <div class="justify-between w-full">
showJWTToken = !showJWTToken; <div class="flex justify-between w-full">
}} <div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
>
{#if showJWTToken}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
clip-rule="evenodd"
/>
<path
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<path
fill-rule="evenodd"
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div> </div>
<button <div class="flex mt-2">
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg"
on:click={() => {
copyToClipboard(localStorage.token);
JWTTokenCopied = true;
setTimeout(() => {
JWTTokenCopied = false;
}, 2000);
}}
>
{#if JWTTokenCopied}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
</div>
<div class="justify-between w-full">
<div class="flex justify-between w-full">
<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
</div>
<div class="flex mt-2">
{#if APIKey}
<div class="flex w-full"> <div class="flex w-full">
<input <input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
type={showAPIKey ? 'text' : 'password'} type={showJWTToken ? 'text' : 'password'}
value={APIKey} value={localStorage.token}
disabled disabled
/> />
<button <button
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800" class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850"
on:click={() => { on:click={() => {
showAPIKey = !showAPIKey; showJWTToken = !showJWTToken;
}} }}
> >
{#if showAPIKey} {#if showJWTToken}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
...@@ -348,16 +304,16 @@ ...@@ -348,16 +304,16 @@
</div> </div>
<button <button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
on:click={() => { on:click={() => {
copyToClipboard(APIKey); copyToClipboard(localStorage.token);
APIKeyCopied = true; JWTTokenCopied = true;
setTimeout(() => { setTimeout(() => {
APIKeyCopied = false; JWTTokenCopied = false;
}, 2000); }, 2000);
}} }}
> >
{#if APIKeyCopied} {#if JWTTokenCopied}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
...@@ -390,45 +346,146 @@ ...@@ -390,45 +346,146 @@
</svg> </svg>
{/if} {/if}
</button> </button>
</div>
</div>
<div class="justify-between w-full">
<div class="flex justify-between w-full">
<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
</div>
<div class="flex mt-2">
{#if APIKey}
<div class="flex w-full">
<input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
type={showAPIKey ? 'text' : 'password'}
value={APIKey}
disabled
/>
<button
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850"
on:click={() => {
showAPIKey = !showAPIKey;
}}
>
{#if showAPIKey}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
clip-rule="evenodd"
/>
<path
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<path
fill-rule="evenodd"
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
<Tooltip content="Create new key">
<button <button
class=" px-1.5 py-1 dark:hover:bg-gray-800transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
on:click={() => { on:click={() => {
createAPIKeyHandler(); copyToClipboard(APIKey);
APIKeyCopied = true;
setTimeout(() => {
APIKeyCopied = false;
}, 2000);
}} }}
> >
<svg {#if APIKeyCopied}
xmlns="http://www.w3.org/2000/svg" <svg
fill="none" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 20 20"
stroke-width="2" fill="currentColor"
stroke="currentColor" class="w-4 h-4"
class="size-4" >
> <path
<path fill-rule="evenodd"
stroke-linecap="round" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
stroke-linejoin="round" clip-rule="evenodd"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
/> </svg>
</svg> {:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button> </button>
</Tooltip>
{:else}
<button
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition"
on:click={() => {
createAPIKeyHandler();
}}
>
<Plus strokeWidth="2" className=" size-3.5" />
Create new secret key</button <Tooltip content="Create new key">
> <button
{/if} class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
on:click={() => {
createAPIKeyHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
</Tooltip>
{:else}
<button
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
on:click={() => {
createAPIKeyHandler();
}}
>
<Plus strokeWidth="2" className=" size-3.5" />
Create new secret key</button
>
{/if}
</div>
</div> </div>
</div> </div>
</div> {/if}
</div> </div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
......
...@@ -185,7 +185,7 @@ ...@@ -185,7 +185,7 @@
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Desktop Notifications')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Notifications')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
export let show = true; export let show = true;
export let size = 'md'; export let size = 'md';
let modalElement = null;
let mounted = false; let mounted = false;
const sizeToWidth = (size) => { const sizeToWidth = (size) => {
...@@ -19,14 +20,23 @@ ...@@ -19,14 +20,23 @@
} }
}; };
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
console.log('Escape');
show = false;
}
};
onMount(() => { onMount(() => {
mounted = true; mounted = true;
}); });
$: if (mounted) { $: if (mounted) {
if (show) { if (show) {
window.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} else { } else {
window.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'unset'; document.body.style.overflow = 'unset';
} }
} }
...@@ -36,6 +46,7 @@ ...@@ -36,6 +46,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
bind:this={modalElement}
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain" class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
in:fade={{ duration: 10 }} in:fade={{ duration: 10 }}
on:click={() => { on:click={() => {
......
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
import Dropdown from '$lib/components/common/Dropdown.svelte'; import Dropdown from '$lib/components/common/Dropdown.svelte';
import Tags from '$lib/components/common/Tags.svelte'; import Tags from '$lib/components/common/Tags.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import { downloadChatAsPDF } from '$lib/apis/utils';
export let shareEnabled: boolean = false; export let shareEnabled: boolean = false;
export let shareHandler: Function; export let shareHandler: Function;
...@@ -25,7 +27,7 @@ ...@@ -25,7 +27,7 @@
export let onClose: Function = () => {}; export let onClose: Function = () => {};
const downloadChatAsTxt = async () => { const downloadTxt = async () => {
const _chat = chat.chat; const _chat = chat.chat;
console.log('download', chat); console.log('download', chat);
...@@ -40,54 +42,29 @@ ...@@ -40,54 +42,29 @@
saveAs(blob, `chat-${_chat.title}.txt`); saveAs(blob, `chat-${_chat.title}.txt`);
}; };
const downloadChatAsPdf = async () => { const downloadPdf = async () => {
const _chat = chat.chat; const _chat = chat.chat;
console.log('download', chat); console.log('download', chat);
const doc = new jsPDF(); const blob = await downloadChatAsPDF(_chat);
// Initialize y-coordinate for text placement // Create a URL for the blob
let yPos = 10; const url = window.URL.createObjectURL(blob);
const pageHeight = doc.internal.pageSize.height;
// Function to check if new text exceeds the current page height // Create a link element to trigger the download
function checkAndAddNewPage() { const a = document.createElement('a');
if (yPos > pageHeight - 10) { a.href = url;
doc.addPage(); a.download = `chat-${_chat.title}.pdf`;
yPos = 10; // Reset yPos for the new page
}
}
// Function to add text with specific style
function addStyledText(text, isTitle = false) {
// Set font style and size based on the parameters
doc.setFont('helvetica', isTitle ? 'bold' : 'normal');
doc.setFontSize(isTitle ? 12 : 10);
const textMargin = 7;
// Split text into lines to ensure it fits within the page width
const lines = doc.splitTextToSize(text, 180); // Adjust the width as needed
lines.forEach((line) => { // Append the link to the body and click it programmatically
checkAndAddNewPage(); // Check if we need a new page before adding more text document.body.appendChild(a);
doc.text(line, 10, yPos); a.click();
yPos += textMargin; // Increment yPos for the next line
});
// Add extra space after a block of text // Remove the link from the body
yPos += 2; document.body.removeChild(a);
}
_chat.messages.forEach((message, i) => {
// Add user text in bold
doc.setFont('helvetica', 'normal', 'bold');
addStyledText(message.role.toUpperCase(), { isTitle: true });
addStyledText(message.content);
});
doc.save(`chat-${_chat.title}.pdf`); // Revoke the URL to release memory
window.URL.revokeObjectURL(url);
}; };
</script> </script>
...@@ -193,7 +170,7 @@ ...@@ -193,7 +170,7 @@
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => { on:click={() => {
downloadChatAsTxt(); downloadTxt();
}} }}
> >
<div class="flex items-center line-clamp-1">Plain text (.txt)</div> <div class="flex items-center line-clamp-1">Plain text (.txt)</div>
...@@ -202,7 +179,7 @@ ...@@ -202,7 +179,7 @@
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => { on:click={() => {
downloadChatAsPdf(); downloadPdf();
}} }}
> >
<div class="flex items-center line-clamp-1">PDF document (.pdf)</div> <div class="flex items-center line-clamp-1">PDF document (.pdf)</div>
......
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}",
"Deleted {tagName}": "Изтрито {tagName}", "Deleted {tagName}": "Изтрито {tagName}",
"Description": "Описание", "Description": "Описание",
"Desktop Notifications": "Десктоп Известия", "Notifications": "Десктоп Известия",
"Disabled": "Деактивиран", "Disabled": "Деактивиран",
"Discover a modelfile": "Откриване на модфайл", "Discover a modelfile": "Откриване на модфайл",
"Discover a prompt": "Откриване на промпт", "Discover a prompt": "Откриване на промпт",
......
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}", "Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}",
"Deleted {tagName}": "Esborrat {tagName}", "Deleted {tagName}": "Esborrat {tagName}",
"Description": "Descripció", "Description": "Descripció",
"Desktop Notifications": "Notificacions d'Escriptori", "Notifications": "Notificacions d'Escriptori",
"Disabled": "Desactivat", "Disabled": "Desactivat",
"Discover a modelfile": "Descobreix un fitxer de model", "Discover a modelfile": "Descobreix un fitxer de model",
"Discover a prompt": "Descobreix un prompt", "Discover a prompt": "Descobreix un prompt",
......
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht",
"Deleted {tagName}": "{tagName} gelöscht", "Deleted {tagName}": "{tagName} gelöscht",
"Description": "Beschreibung", "Description": "Beschreibung",
"Desktop Notifications": "Desktop-Benachrichtigungen", "Notifications": "Desktop-Benachrichtigungen",
"Disabled": "Deaktiviert", "Disabled": "Deaktiviert",
"Discover a modelfile": "Eine Modelfiles entdecken", "Discover a modelfile": "Eine Modelfiles entdecken",
"Discover a prompt": "Einen Prompt entdecken", "Discover a prompt": "Einen Prompt entdecken",
......
{
"analyze": "analyse",
"analyzed": "analysed",
"analyzes": "analyses",
"apologize": "apologise",
"apologized": "apologised",
"apologizes": "apologises",
"apologizing": "apologising",
"canceled": "cancelled",
"canceling": "cancelling",
"capitalize": "capitalise",
"capitalized": "capitalised",
"capitalizes": "capitalises",
"center": "centre",
"centered": "centred",
"color": "colour",
"colorize": "colourise",
"customize": "customise",
"customizes": "customises",
"defense": "defence",
"dialog": "dialogue",
"emphasize": "emphasise",
"emphasized": "emphasised",
"emphasizes": "emphasises",
"favor": "favour",
"favorable": "favourable",
"favorite": "favourite",
"favoritism": "favouritism",
"labor": "labour",
"labored": "laboured",
"laboring": "labouring",
"maximize": "maximise",
"maximizes": "maximises",
"minimize": "minimise",
"minimizes": "minimises",
"neighbor": "neighbour",
"neighborhood": "neighbourhood",
"offense": "offence",
"organize": "organise",
"organizes": "organises",
"personalize": "personalise",
"personalizes": "personalises",
"program": "programme",
"programmed": "programmed",
"programs": "programmes",
"quantization": "quantisation",
"quantize": "quantise",
"randomize": "randomise",
"randomizes": "randomises",
"realize": "realise",
"realizes": "realises",
"recognize": "recognise",
"recognizes": "recognises",
"summarize": "summarise",
"summarizes": "summarises",
"theater": "theatre",
"theaters": "theatres",
"toward": "towards",
"traveled": "travelled",
"traveler": "traveller",
"traveling": "travelling",
"utilize": "utilise",
"utilizes": "utilises"
}
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
"Deleted {{deleteModelTag}}": "", "Deleted {{deleteModelTag}}": "",
"Deleted {tagName}": "", "Deleted {tagName}": "",
"Description": "", "Description": "",
"Desktop Notifications": "", "Notifications": "",
"Disabled": "", "Disabled": "",
"Discover a modelfile": "", "Discover a modelfile": "",
"Discover a prompt": "", "Discover a prompt": "",
...@@ -151,6 +151,7 @@ ...@@ -151,6 +151,7 @@
"Failed to read clipboard contents": "", "Failed to read clipboard contents": "",
"File Mode": "", "File Mode": "",
"File not found.": "", "File not found.": "",
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
"Focus chat input": "", "Focus chat input": "",
"Format your variables using square brackets like this:": "", "Format your variables using square brackets like this:": "",
"From (Base Model)": "", "From (Base Model)": "",
...@@ -347,6 +348,7 @@ ...@@ -347,6 +348,7 @@
"URL Mode": "", "URL Mode": "",
"Use '#' in the prompt input to load and select your documents.": "", "Use '#' in the prompt input to load and select your documents.": "",
"Use Gravatar": "", "Use Gravatar": "",
"Use Initials": "",
"user": "", "user": "",
"User Permissions": "", "User Permissions": "",
"Users": "", "Users": "",
......
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