refac
This commit is contained in:
@@ -70,6 +70,7 @@
|
||||
|
||||
import {
|
||||
archiveChatById,
|
||||
cloneChatById,
|
||||
compactChatById,
|
||||
createNewChat,
|
||||
deleteChatById,
|
||||
@@ -2265,6 +2266,53 @@
|
||||
document.getElementById('chat-input')?.focus();
|
||||
};
|
||||
|
||||
const handleForkChat = async () => {
|
||||
if (!$chatId || !history?.currentId) {
|
||||
toast.message($i18n.t('No chat to fork'));
|
||||
return;
|
||||
}
|
||||
if (!($user?.role === 'admin' || ($user?.permissions?.chat?.import ?? true))) {
|
||||
toast.error($i18n.t('Access prohibited'));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentMessage = history.messages?.[history.currentId];
|
||||
if (
|
||||
generating ||
|
||||
taskIds?.length ||
|
||||
(currentMessage?.role === 'assistant' && !currentMessage.done)
|
||||
) {
|
||||
toast.warning($i18n.t('Wait for the current response to finish before forking.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const toastId = toast.loading($i18n.t('Forking chat...'));
|
||||
|
||||
try {
|
||||
const result = await cloneChatById(
|
||||
localStorage.token,
|
||||
$chatId,
|
||||
$i18n.t('Clone of {{TITLE}}', {
|
||||
TITLE: $chatTitle || 'Chat'
|
||||
})
|
||||
);
|
||||
|
||||
if (result?.id) {
|
||||
await goto(`/c/${result.id}`);
|
||||
await refreshChatList(localStorage.token, { refreshPinned: true });
|
||||
toast.success($i18n.t('Chat forked'), { id: toastId });
|
||||
} else {
|
||||
toast.error($i18n.t('Failed to fork chat'), { id: toastId });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(`${error}`, { id: toastId });
|
||||
} finally {
|
||||
messageInput?.setText('');
|
||||
prompt = '';
|
||||
document.getElementById('chat-input')?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const submitHandler = async (userPrompt, { _raw = false } = {}) => {
|
||||
console.log('submitHandler', userPrompt, $chatId);
|
||||
|
||||
@@ -2284,6 +2332,10 @@
|
||||
handleStatusCommand();
|
||||
return;
|
||||
}
|
||||
if (String(userPrompt).trim() === '/fork') {
|
||||
await handleForkChat();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingOAuthTools.length > 0) {
|
||||
toast.warning($i18n.t('Please connect all required integrations before sending a message'));
|
||||
@@ -3451,6 +3503,7 @@
|
||||
{contextUsage}
|
||||
compactHandler={handleManualCompact}
|
||||
statusHandler={handleStatusCommand}
|
||||
forkHandler={handleForkChat}
|
||||
toolServers={$toolServers}
|
||||
{generating}
|
||||
{stopResponse}
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
export let stopResponse: Function;
|
||||
export let compactHandler: Function = () => {};
|
||||
export let statusHandler: Function = () => {};
|
||||
export let forkHandler: Function = () => {};
|
||||
export let chatId = '';
|
||||
export let contextUsage = null;
|
||||
|
||||
@@ -1086,9 +1087,12 @@
|
||||
canCompact: () => !!history?.currentId,
|
||||
compactDisabled: () => isActive,
|
||||
canStatus: () => !!history?.currentId,
|
||||
canFork: () => !!history?.currentId,
|
||||
forkDisabled: () => isActive,
|
||||
contextUsage: () => contextUsage,
|
||||
onCompact: compactHandler,
|
||||
onStatus: statusHandler,
|
||||
onFork: forkHandler,
|
||||
onSelect: (e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
|
||||
@@ -14,16 +14,21 @@
|
||||
export let onUpload: (e: any) => void = () => {};
|
||||
export let onCompact: () => void = () => {};
|
||||
export let onStatus: () => void = () => {};
|
||||
export let onFork: () => void = () => {};
|
||||
export let insertTextHandler: (text: string) => void = () => {};
|
||||
export let canCompact: boolean | (() => boolean) = false;
|
||||
export let compactDisabled: boolean | (() => boolean) = false;
|
||||
export let canStatus: boolean | (() => boolean) = false;
|
||||
export let canFork: boolean | (() => boolean) = false;
|
||||
export let forkDisabled: boolean | (() => boolean) = false;
|
||||
export let contextUsage = null;
|
||||
|
||||
$: compactAvailable = typeof canCompact === 'function' ? canCompact() : canCompact;
|
||||
$: isCompactDisabled =
|
||||
typeof compactDisabled === 'function' ? compactDisabled() : compactDisabled;
|
||||
$: statusAvailable = typeof canStatus === 'function' ? canStatus() : canStatus;
|
||||
$: forkAvailable = typeof canFork === 'function' ? canFork() : canFork;
|
||||
$: isForkDisabled = typeof forkDisabled === 'function' ? forkDisabled() : forkDisabled;
|
||||
$: resolvedContextUsage = typeof contextUsage === 'function' ? contextUsage() : contextUsage;
|
||||
$: contextPercent = Math.max(0, Math.round(resolvedContextUsage?.percent ?? 0));
|
||||
|
||||
@@ -77,6 +82,8 @@
|
||||
canCompact={compactAvailable}
|
||||
compactDisabled={isCompactDisabled}
|
||||
canStatus={statusAvailable}
|
||||
canFork={forkAvailable}
|
||||
forkDisabled={isForkDisabled}
|
||||
{contextPercent}
|
||||
onSelect={(e) => {
|
||||
const { type, data } = e;
|
||||
@@ -89,6 +96,9 @@
|
||||
} else if (type === 'command' && data.id === 'status') {
|
||||
insertTextHandler('');
|
||||
onStatus();
|
||||
} else if (type === 'command' && data.id === 'fork') {
|
||||
insertTextHandler('');
|
||||
onFork();
|
||||
} else if (type === 'skill') {
|
||||
command({
|
||||
id: `${data.id}|${data.name}`,
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
export let canCompact = false;
|
||||
export let compactDisabled = false;
|
||||
export let canStatus = false;
|
||||
export let canFork = false;
|
||||
export let forkDisabled = false;
|
||||
export let contextPercent = 0;
|
||||
|
||||
let selectedIdx = 0;
|
||||
@@ -28,6 +30,9 @@
|
||||
...(canCompact && 'compact'.startsWith(query.toLowerCase())
|
||||
? [{ type: 'command', data: { id: 'compact' } }]
|
||||
: []),
|
||||
...(canFork && 'fork'.startsWith(query.toLowerCase())
|
||||
? [{ type: 'command', data: { id: 'fork' } }]
|
||||
: []),
|
||||
...(canStatus && 'status'.startsWith(query.toLowerCase())
|
||||
? [{ type: 'command', data: { id: 'status' } }]
|
||||
: [])
|
||||
@@ -86,6 +91,9 @@
|
||||
if (item?.type === 'command' && item.data?.id === 'compact' && compactDisabled) {
|
||||
return;
|
||||
}
|
||||
if (item?.type === 'command' && item.data?.id === 'fork' && forkDisabled) {
|
||||
return;
|
||||
}
|
||||
if (item) {
|
||||
onSelect(item);
|
||||
}
|
||||
@@ -167,6 +175,50 @@
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else if item.data.id === 'fork'}
|
||||
<Tooltip content="Clone this chat into a new copy." placement="top">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Fork: clone this chat into a new copy."
|
||||
class="slash-command-row flex items-center gap-2 w-full h-6 px-2 rounded-xl text-xs text-left transition-colors duration-75
|
||||
{commandIdx === selectedIdx ? 'app-interactive-active' : ''} disabled:opacity-50"
|
||||
disabled={forkDisabled}
|
||||
on:mousedown={(e) => e.preventDefault()}
|
||||
on:click={() => {
|
||||
if (!forkDisabled) {
|
||||
onSelect(item);
|
||||
}
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
selectedIdx = commandIdx;
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
data-selected={commandIdx === selectedIdx}
|
||||
>
|
||||
<span class="app-icon-muted flex items-center justify-center w-4 shrink-0">
|
||||
<svg
|
||||
class="size-3.5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4 12H9" />
|
||||
<path d="M9 12C12.5 12 12.5 7 16 7H20" />
|
||||
<path d="M17 4L20 7L17 10" />
|
||||
<path d="M9 12C12.5 12 12.5 17 16 17H20" />
|
||||
<path d="M17 14L20 17L17 20" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
|
||||
<span class="truncate">Fork</span>
|
||||
<span class="app-muted text-[0.625rem] truncate shrink-0">Clone chat</span>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else if item.data.id === 'status'}
|
||||
<Tooltip content="Check what is running in this chat." placement="top">
|
||||
<button
|
||||
|
||||
@@ -124,119 +124,108 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="tab-account" class="flex flex-col h-full justify-between text-sm">
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5">
|
||||
<div class="space-y-1">
|
||||
<div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">
|
||||
{$i18n.t('Your Account')}
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500 mt-0.5">
|
||||
{$i18n.t('Manage your account information.')}
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-account" class="flex h-full flex-col text-sm">
|
||||
<div class="flex-1 min-h-0 w-full overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5 py-1">
|
||||
<h2 class="mb-4 text-sm font-medium text-gray-900 dark:text-white">{$i18n.t('Account')}</h2>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- <div class=" text-sm font-normal">{$i18n.t('Account')}</div> -->
|
||||
|
||||
<div class="flex space-x-5 my-4">
|
||||
<UserProfileImage bind:profileImageUrl user={$user} />
|
||||
<UserProfileImage
|
||||
bind:profileImageUrl
|
||||
user={$user}
|
||||
variant="account"
|
||||
displayName={$user?.name}
|
||||
/>
|
||||
|
||||
<div class="flex flex-1 flex-col">
|
||||
<div class=" flex-1">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs font-normal">{$i18n.t('Name')}</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Name')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
aria-label={$i18n.t('Name')}
|
||||
required
|
||||
placeholder={$i18n.t('Enter your name')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
aria-label={$i18n.t('Name')}
|
||||
required
|
||||
placeholder={$i18n.t('Enter your name')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-1 text-xs font-normal">{$i18n.t('Bio')}</div>
|
||||
<div class="mt-2 flex flex-col w-full">
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Bio')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<Textarea
|
||||
className="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
minSize={60}
|
||||
bind:value={bio}
|
||||
ariaLabel={$i18n.t('Bio')}
|
||||
placeholder={$i18n.t('Share your background and interests')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Textarea
|
||||
className="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
minSize={60}
|
||||
bind:value={bio}
|
||||
ariaLabel={$i18n.t('Bio')}
|
||||
placeholder={$i18n.t('Share your background and interests')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-1 text-xs font-normal">{$i18n.t('Gender')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
|
||||
bind:value={_gender}
|
||||
aria-label={$i18n.t('Gender')}
|
||||
on:change={(e) => {
|
||||
console.log(_gender);
|
||||
|
||||
if (_gender === 'custom') {
|
||||
// Handle custom gender input
|
||||
gender = '';
|
||||
} else {
|
||||
gender = _gender;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" selected>{$i18n.t('Prefer not to say')}</option>
|
||||
<option value="male">{$i18n.t('Male')}</option>
|
||||
<option value="female">{$i18n.t('Female')}</option>
|
||||
<option value="custom">{$i18n.t('Custom')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#if _gender === 'custom'}
|
||||
<input
|
||||
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden mt-1"
|
||||
type="text"
|
||||
required
|
||||
aria-label={$i18n.t('Custom Gender')}
|
||||
placeholder={$i18n.t('Enter your gender')}
|
||||
bind:value={gender}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-1 text-xs font-normal">{$i18n.t('Birth Date')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm dark:text-gray-300 dark:placeholder:text-gray-300 bg-transparent outline-hidden"
|
||||
type="date"
|
||||
aria-label={$i18n.t('Birth Date')}
|
||||
bind:value={dateOfBirth}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-col w-full">
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Gender')}
|
||||
</div>
|
||||
|
||||
<select
|
||||
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden dark:text-gray-300"
|
||||
bind:value={_gender}
|
||||
aria-label={$i18n.t('Gender')}
|
||||
on:change={(e) => {
|
||||
console.log(_gender);
|
||||
|
||||
if (_gender === 'custom') {
|
||||
// Handle custom gender input
|
||||
gender = '';
|
||||
} else {
|
||||
gender = _gender;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" selected>{$i18n.t('Prefer not to say')}</option>
|
||||
<option value="male">{$i18n.t('Male')}</option>
|
||||
<option value="female">{$i18n.t('Female')}</option>
|
||||
<option value="custom">{$i18n.t('Custom')}</option>
|
||||
</select>
|
||||
|
||||
{#if _gender === 'custom'}
|
||||
<input
|
||||
class="mt-1 w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
type="text"
|
||||
required
|
||||
aria-label={$i18n.t('Custom Gender')}
|
||||
placeholder={$i18n.t('Enter your gender')}
|
||||
bind:value={gender}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-col w-full">
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Birth Date')}
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
type="date"
|
||||
aria-label={$i18n.t('Birth Date')}
|
||||
bind:value={dateOfBirth}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $config?.features?.enable_user_webhooks && ($user?.role === 'admin' || ($user?.permissions?.features?.webhooks ?? false))}
|
||||
<div class="mt-2">
|
||||
<div class="mt-3">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs font-normal">{$i18n.t('Notification Webhook')}</div>
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Notification Webhook')}
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm outline-hidden"
|
||||
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
type="url"
|
||||
placeholder={$i18n.t('Enter your webhook URL')}
|
||||
aria-label={$i18n.t('Notification Webhook')}
|
||||
@@ -248,7 +237,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-850/30 my-4" />
|
||||
<hr class="my-4 border-gray-100/40 dark:border-white/[0.03]" />
|
||||
|
||||
{#if $config?.features.enable_login_form && $config?.features.enable_password_change_form}
|
||||
<div class="mt-2">
|
||||
@@ -257,10 +246,10 @@
|
||||
{/if}
|
||||
|
||||
{#if ($config?.features?.enable_api_keys ?? true) && ($user?.role === 'admin' || ($user?.permissions?.features?.api_keys ?? false))}
|
||||
<div class="flex justify-between items-center text-sm mt-2">
|
||||
<div class=" font-normal">{$i18n.t('API keys')}</div>
|
||||
<div class="mt-3 flex items-center justify-between text-sm">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('API keys')}</div>
|
||||
<button
|
||||
class=" text-xs font-normal text-gray-500"
|
||||
class="text-xs font-normal text-gray-500 transition-colors hover:text-gray-700 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAPIKeys = !showAPIKeys;
|
||||
@@ -273,14 +262,16 @@
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class="justify-between w-full mt-2">
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="self-center text-xs font-normal mb-1">{$i18n.t('JWT Token')}</div>
|
||||
<div class="mb-1 self-center text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('JWT Token')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<SensitiveInput value={localStorage.token} readOnly={true} />
|
||||
|
||||
<button
|
||||
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
|
||||
class="ml-1.5 rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Copy Token')}
|
||||
on:click={() => {
|
||||
copyToClipboard(localStorage.token);
|
||||
@@ -331,7 +322,9 @@
|
||||
<div class="justify-between w-full mt-2">
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="self-center text-xs font-normal mb-1">{$i18n.t('API Key')}</div>
|
||||
<div class="mb-1 self-center text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('API Key')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex">
|
||||
@@ -339,7 +332,7 @@
|
||||
<SensitiveInput value={APIKey} readOnly={true} />
|
||||
|
||||
<button
|
||||
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
|
||||
class="ml-1.5 rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Copy API Key')}
|
||||
on:click={() => {
|
||||
copyToClipboard(APIKey);
|
||||
@@ -385,7 +378,7 @@
|
||||
|
||||
<Tooltip content={$i18n.t('Create new key')}>
|
||||
<button
|
||||
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
|
||||
class="rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Create new key')}
|
||||
on:click={() => {
|
||||
createAPIKeyHandler();
|
||||
@@ -409,7 +402,7 @@
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<button
|
||||
class="flex gap-1.5 items-center font-normal px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
|
||||
class="flex items-center gap-1.5 text-[0.8125rem] font-normal text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
on:click={() => {
|
||||
createAPIKeyHandler();
|
||||
}}
|
||||
@@ -427,7 +420,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 flex justify-end pt-3 text-sm font-normal">
|
||||
<div class="shrink-0 flex w-full justify-end pt-3 text-sm font-normal">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
on:click={async () => {
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-normal">{$i18n.t('Change Password')}</div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Change Password')}</div>
|
||||
<button
|
||||
class=" text-xs font-normal text-gray-500"
|
||||
class="text-xs font-normal text-gray-500 transition-colors hover:text-gray-700 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
show = !show;
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
export let user = null;
|
||||
|
||||
export let imageClassName = 'size-14 md:size-18';
|
||||
export let variant = 'default';
|
||||
export let displayName = '';
|
||||
|
||||
let profileImageInputElement;
|
||||
</script>
|
||||
@@ -79,10 +81,11 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col self-start group">
|
||||
<div class="self-center flex">
|
||||
{#if variant === 'account'}
|
||||
<div class="mb-6 flex items-center gap-4">
|
||||
<button
|
||||
class="relative rounded-full dark:bg-gray-700"
|
||||
class="group relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-gray-100
|
||||
ring-1 ring-gray-200 transition-all duration-200 hover:ring-2 hover:ring-gray-300 dark:bg-white/8 dark:ring-white/10 dark:hover:ring-white/20"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
profileImageInputElement.click();
|
||||
@@ -91,61 +94,154 @@
|
||||
<img
|
||||
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(user?.name)}
|
||||
alt="profile"
|
||||
class=" rounded-full {imageClassName} object-cover"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
|
||||
<div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition">
|
||||
<div class="p-1 rounded-full bg-white text-black border-gray-100 shadow">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3"
|
||||
>
|
||||
<path
|
||||
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center bg-black/0 transition-colors duration-200 group-hover:bg-black/40"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-white opacity-0 drop-shadow-sm transition-opacity duration-200 group-hover:opacity-100"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col w-full justify-center mt-2">
|
||||
<button
|
||||
class=" text-xs text-center text-gray-500 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
|
||||
}}>{$i18n.t('Remove')}</button
|
||||
>
|
||||
|
||||
<button
|
||||
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
if (canvasPixelTest()) {
|
||||
profileImageUrl = generateInitialsImage(user?.name);
|
||||
} else {
|
||||
toast.info(
|
||||
$i18n.t(
|
||||
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
|
||||
),
|
||||
{
|
||||
duration: 1000 * 10
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white"
|
||||
>{displayName || user?.name}</span
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
profileImageInputElement.click();
|
||||
}}>{$i18n.t('Upload Photo')}</button
|
||||
>
|
||||
<span class="text-[0.6875rem] text-gray-300 dark:text-gray-700">·</span>
|
||||
<button
|
||||
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
|
||||
}}>{$i18n.t('Remove')}</button
|
||||
>
|
||||
<span class="text-[0.6875rem] text-gray-300 dark:text-gray-700">·</span>
|
||||
<button
|
||||
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
if (canvasPixelTest()) {
|
||||
profileImageUrl = generateInitialsImage(user?.name);
|
||||
} else {
|
||||
toast.info(
|
||||
$i18n.t(
|
||||
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
|
||||
),
|
||||
{
|
||||
duration: 1000 * 10
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}}>{$i18n.t('Initials')}</button
|
||||
>
|
||||
}}>{$i18n.t('Initials')}</button
|
||||
>
|
||||
<span class="text-[0.6875rem] text-gray-300 dark:text-gray-700">·</span>
|
||||
<button
|
||||
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
const url = await getGravatarUrl(localStorage.token, user?.email);
|
||||
|
||||
<button
|
||||
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
const url = await getGravatarUrl(localStorage.token, user?.email);
|
||||
|
||||
profileImageUrl = url;
|
||||
}}>{$i18n.t('Gravatar')}</button
|
||||
>
|
||||
profileImageUrl = url;
|
||||
}}>{$i18n.t('Gravatar')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col self-start group">
|
||||
<div class="self-center flex">
|
||||
<button
|
||||
class="relative rounded-full dark:bg-gray-700"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
profileImageInputElement.click();
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(user?.name)}
|
||||
alt="profile"
|
||||
class=" rounded-full {imageClassName} object-cover"
|
||||
/>
|
||||
|
||||
<div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition">
|
||||
<div class="p-1 rounded-full bg-white text-black border-gray-100 shadow">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-3"
|
||||
>
|
||||
<path
|
||||
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col w-full justify-center mt-2">
|
||||
<button
|
||||
class=" text-xs text-center text-gray-500 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
|
||||
}}>{$i18n.t('Remove')}</button
|
||||
>
|
||||
|
||||
<button
|
||||
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
if (canvasPixelTest()) {
|
||||
profileImageUrl = generateInitialsImage(user?.name);
|
||||
} else {
|
||||
toast.info(
|
||||
$i18n.t(
|
||||
'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.'
|
||||
),
|
||||
{
|
||||
duration: 1000 * 10
|
||||
}
|
||||
);
|
||||
}
|
||||
}}>{$i18n.t('Initials')}</button
|
||||
>
|
||||
|
||||
<button
|
||||
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
const url = await getGravatarUrl(localStorage.token, user?.email);
|
||||
|
||||
profileImageUrl = url;
|
||||
}}>{$i18n.t('Gravatar')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -592,7 +592,8 @@
|
||||
|
||||
<Modal
|
||||
size="full"
|
||||
className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl mx-4 md:mx-0 flex flex-col md:flex-row max-h-[85vh] lg:max-h-[90vh] md:h-[35rem] lg:h-[42rem] xl:h-[46rem] bg-white dark:bg-gray-900 rounded-4xl"
|
||||
containerClassName="p-4 sm:p-6 lg:p-8"
|
||||
className="w-full max-w-[88rem] h-[min(50rem,calc(100dvh-4rem))] max-h-[calc(100dvh-4rem)] flex flex-col md:flex-row bg-white dark:bg-gray-900 rounded-4xl"
|
||||
bind:show
|
||||
>
|
||||
<nav
|
||||
@@ -806,9 +807,11 @@
|
||||
</nav>
|
||||
|
||||
<div class="flex-1 min-h-0 p-4 md:px-5 flex flex-col">
|
||||
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||
{$i18n.t(selectedTabTitle)}
|
||||
</h2>
|
||||
{#if selectedTab !== 'account'}
|
||||
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||
{$i18n.t(selectedTabTitle)}
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
{#if selectedTab === 'general'}
|
||||
|
||||
Reference in New Issue
Block a user