Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
45311bfa
Commit
45311bfa
authored
Mar 23, 2024
by
Anuraag Jain
Browse files
Merge branch 'main' into feat/cancel-model-download
# Conflicts: # src/lib/components/chat/Settings/Models.svelte
parents
ae97a963
2fa94956
Changes
169
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1506 additions
and
600 deletions
+1506
-600
src/lib/apis/utils/index.ts
src/lib/apis/utils/index.ts
+32
-0
src/lib/components/AddFilesPlaceholder.svelte
src/lib/components/AddFilesPlaceholder.svelte
+7
-2
src/lib/components/ChangelogModal.svelte
src/lib/components/ChangelogModal.svelte
+118
-0
src/lib/components/admin/EditUserModal.svelte
src/lib/components/admin/EditUserModal.svelte
+10
-8
src/lib/components/admin/Settings/Database.svelte
src/lib/components/admin/Settings/Database.svelte
+65
-0
src/lib/components/admin/Settings/General.svelte
src/lib/components/admin/Settings/General.svelte
+73
-13
src/lib/components/admin/Settings/Users.svelte
src/lib/components/admin/Settings/Users.svelte
+123
-6
src/lib/components/admin/SettingsModal.svelte
src/lib/components/admin/SettingsModal.svelte
+41
-3
src/lib/components/chat/MessageInput.svelte
src/lib/components/chat/MessageInput.svelte
+144
-127
src/lib/components/chat/MessageInput/Documents.svelte
src/lib/components/chat/MessageInput/Documents.svelte
+15
-11
src/lib/components/chat/MessageInput/Models.svelte
src/lib/components/chat/MessageInput/Models.svelte
+25
-17
src/lib/components/chat/MessageInput/PromptCommands.svelte
src/lib/components/chat/MessageInput/PromptCommands.svelte
+25
-10
src/lib/components/chat/MessageInput/Suggestions.svelte
src/lib/components/chat/MessageInput/Suggestions.svelte
+40
-38
src/lib/components/chat/Messages.svelte
src/lib/components/chat/Messages.svelte
+93
-49
src/lib/components/chat/Messages/Placeholder.svelte
src/lib/components/chat/Messages/Placeholder.svelte
+13
-5
src/lib/components/chat/Messages/ResponseMessage.svelte
src/lib/components/chat/Messages/ResponseMessage.svelte
+359
-217
src/lib/components/chat/Messages/UserMessage.svelte
src/lib/components/chat/Messages/UserMessage.svelte
+100
-58
src/lib/components/chat/ModelSelector.svelte
src/lib/components/chat/ModelSelector.svelte
+12
-8
src/lib/components/chat/Settings/About.svelte
src/lib/components/chat/Settings/About.svelte
+91
-17
src/lib/components/chat/Settings/Account.svelte
src/lib/components/chat/Settings/Account.svelte
+120
-11
No files found.
src/lib/apis/utils/index.ts
View file @
45311bfa
...
@@ -21,3 +21,35 @@ export const getGravatarUrl = async (email: string) => {
...
@@ -21,3 +21,35 @@ export const getGravatarUrl = async (email: string) => {
return
res
;
return
res
;
};
};
export
const
downloadDatabase
=
async
(
token
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/utils/db/download`
,
{
method
:
'
GET
'
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
Authorization
:
`Bearer
${
token
}
`
}
})
.
then
((
response
)
=>
{
if
(
!
response
.
ok
)
{
throw
new
Error
(
'
Network response was not ok
'
);
}
return
response
.
blob
();
})
.
then
((
blob
)
=>
{
const
url
=
window
.
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
'
a
'
);
a
.
href
=
url
;
a
.
download
=
'
webui.db
'
;
document
.
body
.
appendChild
(
a
);
a
.
click
();
window
.
URL
.
revokeObjectURL
(
url
);
})
.
catch
((
err
)
=>
{
console
.
log
(
err
);
error
=
err
;
return
null
;
});
};
src/lib/components/AddFilesPlaceholder.svelte
View file @
45311bfa
<script>
import { getContext } from 'svelte';
const i18n = getContext('i18n');
</script>
<div class=" text-center text-6xl mb-3">📄</div>
<div class=" text-center text-6xl mb-3">📄</div>
<div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div>
<div class="text-center dark:text-white text-2xl font-semibold z-50">
{$i18n.t('
Add Files
')}
</div>
<slot
<slot
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to the conversation
{$i18n.t('
Drop any files here to add to the conversation
')}
</div>
</div>
</slot>
</slot>
src/lib/components/ChangelogModal.svelte
0 → 100644
View file @
45311bfa
<script lang="ts">
import { onMount, getContext } from 'svelte';
import { Confetti } from 'svelte-confetti';
import { WEBUI_NAME, config } from '$lib/stores';
import { WEBUI_VERSION } from '$lib/constants';
import { getChangelog } from '$lib/apis';
import Modal from './common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false;
let changelog = null;
onMount(async () => {
const res = await getChangelog();
changelog = res;
});
</script>
<Modal bind:show>
<div class="px-5 py-4 dark:text-gray-300">
<div class="flex justify-between items-start">
<div class="text-xl font-bold">
{$i18n.t('What’s New in')}
{$WEBUI_NAME}
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
</div>
<button
class="self-center"
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>
</button>
</div>
<div class="flex items-center mt-1">
<div class="text-sm dark:text-gray-200">{$i18n.t('Release Notes')}</div>
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
<div class="text-sm dark:text-gray-200">
v{WEBUI_VERSION}
</div>
</div>
</div>
<hr class=" dark:border-gray-800" />
<div class=" w-full p-4 px-5">
<div class=" overflow-y-scroll max-h-80">
<div class="mb-3">
{#if changelog}
{#each Object.keys(changelog) as version}
<div class=" mb-3 pr-2">
<div class="font-bold text-xl mb-1 dark:text-white">
v{version} - {changelog[version].date}
</div>
<hr class=" dark:border-gray-800 my-2" />
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
<div class="">
<div
class="font-bold uppercase text-xs {section === 'added'
? 'text-white bg-blue-600'
: section === 'fixed'
? 'text-white bg-green-600'
: section === 'changed'
? 'text-white bg-yellow-600'
: section === 'removed'
? 'text-white bg-red-600'
: ''} w-fit px-3 rounded-full my-2.5"
>
{section}
</div>
<div class="my-2.5 px-1.5">
{#each Object.keys(changelog[version][section]) as item}
<div class="text-sm mb-2">
<div class="font-semibold uppercase">
{changelog[version][section][item].title}
</div>
<div class="mb-2 mt-1">{changelog[version][section][item].content}</div>
</div>
{/each}
</div>
</div>
{/each}
</div>
{/each}
{/if}
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
on:click={() => {
localStorage.version = $config.version;
show = false;
}}
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
>
<span class="relative">{$i18n.t("Okay, Let's Go!")}</span>
</button>
</div>
</div>
</Modal>
src/lib/components/admin/EditUserModal.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import toast from 'svelte-
french-toast
';
import
{
toast
}
from 'svelte-
sonner
';
import dayjs from 'dayjs';
import dayjs from 'dayjs';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { onMount } from 'svelte';
import { onMount
, getContext
} from 'svelte';
import { updateUserById } from '$lib/apis/users';
import { updateUserById } from '$lib/apis/users';
import Modal from '../common/Modal.svelte';
import Modal from '../common/Modal.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
export let show = false;
export let show = false;
...
@@ -42,7 +43,7 @@
...
@@ -42,7 +43,7 @@
<Modal size="sm" bind:show>
<Modal size="sm" bind:show>
<div>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Edit User</div>
<div class=" text-lg font-medium self-center">
{$i18n.t('
Edit User
')}
</div>
<button
<button
class="self-center"
class="self-center"
on:click={() => {
on:click={() => {
...
@@ -84,7 +85,8 @@
...
@@ -84,7 +85,8 @@
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
<div class="text-xs text-gray-500">
<div class="text-xs text-gray-500">
Created at {dayjs(selectedUser.timestamp * 1000).format('MMMM DD, YYYY')}
{$i18n.t('Created at')}
{dayjs(selectedUser.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -93,7 +95,7 @@
...
@@ -93,7 +95,7 @@
<div class=" flex flex-col space-y-1.5">
<div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Email</div>
<div class=" mb-1 text-xs text-gray-500">
{$i18n.t('
Email
')}
</div>
<div class="flex-1">
<div class="flex-1">
<input
<input
...
@@ -108,7 +110,7 @@
...
@@ -108,7 +110,7 @@
</div>
</div>
<div class="flex flex-col w-full">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Name</div>
<div class=" mb-1 text-xs text-gray-500">
{$i18n.t('
Name
')}
</div>
<div class="flex-1">
<div class="flex-1">
<input
<input
...
@@ -122,7 +124,7 @@
...
@@ -122,7 +124,7 @@
</div>
</div>
<div class="flex flex-col w-full">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">New Password</div>
<div class=" mb-1 text-xs text-gray-500">
{$i18n.t('
New Password
')}
</div>
<div class="flex-1">
<div class="flex-1">
<input
<input
...
@@ -140,7 +142,7 @@
...
@@ -140,7 +142,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
type="submit"
>
>
Save
{$i18n.t('
Save
')}
</button>
</button>
</div>
</div>
</form>
</form>
...
...
src/lib/components/admin/Settings/Database.svelte
0 → 100644
View file @
45311bfa
<script lang="ts">
import { downloadDatabase } from '$lib/apis/utils';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
onMount(async () => {
// permissions = await getUserPermissions(localStorage.token);
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
saveHandler();
}}
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
<div class=" flex w-full justify-between">
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
<button
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
type="button"
on:click={() => {
// exportAllUserChats();
downloadDatabase(localStorage.token);
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
</button>
</div>
</div>
</div>
<!-- <div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
</button>
</div> -->
</form>
src/lib/components/admin/Settings/General.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import { getWebhookUrl, updateWebhookUrl } from '$lib/apis';
import {
import {
getDefaultUserRole,
getDefaultUserRole,
getJWTExpiresDuration,
getSignUpEnabledStatus,
getSignUpEnabledStatus,
toggleSignUpEnabledStatus,
toggleSignUpEnabledStatus,
updateDefaultUserRole
updateDefaultUserRole,
updateJWTExpiresDuration
} from '$lib/apis/auths';
} from '$lib/apis/auths';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
export let saveHandler: Function;
let signUpEnabled = true;
let signUpEnabled = true;
let defaultUserRole = 'pending';
let defaultUserRole = 'pending';
let JWTExpiresIn = '';
let webhookUrl = '';
const toggleSignUpEnabled = async () => {
const toggleSignUpEnabled = async () => {
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
...
@@ -19,25 +27,36 @@
...
@@ -19,25 +27,36 @@
defaultUserRole = await updateDefaultUserRole(localStorage.token, role);
defaultUserRole = await updateDefaultUserRole(localStorage.token, role);
};
};
const updateJWTExpiresDurationHandler = async (duration) => {
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
};
const updateWebhookUrlHandler = async () => {
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
};
onMount(async () => {
onMount(async () => {
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
defaultUserRole = await getDefaultUserRole(localStorage.token);
defaultUserRole = await getDefaultUserRole(localStorage.token);
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
webhookUrl = await getWebhookUrl(localStorage.token);
});
});
</script>
</script>
<form
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
on:submit|preventDefault={() => {
// console.log('submit');
updateJWTExpiresDurationHandler(JWTExpiresIn);
updateWebhookUrlHandler();
saveHandler();
saveHandler();
}}
}}
>
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div>
<div>
<div class=" mb-2 text-sm font-medium">General Settings</div>
<div class=" mb-2 text-sm font-medium">
{$i18n.t('
General Settings
')}
</div>
<div class=" flex w-full justify-between">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Enable New Sign Ups</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Enable New Sign Ups
')}
</div>
<button
<button
class="p-1 px-3 text-xs flex rounded transition"
class="p-1 px-3 text-xs flex rounded transition"
...
@@ -57,7 +76,7 @@
...
@@ -57,7 +76,7 @@
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
/>
/>
</svg>
</svg>
<span class="ml-2 self-center">Enabled</span>
<span class="ml-2 self-center">
{$i18n.t('
Enabled
')}
</span>
{:else}
{:else}
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
...
@@ -72,28 +91,69 @@
...
@@ -72,28 +91,69 @@
/>
/>
</svg>
</svg>
<span class="ml-2 self-center">Disabled</span>
<span class="ml-2 self-center">
{$i18n.t('
Disabled
')}
</span>
{/if}
{/if}
</button>
</button>
</div>
</div>
<div class=" flex w-full justify-between">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Default User Role</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Default User Role
')}
</div>
<div class="flex items-center relative">
<div class="flex items-center relative">
<select
<select
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
class="
dark:bg-gray-900
w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
bind:value={defaultUserRole}
bind:value={defaultUserRole}
placeholder="Select a theme"
placeholder="Select a theme"
on:change={(e) => {
on:change={(e) => {
updateDefaultUserRoleHandler(e.target.value);
updateDefaultUserRoleHandler(e.target.value);
}}
}}
>
>
<option value="pending">
P
ending</option>
<option value="pending">
{$i18n.t('p
ending
')}
</option>
<option value="user">
User
</option>
<option value="user">
{$i18n.t('user')}
</option>
<option value="admin">
A
dmin</option>
<option value="admin">
{$i18n.t('a
dmin
')}
</option>
</select>
</select>
</div>
</div>
</div>
</div>
<hr class=" dark:border-gray-700 my-3" />
<div class=" w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
</div>
<div class="flex mt-2 space-x-2">
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text"
placeholder={`https://example.com/webhook`}
bind:value={webhookUrl}
/>
</div>
</div>
<hr class=" dark:border-gray-700 my-3" />
<div class=" w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
</div>
<div class="flex mt-2 space-x-2">
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text"
placeholder={`e.g.) "30m","1h", "10d". `}
bind:value={JWTExpiresIn}
/>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Valid time units:')}
<span class=" text-gray-300 font-medium"
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -102,7 +162,7 @@
...
@@ -102,7 +162,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
type="submit"
>
>
Save
{$i18n.t('
Save
')}
</button>
</button>
</div>
</div>
</form>
</form>
src/lib/components/admin/Settings/Users.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
import { onMount } from 'svelte';
import { onMount, getContext } from 'svelte';
import { models } from '$lib/stores';
const i18n = getContext('i18n');
export let saveHandler: Function;
export let saveHandler: Function;
let whitelistEnabled = false;
let whitelistModels = [''];
let permissions = {
let permissions = {
chat: {
chat: {
deletion: true
deletion: true
...
@@ -13,6 +20,13 @@
...
@@ -13,6 +20,13 @@
onMount(async () => {
onMount(async () => {
permissions = await getUserPermissions(localStorage.token);
permissions = await getUserPermissions(localStorage.token);
const res = await getModelFilterConfig(localStorage.token);
if (res) {
whitelistEnabled = res.enabled;
whitelistModels = res.models.length > 0 ? res.models : [''];
}
});
});
</script>
</script>
...
@@ -21,15 +35,17 @@
...
@@ -21,15 +35,17 @@
on:submit|preventDefault={async () => {
on:submit|preventDefault={async () => {
// console.log('submit');
// console.log('submit');
await updateUserPermissions(localStorage.token, permissions);
await updateUserPermissions(localStorage.token, permissions);
await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
saveHandler();
saveHandler();
}}
}}
>
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<div>
<div>
<div class=" mb-2 text-sm font-medium">User Permissions</div>
<div class=" mb-2 text-sm font-medium">
{$i18n.t('
User Permissions
')}
</div>
<div class=" flex w-full justify-between">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">Allow Chat Deletion</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('
Allow Chat Deletion
')}
</div>
<button
<button
class="p-1 px-3 text-xs flex rounded transition"
class="p-1 px-3 text-xs flex rounded transition"
...
@@ -49,7 +65,7 @@
...
@@ -49,7 +65,7 @@
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
/>
/>
</svg>
</svg>
<span class="ml-2 self-center">Allow</span>
<span class="ml-2 self-center">
{$i18n.t('
Allow
')}
</span>
{:else}
{:else}
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
...
@@ -64,11 +80,112 @@
...
@@ -64,11 +80,112 @@
/>
/>
</svg>
</svg>
<span class="ml-2 self-center">Don't Allow</span>
<span class="ml-2 self-center">
{$i18n.t("
Don't Allow
")}
</span>
{/if}
{/if}
</button>
</button>
</div>
</div>
</div>
</div>
<hr class=" dark:border-gray-700 my-2" />
<div class="mt-2 space-y-3 pr-1.5">
<div>
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
</div>
</div>
<div class=" space-y-3">
<div>
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
whitelistEnabled = !whitelistEnabled;
}}>{whitelistEnabled ? $i18n.t('On') : $i18n.t('Off')}</button
>
</div>
</div>
{#if whitelistEnabled}
<div>
<div class=" space-y-1.5">
{#each whitelistModels as modelId, modelIdx}
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={modelId}
placeholder="Select a model"
>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"
>{model.name}</option
>
{/each}
</select>
</div>
{#if modelIdx === 0}
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
type="button"
on:click={() => {
if (whitelistModels.at(-1) !== '') {
whitelistModels = [...whitelistModels, ''];
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
{:else}
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition"
type="button"
on:click={() => {
whitelistModels.splice(modelIdx, 1);
whitelistModels = whitelistModels;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</button>
{/if}
</div>
{/each}
</div>
<div class="flex justify-end items-center text-xs mt-1.5 text-right">
<div class=" text-xs font-medium">
{whitelistModels.length}
{$i18n.t('Model(s) Whitelisted')}
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<div class="flex justify-end pt-3 text-sm font-medium">
...
@@ -76,7 +193,7 @@
...
@@ -76,7 +193,7 @@
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
type="submit"
>
>
Save
{$i18n.t('
Save
')}
</button>
</button>
</div>
</div>
</form>
</form>
src/lib/components/admin/SettingsModal.svelte
View file @
45311bfa
<script>
<script>
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
import Modal from '../common/Modal.svelte';
import Database from './Settings/Database.svelte';
import General from './Settings/General.svelte';
import General from './Settings/General.svelte';
import Users from './Settings/Users.svelte';
import Users from './Settings/Users.svelte';
const i18n = getContext('i18n');
export let show = false;
export let show = false;
let selectedTab = 'general';
let selectedTab = 'general';
...
@@ -12,7 +16,7 @@
...
@@ -12,7 +16,7 @@
<Modal bind:show>
<Modal bind:show>
<div>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Admin Settings</div>
<div class=" text-lg font-medium self-center">
{$i18n.t('
Admin Settings
')}
</div>
<button
<button
class="self-center"
class="self-center"
on:click={() => {
on:click={() => {
...
@@ -60,7 +64,7 @@
...
@@ -60,7 +64,7 @@
/>
/>
</svg>
</svg>
</div>
</div>
<div class=" self-center">General</div>
<div class=" self-center">
{$i18n.t('
General
')}
</div>
</button>
</button>
<button
<button
...
@@ -84,7 +88,35 @@
...
@@ -84,7 +88,35 @@
/>
/>
</svg>
</svg>
</div>
</div>
<div class=" self-center">Users</div>
<div class=" self-center">{$i18n.t('Users')}</div>
</button>
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'db'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={() => {
selectedTab = 'db';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8 7c3.314 0 6-1.343 6-3s-2.686-3-6-3-6 1.343-6 3 2.686 3 6 3Z" />
<path
d="M8 8.5c1.84 0 3.579-.37 4.914-1.037A6.33 6.33 0 0 0 14 6.78V8c0 1.657-2.686 3-6 3S2 9.657 2 8V6.78c.346.273.72.5 1.087.683C4.42 8.131 6.16 8.5 8 8.5Z"
/>
<path
d="M8 12.5c1.84 0 3.579-.37 4.914-1.037.366-.183.74-.41 1.086-.684V12c0 1.657-2.686 3-6 3s-6-1.343-6-3v-1.22c.346.273.72.5 1.087.683C4.42 12.131 6.16 12.5 8 12.5Z"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Database')}</div>
</button>
</button>
</div>
</div>
<div class="flex-1 md:min-h-[380px]">
<div class="flex-1 md:min-h-[380px]">
...
@@ -100,6 +132,12 @@
...
@@ -100,6 +132,12 @@
show = false;
show = false;
}}
}}
/>
/>
{:else if selectedTab === 'db'}
<Database
saveHandler={() => {
show = false;
}}
/>
{/if}
{/if}
</div>
</div>
</div>
</div>
...
...
src/lib/components/chat/MessageInput.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import toast from 'svelte-
french-toast
';
import
{
toast
}
from 'svelte-
sonner
';
import { onMount, tick } from 'svelte';
import { onMount, tick
, getContext
} from 'svelte';
import { settings } from '$lib/stores';
import { settings } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
...
@@ -12,13 +12,16 @@
...
@@ -12,13 +12,16 @@
import Documents from './MessageInput/Documents.svelte';
import Documents from './MessageInput/Documents.svelte';
import Models from './MessageInput/Models.svelte';
import Models from './MessageInput/Models.svelte';
import { transcribeAudio } from '$lib/apis/audio';
import { transcribeAudio } from '$lib/apis/audio';
import Tooltip from '../common/Tooltip.svelte';
const i18n = getContext('i18n');
export let submitPrompt: Function;
export let submitPrompt: Function;
export let stopResponse: Function;
export let stopResponse: Function;
export let suggestionPrompts = [];
export let suggestionPrompts = [];
export let autoScroll = true;
export let autoScroll = true;
let chatTextAreaElement: HTMLTextAreaElement;
let filesInputElement;
let filesInputElement;
let promptsElement;
let promptsElement;
...
@@ -42,11 +45,9 @@
...
@@ -42,11 +45,9 @@
let speechRecognition;
let speechRecognition;
$: if (prompt) {
$: if (prompt) {
const chatInput = document.getElementById('chat-textarea');
if (chatTextAreaElement) {
chatTextAreaElement.style.height = '';
if (chatInput) {
chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
chatInput.style.height = '';
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
}
}
}
}
...
@@ -85,9 +86,7 @@
...
@@ -85,9 +86,7 @@
if (res) {
if (res) {
prompt = res.text;
prompt = res.text;
await tick();
await tick();
chatTextAreaElement?.focus();
const inputElement = document.getElementById('chat-textarea');
inputElement?.focus();
if (prompt !== '' && $settings?.speechAutoSend === true) {
if (prompt !== '' && $settings?.speechAutoSend === true) {
submitPrompt(prompt, user);
submitPrompt(prompt, user);
...
@@ -190,8 +189,7 @@
...
@@ -190,8 +189,7 @@
prompt = `${prompt}${transcript}`;
prompt = `${prompt}${transcript}`;
await tick();
await tick();
const inputElement = document.getElementById('chat-textarea');
chatTextAreaElement?.focus();
inputElement?.focus();
// Restart the inactivity timeout
// Restart the inactivity timeout
timeoutId = setTimeout(() => {
timeoutId = setTimeout(() => {
...
@@ -213,11 +211,11 @@
...
@@ -213,11 +211,11 @@
// Event triggered when an error occurs
// Event triggered when an error occurs
speechRecognition.onerror = function (event) {
speechRecognition.onerror = function (event) {
console.log(event);
console.log(event);
toast.error(`Speech recognition error:
$
{event.error
}`
);
toast.error(
$i18n.t(
`Speech recognition error:
{
{
error}}`, { error:
event.error
})
);
isRecording = false;
isRecording = false;
};
};
} else {
} else {
toast.error('SpeechRecognition API is not supported in this browser.');
toast.error(
$i18n.t(
'SpeechRecognition API is not supported in this browser.')
)
;
}
}
}
}
}
}
...
@@ -293,6 +291,8 @@
...
@@ -293,6 +291,8 @@
};
};
onMount(() => {
onMount(() => {
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
const dropZone = document.querySelector('body');
const dropZone = document.querySelector('body');
const onDragOver = (e) => {
const onDragOver = (e) => {
...
@@ -335,12 +335,15 @@
...
@@ -335,12 +335,15 @@
uploadDoc(file);
uploadDoc(file);
} else {
} else {
toast.error(
toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
$i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
);
);
uploadDoc(file);
uploadDoc(file);
}
}
} else {
} else {
toast.error(`File not found.`);
toast.error(
$i18n.t(
`File not found.`)
)
;
}
}
}
}
...
@@ -361,12 +364,12 @@
...
@@ -361,12 +364,12 @@
{#if dragged}
{#if dragged}
<div
<div
class="fixed w-full h-full flex z-50 touch-none pointer-events-none"
class="fixed
lg:w-[calc(100%-260px)]
w-full h-full flex z-50 touch-none pointer-events-none"
id="dropzone"
id="dropzone"
role="region"
role="region"
aria-label="Drag and Drop Container"
aria-label="Drag and Drop Container"
>
>
<div class="absolute
rounded-xl
w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center">
<div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md">
<div class="max-w-md">
<AddFilesPlaceholder />
<AddFilesPlaceholder />
...
@@ -479,18 +482,21 @@
...
@@ -479,18 +482,21 @@
filesInputElement.value = '';
filesInputElement.value = '';
} else {
} else {
toast.error(
toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
$i18n.t(
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
{ file_type: file['type'] }
)
);
);
uploadDoc(file);
uploadDoc(file);
filesInputElement.value = '';
filesInputElement.value = '';
}
}
} else {
} else {
toast.error(`File not found.`);
toast.error(
$i18n.t(
`File not found.`)
)
;
}
}
}}
}}
/>
/>
<form
<form
class=" flex flex-col relative w-full rounded-xl
border
dark:border-gray-
70
0 bg-white dark:bg-gray-900 dark:text-gray-100"
class=" flex flex-col relative w-full rounded-
3
xl
px-1.5 border border-gray-100
dark:border-gray-
85
0 bg-white dark:bg-gray-900 dark:text-gray-100"
on:submit|preventDefault={() => {
on:submit|preventDefault={() => {
submitPrompt(prompt, user);
submitPrompt(prompt, user);
}}
}}
...
@@ -572,7 +578,7 @@
...
@@ -572,7 +578,7 @@
{file.name}
{file.name}
</div>
</div>
<div class=" text-gray-500 text-sm">Document</div>
<div class=" text-gray-500 text-sm">
{$i18n.t('
Document
')}
</div>
</div>
</div>
</div>
</div>
{:else if file.type === 'collection'}
{:else if file.type === 'collection'}
...
@@ -600,7 +606,7 @@
...
@@ -600,7 +606,7 @@
{file?.title ?? `#${file.name}`}
{file?.title ?? `#${file.name}`}
</div>
</div>
<div class=" text-gray-500 text-sm">Collection</div>
<div class=" text-gray-500 text-sm">
{$i18n.t('
Collection
')}
</div>
</div>
</div>
</div>
</div>
{/if}
{/if}
...
@@ -633,40 +639,41 @@
...
@@ -633,40 +639,41 @@
<div class=" flex">
<div class=" flex">
{#if fileUploadEnabled}
{#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1.5">
<div class=" self-end mb-2 ml-1">
<button
<Tooltip content={$i18n.t('Upload files')}>
class=" text-gray-600 dark:text-gray-200 transition rounded-lg p-1 ml-1"
<button
type="button"
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
on:click={() => {
type="button"
filesInputElement.click();
on:click={() => {
}}
filesInputElement.click();
>
}}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
>
<path
<svg
fill-rule="evenodd"
xmlns="http://www.w3.org/2000/svg"
d="M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z"
viewBox="0 0 16 16"
clip-rule="evenodd"
fill="currentColor"
/>
class="w-[1.2rem] h-[1.2rem]"
</svg>
>
</button>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</Tooltip>
</div>
</div>
{/if}
{/if}
<textarea
<textarea
id="chat-textarea"
id="chat-textarea"
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled
bind:this={chatTextAreaElement}
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
? ''
? ''
: ' pl-4'} rounded-xl resize-none h-[48px]"
: ' pl-4'} rounded-xl resize-none h-[48px]"
placeholder={chatInputPlaceholder !== ''
placeholder={chatInputPlaceholder !== ''
? chatInputPlaceholder
? chatInputPlaceholder
: isRecording
: isRecording
? 'Listening...'
?
$i18n.t(
'Listening...'
)
: 'Send a
m
essage'}
:
$i18n.t(
'Send a
M
essage'
)
}
bind:value={prompt}
bind:value={prompt}
on:keypress={(e) => {
on:keypress={(e) => {
if (e.keyCode == 13 && !e.shiftKey) {
if (e.keyCode == 13 && !e.shiftKey) {
...
@@ -803,92 +810,102 @@
...
@@ -803,92 +810,102 @@
}}
}}
/>
/>
<div class="self-end mb-2 flex space-x-
0.5
mr-
2
">
<div class="self-end mb-2 flex space-x-
1
mr-
1
">
{#if messages.length == 0 || messages.at(-1).done == true}
{#if messages.length == 0 || messages.at(-1).done == true}
{#if speechRecognitionEnabled}
<Tooltip content={$i18n.t('Record voice')}>
<button
{#if speechRecognitionEnabled}
id="voice-input-button"
<button
class=" text-gray-600 dark:text-gray-300 transition rounded-lg p-1.5 mr-0.5 self-center"
id="voice-input-button"
type="button"
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
on:click={() => {
type="button"
speechRecognitionHandler();
on:click={() => {
}}
speechRecognitionHandler();
>
}}
{#if isRecording}
>
<svg
{#if isRecording}
class=" w-5 h-5 translate-y-[0.5px]"
<svg
fill="currentColor"
class=" w-5 h-5 translate-y-[0.5px]"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><style>
xmlns="http://www.w3.org/2000/svg"
.spinner_qM83 {
><style>
animation: spinner_8HQG 1.05s infinite;
.spinner_qM83 {
}
animation: spinner_8HQG 1.05s infinite;
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
}
28.57% {
.spinner_oXPr {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
animation-delay: 0.1s;
transform: translateY(-6px);
}
}
100%
{
.spinner_ZTLf
{
transform: translate(0)
;
animation-delay: 0.2s
;
}
}
}
@keyframes spinner_8HQG {
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
0%,
class="spinner_qM83 spinner_oXPr"
57.14% {
cx="12"
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
cy="12"
transform: translate(0);
r="2.5"
}
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
28.57% {
>
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
{:else}
transform: translateY(-6px);
<svg
}
xmlns="http://www.w3.org/2000/svg"
100% {
viewBox="0 0 20 20"
transform: translate(0);
fill="currentColor"
}
class="w-5 h-5 translate-y-[0.5px]"
}
>
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
class="spinner_qM83 spinner_oXPr"
<path
cx="12"
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
cy="12"
/>
r="2.5"
</svg>
/><circle
{/if}
class="spinner_qM83 spinner_ZTLf"
</button>
cx="20"
{/if}
cy="12"
<button
r="2.5"
class="{prompt !== ''
/></svg
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
>
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-lg p-1 mr-0.5 w-7 h-7 self-center"
{:else}
type="submit"
<svg
disabled={prompt === ''}
xmlns="http://www.w3.org/2000/svg"
>
viewBox="0 0 20 20"
<svg
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 translate-y-[0.5px]"
viewBox="0 0 16 16"
>
fill="currentColor"
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
class="w-4.5 h-4.5 mx-auto"
<path
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
/>
</svg>
{/if}
</button>
{/if}
</Tooltip>
<Tooltip content={$i18n.t('Send message')}>
<button
class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
type="submit"
disabled={prompt === ''}
>
>
<path
<svg
fill-rule="evenodd"
xmlns="http://www.w3.org/2000/svg"
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"
viewBox="0 0 16 16"
clip-rule="evenodd"
fill="currentColor"
/>
class="w-5 h-5"
</svg>
>
</button>
<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>
{:else}
{:else}
<button
<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-l
g
p-1.5"
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-
ful
l p-1.5"
on:click={stopResponse}
on:click={stopResponse}
>
>
<svg
<svg
...
@@ -910,7 +927,7 @@
...
@@ -910,7 +927,7 @@
</form>
</form>
<div class="mt-1.5 text-xs text-gray-500 text-center">
<div class="mt-1.5 text-xs text-gray-500 text-center">
LLMs can make mistakes. Verify important information.
{$i18n.t('
LLMs can make mistakes. Verify important information.
')}
</div>
</div>
</div>
</div>
</div>
</div>
...
...
src/lib/components/chat/MessageInput/Documents.svelte
View file @
45311bfa
...
@@ -3,8 +3,10 @@
...
@@ -3,8 +3,10 @@
import { documents } from '$lib/stores';
import { documents } from '$lib/stores';
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
import { tick } from 'svelte';
import { tick, getContext } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
export let prompt = '';
...
@@ -89,16 +91,16 @@
...
@@ -89,16 +91,16 @@
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full
rounded-lg border border-gray-100 dark:border-gray-700
">
<div class="flex w-full
px-2
">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-l
g
text-center">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-
x
l text-center">
<div class=" text-lg font-semibold mt-2">#</div>
<div class=" text-lg font-semibold mt-2">#</div>
</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-
lg
">
<div class="max-h-60 flex flex-col w-full rounded-r-
xl bg-white
">
<div class=" overflow-y-auto
bg-white
p-
2
rounded-
t
r-l
g
space-y-0.5">
<div class="
m-1
overflow-y-auto p-
1
rounded-r-
x
l space-y-0.5">
{#each filteredItems as doc, docIdx}
{#each filteredItems as doc, docIdx}
<button
<button
class=" px-3 py-1.5 rounded-l
g
w-full text-left {docIdx === selectedIdx
class=" px-3 py-1.5 rounded-
x
l w-full text-left {docIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button'
? ' bg-gray-100 selected-command-option-button'
: ''}"
: ''}"
type="button"
type="button"
...
@@ -117,7 +119,7 @@
...
@@ -117,7 +119,7 @@
{doc?.title ?? `#${doc.name}`}
{doc?.title ?? `#${doc.name}`}
</div>
</div>
<div class=" text-xs text-gray-600 line-clamp-1">Collection</div>
<div class=" text-xs text-gray-600 line-clamp-1">
{$i18n.t('
Collection
')}
</div>
{:else}
{:else}
<div class=" font-medium text-black line-clamp-1">
<div class=" font-medium text-black line-clamp-1">
#{doc.name} ({doc.filename})
#{doc.name} ({doc.filename})
...
@@ -132,7 +134,7 @@
...
@@ -132,7 +134,7 @@
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<button
<button
class="px-3 py-1.5 rounded-l
g
w-full text-left bg-gray-100 selected-command-option-button"
class="px-3 py-1.5 rounded-
x
l w-full text-left bg-gray-100 selected-command-option-button"
type="button"
type="button"
on:click={() => {
on:click={() => {
const url = prompt.split(' ')?.at(0)?.substring(1);
const url = prompt.split(' ')?.at(0)?.substring(1);
...
@@ -140,7 +142,9 @@
...
@@ -140,7 +142,9 @@
confirmSelectWeb(url);
confirmSelectWeb(url);
} else {
} else {
toast.error(
toast.error(
'Oops! Looks like the URL is invalid. Please double-check and try again.'
$i18n.t(
'Oops! Looks like the URL is invalid. Please double-check and try again.'
)
);
);
}
}
}}
}}
...
@@ -149,7 +153,7 @@
...
@@ -149,7 +153,7 @@
{prompt.split(' ')?.at(0)?.substring(1)}
{prompt.split(' ')?.at(0)?.substring(1)}
</div>
</div>
<div class=" text-xs text-gray-600 line-clamp-1">
Web
</div>
<div class=" text-xs text-gray-600 line-clamp-1">
{$i18n.t('Web')}
</div>
</button>
</button>
{/if}
{/if}
</div>
</div>
...
...
src/lib/components/chat/MessageInput/Models.svelte
View file @
45311bfa
...
@@ -2,8 +2,10 @@
...
@@ -2,8 +2,10 @@
import { generatePrompt } from '$lib/apis/ollama';
import { generatePrompt } from '$lib/apis/ollama';
import { models } from '$lib/stores';
import { models } from '$lib/stores';
import { splitStream } from '$lib/utils';
import { splitStream } from '$lib/utils';
import { tick } from 'svelte';
import { tick, getContext } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
export let prompt = '';
export let user = null;
export let user = null;
...
@@ -41,7 +43,7 @@
...
@@ -41,7 +43,7 @@
user = JSON.parse(JSON.stringify(model.name));
user = JSON.parse(JSON.stringify(model.name));
await tick();
await tick();
chatInputPlaceholder =
`'$
{model
.n
ame}
'
is thinking...
`
;
chatInputPlaceholder =
$i18n.t('{
{model
N
ame}
}
is thinking...
', { modelName: model.name })
;
const chatInputElement = document.getElementById('chat-textarea');
const chatInputElement = document.getElementById('chat-textarea');
...
@@ -79,14 +81,18 @@
...
@@ -79,14 +81,18 @@
throw data;
throw data;
}
}
if (data.done == false) {
if ('id' in data) {
if (prompt == '' && data.response == '\n') {
console.log(data);
continue;
} else {
} else {
if (data.done == false) {
prompt += data.response;
if (prompt == '' && data.response == '\n') {
console.log(data.response);
continue;
chatInputElement.scrollTop = chatInputElement.scrollHeight;
} else {
await tick();
prompt += data.response;
console.log(data.response);
chatInputElement.scrollTop = chatInputElement.scrollHeight;
await tick();
}
}
}
}
}
}
}
...
@@ -109,7 +115,9 @@
...
@@ -109,7 +115,9 @@
toast.error(error.error);
toast.error(error.error);
}
}
} else {
} else {
toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
toast.error(
$i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' })
);
}
}
}
}
...
@@ -121,16 +129,16 @@
...
@@ -121,16 +129,16 @@
{#if filteredModels.length > 0}
{#if filteredModels.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full
rounded-lg border border-gray-100 dark:border-gray-700
">
<div class="flex w-full
px-2
">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-l
g
text-center">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-
x
l text-center">
<div class=" text-lg font-semibold mt-2">@</div>
<div class=" text-lg font-semibold mt-2">@</div>
</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-
lg
">
<div class="max-h-60 flex flex-col w-full rounded-r-
xl bg-white
">
<div class=" overflow-y-auto
bg-white
p-
2
rounded-
t
r-l
g
space-y-0.5">
<div class="
m-1
overflow-y-auto p-
1
rounded-r-
x
l space-y-0.5">
{#each filteredModels as model, modelIdx}
{#each filteredModels as model, modelIdx}
<button
<button
class=" px-3 py-1.5 rounded-l
g
w-full text-left {modelIdx === selectedIdx
class=" px-3 py-1.5 rounded-
x
l w-full text-left {modelIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button'
? ' bg-gray-100 selected-command-option-button'
: ''}"
: ''}"
type="button"
type="button"
...
...
src/lib/components/chat/MessageInput/PromptCommands.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import { prompts } from '$lib/stores';
import { prompts } from '$lib/stores';
import { findWordIndices } from '$lib/utils';
import { findWordIndices } from '$lib/utils';
import { tick } from 'svelte';
import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
export let prompt = '';
let selectedCommandIdx = 0;
let selectedCommandIdx = 0;
...
@@ -24,7 +27,18 @@
...
@@ -24,7 +27,18 @@
};
};
const confirmCommand = async (command) => {
const confirmCommand = async (command) => {
prompt = command.content;
let text = command.content;
if (command.content.includes('{{CLIPBOARD}}')) {
const clipboardText = await navigator.clipboard.readText().catch((err) => {
toast.error($i18n.t('Failed to read clipboard contents'));
return '{{CLIPBOARD}}';
});
text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
}
prompt = text;
const chatInputElement = document.getElementById('chat-textarea');
const chatInputElement = document.getElementById('chat-textarea');
...
@@ -48,16 +62,16 @@
...
@@ -48,16 +62,16 @@
{#if filteredPromptCommands.length > 0}
{#if filteredPromptCommands.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full
rounded-lg border border-gray-100 dark:border-gray-700
">
<div class="flex w-full
px-2
">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-l
g
text-center">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-
x
l text-center">
<div class=" text-lg font-semibold mt-2">/</div>
<div class=" text-lg font-semibold mt-2">/</div>
</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-
lg
">
<div class="max-h-60 flex flex-col w-full rounded-r-
xl bg-white
">
<div class=" overflow-y-auto
bg-white
p-
2
rounded-
t
r-l
g
space-y-0.5">
<div class="
m-1
overflow-y-auto p-
1
rounded-r-
x
l space-y-0.5">
{#each filteredPromptCommands as command, commandIdx}
{#each filteredPromptCommands as command, commandIdx}
<button
<button
class=" px-3 py-1.5 rounded-l
g
w-full text-left {commandIdx === selectedCommandIdx
class=" px-3 py-1.5 rounded-
x
l w-full text-left {commandIdx === selectedCommandIdx
? ' bg-gray-100 selected-command-option-button'
? ' bg-gray-100 selected-command-option-button'
: ''}"
: ''}"
type="button"
type="button"
...
@@ -81,7 +95,7 @@
...
@@ -81,7 +95,7 @@
</div>
</div>
<div
<div
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-l
g
flex items-center space-x-1"
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-
x
l flex items-center space-x-1"
>
>
<div>
<div>
<svg
<svg
...
@@ -101,8 +115,9 @@
...
@@ -101,8 +115,9 @@
</div>
</div>
<div class="line-clamp-1">
<div class="line-clamp-1">
Tip: Update multiple variable slots consecutively by pressing the tab key in the chat
{$i18n.t(
input after each replacement.
'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
)}
</div>
</div>
</div>
</div>
</div>
</div>
...
...
src/lib/components/chat/MessageInput/Suggestions.svelte
View file @
45311bfa
...
@@ -10,45 +10,47 @@
...
@@ -10,45 +10,47 @@
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
</script>
</script>
<div class=" flex flex-wrap-reverse mb-3 md:p-1 text-left w-full">
<div class=" mb-3 md:p-1 text-left w-full">
{#each prompts as prompt, promptIdx}
<div class=" flex flex-wrap-reverse px-2 text-left">
<div
{#each prompts as prompt, promptIdx}
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-2"
<div
>
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-1"
<button
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-white hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg transition group"
on:click={() => {
submitPrompt(prompt.content);
}}
>
>
<div class="flex flex-col text-left self-center">
<button
{#if prompt.title && prompt.title[0] !== ''}
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-2xl transition group"
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
on:click={() => {
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
submitPrompt(prompt.content);
{:else}
}}
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
{prompt.content}
</div>
{/if}
</div>
<div
class="self-center p-1 rounded-lg text-white group-hover:bg-gray-100 group-hover:text-gray-800 dark:group-hover:bg-gray-800 dark:group-hover:text-gray-100 dark:text-gray-900 transition"
>
>
<svg
<div class="flex flex-col text-left self-center">
xmlns="http://www.w3.org/2000/svg"
{#if prompt.title && prompt.title[0] !== ''}
viewBox="0 0 16 16"
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
fill="currentColor"
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
class="w-4 h-4"
{:else}
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
{prompt.content}
</div>
{/if}
</div>
<div
class="self-center p-1 rounded-lg text-gray-50 group-hover:text-gray-800 dark:text-gray-850 dark:group-hover:text-gray-100 transition"
>
>
<path
<svg
fill-rule="evenodd"
xmlns="http://www.w3.org/2000/svg"
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"
viewBox="0 0 16 16"
clip-rule="evenodd"
fill="currentColor"
/>
class="w-4 h-4"
</svg>
>
</div>
<path
</button>
fill-rule="evenodd"
</div>
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"
{/each}
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
{/each}
</div>
</div>
</div>
src/lib/components/chat/Messages.svelte
View file @
45311bfa
...
@@ -2,15 +2,18 @@
...
@@ -2,15 +2,18 @@
import { v4 as uuidv4 } from 'uuid';
import { v4 as uuidv4 } from 'uuid';
import { chats, config, modelfiles, settings, user } from '$lib/stores';
import { chats, config, modelfiles, settings, user } from '$lib/stores';
import { tick } from 'svelte';
import { tick
, getContext
} from 'svelte';
import toast from 'svelte-
french-toast
';
import
{
toast
}
from 'svelte-
sonner
';
import { getChatList, updateChatById } from '$lib/apis/chats';
import { getChatList, updateChatById } from '$lib/apis/chats';
import UserMessage from './Messages/UserMessage.svelte';
import UserMessage from './Messages/UserMessage.svelte';
import ResponseMessage from './Messages/ResponseMessage.svelte';
import ResponseMessage from './Messages/ResponseMessage.svelte';
import Placeholder from './Messages/Placeholder.svelte';
import Placeholder from './Messages/Placeholder.svelte';
import Spinner from '../common/Spinner.svelte';
import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images';
const i18n = getContext('i18n');
export let chatId = '';
export let chatId = '';
export let sendPrompt: Function;
export let sendPrompt: Function;
...
@@ -66,7 +69,7 @@
...
@@ -66,7 +69,7 @@
navigator.clipboard.writeText(text).then(
navigator.clipboard.writeText(text).then(
function () {
function () {
console.log('Async: Copying to clipboard was successful!');
console.log('Async: Copying to clipboard was successful!');
toast.success('Copying to clipboard was successful!');
toast.success(
$i18n.t(
'Copying to clipboard was successful!')
)
;
},
},
function (err) {
function (err) {
console.error('Async: Could not copy text: ', err);
console.error('Async: Could not copy text: ', err);
...
@@ -221,6 +224,81 @@
...
@@ -221,6 +224,81 @@
scrollToBottom();
scrollToBottom();
}, 100);
}, 100);
};
};
const messageDeleteHandler = async (messageId) => {
const messageToDelete = history.messages[messageId];
const messageParentId = messageToDelete.parentId;
const messageChildrenIds = messageToDelete.childrenIds ?? [];
const hasSibling = messageChildrenIds.some(
(childId) => history.messages[childId]?.childrenIds?.length > 0
);
messageChildrenIds.forEach((childId) => {
const child = history.messages[childId];
if (child && child.childrenIds) {
if (child.childrenIds.length === 0 && !hasSibling) {
// if last prompt/response pair
history.messages[messageParentId].childrenIds = [];
history.currentId = messageParentId;
} else {
child.childrenIds.forEach((grandChildId) => {
if (history.messages[grandChildId]) {
history.messages[grandChildId].parentId = messageParentId;
history.messages[messageParentId].childrenIds.push(grandChildId);
}
});
}
}
// remove response
history.messages[messageParentId].childrenIds = history.messages[
messageParentId
].childrenIds.filter((id) => id !== childId);
});
// remove prompt
history.messages[messageParentId].childrenIds = history.messages[
messageParentId
].childrenIds.filter((id) => id !== messageId);
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
};
// const messageDeleteHandler = async (messageId) => {
// const message = history.messages[messageId];
// const parentId = message.parentId;
// const childrenIds = message.childrenIds ?? [];
// const grandchildrenIds = [];
// // Iterate through childrenIds to find grandchildrenIds
// for (const childId of childrenIds) {
// const childMessage = history.messages[childId];
// const grandChildrenIds = childMessage.childrenIds ?? [];
// for (const grandchildId of grandchildrenIds) {
// const childMessage = history.messages[grandchildId];
// childMessage.parentId = parentId;
// }
// grandchildrenIds.push(...grandChildrenIds);
// }
// history.messages[parentId].childrenIds.push(...grandchildrenIds);
// history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
// (id) => id !== messageId
// );
// // Select latest message
// let currentMessageId = grandchildrenIds.at(-1);
// if (currentMessageId) {
// let messageChildrenIds = history.messages[currentMessageId].childrenIds;
// while (messageChildrenIds.length !== 0) {
// currentMessageId = messageChildrenIds.at(-1);
// messageChildrenIds = history.messages[currentMessageId].childrenIds;
// }
// history.currentId = currentMessageId;
// }
// await updateChatById(localStorage.token, chatId, { messages, history });
// };
</script>
</script>
{#if messages.length == 0}
{#if messages.length == 0}
...
@@ -237,8 +315,10 @@
...
@@ -237,8 +315,10 @@
>
>
{#if message.role === 'user'}
{#if message.role === 'user'}
<UserMessage
<UserMessage
on:delete={() => messageDeleteHandler(message.id)}
user={$user}
user={$user}
{message}
{message}
isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null
siblings={message.parentId !== null
? history.messages[message.parentId]?.childrenIds ?? []
? history.messages[message.parentId]?.childrenIds ?? []
: Object.values(history.messages)
: Object.values(history.messages)
...
@@ -249,52 +329,6 @@
...
@@ -249,52 +329,6 @@
{showNextMessage}
{showNextMessage}
{copyToClipboard}
{copyToClipboard}
/>
/>
{#if messages.length - 1 === messageIdx && processing !== ''}
<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
<div class=" dark:text-blue-100">
<svg
class=" w-4 h-4 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
>
</div>
<div class=" text-sm font-medium">
{processing}
</div>
</div>
{/if}
{:else}
{:else}
<ResponseMessage
<ResponseMessage
{message}
{message}
...
@@ -308,6 +342,16 @@
...
@@ -308,6 +342,16 @@
{copyToClipboard}
{copyToClipboard}
{continueGeneration}
{continueGeneration}
{regenerateResponse}
{regenerateResponse}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/>
/>
{/if}
{/if}
</div>
</div>
...
...
src/lib/components/chat/Messages/Placeholder.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import { onMount } from 'svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import { user } from '$lib/stores';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let models = [];
export let models = [];
export let modelfiles = [];
export let modelfiles = [];
...
@@ -27,14 +31,16 @@
...
@@ -27,14 +31,16 @@
>
>
{#if model in modelfiles}
{#if model in modelfiles}
<img
<img
src={modelfiles[model]?.imageUrl ??
'.
/favicon.png
'
}
src={modelfiles[model]?.imageUrl ??
`${WEBUI_BASE_URL}/static
/favicon.png
`
}
alt="modelfile"
alt="modelfile"
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
draggable="false"
draggable="false"
/>
/>
{:else}
{:else}
<img
<img
src={models.length === 1 ? '/favicon.png' : '/favicon.png'}
src={models.length === 1
? `${WEBUI_BASE_URL}/static/favicon.png`
: `${WEBUI_BASE_URL}/static/favicon.png`}
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo"
alt="logo"
draggable="false"
draggable="false"
...
@@ -44,7 +50,7 @@
...
@@ -44,7 +50,7 @@
{/each}
{/each}
</div>
</div>
</div>
</div>
<div class=" mt-2 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
<div class=" mt-2
mb-5
text-2xl text-gray-800 dark:text-gray-100 font-semibold">
{#if modelfile}
{#if modelfile}
<span class=" capitalize">
<span class=" capitalize">
{modelfile.title}
{modelfile.title}
...
@@ -60,7 +66,9 @@
...
@@ -60,7 +66,9 @@
</div>
</div>
{/if}
{/if}
{:else}
{:else}
How can I help you today?
<div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
<div>{$i18n.t('How can I help you today?')}</div>
{/if}
{/if}
</div>
</div>
</div>
</div>
...
...
src/lib/components/chat/Messages/ResponseMessage.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import toast from 'svelte-
french-toast
';
import
{
toast
}
from 'svelte-
sonner
';
import dayjs from 'dayjs';
import dayjs from 'dayjs';
import { marked } from 'marked';
import { marked } from 'marked';
import { settings } from '$lib/stores';
import tippy from 'tippy.js';
import tippy from 'tippy.js';
import auto_render from 'katex/dist/contrib/auto-render.mjs';
import auto_render from 'katex/dist/contrib/auto-render.mjs';
import 'katex/dist/katex.min.css';
import 'katex/dist/katex.min.css';
import { onMount, tick } from 'svelte';
import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
import { config, settings } from '$lib/stores';
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
import { imageGenerations } from '$lib/apis/images';
import { extractSentences } from '$lib/utils';
import Name from './Name.svelte';
import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte';
import ProfileImage from './ProfileImage.svelte';
import Skeleton from './Skeleton.svelte';
import Skeleton from './Skeleton.svelte';
import CodeBlock from './CodeBlock.svelte';
import CodeBlock from './CodeBlock.svelte';
import Image from '$lib/components/common/Image.svelte';
import {
synthesizeOpenAISpeech
} from '$lib/
apis/openai
';
import {
WEBUI_BASE_URL
} from '$lib/
constants
';
import
{ extractSentences } from '$lib/utils
';
import
Tooltip from '$lib/components/common/Tooltip.svelte
';
export let modelfiles = [];
export let modelfiles = [];
export let message;
export let message;
...
@@ -34,7 +44,7 @@
...
@@ -34,7 +44,7 @@
let edit = false;
let edit = false;
let editedContent = '';
let editedContent = '';
let editTextAreaElement: HTMLTextAreaElement;
let tooltipInstance = null;
let tooltipInstance = null;
let sentencesAudio = {};
let sentencesAudio = {};
...
@@ -42,6 +52,7 @@
...
@@ -42,6 +52,7 @@
let speakingIdx = null;
let speakingIdx = null;
let loadingSpeech = false;
let loadingSpeech = false;
let generatingImage = false;
$: tokens = marked.lexer(message.content);
$: tokens = marked.lexer(message.content);
...
@@ -72,13 +83,20 @@
...
@@ -72,13 +83,20 @@
if (message.info) {
if (message.info) {
tooltipInstance = tippy(`#info-${message.id}`, {
tooltipInstance = tippy(`#info-${message.id}`, {
content: `<span class="text-xs" id="tooltip-${message.id}">token/s: ${
content: `<span class="text-xs" id="tooltip-${message.id}">
response_
token/s: ${
`${
`${
Math.round(
Math.round(
((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100
((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100
) / 100
) / 100
} tokens` ?? 'N/A'
} tokens` ?? 'N/A'
}<br/>
}<br/>
prompt_token/s: ${
Math.round(
((message.info.prompt_eval_count ?? 0) /
(message.info.prompt_eval_duration / 1000000000)) *
100
) / 100 ?? 'N/A'
} tokens<br/>
total_duration: ${
total_duration: ${
Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ??
Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ??
'N/A'
'N/A'
...
@@ -109,10 +127,11 @@
...
@@ -109,10 +127,11 @@
// customised options
// customised options
// • auto-render specific keys, e.g.:
// • auto-render specific keys, e.g.:
delimiters: [
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$$', right: '$$', display: false },
// { left: '$', right: '$', display: false },
{ left: '$', right: '$', display: false },
{ left: '\\(', right: '\\)', display: true },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true }
{ left: '\\[', right: '\\]', display: false },
{ left: '[ ', right: ' ]', display: false }
],
],
// • rendering keys, e.g.:
// • rendering keys, e.g.:
throwOnError: false
throwOnError: false
...
@@ -232,10 +251,9 @@
...
@@ -232,10 +251,9 @@
editedContent = message.content;
editedContent = message.content;
await tick();
await tick();
const editElement = document.getElementById(`message-edit-${message.id}`);
editElement.style.height = '';
edit
TextArea
Element.style.height = '';
editElement.style.height = `${editElement.scrollHeight}px`;
edit
TextArea
Element.style.height = `${edit
TextArea
Element.scrollHeight}px`;
};
};
const editMessageConfirmHandler = async () => {
const editMessageConfirmHandler = async () => {
...
@@ -259,6 +277,25 @@
...
@@ -259,6 +277,25 @@
renderStyling();
renderStyling();
};
};
const generateImage = async (message) => {
generatingImage = true;
const res = await imageGenerations(localStorage.token, message.content).catch((error) => {
toast.error(error);
});
console.log(res);
if (res) {
message.files = res.map((image) => ({
type: 'image',
url: `${image.url}`
}));
dispatch('save', message);
}
generatingImage = false;
};
onMount(async () => {
onMount(async () => {
await tick();
await tick();
renderStyling();
renderStyling();
...
@@ -267,7 +304,9 @@
...
@@ -267,7 +304,9 @@
{#key message.id}
{#key message.id}
<div class=" flex w-full message-{message.id}">
<div class=" flex w-full message-{message.id}">
<ProfileImage src={modelfiles[message.model]?.imageUrl ?? '/favicon.png'} />
<ProfileImage
src={modelfiles[message.model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
/>
<div class="w-full overflow-hidden">
<div class="w-full overflow-hidden">
<Name>
<Name>
...
@@ -279,7 +318,7 @@
...
@@ -279,7 +318,7 @@
{#if message.timestamp}
{#if message.timestamp}
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')}
{dayjs(message.timestamp * 1000).format(
$i18n.t(
'DD/MM/YYYY HH:mm')
)
}
</span>
</span>
{/if}
{/if}
</Name>
</Name>
...
@@ -287,17 +326,31 @@
...
@@ -287,17 +326,31 @@
{#if message.content === ''}
{#if message.content === ''}
<Skeleton />
<Skeleton />
{:else}
{:else}
{#if message.files}
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
{#each message.files as file}
<div>
{#if file.type === 'image'}
<Image src={file.url} />
{/if}
</div>
{/each}
</div>
{/if}
<div
<div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8
prose-ol:p-0
prose-li:-mb-4 whitespace-pre-line"
>
>
<div>
<div>
{#if edit === true}
{#if edit === true}
<div class=" w-full">
<div class=" w-full">
<textarea
<textarea
id="message-edit-{message.id}"
id="message-edit-{message.id}"
bind:this={editTextAreaElement}
class=" bg-transparent outline-none w-full resize-none"
class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent}
bind:value={editedContent}
on:input={(e) => {
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`;
e.target.style.height = `${e.target.scrollHeight}px`;
}}
}}
/>
/>
...
@@ -309,7 +362,7 @@
...
@@ -309,7 +362,7 @@
editMessageConfirmHandler();
editMessageConfirmHandler();
}}
}}
>
>
Save
{$i18n.t('
Save
')}
</button>
</button>
<button
<button
...
@@ -318,7 +371,7 @@
...
@@ -318,7 +371,7 @@
cancelEditMessage();
cancelEditMessage();
}}
}}
>
>
Cancel
{$i18n.t('
Cancel
')}
</button>
</button>
</div>
</div>
</div>
</div>
...
@@ -366,10 +419,10 @@
...
@@ -366,10 +419,10 @@
{#if message.done}
{#if message.done}
<div
<div
class=" flex justify-start space-x-1
-mt-1
overflow-x-auto buttons text-gray-700 dark:text-gray-500"
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
>
>
{#if siblings.length > 1}
{#if siblings.length > 1}
<div class="flex self-center min-w-fit">
<div class="flex self-center min-w-fit
-mt-1
">
<button
<button
class="self-center dark:hover:text-white hover:text-black transition"
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
on:click={() => {
...
@@ -416,257 +469,346 @@
...
@@ -416,257 +469,346 @@
</div>
</div>
{/if}
{/if}
<button
<Tooltip content="Edit" placement="bottom">
class="{isLastMessage
<button
? 'visible'
class="{isLastMessage
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
? 'visible'
on:click={() => {
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
editMessageHandler();
on:click={() => {
}}
editMessageHandler();
>
}}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
on:click={() => {
copyToClipboard(message.content);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, 1);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
>
</button>
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, -1);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
>
>
</button>
<button
id="speak-button-{message.id}"
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
if (!loadingSpeech) {
toggleSpeakMessage(message);
}
}}
>
{#if loadingSpeech}
<svg
class=" w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s;
}
.spinner_Km9P {
animation-delay: -0.65s;
}
.spinner_JApP {
animation-delay: -0.5s;
}
@keyframes spinner_MGfb {
93.75%,
100% {
opacity: 0.2;
}
}
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
class="spinner_S1WN spinner_Km9P"
cx="12"
cy="12"
r="3"
/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
>
{:else if speaking}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
/>
</svg>
{:else}
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
fill="none"
fill="none"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-width="
1.5
"
stroke-width="
2
"
stroke="currentColor"
stroke="currentColor"
class="w-4 h-4"
class="w-4 h-4"
>
>
<path
<path
stroke-linecap="round"
stroke-linecap="round"
stroke-linejoin="round"
stroke-linejoin="round"
d="M1
9.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z
"
d="M1
6.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125
"
/>
/>
</svg>
</svg>
{/if}
</button>
</
button
>
</
Tooltip
>
{#if message.info}
<Tooltip content="Copy" placement="bottom">
<button
<button
class="
{isLastMessage
class="{isLastMessage
? 'visible'
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition
whitespace-pre-wrap
"
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition
copy-response-button
"
on:click={() => {
on:click={() => {
co
nsole.log(message
);
co
pyToClipboard(message.content
);
}}
}}
id="info-{message.id}"
>
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
fill="none"
fill="none"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-width="
1.5
"
stroke-width="
2
"
stroke="currentColor"
stroke="currentColor"
class="w-4 h-4"
class="w-4 h-4"
>
>
<path
<path
stroke-linecap="round"
stroke-linecap="round"
stroke-linejoin="round"
stroke-linejoin="round"
d="M1
1.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z
"
d="M1
5.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184
"
/>
/>
</svg>
</svg>
</button>
</button>
{/if}
</Tooltip>
{#if isLastMessage}
<Tooltip content="Good Response" placement="bottom">
<button
<button
type="button"
class="{isLastMessage
class="{isLastMessage
? 'visible'
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
on:click={() => {
continueGeneration(
);
rateMessage(message.id, 1
);
}}
}}
>
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg
"
stroke="currentColor
"
fill="none"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-
width="1.5
"
stroke-
linecap="round
"
stroke
="currentColor
"
stroke
-linejoin="round
"
class="w-4 h-4"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
>
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
/>
</svg>
</button>
</button>
</Tooltip>
<Tooltip content="Bad Response" placement="bottom">
<button
<button
type="button"
class="{isLastMessage
class="{isLastMessage
? 'visible'
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
on:click={regenerateResponse}
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, -1);
}}
>
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg
"
stroke="currentColor
"
fill="none"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-
width="1.5
"
stroke-
linecap="round
"
stroke
="currentColor
"
stroke
-linejoin="round
"
class="w-4 h-4"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
>
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
</button>
</Tooltip>
<Tooltip content="Read Aloud" placement="bottom">
<button
id="speak-button-{message.id}"
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
if (!loadingSpeech) {
toggleSpeakMessage(message);
}
}}
>
{#if loadingSpeech}
<svg
class=" w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s;
}
.spinner_Km9P {
animation-delay: -0.65s;
}
.spinner_JApP {
animation-delay: -0.5s;
}
@keyframes spinner_MGfb {
93.75%,
100% {
opacity: 0.2;
}
}
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
class="spinner_S1WN spinner_Km9P"
cx="12"
cy="12"
r="3"
/><circle
class="spinner_S1WN spinner_JApP"
cx="20"
cy="12"
r="3"
/></svg
>
{:else if speaking}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
/>
</svg>
{/if}
</button>
</Tooltip>
{#if $config.images}
<Tooltip content="Generate Image" placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
if (!generatingImage) {
generateImage(message);
}
}}
>
{#if generatingImage}
<svg
class=" w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s;
}
.spinner_Km9P {
animation-delay: -0.65s;
}
.spinner_JApP {
animation-delay: -0.5s;
}
@keyframes spinner_MGfb {
93.75%,
100% {
opacity: 0.2;
}
}
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
class="spinner_S1WN spinner_Km9P"
cx="12"
cy="12"
r="3"
/><circle
class="spinner_S1WN spinner_JApP"
cx="20"
cy="12"
r="3"
/></svg
>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
/>
</svg>
{/if}
</button>
</Tooltip>
{/if}
{#if message.info}
<Tooltip content="Generation Info" placement="bottom">
<button
class=" {isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
on:click={() => {
console.log(message);
}}
id="info-{message.id}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
</button>
</Tooltip>
{/if}
{#if isLastMessage}
<Tooltip content="Continue Response" placement="bottom">
<button
type="button"
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={() => {
continueGeneration();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
/>
</svg>
</button>
</Tooltip>
<Tooltip content="Regenerate" placement="bottom">
<button
type="button"
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={regenerateResponse}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
</Tooltip>
{/if}
{/if}
</div>
</div>
{/if}
{/if}
...
...
src/lib/components/chat/Messages/UserMessage.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import dayjs from 'dayjs';
import dayjs from 'dayjs';
import { tick } from 'svelte';
import { tick
, createEventDispatcher, getContext
} from 'svelte';
import Name from './Name.svelte';
import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte';
import ProfileImage from './ProfileImage.svelte';
import { modelfiles, settings } from '$lib/stores';
import { modelfiles, settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let user;
export let user;
export let message;
export let message;
export let siblings;
export let siblings;
export let isFirstMessage: boolean;
export let confirmEditMessage: Function;
export let confirmEditMessage: Function;
export let showPreviousMessage: Function;
export let showPreviousMessage: Function;
...
@@ -17,18 +23,17 @@
...
@@ -17,18 +23,17 @@
let edit = false;
let edit = false;
let editedContent = '';
let editedContent = '';
let messageEditTextAreaElement: HTMLTextAreaElement;
const editMessageHandler = async () => {
const editMessageHandler = async () => {
edit = true;
edit = true;
editedContent = message.content;
editedContent = message.content;
await tick();
await tick();
const editElement = document.getElementById(`message-edit-${message.id}`);
edit
Element.style.height = '';
messageEditTextArea
Element.style.height = '';
edit
Element.style.height = `${
edit
Element.scrollHeight}px`;
messageEditTextArea
Element.style.height = `${
messageEditTextArea
Element.scrollHeight}px`;
edit
Element?.focus();
messageEditTextArea
Element?.focus();
};
};
const editMessageConfirmHandler = async () => {
const editMessageConfirmHandler = async () => {
...
@@ -42,6 +47,10 @@
...
@@ -42,6 +47,10 @@
edit = false;
edit = false;
editedContent = '';
editedContent = '';
};
};
const deleteMessageHandler = async () => {
dispatch('delete', message.id);
};
</script>
</script>
<div class=" flex w-full">
<div class=" flex w-full">
...
@@ -58,17 +67,18 @@
...
@@ -58,17 +67,18 @@
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
{:else}
{:else}
You <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
{$i18n.t('You')}
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
{/if}
{/if}
{:else if $settings.showUsername}
{:else if $settings.showUsername}
{user.name}
{user.name}
{:else}
{:else}
You
{$i18n.t('You')}
{/if}
{/if}
{#if message.timestamp}
{#if message.timestamp}
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')}
{dayjs(message.timestamp * 1000).format(
$i18n.t(
'DD/MM/YYYY HH:mm')
)
}
</span>
</span>
{/if}
{/if}
</Name>
</Name>
...
@@ -116,7 +126,7 @@
...
@@ -116,7 +126,7 @@
{file.name}
{file.name}
</div>
</div>
<div class=" text-gray-500 text-sm">Document</div>
<div class=" text-gray-500 text-sm">
{$i18n.t('
Document
')}
</div>
</div>
</div>
</button>
</button>
{:else if file.type === 'collection'}
{:else if file.type === 'collection'}
...
@@ -145,7 +155,7 @@
...
@@ -145,7 +155,7 @@
{file?.title ?? `#${file.name}`}
{file?.title ?? `#${file.name}`}
</div>
</div>
<div class=" text-gray-500 text-sm">Collection</div>
<div class=" text-gray-500 text-sm">
{$i18n.t('
Collection
')}
</div>
</div>
</div>
</button>
</button>
{/if}
{/if}
...
@@ -158,9 +168,11 @@
...
@@ -158,9 +168,11 @@
<div class=" w-full">
<div class=" w-full">
<textarea
<textarea
id="message-edit-{message.id}"
id="message-edit-{message.id}"
bind:this={messageEditTextAreaElement}
class=" bg-transparent outline-none w-full resize-none"
class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent}
bind:value={editedContent}
on:input={(e) => {
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`;
e.target.style.height = `${e.target.scrollHeight}px`;
}}
}}
/>
/>
...
@@ -172,7 +184,7 @@
...
@@ -172,7 +184,7 @@
editMessageConfirmHandler();
editMessageConfirmHandler();
}}
}}
>
>
Save & Submit
{$i18n.t('
Save & Submit
')}
</button>
</button>
<button
<button
...
@@ -181,7 +193,7 @@
...
@@ -181,7 +193,7 @@
cancelEditMessage();
cancelEditMessage();
}}
}}
>
>
Cancel
{$i18n.t('
Cancel
')}
</button>
</button>
</div>
</div>
</div>
</div>
...
@@ -189,11 +201,11 @@
...
@@ -189,11 +201,11 @@
<div class="w-full">
<div class="w-full">
<pre id="user-message">{message.content}</pre>
<pre id="user-message">{message.content}</pre>
<div class=" flex justify-start space-x-1">
<div class=" flex justify-start space-x-1
text-gray-700 dark:text-gray-500
">
{#if siblings.length > 1}
{#if siblings.length > 1}
<div class="flex self-center">
<div class="flex self-center
-mt-1
">
<button
<button
class="self-center"
class="self-center
dark:hover:text-white hover:text-black transition
"
on:click={() => {
on:click={() => {
showPreviousMessage(message);
showPreviousMessage(message);
}}
}}
...
@@ -212,12 +224,12 @@
...
@@ -212,12 +224,12 @@
</svg>
</svg>
</button>
</button>
<div class="text-xs font-bold self-center">
<div class="text-xs font-bold self-center
dark:text-gray-100
">
{siblings.indexOf(message.id) + 1} / {siblings.length}
{siblings.indexOf(message.id) + 1} / {siblings.length}
</div>
</div>
<button
<button
class="self-center"
class="self-center
dark:hover:text-white hover:text-black transition
"
on:click={() => {
on:click={() => {
showNextMessage(message);
showNextMessage(message);
}}
}}
...
@@ -238,49 +250,79 @@
...
@@ -238,49 +250,79 @@
</div>
</div>
{/if}
{/if}
<button
<Tooltip content="Edit" placement="bottom">
class="invisible group-hover:visible p-1 rounded dark:hover:text-white transition edit-user-message-button"
<button
on:click={() => {
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
editMessageHandler();
on:click={() => {
}}
editMessageHandler();
>
}}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
>
<path
<svg
stroke-linecap="round"
xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round"
fill="none"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
viewBox="0 0 24 24"
/>
stroke-width="2"
</svg>
stroke="currentColor"
</button>
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
</Tooltip>
<button
<Tooltip content="Copy" placement="bottom">
class="invisible group-hover:visible p-1 rounded dark:hover:text-white transition"
<button
on:click={() => {
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
copyToClipboard(message.content);
on:click={() => {
}}
copyToClipboard(message.content);
>
}}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
>
<path
<svg
stroke-linecap="round"
xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round"
fill="none"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
viewBox="0 0 24 24"
/>
stroke-width="2"
</svg>
stroke="currentColor"
</button>
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
</Tooltip>
{#if !isFirstMessage}
<Tooltip content="Delete" placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
deleteMessageHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</Tooltip>
{/if}
</div>
</div>
</div>
</div>
{/if}
{/if}
...
...
src/lib/components/chat/ModelSelector.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import { setDefaultModels } from '$lib/apis/configs';
import { setDefaultModels } from '$lib/apis/configs';
import { models, showSettings, settings, user } from '$lib/stores';
import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
import toast from 'svelte-french-toast';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let selectedModels = [''];
export let selectedModels = [''];
export let disabled = false;
export let disabled = false;
...
@@ -10,7 +12,7 @@
...
@@ -10,7 +12,7 @@
const saveDefaultModel = async () => {
const saveDefaultModel = async () => {
const hasEmptyModel = selectedModels.filter((it) => it === '');
const hasEmptyModel = selectedModels.filter((it) => it === '');
if (hasEmptyModel.length) {
if (hasEmptyModel.length) {
toast.error('Choose a model before saving...');
toast.error(
$i18n.t(
'Choose a model before saving...')
)
;
return;
return;
}
}
settings.set({ ...$settings, models: selectedModels });
settings.set({ ...$settings, models: selectedModels });
...
@@ -20,12 +22,12 @@
...
@@ -20,12 +22,12 @@
console.log('setting default models globally');
console.log('setting default models globally');
await setDefaultModels(localStorage.token, selectedModels.join(','));
await setDefaultModels(localStorage.token, selectedModels.join(','));
}
}
toast.success('Default model updated');
toast.success(
$i18n.t(
'Default model updated')
)
;
};
};
$: if (selectedModels.length > 0 && $models.length > 0) {
$: if (selectedModels.length > 0 && $models.length > 0) {
selectedModels = selectedModels.map((model) =>
selectedModels = selectedModels.map((model) =>
$models.map((m) => m.
name
).includes(model) ? model : ''
$models.map((m) => m.
id
).includes(model) ? model : ''
);
);
}
}
</script>
</script>
...
@@ -39,13 +41,15 @@
...
@@ -39,13 +41,15 @@
bind:value={selectedModel}
bind:value={selectedModel}
{disabled}
{disabled}
>
>
<option class=" text-gray-700" value="" selected disabled>Select a model</option>
<option class=" text-gray-700" value="" selected disabled
>{$i18n.t('Select a model')}</option
>
{#each $models as model}
{#each $models as model}
{#if model.name === 'hr'}
{#if model.name === 'hr'}
<hr />
<hr />
{:else}
{:else}
<option value={model.
name
} class="text-gray-700 text-lg"
<option value={model.
id
} class="text-gray-700 text-lg"
>{model.name +
>{model.name +
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
>
>
...
@@ -133,5 +137,5 @@
...
@@ -133,5 +137,5 @@
</div>
</div>
<div class="text-left mt-1.5 text-xs text-gray-500">
<div class="text-left mt-1.5 text-xs text-gray-500">
<button on:click={saveDefaultModel}> Set as default</button>
<button on:click={saveDefaultModel}>
{$i18n.t('
Set as default
')}
</button>
</div>
</div>
src/lib/components/chat/Settings/About.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import { getVersionUpdates } from '$lib/apis';
import { getOllamaVersion } from '$lib/apis/ollama';
import { getOllamaVersion } from '$lib/apis/ollama';
import { WEBUI_NAME, WEB_UI_VERSION } from '$lib/constants';
import { WEBUI_VERSION } from '$lib/constants';
import { config } from '$lib/stores';
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
import { onMount } from 'svelte';
import { compareVersion } from '$lib/utils';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
let ollamaVersion = '';
let ollamaVersion = '';
let updateAvailable = null;
let version = {
current: '',
latest: ''
};
const checkForVersionUpdates = async () => {
updateAvailable = null;
version = await getVersionUpdates(localStorage.token).catch((error) => {
return {
current: WEBUI_VERSION,
latest: WEBUI_VERSION
};
});
console.log(version);
updateAvailable = compareVersion(version.latest, version.current);
console.log(updateAvailable);
};
onMount(async () => {
onMount(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
return '';
return '';
});
});
checkForVersionUpdates();
});
});
</script>
</script>
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
<div class=" space-y-3">
<div class=" space-y-3">
<div>
<div>
<div class=" mb-2.5 text-sm font-medium
">{WEBUI_NAME} Version</div
>
<div class=" mb-2.5 text-sm font-medium
flex space-x-2 items-center"
>
<div
class="flex w-full"
>
<div>
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
{$WEBUI_NAME}
{$
config && $config.version ? $config.version : WEB_UI_VERSION
}
{$
i18n.t('Version')
}
</div>
</div>
</div>
</div>
</div>
<div class="flex w-full justify-between items-center">
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
<div>
v{WEBUI_VERSION}
<hr class=" dark:border-gray-700" />
<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>
</div>
<div>
<button
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
<div class="flex w-full">
on:click={() => {
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
showChangelog.set(true);
{ollamaVersion ?? 'N/A'}
}}
>
<div>{$i18n.t("See what's new")}</div>
</button>
</div>
</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>
</div>
</div>
</div>
</div>
{#if ollamaVersion}
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
<div class="flex w-full">
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
{ollamaVersion ?? 'N/A'}
</div>
</div>
</div>
{/if}
<hr class=" dark:border-gray-700" />
<hr class=" dark:border-gray-700" />
<div class="flex space-x-1">
<div class="flex space-x-1">
...
@@ -44,16 +110,24 @@
...
@@ -44,16 +110,24 @@
/>
/>
</a>
</a>
<a href="https://github.com/ollama-webui/ollama-webui" target="_blank">
<a href="https://twitter.com/OpenWebUI" target="_blank">
<img
alt="X (formerly Twitter) Follow"
src="https://img.shields.io/twitter/follow/OpenWebUI"
/>
</a>
<a href="https://github.com/open-webui/open-webui" target="_blank">
<img
<img
alt="Github Repo"
alt="Github Repo"
src="https://img.shields.io/github/stars/o
llama
-webui/o
llama
-webui?style=social&label=Star us on Github"
src="https://img.shields.io/github/stars/o
pen
-webui/o
pen
-webui?style=social&label=Star us on Github"
/>
/>
</a>
</a>
</div>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Created by <a
{$i18n.t('Created by')}
<a
class=" text-gray-500 dark:text-gray-300 font-medium"
class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://github.com/tjbck"
href="https://github.com/tjbck"
target="_blank">Timothy J. Baek</a
target="_blank">Timothy J. Baek</a
...
...
src/lib/components/chat/Settings/Account.svelte
View file @
45311bfa
<script lang="ts">
<script lang="ts">
import toast from 'svelte-
french-toast
';
import
{
toast
}
from 'svelte-
sonner
';
import { onMount } from 'svelte';
import { onMount
, getContext
} from 'svelte';
import { user } from '$lib/stores';
import { user } from '$lib/stores';
import { updateUserProfile } from '$lib/apis/auths';
import { updateUserProfile } from '$lib/apis/auths';
import UpdatePassword from './Account/UpdatePassword.svelte';
import UpdatePassword from './Account/UpdatePassword.svelte';
import { getGravatarUrl } from '$lib/apis/utils';
import { getGravatarUrl } from '$lib/apis/utils';
import { copyToClipboard } from '$lib/utils';
const i18n = getContext('i18n');
export let saveHandler: Function;
export let saveHandler: Function;
let profileImageUrl = '';
let profileImageUrl = '';
let name = '';
let name = '';
let showJWTToken = false;
let JWTTokenCopied = false;
let profileImageInputElement: HTMLInputElement;
const submitHandler = async () => {
const submitHandler = async () => {
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
...
@@ -34,14 +40,15 @@
...
@@ -34,14 +40,15 @@
</script>
</script>
<div class="flex flex-col h-full justify-between text-sm">
<div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-
80
">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-
[22rem]
">
<input
<input
id="profile-image-input"
id="profile-image-input"
bind:this={profileImageInputElement}
type="file"
type="file"
hidden
hidden
accept="image/*"
accept="image/*"
on:change={(e) => {
on:change={(e) => {
const files =
e?.target?
.files ?? [];
const files =
profileImageInputElement
.files ?? [];
let reader = new FileReader();
let reader = new FileReader();
reader.onload = (event) => {
reader.onload = (event) => {
let originalImageUrl = `${event.target.result}`;
let originalImageUrl = `${event.target.result}`;
...
@@ -83,7 +90,7 @@
...
@@ -83,7 +90,7 @@
// Display the compressed image
// Display the compressed image
profileImageUrl = compressedSrc;
profileImageUrl = compressedSrc;
e.targe
t.files = null;
profileImageInputElemen
t.files = null;
};
};
};
};
...
@@ -96,7 +103,7 @@
...
@@ -96,7 +103,7 @@
}}
}}
/>
/>
<div class=" mb-2.5 text-sm font-medium">Profile</div>
<div class=" mb-2.5 text-sm font-medium">
{$i18n.t('
Profile
')}
</div>
<div class="flex space-x-5">
<div class="flex space-x-5">
<div class="flex flex-col">
<div class="flex flex-col">
...
@@ -105,7 +112,7 @@
...
@@ -105,7 +112,7 @@
class="relative rounded-full dark:bg-gray-700"
class="relative rounded-full dark:bg-gray-700"
type="button"
type="button"
on:click={() => {
on:click={() => {
document.getElementById('
profile
-i
mage
-i
nput
')?
.click();
profile
I
mage
I
nput
Element
.click();
}}
}}
>
>
<img
<img
...
@@ -138,13 +145,13 @@
...
@@ -138,13 +145,13 @@
const url = await getGravatarUrl($user.email);
const url = await getGravatarUrl($user.email);
profileImageUrl = url;
profileImageUrl = url;
}}>Use Gravatar</button
}}>
{$i18n.t('
Use Gravatar
')}
</button
>
>
</div>
</div>
<div class="flex-1">
<div class="flex-1">
<div class="flex flex-col w-full">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Name</div>
<div class=" mb-1 text-xs text-gray-500">
{$i18n.t('
Name
')}
</div>
<div class="flex-1">
<div class="flex-1">
<input
<input
...
@@ -160,11 +167,113 @@
...
@@ -160,11 +167,113 @@
<hr class=" dark:border-gray-700 my-4" />
<hr class=" dark:border-gray-700 my-4" />
<UpdatePassword />
<UpdatePassword />
<hr class=" dark:border-gray-700 my-4" />
<div class=" w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
</div>
<div class="flex mt-2">
<div class="flex w-full">
<input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
type={showJWTToken ? 'text' : 'password'}
value={localStorage.token}
disabled
/>
<button
class="dark:bg-gray-800 px-2 transition rounded-r-lg"
on:click={() => {
showJWTToken = !showJWTToken;
}}
>
{#if showJWTToken}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
clip-rule="evenodd"
/>
<path
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<path
fill-rule="evenodd"
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
<button
class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg"
on:click={() => {
copyToClipboard(localStorage.token);
JWTTokenCopied = true;
setTimeout(() => {
JWTTokenCopied = false;
}, 2000);
}}
>
{#if JWTTokenCopied}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<div class="flex justify-end pt-3 text-sm font-medium">
<button
<button
class=" px-4 py-2 bg-emerald-
6
00 hover:bg-emerald-
7
00 text-gray-100 transition rounded"
class="
px-4 py-2 bg-emerald-
7
00 hover:bg-emerald-
8
00 text-gray-100 transition rounded
-lg
"
on:click={async () => {
on:click={async () => {
const res = await submitHandler();
const res = await submitHandler();
...
@@ -173,7 +282,7 @@
...
@@ -173,7 +282,7 @@
}
}
}}
}}
>
>
Save
{$i18n.t('
Save
')}
</button>
</button>
</div>
</div>
</div>
</div>
Prev
1
2
3
4
5
6
7
8
9
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment