Merge branch 'dev' into amberyufangchiu-dev
This commit is contained in:
+143
-3
@@ -65,19 +65,23 @@ textarea::placeholder {
|
||||
}
|
||||
|
||||
.input-prose {
|
||||
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-0.5 whitespace-pre-line;
|
||||
}
|
||||
|
||||
.input-prose-sm {
|
||||
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line text-sm;
|
||||
@apply prose dark:prose-invert prose-headings:font-medium prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-1 whitespace-pre-line text-sm;
|
||||
}
|
||||
|
||||
.markdown-prose {
|
||||
@apply prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||
}
|
||||
|
||||
.markdown-prose-sm {
|
||||
@apply text-sm prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-2 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||
}
|
||||
|
||||
.markdown-prose-xs {
|
||||
@apply text-xs prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||
@apply text-xs prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0.5 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
@@ -326,6 +330,138 @@ input[type='number'] {
|
||||
@apply line-clamp-1 absolute;
|
||||
}
|
||||
|
||||
.tiptap ul[data-type='taskList'] {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
align-items: start;
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
margin-top: 0.2rem;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* checked data-checked="true" */
|
||||
|
||||
li[data-checked='true'] {
|
||||
> div {
|
||||
opacity: 0.5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Reset nested regular ul elements to default styling */
|
||||
ul:not([data-type='taskList']) {
|
||||
list-style: disc;
|
||||
padding-left: 1rem;
|
||||
|
||||
li {
|
||||
align-items: initial;
|
||||
display: list-item;
|
||||
|
||||
label {
|
||||
flex: initial;
|
||||
margin-right: initial;
|
||||
margin-top: initial;
|
||||
user-select: initial;
|
||||
display: initial;
|
||||
}
|
||||
|
||||
div {
|
||||
flex: initial;
|
||||
align-items: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-prose .tiptap ul[data-type='taskList'] {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
align-items: start;
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
margin-top: 0.4rem;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* checked data-checked="true" */
|
||||
|
||||
li[data-checked='true'] {
|
||||
> div {
|
||||
opacity: 0.5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Reset nested regular ul elements to default styling */
|
||||
ul:not([data-type='taskList']) {
|
||||
list-style: disc;
|
||||
padding-left: 1rem;
|
||||
|
||||
li {
|
||||
align-items: initial;
|
||||
display: list-item;
|
||||
|
||||
label {
|
||||
flex: initial;
|
||||
margin-right: initial;
|
||||
margin-top: initial;
|
||||
user-select: initial;
|
||||
display: initial;
|
||||
}
|
||||
|
||||
div {
|
||||
flex: initial;
|
||||
align-items: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
color: #757575;
|
||||
@@ -442,3 +578,7 @@ input[type='number'] {
|
||||
.tiptap tr {
|
||||
@apply bg-white dark:bg-gray-900 dark:border-gray-850 text-xs;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~='transparent'] {
|
||||
@apply bg-transparent p-0 m-0;
|
||||
}
|
||||
|
||||
+10
-33
@@ -77,28 +77,18 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
|
||||
function setSplashImage() {
|
||||
const logo = document.getElementById('logo');
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
const logo = document.createElement('img');
|
||||
logo.id = 'logo';
|
||||
logo.style =
|
||||
'position: absolute; width: auto; height: 6rem; top: 44%; left: 50%; transform: translateX(-50%); display:block;';
|
||||
logo.src = isDarkMode ? '/static/splash-dark.png' : '/static/splash.png';
|
||||
|
||||
if (isDarkMode) {
|
||||
const darkImage = new Image();
|
||||
darkImage.src = '/static/splash-dark.png';
|
||||
|
||||
darkImage.onload = () => {
|
||||
logo.src = '/static/splash-dark.png';
|
||||
logo.style.filter = ''; // Ensure no inversion is applied if splash-dark.png exists
|
||||
};
|
||||
|
||||
darkImage.onerror = () => {
|
||||
logo.style.filter = 'invert(1)'; // Invert image if splash-dark.png is missing
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Runs after classes are assigned
|
||||
window.onload = setSplashImage;
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const splash = document.getElementById('splash-screen');
|
||||
if (splash) splash.prepend(logo);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -120,19 +110,6 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<img
|
||||
id="logo"
|
||||
style="
|
||||
position: absolute;
|
||||
width: auto;
|
||||
height: 6rem;
|
||||
top: 44%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
"
|
||||
src="/static/splash.png"
|
||||
/>
|
||||
|
||||
<div
|
||||
style="
|
||||
position: absolute;
|
||||
|
||||
@@ -347,6 +347,8 @@ export const userSignOut = async () => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
sessionStorage.clear();
|
||||
return res;
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ export const importChat = async (
|
||||
chat: object,
|
||||
meta: object | null,
|
||||
pinned?: boolean,
|
||||
folderId?: string | null
|
||||
folderId?: string | null,
|
||||
createdAt: number | null = null,
|
||||
updatedAt: number | null = null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
@@ -52,7 +54,9 @@ export const importChat = async (
|
||||
chat: chat,
|
||||
meta: meta ?? {},
|
||||
pinned: pinned,
|
||||
folder_id: folderId
|
||||
folder_id: folderId,
|
||||
created_at: createdAt ?? null,
|
||||
updated_at: updatedAt ?? null
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
||||
@@ -58,10 +58,10 @@ export const exportConfig = async (token: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getDirectConnectionsConfig = async (token: string) => {
|
||||
export const getConnectionsConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -85,10 +85,10 @@ export const getDirectConnectionsConfig = async (token: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const setDirectConnectionsConfig = async (token: string, config: object) => {
|
||||
export const setConnectionsConfig = async (token: string, config: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
+18
-8
@@ -8,17 +8,26 @@ import { toast } from 'svelte-sonner';
|
||||
export const getModels = async (
|
||||
token: string = '',
|
||||
connections: object | null = null,
|
||||
base: boolean = false
|
||||
base: boolean = false,
|
||||
refresh: boolean = false
|
||||
) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (refresh) {
|
||||
searchParams.append('refresh', 'true');
|
||||
}
|
||||
|
||||
let error = null;
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
const res = await fetch(
|
||||
`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}?${searchParams.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
@@ -1587,6 +1596,7 @@ export interface ModelConfig {
|
||||
}
|
||||
|
||||
export interface ModelMeta {
|
||||
toolIds: never[];
|
||||
description?: string;
|
||||
capabilities?: object;
|
||||
profile_image_url?: string;
|
||||
|
||||
@@ -39,7 +39,7 @@ export const createNewNote = async (token: string, note: NoteItem) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getNotes = async (token: string = '') => {
|
||||
export const getNotes = async (token: string = '', raw: boolean = false) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/`, {
|
||||
@@ -67,6 +67,10 @@ export const getNotes = async (token: string = '') => {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
return res; // Return raw response if requested
|
||||
}
|
||||
|
||||
if (!Array.isArray(res)) {
|
||||
return {}; // or throw new Error("Notes response is not an array")
|
||||
}
|
||||
@@ -87,6 +91,37 @@ export const getNotes = async (token: string = '') => {
|
||||
return grouped;
|
||||
};
|
||||
|
||||
export const getNoteList = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getNoteById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -366,7 +366,7 @@ export const unloadModel = async (token: string, tagName: string) => {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName
|
||||
model: tagName
|
||||
})
|
||||
}).catch((err) => {
|
||||
error = err;
|
||||
@@ -419,7 +419,7 @@ export const deleteModel = async (token: string, tagName: string, urlIdx: string
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName
|
||||
model: tagName
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -403,6 +403,7 @@ export const deleteUserById = async (token: string, userId: string) => {
|
||||
};
|
||||
|
||||
type UserUpdateForm = {
|
||||
role: string;
|
||||
profile_image_url: string;
|
||||
email: string;
|
||||
name: string;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tags from './common/Tags.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onDelete: Function = () => {};
|
||||
@@ -208,17 +210,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -524,29 +516,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Minus from '$lib/components/icons/Minus.svelte';
|
||||
@@ -14,6 +15,8 @@
|
||||
import { getToolServerData } from '$lib/apis';
|
||||
import { verifyToolServerConnection } from '$lib/apis/configs';
|
||||
import AccessControl from './workspace/common/AccessControl.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onDelete: Function = () => {};
|
||||
@@ -153,29 +156,21 @@
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center font-primary">
|
||||
<h1 class=" text-lg font-medium self-center font-primary">
|
||||
{#if edit}
|
||||
{$i18n.t('Edit Connection')}
|
||||
{:else}
|
||||
{$i18n.t('Add Connection')}
|
||||
{/if}
|
||||
</div>
|
||||
</h1>
|
||||
<button
|
||||
class="self-center"
|
||||
aria-label={$i18n.t('Close Configure Connection Modal')}
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -192,12 +187,17 @@
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div class=" text-xs text-gray-500">{$i18n.t('URL')}</div>
|
||||
<label
|
||||
for="api-base-url"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>{$i18n.t('URL')}</label
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
class="w-full flex-1 text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
id="api-base-url"
|
||||
class={`w-full flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
@@ -214,6 +214,7 @@
|
||||
on:click={() => {
|
||||
verifyHandler();
|
||||
}}
|
||||
aria-label={$i18n.t('Verify Connection')}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -221,6 +222,7 @@
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
@@ -237,9 +239,13 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex items-center">
|
||||
<label for="url-or-path" class="sr-only"
|
||||
>{$i18n.t('openapi.json URL or Path')}</label
|
||||
>
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
type="text"
|
||||
id="url-or-path"
|
||||
bind:value={path}
|
||||
placeholder={$i18n.t('openapi.json URL or Path')}
|
||||
autocomplete="off"
|
||||
@@ -249,7 +255,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
<div
|
||||
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
{$i18n.t(`WebUI will make requests to "{{url}}"`, {
|
||||
url: path.includes('://') ? path : `${url}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
})}
|
||||
@@ -257,12 +265,17 @@
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" text-xs text-gray-500">{$i18n.t('Auth')}</div>
|
||||
<label
|
||||
for="select-bearer-or-session"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>{$i18n.t('Auth')}</label
|
||||
>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-shrink-0 self-start">
|
||||
<select
|
||||
class="w-full text-sm bg-transparent dark:bg-gray-900 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden pr-5"
|
||||
id="select-bearer-or-session"
|
||||
class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
bind:value={auth_type}
|
||||
>
|
||||
<option value="bearer">Bearer</option>
|
||||
@@ -273,13 +286,14 @@
|
||||
<div class="flex flex-1 items-center">
|
||||
{#if auth_type === 'bearer'}
|
||||
<SensitiveInput
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
bind:value={key}
|
||||
placeholder={$i18n.t('API Key')}
|
||||
required={false}
|
||||
/>
|
||||
{:else if auth_type === 'session'}
|
||||
<div class="text-xs text-gray-500 self-center translate-y-[1px]">
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
{$i18n.t('Forwards system user session credentials to authenticate')}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -293,11 +307,16 @@
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||
<label
|
||||
for="enter-name"
|
||||
class={`mb-0.5 text-xs" ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>{$i18n.t('Name')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
id="enter-name"
|
||||
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$i18n.t('Enter name')}
|
||||
@@ -309,11 +328,16 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Description')}</div>
|
||||
<label
|
||||
for="description"
|
||||
class={`mb-1 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100 placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-gray-500'}`}
|
||||
>{$i18n.t('Description')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
id="description"
|
||||
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
type="text"
|
||||
bind:value={description}
|
||||
placeholder={$i18n.t('Enter description')}
|
||||
@@ -357,29 +381,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import Modal from './common/Modal.svelte';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -36,18 +37,11 @@
|
||||
localStorage.version = $config.version;
|
||||
show = false;
|
||||
}}
|
||||
aria-label={$i18n.t('Close')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<XMark className={'size-5'}>
|
||||
<p class="sr-only">{$i18n.t('Close')}</p>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</XMark>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center mt-1">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { extractFrontmatter } from '$lib/utils';
|
||||
|
||||
export let show = false;
|
||||
@@ -69,16 +71,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -120,29 +113,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { settings, playingNotificationSound, isLastActiveTab } from '$lib/stores';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
@@ -38,7 +39,7 @@
|
||||
}}
|
||||
>
|
||||
<div class="shrink-0 self-top -translate-y-0.5">
|
||||
<img src={'/static/favicon.png'} alt="favicon" class="size-7 rounded-full" />
|
||||
<img src="{WEBUI_BASE_URL}/static/favicon.png" alt="favicon" class="size-7 rounded-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
|
||||
if (isDarkMode) {
|
||||
const darkImage = new Image();
|
||||
darkImage.src = '/static/favicon-dark.png';
|
||||
darkImage.src = `${WEBUI_BASE_URL}/static/favicon-dark.png`;
|
||||
|
||||
darkImage.onload = () => {
|
||||
logo.src = '/static/favicon-dark.png';
|
||||
logo.src = `${WEBUI_BASE_URL}/static/favicon-dark.png`;
|
||||
logo.style.filter = ''; // Ensure no inversion is applied if splash-dark.png exists
|
||||
};
|
||||
|
||||
|
||||
@@ -2,16 +2,41 @@
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { getFeedbackById } from '$lib/apis/evaluations';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let selectedFeedback = null;
|
||||
|
||||
export let onClose: () => void = () => {};
|
||||
|
||||
let loaded = false;
|
||||
|
||||
let feedbackData = null;
|
||||
|
||||
const close = () => {
|
||||
show = false;
|
||||
onClose();
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
loaded = false;
|
||||
feedbackData = null;
|
||||
if (selectedFeedback) {
|
||||
feedbackData = await getFeedbackById(localStorage.token, selectedFeedback.id).catch((err) => {
|
||||
return null;
|
||||
});
|
||||
|
||||
console.log('Feedback Data:', selectedFeedback, feedbackData);
|
||||
}
|
||||
loaded = true;
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
@@ -22,58 +47,89 @@
|
||||
{$i18n.t('Feedback Details')}
|
||||
</div>
|
||||
<button class="self-center" on:click={close} aria-label="Close">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Rating')}</div>
|
||||
{#if loaded}
|
||||
<div class="flex flex-col w-full">
|
||||
{#if feedbackData}
|
||||
{@const messageId = feedbackData?.meta?.message_id}
|
||||
{@const messages = feedbackData?.snapshot?.chat?.chat?.history.messages}
|
||||
|
||||
<div class="flex-1">
|
||||
<span>{selectedFeedback?.data?.details?.rating ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Reason')}</div>
|
||||
{#if messages[messages[messageId]?.parentId]}
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class="mb-1 text-xs text-gray-500">{$i18n.t('Prompt')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<span>{selectedFeedback?.data?.reason || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 text-xs whitespace-pre-line break-words">
|
||||
<span>{messages[messages[messageId]?.parentId]?.content || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-2">
|
||||
{#if selectedFeedback?.data?.tags && selectedFeedback?.data?.tags.length}
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
{#each selectedFeedback?.data?.tags as tag}
|
||||
<span class="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-xs">{tag}</span
|
||||
{#if messages[messageId]}
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class="mb-1 text-xs text-gray-500">{$i18n.t('Response')}</div>
|
||||
<div
|
||||
class="flex-1 text-xs whitespace-pre-line break-words max-h-32 overflow-y-auto"
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<span>-</span>
|
||||
<span>{messages[messageId]?.content || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Rating')}</div>
|
||||
|
||||
<div class="flex-1 text-xs">
|
||||
<span>{selectedFeedback?.data?.details?.rating ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Reason')}</div>
|
||||
|
||||
<div class="flex-1 text-xs">
|
||||
<span>{selectedFeedback?.data?.reason || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Comment')}</div>
|
||||
|
||||
<div class="flex-1 text-xs">
|
||||
<span>{selectedFeedback?.data?.comment || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedFeedback?.data?.tags && selectedFeedback?.data?.tags.length}
|
||||
<div class="mb-2 -mx-1">
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
{#each selectedFeedback?.data?.tags as tag}
|
||||
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-[9px]"
|
||||
>{tag}</span
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="button"
|
||||
on:click={close}
|
||||
>
|
||||
{$i18n.t('Close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="button"
|
||||
on:click={close}
|
||||
>
|
||||
{$i18n.t('Close')}
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center w-full h-32">
|
||||
<Spinner className={'size-5'} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let feedbacks = [];
|
||||
|
||||
@@ -305,7 +306,7 @@
|
||||
<tbody class="">
|
||||
{#each paginatedFeedbacks as feedback (feedback.id)}
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
on:click={() => openFeedbackModal(feedback)}
|
||||
>
|
||||
<td class=" py-0.5 text-right font-semibold">
|
||||
@@ -313,7 +314,7 @@
|
||||
<Tooltip content={feedback?.user?.name}>
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
src={feedback?.user?.profile_image_url ?? '/user.png'}
|
||||
src={feedback?.user?.profile_image_url ?? `${WEBUI_BASE_URL}/user.png`}
|
||||
alt={feedback?.user?.name}
|
||||
class="size-5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
@@ -369,7 +370,7 @@
|
||||
{dayjs(feedback.updated_at * 1000).fromNow()}
|
||||
</td>
|
||||
|
||||
<td class=" px-3 py-1 text-right font-semibold">
|
||||
<td class=" px-3 py-1 text-right font-semibold" on:click={(e) => e.stopPropagation()}>
|
||||
<FeedbackMenu
|
||||
on:delete={(e) => {
|
||||
deleteFeedbackHandler(feedback.id);
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -77,7 +78,7 @@
|
||||
let showLeaderboardModal = false;
|
||||
let selectedModel = null;
|
||||
|
||||
const openFeedbackModal = (model) => {
|
||||
const openLeaderboardModelModal = (model) => {
|
||||
showLeaderboardModal = true;
|
||||
selectedModel = model;
|
||||
};
|
||||
@@ -350,7 +351,7 @@
|
||||
<Tooltip content={$i18n.t('Re-rank models by topic similarity')}>
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<MagnifyingGlass className="size-3" />
|
||||
<Search className="size-3" />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
@@ -371,7 +372,7 @@
|
||||
{#if loadingLeaderboard}
|
||||
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
||||
<div class="m-auto">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -504,8 +505,8 @@
|
||||
<tbody class="">
|
||||
{#each sortedModels as model, modelIdx (model.id)}
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => openFeedbackModal(model)}
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
on:click={() => openLeaderboardModelModal(model)}
|
||||
>
|
||||
<td class="px-3 py-1.5 text-left font-medium text-gray-900 dark:text-white w-fit">
|
||||
<div class=" line-clamp-1">
|
||||
@@ -516,7 +517,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
|
||||
src={model?.info?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/favicon.png`}
|
||||
alt={model.name}
|
||||
class="size-5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let feedbacks = [];
|
||||
export let onClose: () => void = () => {};
|
||||
const i18n = getContext('i18n');
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const close = () => {
|
||||
show = false;
|
||||
@@ -37,25 +38,16 @@
|
||||
{model.name}
|
||||
</div>
|
||||
<button class="self-center" on:click={close} aria-label="Close">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-5 pb-4 dark:text-gray-200">
|
||||
<div class="mb-2">
|
||||
{#if topTags.length}
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
<div class="flex flex-wrap gap-1 mt-1 -mx-1">
|
||||
{#each topTags as tagInfo}
|
||||
<span class="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-xs">
|
||||
{tagInfo.tag} <span class="text-gray-500">({tagInfo.count})</span>
|
||||
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-xs">
|
||||
{tagInfo.tag} <span class="text-gray-500 font-medium">{tagInfo.count}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -63,7 +55,7 @@
|
||||
<span>-</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end pt-3">
|
||||
<div class="flex justify-end pt-2">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="button"
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
let showConfirm = false;
|
||||
let query = '';
|
||||
|
||||
let selectedType = 'all';
|
||||
|
||||
let showManifestModal = false;
|
||||
let showValvesModal = false;
|
||||
let selectedFunction = null;
|
||||
@@ -59,9 +61,10 @@
|
||||
$: filteredItems = $functions
|
||||
.filter(
|
||||
(f) =>
|
||||
query === '' ||
|
||||
f.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
f.id.toLowerCase().includes(query.toLowerCase())
|
||||
(selectedType !== 'all' ? f.type === selectedType : true) &&
|
||||
(query === '' ||
|
||||
f.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
f.id.toLowerCase().includes(query.toLowerCase()))
|
||||
)
|
||||
.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
|
||||
|
||||
@@ -135,7 +138,9 @@
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -161,7 +166,9 @@
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -215,8 +222,8 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex flex-col mt-1.5 mb-0.5">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<div class="flex md:self-center text-xl items-center font-medium px-0.5">
|
||||
{$i18n.t('Functions')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||
@@ -266,12 +273,54 @@
|
||||
</AddFunctionMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full">
|
||||
<div
|
||||
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent"
|
||||
>
|
||||
<button
|
||||
class="min-w-fit p-1.5 {selectedType === 'all'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
on:click={() => {
|
||||
selectedType = 'all';
|
||||
}}>{$i18n.t('All')}</button
|
||||
>
|
||||
|
||||
<button
|
||||
class="min-w-fit p-1.5 {selectedType === 'pipe'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
on:click={() => {
|
||||
selectedType = 'pipe';
|
||||
}}>{$i18n.t('Pipe')}</button
|
||||
>
|
||||
|
||||
<button
|
||||
class="min-w-fit p-1.5 {selectedType === 'filter'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
on:click={() => {
|
||||
selectedType = 'filter';
|
||||
}}>{$i18n.t('Filter')}</button
|
||||
>
|
||||
|
||||
<button
|
||||
class="min-w-fit p-1.5 {selectedType === 'action'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
on:click={() => {
|
||||
selectedType = 'action';
|
||||
}}>{$i18n.t('Action')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
{#each filteredItems as func (func.id)}
|
||||
<div
|
||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||
class=" flex space-x-4 cursor-pointer w-full px-2 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||
>
|
||||
<a
|
||||
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
||||
@@ -413,7 +462,9 @@
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections &&
|
||||
($settings?.directConnections ?? null)
|
||||
($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}}
|
||||
@@ -559,7 +610,9 @@
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}}
|
||||
@@ -585,7 +638,9 @@
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
} from '$lib/apis/audio';
|
||||
import { config, settings } from '$lib/stores';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
|
||||
import { TTS_RESPONSE_SPLIT } from '$lib/types';
|
||||
@@ -373,33 +374,7 @@
|
||||
>
|
||||
{#if STT_WHISPER_MODEL_LOADING}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/>
|
||||
</svg>
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
|
||||
import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
|
||||
import { getModels as _getModels } from '$lib/apis';
|
||||
import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
|
||||
import { getConnectionsConfig, setConnectionsConfig } from '$lib/apis/configs';
|
||||
|
||||
import { config, models, settings, user } from '$lib/stores';
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
const getModels = async () => {
|
||||
const models = await _getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
);
|
||||
return models;
|
||||
};
|
||||
@@ -41,7 +43,7 @@
|
||||
let ENABLE_OPENAI_API: null | boolean = null;
|
||||
let ENABLE_OLLAMA_API: null | boolean = null;
|
||||
|
||||
let directConnectionsConfig = null;
|
||||
let connectionsConfig = null;
|
||||
|
||||
let pipelineUrls = {};
|
||||
let showAddOpenAIConnectionModal = false;
|
||||
@@ -104,15 +106,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
const updateDirectConnectionsHandler = async () => {
|
||||
const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
}
|
||||
);
|
||||
const updateConnectionsHandler = async () => {
|
||||
const res = await setConnectionsConfig(localStorage.token, connectionsConfig).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Direct Connections settings updated'));
|
||||
toast.success($i18n.t('Connections settings updated'));
|
||||
await models.set(await getModels());
|
||||
}
|
||||
};
|
||||
@@ -148,7 +148,7 @@
|
||||
openaiConfig = await getOpenAIConfig(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
|
||||
connectionsConfig = await getConnectionsConfig(localStorage.token);
|
||||
})()
|
||||
]);
|
||||
|
||||
@@ -215,36 +215,103 @@
|
||||
|
||||
<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
|
||||
<div class=" overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
|
||||
<div class="my-2">
|
||||
<div class="mt-2 space-y-2 pr-1.5">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && connectionsConfig !== null}
|
||||
<div class="mb-3.5">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={ENABLE_OPENAI_API}
|
||||
on:change={async () => {
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
/>
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="my-2">
|
||||
<div class="mt-2 space-y-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={ENABLE_OPENAI_API}
|
||||
on:change={async () => {
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OPENAI_API}
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-medium text-xs">{$i18n.t('Manage OpenAI API Connections')}</div>
|
||||
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
showAddOpenAIConnectionModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 mt-1.5">
|
||||
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||
<OpenAIConnection
|
||||
pipeline={pipelineUrls[url] ? true : false}
|
||||
bind:url
|
||||
bind:key={OPENAI_API_KEYS[idx]}
|
||||
bind:config={OPENAI_API_CONFIGS[idx]}
|
||||
onSubmit={() => {
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||
|
||||
let newConfig = {};
|
||||
OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] =
|
||||
OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OPENAI_API_CONFIGS = newConfig;
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" my-2">
|
||||
<div class="flex justify-between items-center text-sm mb-2">
|
||||
<div class=" font-medium">{$i18n.t('Ollama API')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch
|
||||
bind:state={ENABLE_OLLAMA_API}
|
||||
on:change={async () => {
|
||||
updateOllamaHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OPENAI_API}
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
{#if ENABLE_OLLAMA_API}
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-medium">{$i18n.t('Manage OpenAI API Connections')}</div>
|
||||
<div class="font-medium text-xs">{$i18n.t('Manage Ollama API Connections')}</div>
|
||||
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
showAddOpenAIConnectionModal = true;
|
||||
showAddOllamaConnectionModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
@@ -253,133 +320,89 @@
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 mt-1.5">
|
||||
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||
<OpenAIConnection
|
||||
pipeline={pipelineUrls[url] ? true : false}
|
||||
bind:url
|
||||
bind:key={OPENAI_API_KEYS[idx]}
|
||||
bind:config={OPENAI_API_CONFIGS[idx]}
|
||||
onSubmit={() => {
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||
<div class="flex w-full gap-1.5">
|
||||
<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
|
||||
{#each OLLAMA_BASE_URLS as url, idx}
|
||||
<OllamaConnection
|
||||
bind:url
|
||||
bind:config={OLLAMA_API_CONFIGS[idx]}
|
||||
{idx}
|
||||
onSubmit={() => {
|
||||
updateOllamaHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
||||
|
||||
let newConfig = {};
|
||||
OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OPENAI_API_CONFIGS = newConfig;
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
let newConfig = {};
|
||||
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] =
|
||||
OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OLLAMA_API_CONFIGS = newConfig;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Trouble accessing Ollama?')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://github.com/open-webui/open-webui#troubleshooting"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('Click here for help.')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
<div class="my-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('Direct Connections')}</div>
|
||||
|
||||
<div class="pr-1.5 my-2">
|
||||
<div class="flex justify-between items-center text-sm mb-2">
|
||||
<div class=" font-medium">{$i18n.t('Ollama API')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch
|
||||
bind:state={ENABLE_OLLAMA_API}
|
||||
on:change={async () => {
|
||||
updateOllamaHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OLLAMA_API}
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-medium">{$i18n.t('Manage Ollama API Connections')}</div>
|
||||
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
showAddOllamaConnectionModal = true;
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={connectionsConfig.ENABLE_DIRECT_CONNECTIONS}
|
||||
on:change={async () => {
|
||||
updateConnectionsHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full gap-1.5">
|
||||
<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
|
||||
{#each OLLAMA_BASE_URLS as url, idx}
|
||||
<OllamaConnection
|
||||
bind:url
|
||||
bind:config={OLLAMA_API_CONFIGS[idx]}
|
||||
{idx}
|
||||
onSubmit={() => {
|
||||
updateOllamaHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
||||
|
||||
let newConfig = {};
|
||||
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OLLAMA_API_CONFIGS = newConfig;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Trouble accessing Ollama?')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://github.com/open-webui/open-webui#troubleshooting"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('Click here for help.')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class="pr-1.5 my-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('Direct Connections')}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
|
||||
on:change={async () => {
|
||||
updateDirectConnectionsHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(
|
||||
'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1.5">
|
||||
<div class="text-xs text-gray-500">
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="my-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" text-xs font-medium">{$i18n.t('Cache Base Model List')}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={connectionsConfig.ENABLE_BASE_MODELS_CACHE}
|
||||
on:change={async () => {
|
||||
updateConnectionsHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(
|
||||
'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
|
||||
'Base Model List Cache speeds up access by fetching base models only at startup or on settings save—faster, but may not show recent base model changes.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import ManageOllama from '../Models/Manage/ManageOllama.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let urlIdx: number | null = null;
|
||||
@@ -26,16 +27,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -90,10 +90,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (embeddingEngine === 'openai' && (OpenAIKey === '' || OpenAIUrl === '')) {
|
||||
toast.error($i18n.t('OpenAI URL/Key required.'));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
embeddingEngine === 'azure_openai' &&
|
||||
(AzureOpenAIKey === '' || AzureOpenAIUrl === '' || AzureOpenAIVersion === '')
|
||||
@@ -643,6 +639,7 @@
|
||||
>
|
||||
<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
|
||||
<option value="token">{$i18n.t('Token')} ({$i18n.t('Tiktoken')})</option>
|
||||
<option value="markdown_header">{$i18n.t('Markdown (Header)')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -731,7 +728,11 @@
|
||||
required
|
||||
/>
|
||||
|
||||
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} />
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={OpenAIKey}
|
||||
required={false}
|
||||
/>
|
||||
</div>
|
||||
{:else if embeddingEngine === 'ollama'}
|
||||
<div class="my-0.5 flex gap-2 pr-2">
|
||||
@@ -808,33 +809,7 @@
|
||||
>
|
||||
{#if updateEmbeddingModelLoading}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/>
|
||||
</svg>
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
@@ -1272,7 +1247,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { models } from '$lib/stores';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
@@ -11,6 +12,8 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
export let edit = false;
|
||||
@@ -34,7 +37,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
let profileImageUrl = '/favicon.png';
|
||||
let profileImageUrl = `${WEBUI_BASE_URL}/favicon.png`;
|
||||
let description = '';
|
||||
|
||||
let selectedModelId = '';
|
||||
@@ -90,7 +93,7 @@
|
||||
|
||||
name = '';
|
||||
id = '';
|
||||
profileImageUrl = '/favicon.png';
|
||||
profileImageUrl = `${WEBUI_BASE_URL}/favicon.png`;
|
||||
description = '';
|
||||
modelIds = [];
|
||||
selectedModelId = '';
|
||||
@@ -141,16 +144,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -406,29 +400,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -90,7 +90,9 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
checkForVersionUpdates();
|
||||
if ($config?.features?.enable_version_update_check) {
|
||||
checkForVersionUpdates();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
@@ -137,16 +139,18 @@
|
||||
v{WEBUI_VERSION}
|
||||
</Tooltip>
|
||||
|
||||
<a
|
||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||
target="_blank"
|
||||
>
|
||||
{updateAvailable === null
|
||||
? $i18n.t('Checking for updates...')
|
||||
: updateAvailable
|
||||
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||
: $i18n.t('(latest)')}
|
||||
</a>
|
||||
{#if $config?.features?.enable_version_update_check}
|
||||
<a
|
||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||
target="_blank"
|
||||
>
|
||||
{updateAvailable === null
|
||||
? $i18n.t('Checking for updates...')
|
||||
: updateAvailable
|
||||
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||
: $i18n.t('(latest)')}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -160,15 +164,17 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
{#if $config?.features?.enable_version_update_check}
|
||||
<button
|
||||
class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
updateConfig,
|
||||
verifyConfigUrl
|
||||
} from '$lib/apis/images';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -504,7 +506,7 @@
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
|
||||
|
||||
{#if config.comfyui.COMFYUI_WORKFLOW}
|
||||
<textarea
|
||||
<Textarea
|
||||
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
|
||||
rows="10"
|
||||
bind:value={config.comfyui.COMFYUI_WORKFLOW}
|
||||
@@ -533,7 +535,7 @@
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-50 border border-dashed border-gray-50 dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('upload-comfyui-workflow-input')?.click();
|
||||
@@ -555,10 +557,10 @@
|
||||
|
||||
<div class="text-xs flex flex-col gap-1.5">
|
||||
{#each requiredWorkflowNodes as node}
|
||||
<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
|
||||
<div class="flex w-full items-center">
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
|
||||
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center bg-green-500/10 text-green-700 dark:text-green-200"
|
||||
>
|
||||
{node.type}{node.type === 'prompt' ? '*' : ''}
|
||||
</div>
|
||||
@@ -566,7 +568,7 @@
|
||||
<div class="">
|
||||
<Tooltip content="Input Key (e.g. text, unet_name, steps)">
|
||||
<input
|
||||
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r dark:border-gray-850"
|
||||
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r border-gray-50 dark:border-gray-850"
|
||||
placeholder="Key"
|
||||
bind:value={node.key}
|
||||
required
|
||||
@@ -580,7 +582,7 @@
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-hidden"
|
||||
class="w-full py-1 px-4 text-xs bg-transparent outline-hidden"
|
||||
placeholder="Node Ids"
|
||||
bind:value={node.node_ids}
|
||||
/>
|
||||
@@ -711,29 +713,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
|
||||
<div class="mb-2.5">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-sm">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Banners')}
|
||||
</div>
|
||||
|
||||
@@ -432,7 +432,7 @@
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class=" space-y-3">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Default Prompt Suggestions')}
|
||||
</div>
|
||||
|
||||
@@ -636,6 +636,6 @@
|
||||
</form>
|
||||
{:else}
|
||||
<div class=" h-full w-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Sortable from 'sortablejs';
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
@@ -23,6 +25,13 @@
|
||||
});
|
||||
};
|
||||
|
||||
const classNames: Record<string, string> = {
|
||||
info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ',
|
||||
success: 'bg-green-500/20 text-green-700 dark:text-green-200',
|
||||
warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200',
|
||||
error: 'bg-red-500/20 text-red-700 dark:text-red-200'
|
||||
};
|
||||
|
||||
$: if (banners) {
|
||||
init();
|
||||
}
|
||||
@@ -44,14 +53,14 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class=" flex flex-col space-y-0.5" bind:this={bannerListElement}>
|
||||
<div class=" flex flex-col gap-3 {banners?.length > 0 ? 'mt-2' : ''}" bind:this={bannerListElement}>
|
||||
{#each banners as banner, bannerIdx (banner.id)}
|
||||
<div class=" flex justify-between items-center -ml-1" id="banner-item-{banner.id}">
|
||||
<div class=" flex justify-between items-start -ml-1" id="banner-item-{banner.id}">
|
||||
<EllipsisVertical className="size-4 cursor-move item-handle" />
|
||||
|
||||
<div class="flex flex-row flex-1 gap-2 items-center">
|
||||
<div class="flex flex-row flex-1 gap-2 items-start">
|
||||
<select
|
||||
class="w-fit capitalize rounded-xl text-xs bg-transparent outline-hidden text-left pl-1 pr-2"
|
||||
class="w-fit capitalize rounded-xl text-xs bg-transparent outline-hidden pl-1 pr-5"
|
||||
bind:value={banner.type}
|
||||
required
|
||||
>
|
||||
@@ -64,14 +73,15 @@
|
||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-hidden"
|
||||
<Textarea
|
||||
className="mr-2 text-xs w-full bg-transparent outline-hidden resize-none"
|
||||
placeholder={$i18n.t('Content')}
|
||||
bind:value={banner.content}
|
||||
maxSize={100}
|
||||
/>
|
||||
|
||||
<div class="relative -left-2">
|
||||
<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
|
||||
<Tooltip content={$i18n.t('Remember Dismissal')} className="flex h-fit items-center">
|
||||
<Switch bind:state={banner.dismissible} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -85,16 +95,7 @@
|
||||
banners = banners;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
toggleModelById,
|
||||
updateModelById
|
||||
} from '$lib/apis/models';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { getModels } from '$lib/apis';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
@@ -34,7 +36,7 @@
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
|
||||
import Eye from '$lib/components/icons/Eye.svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
let shiftKey = false;
|
||||
|
||||
@@ -205,6 +207,11 @@
|
||||
|
||||
onMount(async () => {
|
||||
await init();
|
||||
const id = $page.url.searchParams.get('id');
|
||||
|
||||
if (id) {
|
||||
selectedModelId = id;
|
||||
}
|
||||
|
||||
const onKeyDown = (event) => {
|
||||
if (event.key === 'Shift') {
|
||||
@@ -326,7 +333,7 @@
|
||||
: 'opacity-50 dark:opacity-50'} "
|
||||
>
|
||||
<img
|
||||
src={model?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
src={model?.meta?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
alt="modelfile profile"
|
||||
class=" rounded-full w-full h-auto object-cover"
|
||||
/>
|
||||
@@ -563,6 +570,6 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<div class=" h-full w-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let initHandler = () => {};
|
||||
@@ -129,16 +130,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -278,29 +270,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -308,7 +278,7 @@
|
||||
</form>
|
||||
{:else}
|
||||
<div>
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1057,6 +1057,6 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center w-full h-full py-3">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import ManageOllama from './Manage/ManageOllama.svelte';
|
||||
import { getOllamaConfig } from '$lib/apis/ollama';
|
||||
@@ -48,16 +49,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +67,7 @@
|
||||
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent dark:text-gray-200"
|
||||
>
|
||||
<button
|
||||
class="min-w-fit rounded-full p-1.5 {selected === 'ollama'
|
||||
class="min-w-fit p-1.5 {selected === 'ollama'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
on:click={() => {
|
||||
@@ -84,7 +76,7 @@
|
||||
>
|
||||
|
||||
<!-- <button
|
||||
class="min-w-fit rounded-full p-1.5 {selected === 'llamacpp'
|
||||
class="min-w-fit p-1.5 {selected === 'llamacpp'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
on:click={() => {
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
placeholder={$i18n.t('Enter Searxng Query URL')}
|
||||
bind:value={webConfig.SEARXNG_QUERY_URL}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,7 +249,6 @@
|
||||
bind:value={webConfig.KAGI_SEARCH_API_KEY}
|
||||
/>
|
||||
</div>
|
||||
.
|
||||
</div>
|
||||
{:else if webConfig.WEB_SEARCH_ENGINE === 'mojeek'}
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
|
||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import User from '$lib/components/icons/User.svelte';
|
||||
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||
import GroupModal from './Groups/EditGroupModal.svelte';
|
||||
@@ -159,18 +160,7 @@
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<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="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Search />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
export let onSubmit: Function = () => {};
|
||||
export let show = false;
|
||||
|
||||
@@ -45,16 +47,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -111,29 +104,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Display from './Display.svelte';
|
||||
import Permissions from './Permissions.svelte';
|
||||
@@ -10,6 +11,7 @@
|
||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onDelete: Function = () => {};
|
||||
@@ -124,16 +126,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -305,29 +298,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
|
||||
export let users = [];
|
||||
export let userIds = [];
|
||||
@@ -50,18 +51,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center mr-3">
|
||||
<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="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Search />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
|
||||
@@ -98,7 +88,7 @@
|
||||
user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
user.profile_image_url.startsWith('data:')
|
||||
? user.profile_image_url
|
||||
: `/user.png`}
|
||||
: `${WEBUI_BASE_URL}/user.png`}
|
||||
alt="user"
|
||||
/>
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
|
||||
{#if users === null || total === null}
|
||||
<div class="my-10">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||
@@ -396,7 +396,7 @@
|
||||
user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
user.profile_image_url.startsWith('data:')
|
||||
? user.profile_image_url
|
||||
: `/user.png`}
|
||||
: `${WEBUI_BASE_URL}/user.png`}
|
||||
alt="user"
|
||||
/>
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { generateInitialsImage } from '$lib/utils';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -132,16 +134,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -157,7 +150,7 @@
|
||||
class="flex -mt-2 mb-1.5 gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent dark:text-gray-200"
|
||||
>
|
||||
<button
|
||||
class="min-w-fit rounded-full p-1.5 {tab === ''
|
||||
class="min-w-fit p-1.5 {tab === ''
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
type="button"
|
||||
@@ -167,7 +160,7 @@
|
||||
>
|
||||
|
||||
<button
|
||||
class="min-w-fit rounded-full p-1.5 {tab === 'import'
|
||||
class="min-w-fit p-1.5 {tab === 'import'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
type="button"
|
||||
@@ -293,29 +286,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -54,16 +55,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
let selected = '';
|
||||
</script>
|
||||
@@ -25,7 +26,7 @@
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/static/splash.png"
|
||||
src="{WEBUI_BASE_URL}/static/splash.png"
|
||||
class="size-11 dark:invert p-0.5"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
@@ -49,7 +50,7 @@
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/static/favicon.png"
|
||||
src="{WEBUI_BASE_URL}/static/favicon.png"
|
||||
class="size-10 {selected === '' ? 'rounded-2xl' : 'rounded-full'}"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" pb-[1rem]">
|
||||
<div class=" pb-[1rem] px-2.5">
|
||||
<MessageInput
|
||||
id="root"
|
||||
{typingUsers}
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import heic2any from 'heic2any';
|
||||
|
||||
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { config, mobile, settings, socket } from '$lib/stores';
|
||||
import { blobToFile, compressImage } from '$lib/utils';
|
||||
import { config, mobile, settings, socket, user } from '$lib/stores';
|
||||
import {
|
||||
blobToFile,
|
||||
compressImage,
|
||||
extractInputVariables,
|
||||
getCurrentDateTime,
|
||||
getFormattedDate,
|
||||
getFormattedTime,
|
||||
getUserPosition,
|
||||
getUserTimezone,
|
||||
getWeekday
|
||||
} from '$lib/utils';
|
||||
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import RichTextInput from '../common/RichTextInput.svelte';
|
||||
@@ -18,6 +29,8 @@
|
||||
import FileItem from '../common/FileItem.svelte';
|
||||
import Image from '../common/Image.svelte';
|
||||
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
||||
import Commands from '../chat/MessageInput/Commands.svelte';
|
||||
import InputVariablesModal from '../chat/MessageInput/InputVariablesModal.svelte';
|
||||
|
||||
export let placeholder = $i18n.t('Send a Message');
|
||||
export let transparentBackground = false;
|
||||
@@ -30,16 +43,185 @@
|
||||
let content = '';
|
||||
let files = [];
|
||||
|
||||
export let chatInputElement;
|
||||
|
||||
let commandsElement;
|
||||
let filesInputElement;
|
||||
let inputFiles;
|
||||
|
||||
export let typingUsers = [];
|
||||
export let inputLoading = false;
|
||||
|
||||
export let onSubmit: Function = (e) => {};
|
||||
export let onChange: Function = (e) => {};
|
||||
export let onStop: Function = (e) => {};
|
||||
|
||||
export let onSubmit: Function;
|
||||
export let onChange: Function;
|
||||
export let scrollEnd = true;
|
||||
export let scrollToBottom: Function = () => {};
|
||||
|
||||
export let acceptFiles = true;
|
||||
export let showFormattingButtons = true;
|
||||
|
||||
let showInputVariablesModal = false;
|
||||
let inputVariables: Record<string, any> = {};
|
||||
let inputVariableValues = {};
|
||||
|
||||
const inputVariableHandler = async (text: string) => {
|
||||
inputVariables = extractInputVariables(text);
|
||||
if (Object.keys(inputVariables).length > 0) {
|
||||
showInputVariablesModal = true;
|
||||
}
|
||||
};
|
||||
|
||||
const textVariableHandler = async (text: string) => {
|
||||
if (text.includes('{{CLIPBOARD}}')) {
|
||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||
return '{{CLIPBOARD}}';
|
||||
});
|
||||
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
|
||||
let imageUrl = null;
|
||||
for (const item of clipboardItems) {
|
||||
// Check for known image types
|
||||
for (const type of item.types) {
|
||||
if (type.startsWith('image/')) {
|
||||
const blob = await item.getType(type);
|
||||
imageUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: 'image',
|
||||
url: imageUrl
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
}
|
||||
|
||||
if (text.includes('{{USER_LOCATION}}')) {
|
||||
let location;
|
||||
try {
|
||||
location = await getUserPosition();
|
||||
} catch (error) {
|
||||
toast.error($i18n.t('Location access not allowed'));
|
||||
location = 'LOCATION_UNKNOWN';
|
||||
}
|
||||
text = text.replaceAll('{{USER_LOCATION}}', String(location));
|
||||
}
|
||||
|
||||
if (text.includes('{{USER_NAME}}')) {
|
||||
const name = $user?.name || 'User';
|
||||
text = text.replaceAll('{{USER_NAME}}', name);
|
||||
}
|
||||
|
||||
if (text.includes('{{USER_LANGUAGE}}')) {
|
||||
const language = localStorage.getItem('locale') || 'en-US';
|
||||
text = text.replaceAll('{{USER_LANGUAGE}}', language);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_DATE}}')) {
|
||||
const date = getFormattedDate();
|
||||
text = text.replaceAll('{{CURRENT_DATE}}', date);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_TIME}}')) {
|
||||
const time = getFormattedTime();
|
||||
text = text.replaceAll('{{CURRENT_TIME}}', time);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_DATETIME}}')) {
|
||||
const dateTime = getCurrentDateTime();
|
||||
text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_TIMEZONE}}')) {
|
||||
const timezone = getUserTimezone();
|
||||
text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_WEEKDAY}}')) {
|
||||
const weekday = getWeekday();
|
||||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||
}
|
||||
|
||||
inputVariableHandler(text);
|
||||
return text;
|
||||
};
|
||||
|
||||
const replaceVariables = (variables: Record<string, any>) => {
|
||||
if (!chatInputElement) return;
|
||||
console.log('Replacing variables:', variables);
|
||||
|
||||
chatInputElement.replaceVariables(variables);
|
||||
chatInputElement.focus();
|
||||
};
|
||||
|
||||
export const setText = async (text?: string) => {
|
||||
if (!chatInputElement) return;
|
||||
|
||||
text = await textVariableHandler(text || '');
|
||||
|
||||
chatInputElement?.setText(text);
|
||||
chatInputElement?.focus();
|
||||
};
|
||||
|
||||
const getCommand = () => {
|
||||
if (!chatInputElement) return;
|
||||
|
||||
let word = '';
|
||||
word = chatInputElement?.getWordAtDocPos();
|
||||
|
||||
return word;
|
||||
};
|
||||
|
||||
const replaceCommandWithText = (text) => {
|
||||
if (!chatInputElement) return;
|
||||
|
||||
chatInputElement?.replaceCommandWithText(text);
|
||||
};
|
||||
|
||||
const insertTextAtCursor = async (text: string) => {
|
||||
text = await textVariableHandler(text);
|
||||
|
||||
if (command) {
|
||||
replaceCommandWithText(text);
|
||||
} else {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode(text));
|
||||
range.collapse(false);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
|
||||
await tick();
|
||||
const chatInputContainer = document.getElementById('chat-input-container');
|
||||
if (chatInputContainer) {
|
||||
chatInputContainer.scrollTop = chatInputContainer.scrollHeight;
|
||||
}
|
||||
|
||||
await tick();
|
||||
if (chatInputElement) {
|
||||
chatInputElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
let command = '';
|
||||
|
||||
export let showCommands = false;
|
||||
$: showCommands = ['/'].includes(command?.charAt(0));
|
||||
|
||||
const screenCaptureHandler = async () => {
|
||||
try {
|
||||
// Request screen media
|
||||
@@ -78,7 +260,7 @@
|
||||
};
|
||||
|
||||
const inputFilesHandler = async (inputFiles) => {
|
||||
inputFiles.forEach((file) => {
|
||||
inputFiles.forEach(async (file) => {
|
||||
console.info('Processing file:', {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
@@ -102,43 +284,50 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
|
||||
) {
|
||||
if (file['type'].startsWith('image/')) {
|
||||
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
|
||||
// Quick shortcut so we don’t do unnecessary work.
|
||||
const settingsCompression = settings?.imageCompression ?? false;
|
||||
const configWidth = config?.file?.image_compression?.width ?? null;
|
||||
const configHeight = config?.file?.image_compression?.height ?? null;
|
||||
|
||||
// If neither settings nor config wants compression, return original URL.
|
||||
if (!settingsCompression && !configWidth && !configHeight) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Default to null (no compression unless set)
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
// If user/settings want compression, pick their preferred size.
|
||||
if (settingsCompression) {
|
||||
width = settings?.imageCompressionSize?.width ?? null;
|
||||
height = settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
// Apply config limits as an upper bound if any
|
||||
if (configWidth && (width === null || width > configWidth)) {
|
||||
width = configWidth;
|
||||
}
|
||||
if (configHeight && (height === null || height > configHeight)) {
|
||||
height = configHeight;
|
||||
}
|
||||
|
||||
// Do the compression if required
|
||||
if (width || height) {
|
||||
return await compressImage(imageUrl, width, height);
|
||||
}
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = async (event) => {
|
||||
let imageUrl = event.target.result;
|
||||
|
||||
if (
|
||||
($settings?.imageCompression ?? false) ||
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
if ($settings?.imageCompression ?? false) {
|
||||
width = $settings?.imageCompressionSize?.width ?? null;
|
||||
height = $settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
if (width > ($config?.file?.image_compression?.width ?? null)) {
|
||||
width = $config?.file?.image_compression?.width ?? null;
|
||||
}
|
||||
if (height > ($config?.file?.image_compression?.height ?? null)) {
|
||||
height = $config?.file?.image_compression?.height ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (width || height) {
|
||||
imageUrl = await compressImage(imageUrl, width, height);
|
||||
}
|
||||
}
|
||||
// Compress the image if settings or config require it
|
||||
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
||||
|
||||
files = [
|
||||
...files,
|
||||
@@ -149,7 +338,11 @@
|
||||
];
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
reader.readAsDataURL(
|
||||
file['type'] === 'image/heic'
|
||||
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
||||
: file
|
||||
);
|
||||
} else {
|
||||
uploadFileHandler(file);
|
||||
}
|
||||
@@ -247,7 +440,7 @@
|
||||
const onDrop = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.dataTransfer?.files) {
|
||||
if (e.dataTransfer?.files && acceptFiles) {
|
||||
const inputFiles = Array.from(e.dataTransfer?.files);
|
||||
if (inputFiles && inputFiles.length > 0) {
|
||||
console.log(inputFiles);
|
||||
@@ -273,10 +466,13 @@
|
||||
content = '';
|
||||
files = [];
|
||||
|
||||
await tick();
|
||||
if (chatInputElement) {
|
||||
chatInputElement?.setText('');
|
||||
|
||||
const chatInputElement = document.getElementById(`chat-input-${id}`);
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
|
||||
chatInputElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
$: if (content) {
|
||||
@@ -285,9 +481,10 @@
|
||||
|
||||
onMount(async () => {
|
||||
window.setTimeout(() => {
|
||||
const chatInput = document.getElementById(`chat-input-${id}`);
|
||||
chatInput?.focus();
|
||||
}, 0);
|
||||
if (chatInputElement) {
|
||||
chatInputElement.focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
await tick();
|
||||
@@ -314,27 +511,39 @@
|
||||
|
||||
<FilesOverlay show={draggedOver} />
|
||||
|
||||
<input
|
||||
bind:this={filesInputElement}
|
||||
bind:files={inputFiles}
|
||||
type="file"
|
||||
hidden
|
||||
multiple
|
||||
on:change={async () => {
|
||||
if (inputFiles && inputFiles.length > 0) {
|
||||
inputFilesHandler(Array.from(inputFiles));
|
||||
} else {
|
||||
toast.error($i18n.t(`File not found.`));
|
||||
}
|
||||
{#if acceptFiles}
|
||||
<input
|
||||
bind:this={filesInputElement}
|
||||
bind:files={inputFiles}
|
||||
type="file"
|
||||
hidden
|
||||
multiple
|
||||
on:change={async () => {
|
||||
if (inputFiles && inputFiles.length > 0) {
|
||||
inputFilesHandler(Array.from(inputFiles));
|
||||
} else {
|
||||
toast.error($i18n.t(`File not found.`));
|
||||
}
|
||||
|
||||
filesInputElement.value = '';
|
||||
filesInputElement.value = '';
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<InputVariablesModal
|
||||
bind:show={showInputVariablesModal}
|
||||
variables={inputVariables}
|
||||
onSave={(variableValues) => {
|
||||
inputVariableValues = { ...inputVariableValues, ...variableValues };
|
||||
replaceVariables(inputVariableValues);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="bg-transparent">
|
||||
<div
|
||||
class="{($settings?.widescreenMode ?? null)
|
||||
? 'max-w-full'
|
||||
: 'max-w-6xl'} px-2.5 mx-auto inset-x-0 relative"
|
||||
: 'max-w-6xl'} mx-auto inset-x-0 relative"
|
||||
>
|
||||
<div class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center">
|
||||
<div class="flex flex-col px-3 w-full">
|
||||
@@ -378,6 +587,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Commands
|
||||
bind:this={commandsElement}
|
||||
show={showCommands}
|
||||
{command}
|
||||
insertTextHandler={insertTextAtCursor}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -390,7 +606,10 @@
|
||||
recording = false;
|
||||
|
||||
await tick();
|
||||
document.getElementById(`chat-input-${id}`)?.focus();
|
||||
|
||||
if (chatInputElement) {
|
||||
chatInputElement.focus();
|
||||
}
|
||||
}}
|
||||
onConfirm={async (data) => {
|
||||
const { text, filename } = data;
|
||||
@@ -398,7 +617,10 @@
|
||||
recording = false;
|
||||
|
||||
await tick();
|
||||
document.getElementById(`chat-input-${id}`)?.focus();
|
||||
|
||||
if (chatInputElement) {
|
||||
chatInputElement.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
@@ -470,40 +692,95 @@
|
||||
|
||||
<div class="px-2.5">
|
||||
<div
|
||||
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
||||
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
|
||||
>
|
||||
<RichTextInput
|
||||
bind:value={content}
|
||||
id={`chat-input-${id}`}
|
||||
bind:this={chatInputElement}
|
||||
json={true}
|
||||
messageInput={true}
|
||||
shiftEnter={!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)}
|
||||
{placeholder}
|
||||
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
||||
on:keydown={async (e) => {
|
||||
e = e.detail.event;
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||
if (
|
||||
!$mobile ||
|
||||
{showFormattingButtons}
|
||||
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||
(!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)
|
||||
) {
|
||||
// Prevent Enter key from creating a new line
|
||||
// Uses keyCode '13' for Enter key for chinese/japanese keyboards
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
))}
|
||||
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
||||
onChange={(e) => {
|
||||
const { md } = e;
|
||||
content = md;
|
||||
command = getCommand();
|
||||
}}
|
||||
on:keydown={async (e) => {
|
||||
e = e.detail.event;
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||
|
||||
const commandsContainerElement = document.getElementById('commands-container');
|
||||
|
||||
if (commandsContainerElement) {
|
||||
if (commandsContainerElement && e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectUp();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
// Submit the content when Enter key is pressed
|
||||
if (content !== '' && e.keyCode === 13 && !e.shiftKey) {
|
||||
submitHandler();
|
||||
if (commandsContainerElement && e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
commandsElement.selectDown();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
commandOptionButton?.click();
|
||||
}
|
||||
|
||||
if (commandsContainerElement && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
]?.at(-1);
|
||||
|
||||
if (commandOptionButton) {
|
||||
commandOptionButton?.click();
|
||||
} else {
|
||||
document.getElementById('send-message-button')?.click();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!$mobile ||
|
||||
!(
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
)
|
||||
) {
|
||||
// Prevent Enter key from creating a new line
|
||||
// Uses keyCode '13' for Enter key for chinese/japanese keyboards
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Submit the content when Enter key is pressed
|
||||
if (content !== '' && e.keyCode === 13 && !e.shiftKey) {
|
||||
submitHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,30 +797,34 @@
|
||||
</div>
|
||||
|
||||
<div class=" flex justify-between mb-2.5 mt-1.5 mx-0.5">
|
||||
<div class="ml-1 self-end flex space-x-1">
|
||||
<InputMenu
|
||||
{screenCaptureHandler}
|
||||
uploadFilesHandler={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
||||
type="button"
|
||||
aria-label="More"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
<div class="ml-1 self-end flex space-x-1 flex-1">
|
||||
<slot name="menu">
|
||||
{#if acceptFiles}
|
||||
<InputMenu
|
||||
{screenCaptureHandler}
|
||||
uploadFilesHandler={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</InputMenu>
|
||||
<button
|
||||
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
||||
type="button"
|
||||
aria-label="More"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</InputMenu>
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="self-end flex space-x-1 mr-1">
|
||||
@@ -594,31 +875,57 @@
|
||||
{/if}
|
||||
|
||||
<div class=" flex items-center">
|
||||
<div class=" flex items-center">
|
||||
<Tooltip content={$i18n.t('Send message')}>
|
||||
<button
|
||||
id="send-message-button"
|
||||
class="{content !== '' || files.length !== 0
|
||||
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
||||
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
|
||||
type="submit"
|
||||
disabled={content === '' && files.length === 0}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
{#if inputLoading && onStop}
|
||||
<div class=" flex items-center">
|
||||
<Tooltip content={$i18n.t('Stop')}>
|
||||
<button
|
||||
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
|
||||
on:click={() => {
|
||||
onStop();
|
||||
}}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" flex items-center">
|
||||
<Tooltip content={$i18n.t('Send message')}>
|
||||
<button
|
||||
id="send-message-button"
|
||||
class="{content !== '' || files.length !== 0
|
||||
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
|
||||
: 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
|
||||
type="submit"
|
||||
disabled={content === '' && files.length === 0}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +144,9 @@
|
||||
<ProfilePreview user={message.user}>
|
||||
<ProfileImage
|
||||
src={message.user?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
($i18n.language === 'dg-DG'
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
className={'size-8 translate-y-1 ml-0.5'}
|
||||
/>
|
||||
</ProfilePreview>
|
||||
@@ -275,7 +277,7 @@
|
||||
>
|
||||
{#if $shortCodesToEmojis[reaction.name]}
|
||||
<img
|
||||
src="/assets/emojis/{$shortCodesToEmojis[
|
||||
src="{WEBUI_BASE_URL}/assets/emojis/{$shortCodesToEmojis[
|
||||
reaction.name
|
||||
].toLowerCase()}.svg"
|
||||
alt={reaction.name}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import emojiShortCodes from '$lib/emoji-shortcodes.json';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import VirtualList from '@sveltejs/svelte-virtual-list';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let onClose = () => {};
|
||||
export let onSubmit = (name) => {};
|
||||
@@ -147,7 +148,7 @@
|
||||
on:click={() => selectEmoji(emojiItem)}
|
||||
>
|
||||
<img
|
||||
src="/assets/emojis/{emojiItem.name.toLowerCase()}.svg"
|
||||
src="{WEBUI_BASE_URL}/assets/emojis/{emojiItem.name.toLowerCase()}.svg"
|
||||
alt={emojiItem.name}
|
||||
class="size-5"
|
||||
loading="lazy"
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
};
|
||||
|
||||
const submitHandler = async ({ content, data }) => {
|
||||
if (!content) {
|
||||
if (!content && (data?.files ?? []).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" pb-[1rem]">
|
||||
<div class=" pb-[1rem] px-2.5">
|
||||
<MessageInput id={threadId} {typingUsers} {onChange} onSubmit={submitHandler} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,10 +55,7 @@
|
||||
|
||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
||||
import {
|
||||
addTagById,
|
||||
createNewChat,
|
||||
deleteTagById,
|
||||
deleteTagsById,
|
||||
getAllTags,
|
||||
getChatById,
|
||||
getChatList,
|
||||
@@ -99,6 +96,8 @@
|
||||
let controlPane;
|
||||
let controlPaneComponent;
|
||||
|
||||
let messageInput;
|
||||
|
||||
let autoScroll = true;
|
||||
let processing = '';
|
||||
let messagesContainerElement: HTMLDivElement;
|
||||
@@ -126,6 +125,8 @@
|
||||
let webSearchEnabled = false;
|
||||
let codeInterpreterEnabled = false;
|
||||
|
||||
let showCommands = false;
|
||||
|
||||
let chat = null;
|
||||
let tags = [];
|
||||
|
||||
@@ -143,24 +144,38 @@
|
||||
let params = {};
|
||||
|
||||
$: if (chatIdProp) {
|
||||
(async () => {
|
||||
loading = true;
|
||||
navigateHandler();
|
||||
}
|
||||
|
||||
prompt = '';
|
||||
files = [];
|
||||
selectedToolIds = [];
|
||||
selectedFilterIds = [];
|
||||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
const navigateHandler = async () => {
|
||||
loading = true;
|
||||
|
||||
if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
||||
prompt = '';
|
||||
messageInput?.setText('');
|
||||
|
||||
files = [];
|
||||
selectedToolIds = [];
|
||||
selectedFilterIds = [];
|
||||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
|
||||
const storageChatInput = sessionStorage.getItem(
|
||||
`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`
|
||||
);
|
||||
|
||||
if (chatIdProp && (await loadChat())) {
|
||||
await tick();
|
||||
loading = false;
|
||||
window.setTimeout(() => scrollToBottom(), 0);
|
||||
|
||||
await tick();
|
||||
|
||||
if (storageChatInput) {
|
||||
try {
|
||||
const input = JSON.parse(
|
||||
localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
|
||||
);
|
||||
const input = JSON.parse(storageChatInput);
|
||||
|
||||
if (!$temporaryChatEnabled) {
|
||||
prompt = input.prompt;
|
||||
messageInput?.setText(input.prompt);
|
||||
files = input.files;
|
||||
selectedToolIds = input.selectedToolIds;
|
||||
selectedFilterIds = input.selectedFilterIds;
|
||||
@@ -171,17 +186,21 @@
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (chatIdProp && (await loadChat())) {
|
||||
await tick();
|
||||
loading = false;
|
||||
window.setTimeout(() => scrollToBottom(), 0);
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
} else {
|
||||
await goto('/');
|
||||
}
|
||||
})();
|
||||
}
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
chatInput?.focus();
|
||||
} else {
|
||||
await goto('/');
|
||||
}
|
||||
};
|
||||
|
||||
const onSelect = async (e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
if (type === 'prompt') {
|
||||
// Handle prompt selection
|
||||
messageInput?.setText(data);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (selectedModels && chatIdProp !== '') {
|
||||
saveSessionSelectedModels();
|
||||
@@ -408,7 +427,7 @@
|
||||
const inputElement = document.getElementById('chat-input');
|
||||
|
||||
if (inputElement) {
|
||||
prompt = event.data.text;
|
||||
messageInput?.setText(event.data.text);
|
||||
inputElement.focus();
|
||||
}
|
||||
}
|
||||
@@ -446,8 +465,19 @@
|
||||
}
|
||||
});
|
||||
|
||||
if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
||||
const storageChatInput = sessionStorage.getItem(
|
||||
`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`
|
||||
);
|
||||
|
||||
if (!chatIdProp) {
|
||||
loading = false;
|
||||
await tick();
|
||||
}
|
||||
|
||||
if (storageChatInput) {
|
||||
prompt = '';
|
||||
messageInput?.setText('');
|
||||
|
||||
files = [];
|
||||
selectedToolIds = [];
|
||||
selectedFilterIds = [];
|
||||
@@ -456,12 +486,10 @@
|
||||
codeInterpreterEnabled = false;
|
||||
|
||||
try {
|
||||
const input = JSON.parse(
|
||||
localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
|
||||
);
|
||||
const input = JSON.parse(storageChatInput);
|
||||
|
||||
if (!$temporaryChatEnabled) {
|
||||
prompt = input.prompt;
|
||||
messageInput?.setText(input.prompt);
|
||||
files = input.files;
|
||||
selectedToolIds = input.selectedToolIds;
|
||||
selectedFilterIds = input.selectedFilterIds;
|
||||
@@ -472,11 +500,6 @@
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!chatIdProp) {
|
||||
loading = false;
|
||||
await tick();
|
||||
}
|
||||
|
||||
showControls.subscribe(async (value) => {
|
||||
if (controlPane && !$mobile) {
|
||||
try {
|
||||
@@ -708,6 +731,10 @@
|
||||
//////////////////////////
|
||||
|
||||
const initNewChat = async () => {
|
||||
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
|
||||
await temporaryChatEnabled.set(true);
|
||||
}
|
||||
|
||||
const availableModels = $models
|
||||
.filter((m) => !(m?.info?.meta?.hidden ?? false))
|
||||
.map((m) => m.id);
|
||||
@@ -832,11 +859,14 @@
|
||||
}
|
||||
|
||||
if ($page.url.searchParams.get('q')) {
|
||||
prompt = $page.url.searchParams.get('q') ?? '';
|
||||
const q = $page.url.searchParams.get('q') ?? '';
|
||||
messageInput?.setText(q);
|
||||
|
||||
if (prompt) {
|
||||
await tick();
|
||||
submitPrompt(prompt);
|
||||
if (q) {
|
||||
if (($page.url.searchParams.get('submit') ?? 'true') === 'true') {
|
||||
await tick();
|
||||
submitPrompt(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1068,7 +1098,7 @@
|
||||
};
|
||||
|
||||
const createMessagePair = async (userPrompt) => {
|
||||
prompt = '';
|
||||
messageInput?.setText('');
|
||||
if (selectedModels.length === 0) {
|
||||
toast.error($i18n.t('Model not selected'));
|
||||
} else {
|
||||
@@ -1389,7 +1419,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
prompt = '';
|
||||
messageInput?.setText('');
|
||||
|
||||
// Reset chat input textarea
|
||||
if (!($settings?.richTextInput ?? true)) {
|
||||
@@ -1410,7 +1440,7 @@
|
||||
);
|
||||
|
||||
files = [];
|
||||
prompt = '';
|
||||
messageInput?.setText('');
|
||||
|
||||
// Create user message
|
||||
let userMessageId = uuidv4();
|
||||
@@ -1567,9 +1597,8 @@
|
||||
let files = JSON.parse(JSON.stringify(chatFiles));
|
||||
files.push(
|
||||
...(userMessage?.files ?? []).filter((item) =>
|
||||
['doc', 'file', 'collection'].includes(item.type)
|
||||
),
|
||||
...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
|
||||
['doc', 'text', 'file', 'note', 'collection'].includes(item.type)
|
||||
)
|
||||
);
|
||||
// Remove duplicates
|
||||
files = files.filter(
|
||||
@@ -2064,6 +2093,7 @@
|
||||
bind:selectedModels
|
||||
shareEnabled={!!history.currentId}
|
||||
{initNewChat}
|
||||
showBanners={!showCommands}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col flex-auto z-10 w-full @container">
|
||||
@@ -2095,12 +2125,14 @@
|
||||
{chatActionHandler}
|
||||
{addMessages}
|
||||
bottomPadding={files.length > 0}
|
||||
{onSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" pb-2">
|
||||
<MessageInput
|
||||
bind:this={messageInput}
|
||||
{history}
|
||||
{taskIds}
|
||||
{selectedModels}
|
||||
@@ -2113,6 +2145,7 @@
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
bind:showCommands
|
||||
toolServers={$toolServers}
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
{stopResponse}
|
||||
@@ -2120,12 +2153,12 @@
|
||||
onChange={(input) => {
|
||||
if (!$temporaryChatEnabled) {
|
||||
if (input.prompt !== null) {
|
||||
localStorage.setItem(
|
||||
sessionStorage.setItem(
|
||||
`chat-input${$chatId ? `-${$chatId}` : ''}`,
|
||||
JSON.stringify(input)
|
||||
);
|
||||
} else {
|
||||
localStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
|
||||
sessionStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -2163,6 +2196,7 @@
|
||||
<Placeholder
|
||||
{history}
|
||||
{selectedModels}
|
||||
bind:messageInput
|
||||
bind:files
|
||||
bind:prompt
|
||||
bind:autoScroll
|
||||
@@ -2172,10 +2206,12 @@
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
bind:showCommands
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
toolServers={$toolServers}
|
||||
{stopResponse}
|
||||
{createMessagePair}
|
||||
{onSelect}
|
||||
on:upload={async (e) => {
|
||||
const { type, data } = e.detail;
|
||||
|
||||
@@ -2227,7 +2263,7 @@
|
||||
{:else if loading}
|
||||
<div class=" flex items-center justify-center h-full w-full">
|
||||
<div class="m-auto">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
import Overview from './Overview.svelte';
|
||||
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
||||
import Artifacts from './Artifacts.svelte';
|
||||
import { min } from '@floating-ui/utils';
|
||||
|
||||
export let history;
|
||||
export let models = [];
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
export let models = [];
|
||||
export let atSelectedModel;
|
||||
|
||||
export let submitPrompt;
|
||||
export let onSelect = (e) => {};
|
||||
|
||||
let mounted = false;
|
||||
let selectedModelIdx = 0;
|
||||
@@ -46,7 +46,9 @@
|
||||
>
|
||||
<Tooltip
|
||||
content={marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
|
||||
sanitizeResponseContent(
|
||||
models[selectedModelIdx]?.info?.meta?.description ?? ''
|
||||
).replaceAll('\n', '<br>')
|
||||
)}
|
||||
placement="right"
|
||||
>
|
||||
@@ -54,7 +56,7 @@
|
||||
crossorigin="anonymous"
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `/doge.png`
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
class=" size-[2.7rem] rounded-full border-[1px] border-gray-100 dark:border-none"
|
||||
alt="logo"
|
||||
@@ -68,7 +70,7 @@
|
||||
|
||||
{#if $temporaryChatEnabled}
|
||||
<Tooltip
|
||||
content={$i18n.t('This chat won’t appear in history and your messages will not be saved.')}
|
||||
content={$i18n.t("This chat won't appear in history and your messages will not be saved.")}
|
||||
className="w-full flex justify-start mb-0.5"
|
||||
placement="top"
|
||||
>
|
||||
@@ -96,7 +98,9 @@
|
||||
class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown"
|
||||
>
|
||||
{@html marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
|
||||
sanitizeResponseContent(
|
||||
models[selectedModelIdx]?.info?.meta?.description
|
||||
).replaceAll('\n', '<br>')
|
||||
)}
|
||||
</div>
|
||||
{#if models[selectedModelIdx]?.info?.meta?.user}
|
||||
@@ -131,9 +135,7 @@
|
||||
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||
$config?.default_prompt_suggestions ??
|
||||
[]}
|
||||
on:select={(e) => {
|
||||
submitPrompt(e.detail);
|
||||
}}
|
||||
{onSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
import { user, settings } from '$lib/stores';
|
||||
export let models = [];
|
||||
export let chatFiles = [];
|
||||
export let params = {};
|
||||
@@ -74,7 +74,9 @@
|
||||
<div class="" slot="content">
|
||||
<textarea
|
||||
bind:value={params.system}
|
||||
class="w-full text-xs py-1.5 bg-transparent outline-hidden resize-none"
|
||||
class="w-full text-xs outline-hidden resize-vertical {$settings.highContrastMode
|
||||
? 'border-2 border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 p-2.5'
|
||||
: 'py-1.5 bg-transparent'}"
|
||||
rows="4"
|
||||
placeholder={$i18n.t('Enter system prompt')}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script lang="ts">
|
||||
import * as pdfjs from 'pdfjs-dist';
|
||||
import * as pdfWorker from 'pdfjs-dist/build/pdf.worker.mjs';
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = import.meta.url + 'pdfjs-dist/build/pdf.worker.mjs';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import { marked } from 'marked';
|
||||
import heic2any from 'heic2any';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
@@ -22,14 +27,23 @@
|
||||
tools,
|
||||
user as _user,
|
||||
showControls,
|
||||
TTSWorker
|
||||
TTSWorker,
|
||||
temporaryChatEnabled
|
||||
} from '$lib/stores';
|
||||
|
||||
import {
|
||||
blobToFile,
|
||||
compressImage,
|
||||
createMessagesList,
|
||||
extractCurlyBraceWords
|
||||
extractContentFromFile,
|
||||
extractCurlyBraceWords,
|
||||
extractInputVariables,
|
||||
getCurrentDateTime,
|
||||
getFormattedDate,
|
||||
getFormattedTime,
|
||||
getUserPosition,
|
||||
getUserTimezone,
|
||||
getWeekday
|
||||
} from '$lib/utils';
|
||||
import { uploadFile } from '$lib/apis/files';
|
||||
import { generateAutoCompletion } from '$lib/apis';
|
||||
@@ -57,7 +71,7 @@
|
||||
import Sparkles from '../icons/Sparkles.svelte';
|
||||
|
||||
import { KokoroWorker } from '$lib/workers/KokoroWorker';
|
||||
|
||||
import InputVariablesModal from './MessageInput/InputVariablesModal.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let transparentBackground = false;
|
||||
@@ -89,6 +103,10 @@
|
||||
export let webSearchEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
|
||||
let showInputVariablesModal = false;
|
||||
let inputVariables = {};
|
||||
let inputVariableValues = {};
|
||||
|
||||
$: onChange({
|
||||
prompt,
|
||||
files: files
|
||||
@@ -107,6 +125,262 @@
|
||||
codeInterpreterEnabled
|
||||
});
|
||||
|
||||
const inputVariableHandler = async (text: string) => {
|
||||
inputVariables = extractInputVariables(text);
|
||||
if (Object.keys(inputVariables).length > 0) {
|
||||
showInputVariablesModal = true;
|
||||
}
|
||||
};
|
||||
|
||||
const textVariableHandler = async (text: string) => {
|
||||
if (text.includes('{{CLIPBOARD}}')) {
|
||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||
return '{{CLIPBOARD}}';
|
||||
});
|
||||
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
|
||||
let imageUrl = null;
|
||||
for (const item of clipboardItems) {
|
||||
// Check for known image types
|
||||
for (const type of item.types) {
|
||||
if (type.startsWith('image/')) {
|
||||
const blob = await item.getType(type);
|
||||
imageUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: 'image',
|
||||
url: imageUrl
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
}
|
||||
|
||||
if (text.includes('{{USER_LOCATION}}')) {
|
||||
let location;
|
||||
try {
|
||||
location = await getUserPosition();
|
||||
} catch (error) {
|
||||
toast.error($i18n.t('Location access not allowed'));
|
||||
location = 'LOCATION_UNKNOWN';
|
||||
}
|
||||
text = text.replaceAll('{{USER_LOCATION}}', String(location));
|
||||
}
|
||||
|
||||
if (text.includes('{{USER_NAME}}')) {
|
||||
const name = $_user?.name || 'User';
|
||||
text = text.replaceAll('{{USER_NAME}}', name);
|
||||
}
|
||||
|
||||
if (text.includes('{{USER_LANGUAGE}}')) {
|
||||
const language = localStorage.getItem('locale') || 'en-US';
|
||||
text = text.replaceAll('{{USER_LANGUAGE}}', language);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_DATE}}')) {
|
||||
const date = getFormattedDate();
|
||||
text = text.replaceAll('{{CURRENT_DATE}}', date);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_TIME}}')) {
|
||||
const time = getFormattedTime();
|
||||
text = text.replaceAll('{{CURRENT_TIME}}', time);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_DATETIME}}')) {
|
||||
const dateTime = getCurrentDateTime();
|
||||
text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_TIMEZONE}}')) {
|
||||
const timezone = getUserTimezone();
|
||||
text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone);
|
||||
}
|
||||
|
||||
if (text.includes('{{CURRENT_WEEKDAY}}')) {
|
||||
const weekday = getWeekday();
|
||||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||
}
|
||||
|
||||
inputVariableHandler(text);
|
||||
return text;
|
||||
};
|
||||
|
||||
const replaceVariables = (variables: Record<string, any>) => {
|
||||
console.log('Replacing variables:', variables);
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
|
||||
if (chatInput) {
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
chatInputElement.replaceVariables(variables);
|
||||
chatInputElement.focus();
|
||||
} else {
|
||||
// Get current value from the input element
|
||||
let currentValue = chatInput.value || '';
|
||||
|
||||
// Replace template variables using regex
|
||||
const updatedValue = currentValue.replace(
|
||||
/{{\s*([^|}]+)(?:\|[^}]*)?\s*}}/g,
|
||||
(match, varName) => {
|
||||
const trimmedVarName = varName.trim();
|
||||
return variables.hasOwnProperty(trimmedVarName)
|
||||
? String(variables[trimmedVarName])
|
||||
: match;
|
||||
}
|
||||
);
|
||||
|
||||
// Update the input value
|
||||
chatInput.value = updatedValue;
|
||||
chatInput.focus();
|
||||
chatInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const setText = async (text?: string) => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
|
||||
if (chatInput) {
|
||||
text = await textVariableHandler(text || '');
|
||||
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
chatInputElement?.setText(text);
|
||||
chatInputElement?.focus();
|
||||
} else {
|
||||
chatInput.value = text;
|
||||
prompt = text;
|
||||
|
||||
chatInput.focus();
|
||||
chatInput.dispatchEvent(new Event('input'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getCommand = () => {
|
||||
const getWordAtCursor = (text, cursor) => {
|
||||
if (typeof text !== 'string' || cursor == null) return '';
|
||||
const left = text.slice(0, cursor);
|
||||
const right = text.slice(cursor);
|
||||
const leftWord = left.match(/(?:^|\s)([^\s]*)$/)?.[1] || '';
|
||||
|
||||
const rightWord = right.match(/^([^\s]*)/)?.[1] || '';
|
||||
return leftWord + rightWord;
|
||||
};
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
let word = '';
|
||||
|
||||
if (chatInput) {
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
word = chatInputElement?.getWordAtDocPos();
|
||||
} else {
|
||||
const cursor = chatInput ? chatInput.selectionStart : prompt.length;
|
||||
word = getWordAtCursor(prompt, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
return word;
|
||||
};
|
||||
|
||||
const replaceCommandWithText = (text) => {
|
||||
const getWordBoundsAtCursor = (text, cursor) => {
|
||||
let start = cursor,
|
||||
end = cursor;
|
||||
while (start > 0 && !/\s/.test(text[start - 1])) --start;
|
||||
while (end < text.length && !/\s/.test(text[end])) ++end;
|
||||
return { start, end };
|
||||
};
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
if (!chatInput) return;
|
||||
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
chatInputElement?.replaceCommandWithText(text);
|
||||
} else {
|
||||
const cursor = chatInput.selectionStart;
|
||||
const { start, end } = getWordBoundsAtCursor(prompt, cursor);
|
||||
prompt = prompt.slice(0, start) + text + prompt.slice(end);
|
||||
chatInput.focus();
|
||||
chatInput.setSelectionRange(start + text.length, start + text.length);
|
||||
}
|
||||
};
|
||||
|
||||
const insertTextAtCursor = async (text: string) => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
if (!chatInput) return;
|
||||
|
||||
text = await textVariableHandler(text);
|
||||
|
||||
if (command) {
|
||||
replaceCommandWithText(text);
|
||||
} else {
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode(text));
|
||||
range.collapse(false);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
} else {
|
||||
const cursor = chatInput.selectionStart;
|
||||
prompt = prompt.slice(0, cursor) + text + prompt.slice(cursor);
|
||||
chatInput.focus();
|
||||
chatInput.setSelectionRange(cursor + text.length, cursor + text.length);
|
||||
}
|
||||
}
|
||||
|
||||
await tick();
|
||||
const chatInputContainer = document.getElementById('chat-input-container');
|
||||
if (chatInputContainer) {
|
||||
chatInputContainer.scrollTop = chatInputContainer.scrollHeight;
|
||||
}
|
||||
|
||||
await tick();
|
||||
if (chatInput) {
|
||||
chatInput.focus();
|
||||
chatInput.dispatchEvent(new Event('input'));
|
||||
|
||||
const words = extractCurlyBraceWords(prompt);
|
||||
|
||||
if (words.length > 0) {
|
||||
const word = words.at(0);
|
||||
await tick();
|
||||
|
||||
if (!($settings?.richTextInput ?? true)) {
|
||||
// Move scroll to the first word
|
||||
chatInput.setSelectionRange(word.startIndex, word.endIndex + 1);
|
||||
chatInput.focus();
|
||||
|
||||
const selectionRow =
|
||||
(word?.startIndex - (word?.startIndex % chatInput.cols)) / chatInput.cols;
|
||||
const lineHeight = chatInput.clientHeight / chatInput.rows;
|
||||
|
||||
chatInput.scrollTop = lineHeight * selectionRow;
|
||||
}
|
||||
} else {
|
||||
chatInput.scrollTop = chatInput.scrollHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let command = '';
|
||||
|
||||
export let showCommands = false;
|
||||
$: showCommands = ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
|
||||
|
||||
let showTools = false;
|
||||
|
||||
let loaded = false;
|
||||
@@ -261,47 +535,77 @@
|
||||
|
||||
files = [...files, fileItem];
|
||||
|
||||
try {
|
||||
// If the file is an audio file, provide the language for STT.
|
||||
let metadata = null;
|
||||
if (
|
||||
(file.type.startsWith('audio/') || file.type.startsWith('video/')) &&
|
||||
$settings?.audio?.stt?.language
|
||||
) {
|
||||
metadata = {
|
||||
language: $settings?.audio?.stt?.language
|
||||
};
|
||||
}
|
||||
|
||||
// During the file upload, file content is automatically extracted.
|
||||
const uploadedFile = await uploadFile(localStorage.token, file, metadata);
|
||||
|
||||
if (uploadedFile) {
|
||||
console.log('File upload completed:', {
|
||||
id: uploadedFile.id,
|
||||
name: fileItem.name,
|
||||
collection: uploadedFile?.meta?.collection_name
|
||||
});
|
||||
|
||||
if (uploadedFile.error) {
|
||||
console.warn('File upload warning:', uploadedFile.error);
|
||||
toast.warning(uploadedFile.error);
|
||||
if (!$temporaryChatEnabled) {
|
||||
try {
|
||||
// If the file is an audio file, provide the language for STT.
|
||||
let metadata = null;
|
||||
if (
|
||||
(file.type.startsWith('audio/') || file.type.startsWith('video/')) &&
|
||||
$settings?.audio?.stt?.language
|
||||
) {
|
||||
metadata = {
|
||||
language: $settings?.audio?.stt?.language
|
||||
};
|
||||
}
|
||||
|
||||
fileItem.status = 'uploaded';
|
||||
fileItem.file = uploadedFile;
|
||||
fileItem.id = uploadedFile.id;
|
||||
fileItem.collection_name =
|
||||
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
||||
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
||||
// During the file upload, file content is automatically extracted.
|
||||
const uploadedFile = await uploadFile(localStorage.token, file, metadata);
|
||||
|
||||
files = files;
|
||||
} else {
|
||||
if (uploadedFile) {
|
||||
console.log('File upload completed:', {
|
||||
id: uploadedFile.id,
|
||||
name: fileItem.name,
|
||||
collection: uploadedFile?.meta?.collection_name
|
||||
});
|
||||
|
||||
if (uploadedFile.error) {
|
||||
console.warn('File upload warning:', uploadedFile.error);
|
||||
toast.warning(uploadedFile.error);
|
||||
}
|
||||
|
||||
fileItem.status = 'uploaded';
|
||||
fileItem.file = uploadedFile;
|
||||
fileItem.id = uploadedFile.id;
|
||||
fileItem.collection_name =
|
||||
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
|
||||
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
||||
|
||||
files = files;
|
||||
} else {
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(`${e}`);
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(`${e}`);
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
} else {
|
||||
// If temporary chat is enabled, we just add the file to the list without uploading it.
|
||||
|
||||
const content = await extractContentFromFile(file, pdfjsLib).catch((error) => {
|
||||
toast.error(
|
||||
$i18n.t('Failed to extract content from the file: {{error}}', { error: error })
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (content === null) {
|
||||
toast.error($i18n.t('Failed to extract content from the file.'));
|
||||
files = files.filter((item) => item?.itemId !== tempItemId);
|
||||
return null;
|
||||
} else {
|
||||
console.log('Extracted content from file:', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
content: content
|
||||
});
|
||||
|
||||
fileItem.status = 'uploaded';
|
||||
fileItem.type = 'text';
|
||||
fileItem.content = content;
|
||||
fileItem.id = uuidv4(); // Temporary ID for the file
|
||||
|
||||
files = files;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -320,7 +624,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
inputFiles.forEach((file) => {
|
||||
inputFiles.forEach(async (file) => {
|
||||
console.log('Processing file:', {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
@@ -344,46 +648,53 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
|
||||
) {
|
||||
if (file['type'].startsWith('image/')) {
|
||||
if (visionCapableModels.length === 0) {
|
||||
toast.error($i18n.t('Selected model(s) do not support image inputs'));
|
||||
return;
|
||||
}
|
||||
|
||||
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
|
||||
// Quick shortcut so we don’t do unnecessary work.
|
||||
const settingsCompression = settings?.imageCompression ?? false;
|
||||
const configWidth = config?.file?.image_compression?.width ?? null;
|
||||
const configHeight = config?.file?.image_compression?.height ?? null;
|
||||
|
||||
// If neither settings nor config wants compression, return original URL.
|
||||
if (!settingsCompression && !configWidth && !configHeight) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Default to null (no compression unless set)
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
// If user/settings want compression, pick their preferred size.
|
||||
if (settingsCompression) {
|
||||
width = settings?.imageCompressionSize?.width ?? null;
|
||||
height = settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
// Apply config limits as an upper bound if any
|
||||
if (configWidth && (width === null || width > configWidth)) {
|
||||
width = configWidth;
|
||||
}
|
||||
if (configHeight && (height === null || height > configHeight)) {
|
||||
height = configHeight;
|
||||
}
|
||||
|
||||
// Do the compression if required
|
||||
if (width || height) {
|
||||
return await compressImage(imageUrl, width, height);
|
||||
}
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
let imageUrl = event.target.result;
|
||||
|
||||
if (
|
||||
($settings?.imageCompression ?? false) ||
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
if ($settings?.imageCompression ?? false) {
|
||||
width = $settings?.imageCompressionSize?.width ?? null;
|
||||
height = $settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
if (width > ($config?.file?.image_compression?.width ?? null)) {
|
||||
width = $config?.file?.image_compression?.width ?? null;
|
||||
}
|
||||
if (height > ($config?.file?.image_compression?.height ?? null)) {
|
||||
height = $config?.file?.image_compression?.height ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (width || height) {
|
||||
imageUrl = await compressImage(imageUrl, width, height);
|
||||
}
|
||||
}
|
||||
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
||||
|
||||
files = [
|
||||
...files,
|
||||
@@ -393,7 +704,11 @@
|
||||
}
|
||||
];
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
reader.readAsDataURL(
|
||||
file['type'] === 'image/heic'
|
||||
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
||||
: file
|
||||
);
|
||||
} else {
|
||||
uploadFileHandler(file);
|
||||
}
|
||||
@@ -496,6 +811,14 @@
|
||||
|
||||
<FilesOverlay show={dragged} />
|
||||
<ToolServersModal bind:show={showTools} {selectedToolIds} />
|
||||
<InputVariablesModal
|
||||
bind:show={showInputVariablesModal}
|
||||
variables={inputVariables}
|
||||
onSave={(variableValues) => {
|
||||
inputVariableValues = { ...inputVariableValues, ...variableValues };
|
||||
replaceVariables(inputVariableValues);
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if loaded}
|
||||
<div class="w-full font-primary">
|
||||
@@ -548,7 +871,7 @@
|
||||
src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
|
||||
?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `/doge.png`
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
/>
|
||||
<div class="translate-y-[0.5px]">
|
||||
@@ -571,20 +894,36 @@
|
||||
|
||||
<Commands
|
||||
bind:this={commandsElement}
|
||||
bind:prompt
|
||||
bind:files
|
||||
on:upload={(e) => {
|
||||
dispatch('upload', e.detail);
|
||||
}}
|
||||
on:select={(e) => {
|
||||
const data = e.detail;
|
||||
show={showCommands}
|
||||
{command}
|
||||
insertTextHandler={insertTextAtCursor}
|
||||
onUpload={(e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
if (data?.type === 'model') {
|
||||
atSelectedModel = data.data;
|
||||
if (type === 'file') {
|
||||
if (files.find((f) => f.id === data.id)) {
|
||||
return;
|
||||
}
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
...data,
|
||||
status: 'processed'
|
||||
}
|
||||
];
|
||||
} else {
|
||||
dispatch('upload', e);
|
||||
}
|
||||
}}
|
||||
onSelect={(e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
if (type === 'model') {
|
||||
atSelectedModel = data;
|
||||
}
|
||||
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
chatInputElement?.focus();
|
||||
document.getElementById('chat-input')?.focus();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -659,7 +998,7 @@
|
||||
<div class="relative flex items-center">
|
||||
<Image
|
||||
src={file.url}
|
||||
alt="input"
|
||||
alt=""
|
||||
imageClassName=" size-14 rounded-xl object-cover"
|
||||
/>
|
||||
{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
|
||||
@@ -677,6 +1016,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class="size-4 fill-yellow-300"
|
||||
>
|
||||
<path
|
||||
@@ -690,8 +1030,12 @@
|
||||
</div>
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
|
||||
class=" bg-white text-black border border-white rounded-full {($settings?.highContrastMode ??
|
||||
false)
|
||||
? ''
|
||||
: 'outline-hidden focus:outline-hidden group-hover:visible invisible transition'}"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Remove file')}
|
||||
on:click={() => {
|
||||
files.splice(fileIdx, 1);
|
||||
files = files;
|
||||
@@ -701,6 +1045,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
@@ -719,18 +1064,8 @@
|
||||
loading={file.status === 'uploading'}
|
||||
dismissible={true}
|
||||
edit={true}
|
||||
modal={['file', 'collection'].includes(file?.type)}
|
||||
on:dismiss={async () => {
|
||||
try {
|
||||
if (file.type !== 'collection' && !file?.collection) {
|
||||
if (file.id) {
|
||||
// This will handle both file deletion and Chroma cleanup
|
||||
await deleteFileById(localStorage.token, file.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
}
|
||||
|
||||
// Remove from UI state
|
||||
files.splice(fileIdx, 1);
|
||||
files = files;
|
||||
@@ -752,9 +1087,15 @@
|
||||
>
|
||||
<RichTextInput
|
||||
bind:this={chatInputElement}
|
||||
bind:value={prompt}
|
||||
id="chat-input"
|
||||
onChange={(e) => {
|
||||
prompt = e.md;
|
||||
command = getCommand();
|
||||
}}
|
||||
json={true}
|
||||
messageInput={true}
|
||||
showFormattingButtons={false}
|
||||
insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false}
|
||||
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||
(!$mobile ||
|
||||
!(
|
||||
@@ -972,6 +1313,12 @@
|
||||
class="scrollbar-hidden bg-transparent dark:text-gray-200 outline-hidden w-full pt-3 px-1 resize-none"
|
||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||
bind:value={prompt}
|
||||
on:input={() => {
|
||||
command = getCommand();
|
||||
}}
|
||||
on:click={() => {
|
||||
command = getCommand();
|
||||
}}
|
||||
on:compositionstart={() => (isComposing = true)}
|
||||
on:compositionend={() => (isComposing = false)}
|
||||
on:keydown={async (e) => {
|
||||
@@ -1119,17 +1466,20 @@
|
||||
|
||||
if (words.length > 0) {
|
||||
const word = words.at(0);
|
||||
const fullPrompt = prompt;
|
||||
|
||||
prompt = prompt.substring(0, word?.endIndex + 1);
|
||||
await tick();
|
||||
if (word && e.target instanceof HTMLTextAreaElement) {
|
||||
// Prevent default tab behavior
|
||||
e.preventDefault();
|
||||
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
e.target.focus();
|
||||
|
||||
e.target.scrollTop = e.target.scrollHeight;
|
||||
prompt = fullPrompt;
|
||||
await tick();
|
||||
const selectionRow =
|
||||
(word?.startIndex - (word?.startIndex % e.target.cols)) /
|
||||
e.target.cols;
|
||||
const lineHeight = e.target.clientHeight / e.target.rows;
|
||||
|
||||
e.preventDefault();
|
||||
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
e.target.scrollTop = lineHeight * selectionRow;
|
||||
}
|
||||
}
|
||||
|
||||
e.target.style.height = '';
|
||||
@@ -1250,14 +1600,13 @@
|
||||
chatInput?.focus();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
||||
type="button"
|
||||
aria-label="More"
|
||||
<div
|
||||
class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
@@ -1265,7 +1614,7 @@
|
||||
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</InputMenu>
|
||||
|
||||
{#if $_user && (showToolsButton || (toggleFilters && toggleFilters.length > 0) || showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton)}
|
||||
@@ -1379,12 +1728,19 @@
|
||||
{#if showCodeInterpreterButton}
|
||||
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
|
||||
<button
|
||||
aria-label={codeInterpreterEnabled
|
||||
? $i18n.t('Disable Code Interpreter')
|
||||
: $i18n.t('Enable Code Interpreter')}
|
||||
aria-pressed={codeInterpreterEnabled}
|
||||
on:click|preventDefault={() =>
|
||||
(codeInterpreterEnabled = !codeInterpreterEnabled)}
|
||||
type="button"
|
||||
class="px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {codeInterpreterEnabled
|
||||
class="px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm transition-colors duration-300 max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {codeInterpreterEnabled
|
||||
? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
|
||||
: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
|
||||
: 'bg-transparent text-gray-600 dark:text-gray-300 '} {($settings?.highContrastMode ??
|
||||
false)
|
||||
? 'm-1'
|
||||
: 'focus:outline-hidden rounded-full'}"
|
||||
>
|
||||
<CommandLine className="size-4" strokeWidth="1.75" />
|
||||
<span
|
||||
@@ -1530,7 +1886,7 @@
|
||||
);
|
||||
}
|
||||
}}
|
||||
aria-label="Call"
|
||||
aria-label={$i18n.t('Voice mode')}
|
||||
>
|
||||
<Headphone className="size-5" />
|
||||
</button>
|
||||
|
||||
@@ -854,6 +854,7 @@
|
||||
</button>
|
||||
{:else}
|
||||
<div class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full">
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video
|
||||
id="camera-feed"
|
||||
autoplay
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { knowledge, prompts } from '$lib/stores';
|
||||
|
||||
import { removeLastWordFromString } from '$lib/utils';
|
||||
@@ -15,8 +10,15 @@
|
||||
import Models from './Commands/Models.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
export let prompt = '';
|
||||
export let show = false;
|
||||
|
||||
export let files = [];
|
||||
export let command = '';
|
||||
|
||||
export let onSelect = (e) => {};
|
||||
export let onUpload = (e) => {};
|
||||
|
||||
export let insertTextHandler = (text) => {};
|
||||
|
||||
let loading = false;
|
||||
let commandElement = null;
|
||||
@@ -29,12 +31,6 @@
|
||||
commandElement?.selectDown();
|
||||
};
|
||||
|
||||
let command = '';
|
||||
$: command = prompt?.split('\n').pop()?.split(' ')?.pop() ?? '';
|
||||
|
||||
let show = false;
|
||||
$: show = ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command.slice(0, 2);
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
@@ -56,54 +52,63 @@
|
||||
{#if show}
|
||||
{#if !loading}
|
||||
{#if command?.charAt(0) === '/'}
|
||||
<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
|
||||
<Prompts
|
||||
bind:this={commandElement}
|
||||
{command}
|
||||
onSelect={(e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
if (type === 'prompt') {
|
||||
insertTextHandler(data.content);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))}
|
||||
<Knowledge
|
||||
bind:this={commandElement}
|
||||
bind:prompt
|
||||
command={command.includes('\\#') ? command.slice(2) : command}
|
||||
on:youtube={(e) => {
|
||||
console.log(e);
|
||||
dispatch('upload', {
|
||||
type: 'youtube',
|
||||
data: e.detail
|
||||
});
|
||||
}}
|
||||
on:url={(e) => {
|
||||
console.log(e);
|
||||
dispatch('upload', {
|
||||
type: 'web',
|
||||
data: e.detail
|
||||
});
|
||||
}}
|
||||
on:select={(e) => {
|
||||
console.log(e);
|
||||
if (files.find((f) => f.id === e.detail.id)) {
|
||||
return;
|
||||
onSelect={(e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
if (type === 'knowledge') {
|
||||
insertTextHandler('');
|
||||
|
||||
onUpload({
|
||||
type: 'file',
|
||||
data: data
|
||||
});
|
||||
} else if (type === 'youtube') {
|
||||
insertTextHandler('');
|
||||
|
||||
onUpload({
|
||||
type: 'youtube',
|
||||
data: data
|
||||
});
|
||||
} else if (type === 'web') {
|
||||
insertTextHandler('');
|
||||
|
||||
onUpload({
|
||||
type: 'web',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
...e.detail,
|
||||
status: 'processed'
|
||||
}
|
||||
];
|
||||
|
||||
dispatch('select');
|
||||
}}
|
||||
/>
|
||||
{:else if command?.charAt(0) === '@'}
|
||||
<Models
|
||||
bind:this={commandElement}
|
||||
{command}
|
||||
on:select={(e) => {
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
onSelect={(e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
dispatch('select', {
|
||||
type: 'model',
|
||||
data: e.detail
|
||||
});
|
||||
if (type === 'model') {
|
||||
insertTextHandler('');
|
||||
|
||||
onSelect({
|
||||
type: 'model',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
import { createEventDispatcher, tick, getContext, onMount } from 'svelte';
|
||||
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
|
||||
import { knowledge } from '$lib/stores';
|
||||
import { getNoteList, getNotes } from '$lib/apis/notes';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let prompt = '';
|
||||
export let command = '';
|
||||
export let onSelect = (e) => {};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let selectedIdx = 0;
|
||||
|
||||
let items = [];
|
||||
@@ -42,40 +42,57 @@
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
||||
};
|
||||
|
||||
const confirmSelect = async (item) => {
|
||||
dispatch('select', item);
|
||||
let container;
|
||||
let adjustHeightDebounce;
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
const adjustHeight = () => {
|
||||
if (container) {
|
||||
if (adjustHeightDebounce) {
|
||||
clearTimeout(adjustHeightDebounce);
|
||||
}
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
adjustHeightDebounce = setTimeout(() => {
|
||||
if (!container) return;
|
||||
|
||||
// Ensure the container is visible before adjusting height
|
||||
const rect = container.getBoundingClientRect();
|
||||
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmSelectWeb = async (url) => {
|
||||
dispatch('url', url);
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
const confirmSelect = async (type, data) => {
|
||||
onSelect({
|
||||
type: type,
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
const confirmSelectYoutube = async (url) => {
|
||||
dispatch('youtube', url);
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
const decodeString = (str: string) => {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
window.addEventListener('resize', adjustHeight);
|
||||
adjustHeight();
|
||||
|
||||
let notes = await getNoteList(localStorage.token).catch(() => {
|
||||
return [];
|
||||
});
|
||||
|
||||
notes = notes.map((note) => {
|
||||
return {
|
||||
...note,
|
||||
type: 'note',
|
||||
name: note.title,
|
||||
description: dayjs(note.updated_at / 1000000).fromNow()
|
||||
};
|
||||
});
|
||||
|
||||
let legacy_documents = $knowledge
|
||||
.filter((item) => item?.meta?.document)
|
||||
.map((item) => ({
|
||||
@@ -141,30 +158,30 @@
|
||||
]
|
||||
: [];
|
||||
|
||||
items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
|
||||
(item) => {
|
||||
return {
|
||||
...item,
|
||||
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
|
||||
};
|
||||
}
|
||||
);
|
||||
items = [
|
||||
...notes,
|
||||
...collections,
|
||||
...collection_files,
|
||||
...legacy_collections,
|
||||
...legacy_documents
|
||||
].map((item) => {
|
||||
return {
|
||||
...item,
|
||||
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
|
||||
};
|
||||
});
|
||||
|
||||
fuse = new Fuse(items, {
|
||||
keys: ['name', 'description']
|
||||
});
|
||||
});
|
||||
|
||||
const decodeString = (str: string) => {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('resize', adjustHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
{#if filteredItems.length > 0 || command?.substring(1).startsWith('http')}
|
||||
<div
|
||||
id="commands-container"
|
||||
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
@@ -174,6 +191,7 @@
|
||||
<div
|
||||
class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
|
||||
id="command-options-container"
|
||||
bind:this={container}
|
||||
>
|
||||
{#each filteredItems as item, idx}
|
||||
<button
|
||||
@@ -184,7 +202,7 @@
|
||||
type="button"
|
||||
on:click={() => {
|
||||
console.log(item);
|
||||
confirmSelect(item);
|
||||
confirmSelect('knowledge', item);
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = idx;
|
||||
@@ -210,6 +228,12 @@
|
||||
>
|
||||
File
|
||||
</div>
|
||||
{:else if item?.type === 'note'}
|
||||
<div
|
||||
class="bg-blue-500/20 text-blue-700 dark:text-blue-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
|
||||
>
|
||||
Note
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
|
||||
@@ -272,18 +296,15 @@
|
||||
</div> -->
|
||||
{/each}
|
||||
|
||||
{#if prompt
|
||||
.split(' ')
|
||||
.some((s) => s.substring(1).startsWith('https://www.youtube.com') || s
|
||||
.substring(1)
|
||||
.startsWith('https://youtu.be'))}
|
||||
{#if command.substring(1).startsWith('https://www.youtube.com') || command
|
||||
.substring(1)
|
||||
.startsWith('https://youtu.be')}
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
const url = prompt.split(' ')?.at(0)?.substring(1);
|
||||
if (isValidHttpUrl(url)) {
|
||||
confirmSelectYoutube(url);
|
||||
if (isValidHttpUrl(command.substring(1))) {
|
||||
confirmSelect('youtube', command.substring(1));
|
||||
} else {
|
||||
toast.error(
|
||||
$i18n.t(
|
||||
@@ -294,19 +315,18 @@
|
||||
}}
|
||||
>
|
||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
||||
{command.substring(1)}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
|
||||
</button>
|
||||
{:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
{:else if command.substring(1).startsWith('http')}
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
const url = prompt.split(' ')?.at(0)?.substring(1);
|
||||
if (isValidHttpUrl(url)) {
|
||||
confirmSelectWeb(url);
|
||||
if (isValidHttpUrl(command.substring(1))) {
|
||||
confirmSelect('web', command.substring(1));
|
||||
} else {
|
||||
toast.error(
|
||||
$i18n.t(
|
||||
@@ -317,7 +337,7 @@
|
||||
}}
|
||||
>
|
||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
||||
{command}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { tick, getContext } from 'svelte';
|
||||
|
||||
import { models } from '$lib/stores';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let command = '';
|
||||
export let onSelect = (e) => {};
|
||||
|
||||
let selectedIdx = 0;
|
||||
let filteredItems = [];
|
||||
@@ -51,18 +51,43 @@
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
||||
};
|
||||
|
||||
let container;
|
||||
let adjustHeightDebounce;
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (container) {
|
||||
if (adjustHeightDebounce) {
|
||||
clearTimeout(adjustHeightDebounce);
|
||||
}
|
||||
|
||||
adjustHeightDebounce = setTimeout(() => {
|
||||
if (!container) return;
|
||||
|
||||
// Ensure the container is visible before adjusting height
|
||||
const rect = container.getBoundingClientRect();
|
||||
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmSelect = async (model) => {
|
||||
command = '';
|
||||
dispatch('select', model);
|
||||
onSelect({ type: 'model', data: model });
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
window.addEventListener('resize', adjustHeight);
|
||||
adjustHeight();
|
||||
|
||||
await tick();
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('resize', adjustHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length > 0}
|
||||
@@ -75,6 +100,7 @@
|
||||
<div
|
||||
class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden max-h-60"
|
||||
id="command-options-container"
|
||||
bind:this={container}
|
||||
>
|
||||
{#each filteredItems as model, modelIdx}
|
||||
<button
|
||||
@@ -92,7 +118,8 @@
|
||||
>
|
||||
<div class="flex font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
<img
|
||||
src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
`${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
alt={model?.name ?? model.id}
|
||||
class="rounded-full size-6 items-center mr-2"
|
||||
/>
|
||||
|
||||
@@ -9,15 +9,13 @@
|
||||
getUserTimezone,
|
||||
getWeekday
|
||||
} from '$lib/utils';
|
||||
import { tick, getContext } from 'svelte';
|
||||
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let files;
|
||||
|
||||
export let prompt = '';
|
||||
export let command = '';
|
||||
export let onSelect = (e) => {};
|
||||
|
||||
let selectedPromptIdx = 0;
|
||||
let filteredPrompts = [];
|
||||
@@ -38,140 +36,37 @@
|
||||
selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
|
||||
};
|
||||
|
||||
const confirmPrompt = async (command) => {
|
||||
let text = command.content;
|
||||
let container;
|
||||
let adjustHeightDebounce;
|
||||
|
||||
if (command.content.includes('{{CLIPBOARD}}')) {
|
||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||
return '{{CLIPBOARD}}';
|
||||
});
|
||||
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
|
||||
let imageUrl = null;
|
||||
for (const item of clipboardItems) {
|
||||
// Check for known image types
|
||||
for (const type of item.types) {
|
||||
if (type.startsWith('image/')) {
|
||||
const blob = await item.getType(type);
|
||||
imageUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
}
|
||||
const adjustHeight = () => {
|
||||
if (container) {
|
||||
if (adjustHeightDebounce) {
|
||||
clearTimeout(adjustHeightDebounce);
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: 'image',
|
||||
url: imageUrl
|
||||
}
|
||||
];
|
||||
}
|
||||
adjustHeightDebounce = setTimeout(() => {
|
||||
if (!container) return;
|
||||
|
||||
text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{USER_LOCATION}}')) {
|
||||
let location;
|
||||
try {
|
||||
location = await getUserPosition();
|
||||
} catch (error) {
|
||||
toast.error($i18n.t('Location access not allowed'));
|
||||
location = 'LOCATION_UNKNOWN';
|
||||
}
|
||||
text = text.replaceAll('{{USER_LOCATION}}', String(location));
|
||||
}
|
||||
|
||||
if (command.content.includes('{{USER_NAME}}')) {
|
||||
console.log($user);
|
||||
const name = $user?.name || 'User';
|
||||
text = text.replaceAll('{{USER_NAME}}', name);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{USER_LANGUAGE}}')) {
|
||||
const language = localStorage.getItem('locale') || 'en-US';
|
||||
text = text.replaceAll('{{USER_LANGUAGE}}', language);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_DATE}}')) {
|
||||
const date = getFormattedDate();
|
||||
text = text.replaceAll('{{CURRENT_DATE}}', date);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_TIME}}')) {
|
||||
const time = getFormattedTime();
|
||||
text = text.replaceAll('{{CURRENT_TIME}}', time);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_DATETIME}}')) {
|
||||
const dateTime = getCurrentDateTime();
|
||||
text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_TIMEZONE}}')) {
|
||||
const timezone = getUserTimezone();
|
||||
text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_WEEKDAY}}')) {
|
||||
const weekday = getWeekday();
|
||||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||
}
|
||||
|
||||
const lines = prompt.split('\n');
|
||||
const lastLine = lines.pop();
|
||||
|
||||
const lastLineWords = lastLine.split(' ');
|
||||
const lastWord = lastLineWords.pop();
|
||||
|
||||
if ($settings?.richTextInput ?? true) {
|
||||
lastLineWords.push(
|
||||
`${text.replace(/</g, '<').replace(/>/g, '>').replaceAll('\n', '<br/>')}`
|
||||
);
|
||||
|
||||
lines.push(lastLineWords.join(' '));
|
||||
prompt = lines.join('<br/>');
|
||||
} else {
|
||||
lastLineWords.push(text);
|
||||
lines.push(lastLineWords.join(' '));
|
||||
prompt = lines.join('\n');
|
||||
}
|
||||
|
||||
const chatInputContainerElement = document.getElementById('chat-input-container');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
await tick();
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.scrollTop = chatInputContainerElement.scrollHeight;
|
||||
}
|
||||
|
||||
await tick();
|
||||
if (chatInputElement) {
|
||||
chatInputElement.focus();
|
||||
chatInputElement.dispatchEvent(new Event('input'));
|
||||
|
||||
const words = extractCurlyBraceWords(prompt);
|
||||
|
||||
if (words.length > 0) {
|
||||
const word = words.at(0);
|
||||
const fullPrompt = prompt;
|
||||
|
||||
prompt = prompt.substring(0, word?.endIndex + 1);
|
||||
await tick();
|
||||
|
||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||
|
||||
prompt = fullPrompt;
|
||||
await tick();
|
||||
|
||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
} else {
|
||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||
}
|
||||
// Ensure the container is visible before adjusting height
|
||||
const rect = container.getBoundingClientRect();
|
||||
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 80), 100) + 'px';
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmPrompt = async (command) => {
|
||||
onSelect({ type: 'prompt', data: command });
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('resize', adjustHeight);
|
||||
adjustHeight();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('resize', adjustHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredPrompts.length > 0}
|
||||
@@ -184,15 +79,16 @@
|
||||
<div
|
||||
class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden max-h-60"
|
||||
id="command-options-container"
|
||||
bind:this={container}
|
||||
>
|
||||
{#each filteredPrompts as prompt, promptIdx}
|
||||
{#each filteredPrompts as promptItem, promptIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
|
||||
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
confirmPrompt(prompt);
|
||||
confirmPrompt(promptItem);
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedPromptIdx = promptIdx;
|
||||
@@ -200,11 +96,11 @@
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class=" font-medium text-black dark:text-gray-100">
|
||||
{prompt.command}
|
||||
{promptItem.command}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 dark:text-gray-100">
|
||||
{prompt.title}
|
||||
{promptItem.title}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
import { models, config } from '$lib/stores';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import MapSelector from '$lib/components/common/Valves/MapSelector.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
export let variables = {};
|
||||
|
||||
export let onSave = (e) => {};
|
||||
|
||||
let loading = false;
|
||||
let variableValues = {};
|
||||
|
||||
const submitHandler = async () => {
|
||||
onSave(variableValues);
|
||||
show = false;
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
loading = true;
|
||||
variableValues = {};
|
||||
for (const variable of Object.keys(variables)) {
|
||||
if (variables[variable]?.default !== undefined) {
|
||||
variableValues[variable] = variables[variable].default;
|
||||
} else {
|
||||
variableValues[variable] = '';
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
|
||||
await tick();
|
||||
|
||||
const firstInputElement = document.getElementById('input-variable-0');
|
||||
if (firstInputElement) {
|
||||
console.log('Focusing first input element:', firstInputElement);
|
||||
firstInputElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="md">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Input Variables')}</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
on:submit|preventDefault={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class="px-1">
|
||||
{#if !loading}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each Object.keys(variables) as variable, idx}
|
||||
{@const { type, ...variableAttributes } = variables[variable] ?? {}}
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between mb-1.5">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{variable}
|
||||
|
||||
{#if variables[variable]?.required ?? true}
|
||||
<span class=" text-gray-500">*required</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-0.5 mb-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
{#if variables[variable]?.type === 'select'}
|
||||
{@const options = variableAttributes?.options ?? []}
|
||||
{@const placeholder = variableAttributes?.placeholder ?? ''}
|
||||
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
bind:value={variableValues[variable]}
|
||||
id="input-variable-{idx}"
|
||||
>
|
||||
{#if placeholder}
|
||||
<option value="" disabled selected>
|
||||
{placeholder}
|
||||
</option>
|
||||
{/if}
|
||||
{#each options as option}
|
||||
<option value={option} selected={option === variableValues[variable]}>
|
||||
{option}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if variables[variable]?.type === 'checkbox'}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative flex justify-center items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={variableValues[variable]}
|
||||
class="size-3.5 rounded cursor-pointer border border-gray-200 dark:border-gray-700"
|
||||
id="input-variable-{idx}"
|
||||
{...variableAttributes}
|
||||
/>
|
||||
|
||||
<label for="input-variable-{idx}" class="text-sm"
|
||||
>{variables[variable]?.label ?? variable}</label
|
||||
>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="flex-1 py-1 text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
placeholder="Enter value (true/false)"
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{:else if variables[variable]?.type === 'color'}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative size-6">
|
||||
<input
|
||||
type="color"
|
||||
class="size-6 rounded cursor-pointer border border-gray-200 dark:border-gray-700"
|
||||
value={variableValues[variable]}
|
||||
id="input-variable-{idx}"
|
||||
on:input={(e) => {
|
||||
// Convert the color value to uppercase immediately
|
||||
variableValues[variable] = e.target.value.toUpperCase();
|
||||
}}
|
||||
{...variableAttributes}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="flex-1 py-2 text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
placeholder="Enter hex color (e.g. #FF0000)"
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{:else if variables[variable]?.type === 'date'}
|
||||
<input
|
||||
type="date"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
{...variableAttributes}
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'datetime-local'}
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
{...variableAttributes}
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'email'}
|
||||
<input
|
||||
type="email"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
{...variableAttributes}
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'month'}
|
||||
<input
|
||||
type="month"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
{...variableAttributes}
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'number'}
|
||||
<input
|
||||
type="number"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
{...variableAttributes}
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'range'}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative flex justify-center items-center gap-2 flex-1">
|
||||
<input
|
||||
type="range"
|
||||
bind:value={variableValues[variable]}
|
||||
class="w-full rounded-lg py-1 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
id="input-variable-{idx}"
|
||||
{...variableAttributes}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class=" py-1 text-sm dark:text-gray-300 bg-transparent outline-hidden text-right"
|
||||
placeholder="Enter value"
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- <input
|
||||
type="range"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
/> -->
|
||||
{:else if variables[variable]?.type === 'tel'}
|
||||
<input
|
||||
type="tel"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
{...variableAttributes}
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'text'}
|
||||
<input
|
||||
type="text"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
{...variableAttributes}
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'time'}
|
||||
<input
|
||||
type="time"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
{...variableAttributes}
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'url'}
|
||||
<input
|
||||
type="url"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
{...variableAttributes}
|
||||
/>
|
||||
{:else if variables[variable]?.type === 'map'}
|
||||
<!-- EXPERIMENTAL INPUT TYPE, DO NOT USE IN PRODUCTION -->
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<MapSelector
|
||||
setViewLocation={((variableValues[variable] ?? '').includes(',') ??
|
||||
false)
|
||||
? variableValues[variable].split(',')
|
||||
: null}
|
||||
onClick={(value) => {
|
||||
variableValues[variable] = value;
|
||||
}}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class=" w-full py-1 text-left text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
placeholder="Enter coordinates (e.g. 51.505, -0.09)"
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<textarea
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder={variables[variable]?.placeholder ?? ''}
|
||||
bind:value={variableValues[variable]}
|
||||
autocomplete="off"
|
||||
id="input-variable-{idx}"
|
||||
required
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- {#if (valvesSpec.properties[property]?.description ?? null) !== null}
|
||||
<div class="text-xs text-gray-500">
|
||||
{valvesSpec.properties[property].description}
|
||||
</div>
|
||||
{/if} -->
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Spinner className="size-5" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-white hover:bg-gray-100 text-black dark:bg-black dark:text-white dark:hover:bg-gray-900 transition rounded-full"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -5,6 +5,7 @@
|
||||
import { blobToFile, calculateSHA256, extractCurlyBraceWords } from '$lib/utils';
|
||||
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
@@ -406,16 +407,7 @@
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="3"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
export let bottomPadding = false;
|
||||
export let autoScroll;
|
||||
|
||||
export let onSelect = (e) => {};
|
||||
|
||||
let messagesCount = 20;
|
||||
let messagesLoading = false;
|
||||
|
||||
@@ -395,25 +397,7 @@
|
||||
|
||||
<div class={className}>
|
||||
{#if Object.keys(history?.messages ?? {}).length == 0}
|
||||
<ChatPlaceholder
|
||||
modelIds={selectedModels}
|
||||
{atSelectedModel}
|
||||
submitPrompt={async (p) => {
|
||||
let text = p;
|
||||
|
||||
if (p.includes('{{CLIPBOARD}}')) {
|
||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||
return '{{CLIPBOARD}}';
|
||||
});
|
||||
|
||||
text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
}
|
||||
|
||||
prompt = text;
|
||||
await tick();
|
||||
}}
|
||||
/>
|
||||
<ChatPlaceholder modelIds={selectedModels} {atSelectedModel} {onSelect} />
|
||||
{:else}
|
||||
<div class="w-full pt-2">
|
||||
{#key chatId}
|
||||
@@ -438,6 +422,7 @@
|
||||
<Message
|
||||
{chatId}
|
||||
bind:history
|
||||
{selectedModels}
|
||||
messageId={message.id}
|
||||
idx={messageIdx}
|
||||
{user}
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
console.log('sources', sources);
|
||||
citations = sources.reduce((acc, source) => {
|
||||
if (Object.keys(source).length === 0) {
|
||||
return acc;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
@@ -67,16 +69,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -224,7 +224,8 @@
|
||||
code.includes('sympy') ? 'sympy' : null,
|
||||
code.includes('tiktoken') ? 'tiktoken' : null,
|
||||
code.includes('matplotlib') ? 'matplotlib' : null,
|
||||
code.includes('pytz') ? 'pytz' : null
|
||||
code.includes('pytz') ? 'pytz' : null,
|
||||
code.includes('openai') ? 'openai' : null
|
||||
].filter(Boolean);
|
||||
|
||||
console.log(packages);
|
||||
@@ -385,7 +386,6 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
console.log('codeblock', lang, code);
|
||||
if (token) {
|
||||
onUpdate(token);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
@@ -49,16 +50,7 @@
|
||||
codeExecution = null;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
export let id;
|
||||
export let content;
|
||||
export let history;
|
||||
export let selectedModels = [];
|
||||
|
||||
export let model = null;
|
||||
export let sources = null;
|
||||
|
||||
@@ -25,11 +27,10 @@
|
||||
export let preview = false;
|
||||
export let floatingButtons = true;
|
||||
|
||||
export let onSave = () => {};
|
||||
export let onSourceClick = () => {};
|
||||
export let onTaskClick = () => {};
|
||||
|
||||
export let onAddMessages = () => {};
|
||||
export let onSave = (e) => {};
|
||||
export let onSourceClick = (e) => {};
|
||||
export let onTaskClick = (e) => {};
|
||||
export let onAddMessages = (e) => {};
|
||||
|
||||
let contentContainerElement;
|
||||
|
||||
@@ -192,7 +193,11 @@
|
||||
<FloatingButtons
|
||||
bind:this={floatingButtonsElement}
|
||||
{id}
|
||||
model={model?.id}
|
||||
model={(selectedModels ?? []).includes(model?.id)
|
||||
? model?.id
|
||||
: (selectedModels ?? []).length > 0
|
||||
? selectedModels.at(0)
|
||||
: model?.id}
|
||||
messages={createMessagesList(history, id)}
|
||||
onAdd={({ modelId, parentId, messages }) => {
|
||||
console.log(modelId, parentId, messages);
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
let tokens = [];
|
||||
|
||||
const options = {
|
||||
throwOnError: false
|
||||
throwOnError: false,
|
||||
breaks: true
|
||||
};
|
||||
|
||||
marked.use(markedKatexExtension(options));
|
||||
|
||||
@@ -84,6 +84,23 @@
|
||||
{:else}
|
||||
{token.text}
|
||||
{/if}
|
||||
{:else if token.text && token.text.includes('<status')}
|
||||
{@const match = token.text.match(/<status title="([^"]+)" done="(true|false)" ?\/?>/)}
|
||||
{@const statusTitle = match && match[1]}
|
||||
{@const statusDone = match && match[2] === 'true'}
|
||||
{#if statusTitle}
|
||||
<div class="flex flex-col justify-center -space-y-0.5">
|
||||
<div
|
||||
class="{statusDone === false
|
||||
? 'shimmer'
|
||||
: ''} text-gray-500 dark:text-gray-500 line-clamp-1 text-wrap"
|
||||
>
|
||||
{statusTitle}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{token.text}
|
||||
{/if}
|
||||
{:else if token.text.includes(`<file type="html"`)}
|
||||
{@const match = token.text.match(/<file type="html" id="([^"]+)"/)}
|
||||
{@const fileId = match && match[1]}
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
{#each row ?? [] as cell, cellIdx}
|
||||
<td
|
||||
class="px-3! py-1.5! text-gray-900 dark:text-white w-max border border-gray-100 dark:border-gray-850"
|
||||
style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
|
||||
style={token.align[cellIdx] ? `text-align: ${token.align[cellIdx]}` : ''}
|
||||
>
|
||||
<div class="break-normal">
|
||||
<MarkdownInlineTokens
|
||||
@@ -220,12 +220,12 @@
|
||||
{/each}
|
||||
</ol>
|
||||
{:else}
|
||||
<ul dir="auto">
|
||||
<ul dir="auto" class="">
|
||||
{#each token.items as item, itemIdx}
|
||||
<li class="text-start">
|
||||
<li class="text-start {item?.task ? 'flex -translate-x-6.5 gap-3 ' : ''}">
|
||||
{#if item?.task}
|
||||
<input
|
||||
class=" translate-y-[1px] -translate-x-1"
|
||||
class=""
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
on:change={(e) => {
|
||||
@@ -239,15 +239,25 @@
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<svelte:self
|
||||
id={`${id}-${tokenIdx}-${itemIdx}`}
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
{onTaskClick}
|
||||
{onSourceClick}
|
||||
/>
|
||||
<div>
|
||||
<svelte:self
|
||||
id={`${id}-${tokenIdx}-${itemIdx}`}
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
{onTaskClick}
|
||||
{onSourceClick}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<svelte:self
|
||||
id={`${id}-${tokenIdx}-${itemIdx}`}
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
{onTaskClick}
|
||||
{onSourceClick}
|
||||
/>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import UserMessage from './UserMessage.svelte';
|
||||
|
||||
export let chatId;
|
||||
export let selectedModels = [];
|
||||
export let idx = 0;
|
||||
|
||||
export let history;
|
||||
@@ -70,6 +71,7 @@
|
||||
{chatId}
|
||||
{history}
|
||||
{messageId}
|
||||
{selectedModels}
|
||||
isLastMessage={messageId === history.currentId}
|
||||
siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
|
||||
{gotoMessage}
|
||||
@@ -92,6 +94,7 @@
|
||||
bind:history
|
||||
{chatId}
|
||||
{messageId}
|
||||
{selectedModels}
|
||||
isLastMessage={messageId === history?.currentId}
|
||||
{updateChat}
|
||||
{editMessage}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
export let chatId;
|
||||
export let history;
|
||||
export let messageId;
|
||||
export let selectedModels = [];
|
||||
|
||||
export let isLastMessage;
|
||||
export let readOnly = false;
|
||||
@@ -252,6 +253,7 @@
|
||||
{chatId}
|
||||
{history}
|
||||
messageId={_messageId}
|
||||
{selectedModels}
|
||||
isLastMessage={true}
|
||||
siblings={groupedMessageIds[modelIdx].messageIds}
|
||||
gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
src.startsWith('data:') ||
|
||||
src.startsWith('/')
|
||||
? src
|
||||
: `/user.png`}
|
||||
: `${WEBUI_BASE_URL}/user.png`}
|
||||
class=" {className} object-cover rounded-full"
|
||||
alt="profile"
|
||||
draggable="false"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { config, models } from '$lib/stores';
|
||||
import Tags from '$lib/components/common/Tags.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -123,16 +124,7 @@
|
||||
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>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
export let chatId = '';
|
||||
export let history;
|
||||
export let messageId;
|
||||
export let selectedModels = [];
|
||||
|
||||
let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||
$: if (history.messages) {
|
||||
@@ -601,15 +602,17 @@
|
||||
id="message-{message.id}"
|
||||
dir={$settings.chatDirection}
|
||||
>
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex `}>
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex mt-1 `}>
|
||||
<ProfileImage
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
($i18n.language === 'dg-DG'
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/favicon.png`)}
|
||||
className={'size-8 assistant-message-profile-image'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-auto w-0 pl-1 relative -translate-y-0.5">
|
||||
<div class="flex-auto w-0 pl-1 relative">
|
||||
<Name>
|
||||
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
||||
<span class="line-clamp-1 text-black dark:text-white">
|
||||
@@ -795,6 +798,7 @@
|
||||
<ContentRenderer
|
||||
id={message.id}
|
||||
{history}
|
||||
{selectedModels}
|
||||
content={message.content}
|
||||
sources={message.sources}
|
||||
floatingButtons={message?.done && !readOnly}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
|
||||
export let status = { urls: [], query: '' };
|
||||
@@ -31,7 +31,7 @@
|
||||
class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<MagnifyingGlass />
|
||||
<Search />
|
||||
|
||||
<div class=" line-clamp-1">
|
||||
{status.query}
|
||||
|
||||
@@ -2,44 +2,29 @@
|
||||
export let size = 'md';
|
||||
</script>
|
||||
|
||||
<div class="w-full mt-2 mb-2">
|
||||
<div class="animate-pulse flex w-full">
|
||||
<div class="{size === 'md' ? 'space-y-2' : 'space-y-1.5'} w-full">
|
||||
<div
|
||||
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm mr-14"
|
||||
/>
|
||||
<span class="relative flex {size === 'md' ? 'size-3 my-2' : 'size-2 my-1'} mx-1">
|
||||
<span
|
||||
class="absolute inline-flex h-full w-full animate-pulse rounded-full bg-gray-700 dark:bg-gray-200 opacity-75"
|
||||
></span>
|
||||
<span
|
||||
class="relative inline-flex {size === 'md'
|
||||
? 'size-3'
|
||||
: 'size-2'} rounded-full bg-black dark:bg-white animate-size"
|
||||
></span>
|
||||
</span>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1 mr-4"
|
||||
/>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes size {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
}
|
||||
|
||||
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.animate-size {
|
||||
animation: size 1.5s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { models, settings } from '$lib/stores';
|
||||
import { user as _user } from '$lib/stores';
|
||||
import { copyToClipboard as _copyToClipboard, formatDate } from '$lib/utils';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
@@ -113,12 +114,12 @@
|
||||
id="message-{message.id}"
|
||||
>
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3`}>
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 mt-1`}>
|
||||
<ProfileImage
|
||||
src={message.user
|
||||
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
|
||||
'/user.png')
|
||||
: (user?.profile_image_url ?? '/user.png')}
|
||||
`${WEBUI_BASE_URL}/user.png`)
|
||||
: (user?.profile_image_url ?? `${WEBUI_BASE_URL}/user.png`)}
|
||||
className={'size-8 user-message-profile-image'}
|
||||
/>
|
||||
</div>
|
||||
@@ -148,9 +149,9 @@
|
||||
</Name>
|
||||
</div>
|
||||
{:else if message.timestamp}
|
||||
<div class="flex justify-end pb-1 pr-2">
|
||||
<div class="flex justify-end pr-2 text-xs">
|
||||
<div
|
||||
class="text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize translate-y-[1px]"
|
||||
class="text-[0.65rem] invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize mb-0.5"
|
||||
>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
|
||||
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
|
||||
@@ -162,7 +163,7 @@
|
||||
<div class="chat-{message.role} w-full min-w-full markdown-prose">
|
||||
{#if edit !== true}
|
||||
{#if message.files}
|
||||
<div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap">
|
||||
<div class="mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap">
|
||||
{#each message.files as file}
|
||||
<div class={($settings?.chatBubble ?? true) ? 'self-end' : ''}>
|
||||
{#if file.type === 'image'}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||
<div class="flex w-full max-w-fit">
|
||||
<div class="overflow-hidden w-full">
|
||||
<div class="mr-1 max-w-full">
|
||||
<div class="max-w-full {($settings?.highContrastMode ?? false) ? 'm-1' : 'mr-1'}">
|
||||
<Selector
|
||||
id={`${selectedModelIdx}`}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import dayjs from '$lib/dayjs';
|
||||
|
||||
import { mobile, settings, user } from '$lib/stores';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { copyToClipboard, sanitizeResponseContent } from '$lib/utils';
|
||||
@@ -73,7 +74,8 @@
|
||||
<div class="flex items-center min-w-fit">
|
||||
<Tooltip content={$user?.role === 'admin' ? (item?.value ?? '') : ''} placement="top-start">
|
||||
<img
|
||||
src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
src={item.model?.info?.meta?.profile_image_url ??
|
||||
`${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
alt="Model"
|
||||
class="rounded-full size-5 flex items-center"
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -345,12 +346,17 @@
|
||||
closeFocus={false}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
class="relative w-full font-primary"
|
||||
class="relative w-full font-primary {($settings?.highContrastMode ?? false)
|
||||
? ''
|
||||
: 'outline-hidden focus:outline-hidden'}"
|
||||
aria-label={placeholder}
|
||||
id="model-selector-{id}-button"
|
||||
>
|
||||
<button
|
||||
class="flex w-full text-left px-0.5 outline-hidden bg-transparent truncate {triggerClassName} justify-between font-medium placeholder-gray-400 focus:outline-hidden"
|
||||
<div
|
||||
class="flex w-full text-left px-0.5 bg-transparent truncate {triggerClassName} justify-between {($settings?.highContrastMode ??
|
||||
false)
|
||||
? 'dark:placeholder-gray-100 placeholder-gray-800'
|
||||
: 'placeholder-gray-400'}"
|
||||
on:mouseenter={async () => {
|
||||
models.set(
|
||||
await getModels(
|
||||
@@ -359,7 +365,6 @@
|
||||
)
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if selectedModel}
|
||||
{selectedModel.label}
|
||||
@@ -367,7 +372,7 @@
|
||||
{placeholder}
|
||||
{/if}
|
||||
<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
@@ -550,29 +555,7 @@
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="-ml-2 mr-2.5 translate-y-0.5">
|
||||
<svg
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col self-start">
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
export let history;
|
||||
export let selectedModels;
|
||||
export let showModelSelector = true;
|
||||
export let showBanners = true;
|
||||
|
||||
let closedBannerIds = [];
|
||||
|
||||
let showShareChatModal = false;
|
||||
let showDownloadChatModal = false;
|
||||
@@ -162,6 +165,24 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if $mobile}
|
||||
<Tooltip content={$i18n.t('New Chat')}>
|
||||
<button
|
||||
class=" flex {$showSidebar
|
||||
? 'md:hidden'
|
||||
: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
on:click={() => {
|
||||
initNewChat();
|
||||
}}
|
||||
aria-label="New Chat"
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<PencilSquare className=" size-5" strokeWidth="2" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if $user !== undefined && $user !== null}
|
||||
<UserMenu
|
||||
className="max-w-[240px]"
|
||||
@@ -173,19 +194,19 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
<div
|
||||
class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
aria-label="User Menu"
|
||||
>
|
||||
<div class=" self-center">
|
||||
<span class="sr-only">{$i18n.t('User menu')}</span>
|
||||
<img
|
||||
src={$user?.profile_image_url}
|
||||
class="size-6 object-cover rounded-full"
|
||||
alt="User profile"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</UserMenu>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -199,52 +220,60 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !history.currentId && !$chatId && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
|
||||
<div class=" w-full z-30 mt-5">
|
||||
<div class=" flex flex-col gap-1 w-full">
|
||||
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
|
||||
<Banner
|
||||
banner={{
|
||||
type: 'info',
|
||||
title: 'Trial License',
|
||||
content: $i18n.t(
|
||||
'You are currently using a trial license. Please contact support to upgrade your license.'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if ($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats}
|
||||
<Banner
|
||||
banner={{
|
||||
type: 'error',
|
||||
title: 'License Error',
|
||||
content: $i18n.t(
|
||||
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
||||
<Banner
|
||||
{banner}
|
||||
on:dismiss={(e) => {
|
||||
const bannerId = e.detail;
|
||||
|
||||
localStorage.setItem(
|
||||
'dismissedBannerIds',
|
||||
JSON.stringify(
|
||||
[
|
||||
bannerId,
|
||||
...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
|
||||
].filter((id) => $banners.find((b) => b.id === id))
|
||||
<div class="absolute top-[100%] left-0 right-0 h-fit">
|
||||
{#if !history.currentId && !$chatId && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
|
||||
<div class=" w-full z-30 mt-5">
|
||||
<div class=" flex flex-col gap-1 w-full">
|
||||
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
|
||||
<Banner
|
||||
banner={{
|
||||
type: 'info',
|
||||
title: 'Trial License',
|
||||
content: $i18n.t(
|
||||
'You are currently using a trial license. Please contact support to upgrade your license.'
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if ($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats}
|
||||
<Banner
|
||||
banner={{
|
||||
type: 'error',
|
||||
title: 'License Error',
|
||||
content: $i18n.t(
|
||||
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if showBanners}
|
||||
{#each $banners.filter((b) => ![...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]'), ...closedBannerIds].includes(b.id)) as banner}
|
||||
<Banner
|
||||
{banner}
|
||||
on:dismiss={(e) => {
|
||||
const bannerId = e.detail;
|
||||
|
||||
if (banner.dismissible) {
|
||||
localStorage.setItem(
|
||||
'dismissedBannerIds',
|
||||
JSON.stringify(
|
||||
[
|
||||
bannerId,
|
||||
...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
|
||||
].filter((id) => $banners.find((b) => b.id === id))
|
||||
)
|
||||
);
|
||||
} else {
|
||||
closedBannerIds = [...closedBannerIds, bannerId];
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{#if data.message.role === 'user'}
|
||||
<div class="flex w-full">
|
||||
<ProfileImage
|
||||
src={data.user?.profile_image_url ?? '/user.png'}
|
||||
src={data.user?.profile_image_url ?? `${WEBUI_BASE_URL}/user.png`}
|
||||
className={'size-5 -translate-y-[1px]'}
|
||||
/>
|
||||
<div class="ml-2">
|
||||
|
||||
@@ -32,53 +32,23 @@
|
||||
|
||||
export let prompt = '';
|
||||
export let files = [];
|
||||
export let messageInput = null;
|
||||
|
||||
export let selectedToolIds = [];
|
||||
export let selectedFilterIds = [];
|
||||
|
||||
export let showCommands = false;
|
||||
|
||||
export let imageGenerationEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
export let webSearchEnabled = false;
|
||||
|
||||
export let onSelect = (e) => {};
|
||||
|
||||
export let toolServers = [];
|
||||
|
||||
let models = [];
|
||||
|
||||
const selectSuggestionPrompt = async (p) => {
|
||||
let text = p;
|
||||
|
||||
if (p.includes('{{CLIPBOARD}}')) {
|
||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||
return '{{CLIPBOARD}}';
|
||||
});
|
||||
|
||||
text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
|
||||
console.log('Clipboard text:', clipboardText, text);
|
||||
}
|
||||
|
||||
prompt = text;
|
||||
|
||||
console.log(prompt);
|
||||
await tick();
|
||||
|
||||
const chatInputContainerElement = document.getElementById('chat-input-container');
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
|
||||
if (chatInputContainerElement) {
|
||||
chatInputContainerElement.scrollTop = chatInputContainerElement.scrollHeight;
|
||||
}
|
||||
|
||||
await tick();
|
||||
if (chatInputElement) {
|
||||
chatInputElement.focus();
|
||||
chatInputElement.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
await tick();
|
||||
};
|
||||
|
||||
let selectedModelIdx = 0;
|
||||
|
||||
$: if (selectedModels.length > 0) {
|
||||
@@ -118,6 +88,10 @@
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
aria-hidden={models.length <= 1}
|
||||
aria-label={$i18n.t('Get information on {{name}} in the UI', {
|
||||
name: models[modelIdx]?.name
|
||||
})}
|
||||
on:click={() => {
|
||||
selectedModelIdx = modelIdx;
|
||||
}}
|
||||
@@ -126,10 +100,10 @@
|
||||
crossorigin="anonymous"
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG'
|
||||
? `/doge.png`
|
||||
? `${WEBUI_BASE_URL}/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
|
||||
alt="logo"
|
||||
aria-hidden="true"
|
||||
draggable="false"
|
||||
/>
|
||||
</button>
|
||||
@@ -164,7 +138,9 @@
|
||||
<Tooltip
|
||||
className=" w-fit"
|
||||
content={marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
|
||||
sanitizeResponseContent(
|
||||
models[selectedModelIdx]?.info?.meta?.description ?? ''
|
||||
).replaceAll('\n', '<br>')
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
@@ -172,7 +148,9 @@
|
||||
class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
|
||||
>
|
||||
{@html marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
|
||||
sanitizeResponseContent(
|
||||
models[selectedModelIdx]?.info?.meta?.description ?? ''
|
||||
).replaceAll('\n', '<br>')
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -199,6 +177,7 @@
|
||||
|
||||
<div class="text-base font-normal @md:max-w-3xl w-full py-3 {atSelectedModel ? 'mt-2' : ''}">
|
||||
<MessageInput
|
||||
bind:this={messageInput}
|
||||
{history}
|
||||
{selectedModels}
|
||||
bind:files
|
||||
@@ -210,6 +189,7 @@
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:atSelectedModel
|
||||
bind:showCommands
|
||||
{toolServers}
|
||||
{transparentBackground}
|
||||
{stopResponse}
|
||||
@@ -218,9 +198,9 @@
|
||||
onChange={(input) => {
|
||||
if (!$temporaryChatEnabled) {
|
||||
if (input.prompt !== null) {
|
||||
localStorage.setItem(`chat-input`, JSON.stringify(input));
|
||||
sessionStorage.setItem(`chat-input`, JSON.stringify(input));
|
||||
} else {
|
||||
localStorage.removeItem(`chat-input`);
|
||||
sessionStorage.removeItem(`chat-input`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -242,9 +222,7 @@
|
||||
$config?.default_prompt_suggestions ??
|
||||
[]}
|
||||
inputValue={prompt}
|
||||
on:select={(e) => {
|
||||
selectSuggestionPrompt(e.detail);
|
||||
}}
|
||||
{onSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
return '';
|
||||
});
|
||||
|
||||
checkForVersionUpdates();
|
||||
if ($config?.features?.enable_version_update_check) {
|
||||
checkForVersionUpdates();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -58,16 +60,18 @@
|
||||
v{WEBUI_VERSION}
|
||||
</Tooltip>
|
||||
|
||||
<a
|
||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||
target="_blank"
|
||||
>
|
||||
{updateAvailable === null
|
||||
? $i18n.t('Checking for updates...')
|
||||
: updateAvailable
|
||||
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||
: $i18n.t('(latest)')}
|
||||
</a>
|
||||
{#if $config?.features?.enable_version_update_check}
|
||||
<a
|
||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||
target="_blank"
|
||||
>
|
||||
{updateAvailable === null
|
||||
? $i18n.t('Checking for updates...')
|
||||
: updateAvailable
|
||||
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||
: $i18n.t('(latest)')}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -80,14 +84,16 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
{#if $config?.features?.enable_version_update_check}
|
||||
<button
|
||||
class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { user, config, settings } from '$lib/stores';
|
||||
import { updateUserProfile, createAPIKey, getAPIKey, getSessionUser } from '$lib/apis/auths';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import UpdatePassword from './Account/UpdatePassword.svelte';
|
||||
import { getGravatarUrl } from '$lib/apis/utils';
|
||||
@@ -223,7 +224,7 @@
|
||||
<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';
|
||||
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
|
||||
}}>{$i18n.t('Remove')}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
|
||||
import {
|
||||
archiveAllChats,
|
||||
createNewChat,
|
||||
deleteAllChats,
|
||||
getAllChats,
|
||||
getAllUserChats,
|
||||
getChatList
|
||||
getChatList,
|
||||
importChat
|
||||
} from '$lib/apis/chats';
|
||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
@@ -58,9 +57,18 @@
|
||||
console.log(chat);
|
||||
|
||||
if (chat.chat) {
|
||||
await createNewChat(localStorage.token, chat.chat);
|
||||
await importChat(
|
||||
localStorage.token,
|
||||
chat.chat,
|
||||
chat.meta ?? {},
|
||||
false,
|
||||
null,
|
||||
chat?.created_at ?? null,
|
||||
chat?.updated_at ?? null
|
||||
);
|
||||
} else {
|
||||
await createNewChat(localStorage.token, chat);
|
||||
// Legacy format
|
||||
await importChat(localStorage.token, chat, {}, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +109,7 @@
|
||||
const handleArchivedChatsChange = async () => {
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
|
||||
scrollPaginationEnabled.set(true);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -274,7 +274,10 @@
|
||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
||||
<Textarea
|
||||
bind:value={system}
|
||||
className="w-full text-sm bg-white dark:text-gray-300 dark:bg-gray-900 outline-hidden resize-none"
|
||||
className={'w-full text-sm outline-hidden resize-vertical' +
|
||||
($settings.highContrastMode
|
||||
? ' p-2.5 border-2 border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-850 text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 overflow-y-hidden'
|
||||
: ' bg-white dark:text-gray-300 dark:bg-gray-900')}
|
||||
rows="4"
|
||||
placeholder={$i18n.t('Enter system prompt here')}
|
||||
/>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
let detectArtifacts = true;
|
||||
|
||||
let richTextInput = true;
|
||||
let insertPromptAsRichText = false;
|
||||
let promptAutocomplete = false;
|
||||
|
||||
let largeTextAsFile = false;
|
||||
@@ -218,6 +219,11 @@
|
||||
saveSettings({ richTextInput });
|
||||
};
|
||||
|
||||
const toggleInsertPromptAsRichText = async () => {
|
||||
insertPromptAsRichText = !insertPromptAsRichText;
|
||||
saveSettings({ insertPromptAsRichText });
|
||||
};
|
||||
|
||||
const toggleLargeTextAsFile = async () => {
|
||||
largeTextAsFile = !largeTextAsFile;
|
||||
saveSettings({ largeTextAsFile });
|
||||
@@ -308,7 +314,9 @@
|
||||
voiceInterruption = $settings?.voiceInterruption ?? false;
|
||||
|
||||
richTextInput = $settings?.richTextInput ?? true;
|
||||
insertPromptAsRichText = $settings?.insertPromptAsRichText ?? false;
|
||||
promptAutocomplete = $settings?.promptAutocomplete ?? false;
|
||||
|
||||
largeTextAsFile = $settings?.largeTextAsFile ?? false;
|
||||
copyFormatted = $settings?.copyFormatted ?? false;
|
||||
|
||||
@@ -761,22 +769,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $config?.features?.enable_autocomplete_generation && richTextInput}
|
||||
{#if richTextInput}
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div id="prompt-autocompletion-label" class=" self-center text-xs">
|
||||
{$i18n.t('Prompt Autocompletion')}
|
||||
<div id="rich-input-label" class=" self-center text-xs">
|
||||
{$i18n.t('Insert Prompt as Rich Text')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="prompt-autocompletion-label"
|
||||
aria-labelledby="rich-input-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
togglePromptAutocomplete();
|
||||
toggleInsertPromptAsRichText();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if promptAutocomplete === true}
|
||||
{#if insertPromptAsRichText === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
@@ -784,6 +792,31 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $config?.features?.enable_autocomplete_generation}
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div id="prompt-autocompletion-label" class=" self-center text-xs">
|
||||
{$i18n.t('Prompt Autocompletion')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="prompt-autocompletion-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
togglePromptAutocomplete();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if promptAutocomplete === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { addNewMemory, updateMemoryById } from '$lib/apis/memories';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -46,16 +48,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -93,29 +86,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
import { updateMemoryById } from '$lib/apis/memories';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -56,16 +58,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -103,29 +96,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -211,9 +211,7 @@
|
||||
: []),
|
||||
|
||||
...($user?.role === 'admin' ||
|
||||
($user?.role === 'user' &&
|
||||
$user?.permissions?.features?.direct_tool_servers &&
|
||||
$config?.features?.direct_tool_servers)
|
||||
($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers)
|
||||
? [
|
||||
{
|
||||
id: 'tools',
|
||||
@@ -688,7 +686,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
{:else if tabId === 'tools'}
|
||||
{#if $user?.role === 'admin' || ($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers && $config?.features?.direct_tool_servers)}
|
||||
{#if $user?.role === 'admin' || ($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers)}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-tools"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Link from '../icons/Link.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let chatId;
|
||||
|
||||
@@ -90,16 +91,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
</script>
|
||||
@@ -18,16 +19,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Fuse from 'fuse.js';
|
||||
import Bolt from '$lib/components/icons/Bolt.svelte';
|
||||
import { onMount, getContext, createEventDispatcher } from 'svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { settings, WEBUI_NAME } from '$lib/stores';
|
||||
import { WEBUI_VERSION } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let suggestionPrompts = [];
|
||||
export let className = '';
|
||||
export let inputValue = '';
|
||||
export let onSelect = (e) => {};
|
||||
|
||||
let sortedPrompts = [];
|
||||
|
||||
@@ -83,14 +83,16 @@
|
||||
|
||||
<div class="h-40 w-full">
|
||||
{#if filteredPrompts.length > 0}
|
||||
<div class="max-h-40 overflow-auto scrollbar-none items-start {className}">
|
||||
<div role="list" class="max-h-40 overflow-auto scrollbar-none items-start {className}">
|
||||
{#each filteredPrompts as prompt, idx (prompt.id || prompt.content)}
|
||||
<!-- svelte-ignore a11y-no-interactive-element-to-noninteractive-role -->
|
||||
<button
|
||||
role="listitem"
|
||||
class="waterfall flex flex-col flex-1 shrink-0 w-full justify-between
|
||||
px-3 py-2 rounded-xl bg-transparent hover:bg-black/5
|
||||
dark:hover:bg-white/5 transition group"
|
||||
style="animation-delay: {idx * 60}ms"
|
||||
on:click={() => dispatch('select', prompt.content)}
|
||||
on:click={() => onSelect({ type: 'prompt', data: prompt.content })}
|
||||
>
|
||||
<div class="flex flex-col text-left">
|
||||
{#if prompt.title && prompt.title[0] !== ''}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Link from '../icons/Link.svelte';
|
||||
import Collapsible from '../common/Collapsible.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let selectedToolIds = [];
|
||||
@@ -30,16 +31,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { marked } from 'marked';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -36,6 +37,8 @@
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
|
||||
console.log('Banner mounted:', banner);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -58,7 +61,7 @@
|
||||
<div class="flex md:hidden group w-fit md:items-center">
|
||||
<a
|
||||
class="text-gray-700 dark:text-white text-xs font-semibold underline"
|
||||
href="/assets/files/whitepaper.pdf"
|
||||
href="{WEBUI_BASE_URL}/assets/files/whitepaper.pdf"
|
||||
target="_blank">Learn More</a
|
||||
>
|
||||
|
||||
@@ -82,9 +85,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-white max-h-20 overflow-y-auto">
|
||||
{@html marked.parse(DOMPurify.sanitize(banner.content))}
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-white max-h-60 overflow-y-auto">
|
||||
{@html marked.parse(DOMPurify.sanitize((banner?.content ?? '').replace(/\n/g, '<br>')))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,15 +116,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex self-start">
|
||||
{#if banner.dismissible}
|
||||
<button
|
||||
on:click={() => {
|
||||
dismiss(banner.id);
|
||||
}}
|
||||
class=" -mt-1 -mb-2 -translate-y-[1px] ml-1.5 mr-1 text-gray-400 dark:hover:text-white"
|
||||
>×</button
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
on:click={() => {
|
||||
dismiss(banner.id);
|
||||
}}
|
||||
class=" -mt-1 -mb-2 -translate-y-[1px] ml-1.5 mr-1 text-gray-400 dark:hover:text-white"
|
||||
>×</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -48,35 +48,28 @@
|
||||
/**
|
||||
* Finds multiple diffs in two strings and generates minimal change edits.
|
||||
*/
|
||||
function findChanges(oldStr, newStr) {
|
||||
let changes = [];
|
||||
let oldIndex = 0,
|
||||
newIndex = 0;
|
||||
|
||||
while (oldIndex < oldStr.length || newIndex < newStr.length) {
|
||||
if (oldStr[oldIndex] !== newStr[newIndex]) {
|
||||
let start = oldIndex;
|
||||
|
||||
// Identify the changed portion
|
||||
while (oldIndex < oldStr.length && oldStr[oldIndex] !== newStr[newIndex]) {
|
||||
oldIndex++;
|
||||
}
|
||||
while (newIndex < newStr.length && newStr[newIndex] !== oldStr[start]) {
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
changes.push({
|
||||
from: start,
|
||||
to: oldIndex, // Replace the differing part
|
||||
insert: newStr.substring(start, newIndex)
|
||||
});
|
||||
} else {
|
||||
oldIndex++;
|
||||
newIndex++;
|
||||
}
|
||||
function findChanges(oldStr: string, newStr: string) {
|
||||
// Find the start of the difference
|
||||
let start = 0;
|
||||
while (start < oldStr.length && start < newStr.length && oldStr[start] === newStr[start]) {
|
||||
start++;
|
||||
}
|
||||
|
||||
return changes;
|
||||
// If equal, nothing to change
|
||||
if (oldStr === newStr) return [];
|
||||
// Find the end of the difference by comparing backwards
|
||||
let endOld = oldStr.length,
|
||||
endNew = newStr.length;
|
||||
while (endOld > start && endNew > start && oldStr[endOld - 1] === newStr[endNew - 1]) {
|
||||
endOld--;
|
||||
endNew--;
|
||||
}
|
||||
return [
|
||||
{
|
||||
from: start,
|
||||
to: endOld,
|
||||
insert: newStr.slice(start, endNew)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export let id = '';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { onMount, getContext, createEventDispatcher, onDestroy } from 'svelte';
|
||||
import { onMount, getContext, createEventDispatcher, onDestroy, tick } from 'svelte';
|
||||
import * as FocusTrap from 'focus-trap';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -25,11 +25,19 @@
|
||||
|
||||
export let show = false;
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
|
||||
let modalElement = null;
|
||||
let mounted = false;
|
||||
|
||||
let focusTrap: FocusTrap.FocusTrap | null = null;
|
||||
|
||||
const init = () => {
|
||||
inputValue = '';
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
console.log('Escape');
|
||||
@@ -44,6 +52,7 @@
|
||||
|
||||
const confirmHandler = async () => {
|
||||
show = false;
|
||||
await tick();
|
||||
await onConfirm();
|
||||
dispatch('confirm', inputValue);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||
import Spinner from './Spinner.svelte';
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -15,6 +17,7 @@
|
||||
export let url: string | null = null;
|
||||
|
||||
export let dismissible = false;
|
||||
export let modal = false;
|
||||
export let loading = false;
|
||||
|
||||
export let item = null;
|
||||
@@ -48,7 +51,7 @@
|
||||
: 'rounded-2xl'} text-left"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
if (item?.file?.data?.content) {
|
||||
if (item?.file?.data?.content || modal) {
|
||||
showModal = !showModal;
|
||||
} else {
|
||||
if (url) {
|
||||
@@ -70,6 +73,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class=" size-5"
|
||||
>
|
||||
<path
|
||||
@@ -93,7 +97,11 @@
|
||||
{decodeString(name)}
|
||||
</div>
|
||||
|
||||
<div class=" flex justify-between text-gray-500 text-xs line-clamp-1">
|
||||
<div
|
||||
class=" flex justify-between text-xs line-clamp-1 {($settings?.highContrastMode ?? false)
|
||||
? 'text-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-500'}"
|
||||
>
|
||||
{#if type === 'file'}
|
||||
{$i18n.t('File')}
|
||||
{:else if type === 'doc'}
|
||||
@@ -127,22 +135,17 @@
|
||||
{#if dismissible}
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-white text-black border border-gray-50 rounded-full group-hover:visible invisible transition"
|
||||
aria-label={$i18n.t('Remove File')}
|
||||
class=" bg-white text-black border border-gray-50 rounded-full {($settings?.highContrastMode ??
|
||||
false)
|
||||
? ''
|
||||
: 'outline-hidden focus:outline-hidden group-hover:visible invisible transition'}"
|
||||
type="button"
|
||||
on:click|stopPropagation={() => {
|
||||
dispatch('dismiss');
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
|
||||
<!-- <button
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import Info from '../icons/Info.svelte';
|
||||
import Switch from './Switch.svelte';
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export let item;
|
||||
export let show = false;
|
||||
@@ -77,6 +78,24 @@
|
||||
<div>
|
||||
<div class="flex flex-col items-center md:flex-row gap-1 justify-between w-full">
|
||||
<div class=" flex flex-wrap text-sm gap-1 text-gray-500">
|
||||
{#if item?.type === 'collection'}
|
||||
{#if item?.type}
|
||||
<div class="capitalize shrink-0">{item.type}</div>
|
||||
•
|
||||
{/if}
|
||||
|
||||
{#if item?.description}
|
||||
<div class="line-clamp-1">{item.description}</div>
|
||||
•
|
||||
{/if}
|
||||
|
||||
{#if item?.created_at}
|
||||
<div class="capitalize shrink-0">
|
||||
{dayjs(item.created_at * 1000).format('LL')}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if item.size}
|
||||
<div class="capitalize shrink-0">{formatFileSize(item.size)}</div>
|
||||
•
|
||||
@@ -127,7 +146,17 @@
|
||||
</div>
|
||||
|
||||
<div class="max-h-[75vh] overflow-auto">
|
||||
{#if isPDF}
|
||||
{#if item?.type === 'collection'}
|
||||
<div>
|
||||
{#each item?.files as file}
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="flex-shrink-0 text-xs">
|
||||
{file?.meta?.name}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if isPDF}
|
||||
<iframe
|
||||
title={item?.name}
|
||||
src={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
|
||||
|
||||
@@ -69,12 +69,22 @@
|
||||
}
|
||||
} else {
|
||||
open = true;
|
||||
|
||||
const dataTransfer = e.dataTransfer.getData('text/plain');
|
||||
const data = JSON.parse(dataTransfer);
|
||||
|
||||
console.log(data);
|
||||
dispatch('drop', data);
|
||||
try {
|
||||
const dataTransfer = e.dataTransfer.getData('text/plain');
|
||||
if (dataTransfer) {
|
||||
const data = JSON.parse(dataTransfer);
|
||||
console.log(data);
|
||||
dispatch('drop', data);
|
||||
} else {
|
||||
console.log('Dropped text data is empty or not text/plain.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'Dropped data is not valid JSON text or is empty. Ignoring drop event for this type of data.'
|
||||
);
|
||||
} finally {
|
||||
draggedOver = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +113,7 @@
|
||||
if (!dragAndDrop) {
|
||||
return;
|
||||
}
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('drop', onDrop);
|
||||
folderElement.removeEventListener('dragleave', onDragLeave);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user