mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-11 03:38:02 +00:00
331 lines
11 KiB
Svelte
331 lines
11 KiB
Svelte
<script lang="ts">
|
|
import Switch from '$lib/components/common/Switch.svelte';
|
|
import { config, settings } from '$lib/stores';
|
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
|
import type { Writable } from 'svelte/store';
|
|
import type { i18n as i18nType } from 'i18next';
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
|
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
|
import ExperimentalBadge from '$lib/components/common/ExperimentalBadge.svelte';
|
|
import MemoryModal from './Personalization/MemoryModal.svelte';
|
|
import { deleteMemoriesByUserId, deleteMemoryById, getMemories } from '$lib/apis/memories';
|
|
import { toast } from 'svelte-sonner';
|
|
import UserSettingRow from './UserSettingRow.svelte';
|
|
import UserSettingSection from './UserSettingSection.svelte';
|
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
|
import Plus from '$lib/components/icons/Plus.svelte';
|
|
import Search from '$lib/components/icons/Search.svelte';
|
|
import Trash from '$lib/components/icons/Trash.svelte';
|
|
import XMark from '$lib/components/icons/XMark.svelte';
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
const i18n = getContext<Writable<i18nType>>('i18n');
|
|
|
|
export let saveSettings: (settings: any) => void | Promise<void>;
|
|
|
|
// Addons
|
|
let enableMemory = false;
|
|
let memories: Memory[] = [];
|
|
let loadingMemories = true;
|
|
|
|
let showMemoryModal = false;
|
|
let selectedMemory: Memory | null = null;
|
|
let showClearConfirmDialog = false;
|
|
let showDeleteConfirm = false;
|
|
let query = '';
|
|
const actionButtonClass =
|
|
'shrink-0 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
|
|
|
|
type Memory = {
|
|
id: string;
|
|
content: string;
|
|
type?: string;
|
|
path?: string;
|
|
updated_at?: number;
|
|
};
|
|
|
|
const loadMemories = async () => {
|
|
loadingMemories = true;
|
|
memories =
|
|
(await getMemories(localStorage.token).catch((error) => {
|
|
toast.error(`${error}`);
|
|
return [];
|
|
})) ?? [];
|
|
loadingMemories = false;
|
|
};
|
|
|
|
const confirmDeleteMemory = (memory: Memory) => {
|
|
selectedMemory = memory;
|
|
showDeleteConfirm = true;
|
|
};
|
|
|
|
const editMemory = (memory: Memory) => {
|
|
selectedMemory = memory;
|
|
showMemoryModal = true;
|
|
};
|
|
|
|
let onClearConfirmed = async () => {
|
|
const res = await deleteMemoriesByUserId(localStorage.token).catch((error) => {
|
|
toast.error(`${error}`);
|
|
return null;
|
|
});
|
|
|
|
if (res && memories.length > 0) {
|
|
toast.success($i18n.t('Memory cleared successfully'));
|
|
memories = [];
|
|
}
|
|
showClearConfirmDialog = false;
|
|
};
|
|
|
|
$: filteredMemories = query
|
|
? memories.filter((memory) => {
|
|
const value = query.toLowerCase();
|
|
return (
|
|
memory.content?.toLowerCase().includes(value) ||
|
|
memory.path?.toLowerCase().includes(value) ||
|
|
memory.type?.toLowerCase().includes(value)
|
|
);
|
|
})
|
|
: memories;
|
|
|
|
$: sortedMemories = [...filteredMemories].sort(
|
|
(a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0)
|
|
);
|
|
|
|
onMount(async () => {
|
|
enableMemory = $settings?.memory ?? $config?.features?.enable_memories ?? false;
|
|
await loadMemories();
|
|
});
|
|
</script>
|
|
|
|
<form
|
|
id="tab-personalization"
|
|
class="flex flex-col h-full justify-between text-sm"
|
|
on:submit|preventDefault={() => {
|
|
dispatch('save');
|
|
}}
|
|
>
|
|
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
|
{$i18n.t('settings.personal.personalization.title')}
|
|
</h2>
|
|
|
|
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
|
<UserSettingSection
|
|
title={$i18n.t('settings.personal.personalization.sections.memory.title')}
|
|
first
|
|
>
|
|
<UserSettingRow
|
|
description={$i18n
|
|
.t('settings.personal.personalization.memory.description')
|
|
.replace(
|
|
$i18n.t('settings.personal.personalization.manageMemories.label'),
|
|
$i18n.t('Add Memory')
|
|
)}
|
|
>
|
|
<div slot="label" class="flex items-center gap-2">
|
|
{$i18n.t('settings.personal.personalization.memory.label')}
|
|
<ExperimentalBadge />
|
|
</div>
|
|
|
|
<Switch
|
|
bind:state={enableMemory}
|
|
on:change={async () => {
|
|
saveSettings({ memory: enableMemory });
|
|
}}
|
|
/>
|
|
</UserSettingRow>
|
|
|
|
{#if enableMemory}
|
|
<div>
|
|
<div class="mb-1 flex items-center">
|
|
<div class="text-xs text-gray-600 dark:text-gray-400">
|
|
{$i18n.t('settings.personal.personalization.savedMemories.label')}
|
|
{#if !loadingMemories}
|
|
<span class="ml-1 text-gray-400 dark:text-gray-600">{memories.length}</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
{#if loadingMemories}
|
|
<div class="flex min-h-16 w-full items-center justify-center">
|
|
<Spinner className="size-4" />
|
|
</div>
|
|
{:else}
|
|
<div class="mb-2 flex min-w-0 items-center justify-between gap-3">
|
|
{#if memories.length > 0}
|
|
<div class="flex min-w-0 flex-1 items-center gap-2">
|
|
<Search className="size-3.5 shrink-0 text-gray-400 dark:text-gray-600" />
|
|
<input
|
|
data-settings-search
|
|
class="min-w-0 flex-1 bg-transparent py-0.5 text-xs text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
|
bind:value={query}
|
|
placeholder={$i18n.t('Search Memories')}
|
|
maxlength="500"
|
|
/>
|
|
{#if query}
|
|
<div class="shrink-0">
|
|
<button
|
|
class="rounded-lg p-0.5 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
|
type="button"
|
|
aria-label={$i18n.t('Clear search')}
|
|
on:click={() => {
|
|
query = '';
|
|
}}
|
|
>
|
|
<XMark className="size-3" strokeWidth="2" />
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="min-w-0 flex-1"></div>
|
|
{/if}
|
|
|
|
<Dropdown closeOnSelect align="end">
|
|
<Tooltip content={$i18n.t('Actions')}>
|
|
<button
|
|
class="flex h-7 items-center gap-1.5 rounded-lg bg-transparent px-1.5 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white"
|
|
type="button"
|
|
>
|
|
<span>{$i18n.t('Actions')}</span>
|
|
<ChevronDown className="size-3" strokeWidth="2.5" />
|
|
</button>
|
|
</Tooltip>
|
|
|
|
<div slot="content">
|
|
<DropdownMenu className="min-w-[10.625rem] shadow-sm">
|
|
<button
|
|
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
|
type="button"
|
|
on:click={() => {
|
|
selectedMemory = null;
|
|
showMemoryModal = true;
|
|
}}
|
|
>
|
|
<Plus className="size-3.5 shrink-0" strokeWidth="1.5" />
|
|
<div class="min-w-0 flex-1 truncate text-left">{$i18n.t('Add Memory')}</div>
|
|
</button>
|
|
|
|
<button
|
|
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
|
disabled={memories.length === 0}
|
|
type="button"
|
|
on:click={() => {
|
|
showClearConfirmDialog = true;
|
|
}}
|
|
>
|
|
<Trash className="size-3.5 shrink-0" strokeWidth="1.5" />
|
|
<div class="min-w-0 flex-1 truncate text-left">
|
|
{$i18n.t('settings.personal.personalization.clearMemory.label')}
|
|
</div>
|
|
</button>
|
|
</DropdownMenu>
|
|
</div>
|
|
</Dropdown>
|
|
</div>
|
|
|
|
{#if sortedMemories.length === 0}
|
|
<div class="min-h-16 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
|
{#if memories.length === 0}
|
|
{$i18n.t('settings.personal.personalization.noResultsFound.description')}
|
|
{:else}
|
|
{$i18n.t('No results found')}
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="flex flex-col gap-2">
|
|
{#each sortedMemories as memory (memory.id)}
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div
|
|
class="min-w-0 whitespace-pre-wrap break-words text-xs text-gray-700 dark:text-gray-300"
|
|
>
|
|
{memory.content}
|
|
</div>
|
|
<div class="flex shrink-0 items-center justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
class="{actionButtonClass} hover:underline"
|
|
on:click={() => editMemory(memory)}
|
|
>
|
|
{$i18n.t('Edit')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="{actionButtonClass} hover:underline"
|
|
on:click={() => confirmDeleteMemory(memory)}
|
|
>
|
|
{$i18n.t('Remove')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</UserSettingSection>
|
|
</div>
|
|
|
|
<div class="shrink-0 flex justify-end text-sm font-normal">
|
|
<button
|
|
class="flex h-7 shrink-0 items-center justify-center gap-1.5 rounded-lg bg-gray-900 px-2.5 text-xs font-normal text-white transition hover:bg-black disabled:opacity-60 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white"
|
|
type="submit"
|
|
>
|
|
{$i18n.t('Save')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<ConfirmDialog
|
|
title={$i18n.t('Clear Memory')}
|
|
message={$i18n.t('Are you sure you want to clear all memories? This action cannot be undone.')}
|
|
show={showClearConfirmDialog}
|
|
on:confirm={onClearConfirmed}
|
|
on:cancel={() => {
|
|
showClearConfirmDialog = false;
|
|
}}
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
title={$i18n.t('Delete Memory?')}
|
|
show={showDeleteConfirm}
|
|
on:confirm={async () => {
|
|
if (!selectedMemory) return;
|
|
|
|
const res = await deleteMemoryById(localStorage.token, selectedMemory.id).catch((error) => {
|
|
toast.error(`${error}`);
|
|
return null;
|
|
});
|
|
|
|
if (res) {
|
|
toast.success($i18n.t('Memory deleted successfully'));
|
|
await loadMemories();
|
|
}
|
|
showDeleteConfirm = false;
|
|
}}
|
|
on:cancel={() => {
|
|
showDeleteConfirm = false;
|
|
}}
|
|
>
|
|
<div class="text-sm text-gray-500 flex-1">
|
|
{$i18n.t('Are you sure you want to delete this memory? This action cannot be undone.')}
|
|
<div
|
|
class="mt-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words rounded-lg border border-gray-100/50 bg-gray-50/40 p-2 text-xs text-gray-600 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-400"
|
|
>
|
|
{selectedMemory?.content}
|
|
</div>
|
|
</div>
|
|
</ConfirmDialog>
|
|
|
|
<MemoryModal
|
|
bind:show={showMemoryModal}
|
|
memory={selectedMemory as any}
|
|
on:save={async () => {
|
|
await loadMemories();
|
|
}}
|
|
/>
|