mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +00:00
refac/feat: note/knowledge/chat select input menu
This commit is contained in:
parent
eadec9e86e
commit
c03ca7270e
10 changed files with 439 additions and 54 deletions
|
|
@ -97,15 +97,26 @@ class NoteTable:
|
||||||
db.commit()
|
db.commit()
|
||||||
return note
|
return note
|
||||||
|
|
||||||
def get_notes(self) -> list[NoteModel]:
|
def get_notes(
|
||||||
|
self, skip: Optional[int] = None, limit: Optional[int] = None
|
||||||
|
) -> list[NoteModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
notes = db.query(Note).order_by(Note.updated_at.desc()).all()
|
query = db.query(Note).order_by(Note.updated_at.desc())
|
||||||
|
if skip is not None:
|
||||||
|
query = query.offset(skip)
|
||||||
|
if limit is not None:
|
||||||
|
query = query.limit(limit)
|
||||||
|
notes = query.all()
|
||||||
return [NoteModel.model_validate(note) for note in notes]
|
return [NoteModel.model_validate(note) for note in notes]
|
||||||
|
|
||||||
def get_notes_by_user_id(
|
def get_notes_by_user_id(
|
||||||
self, user_id: str, permission: str = "write"
|
self,
|
||||||
|
user_id: str,
|
||||||
|
permission: str = "write",
|
||||||
|
skip: Optional[int] = None,
|
||||||
|
limit: Optional[int] = None,
|
||||||
) -> list[NoteModel]:
|
) -> list[NoteModel]:
|
||||||
notes = self.get_notes()
|
notes = self.get_notes(skip=skip, limit=limit)
|
||||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||||
return [
|
return [
|
||||||
note
|
note
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,9 @@ class NoteTitleIdResponse(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
@router.get("/list", response_model=list[NoteTitleIdResponse])
|
@router.get("/list", response_model=list[NoteTitleIdResponse])
|
||||||
async def get_note_list(request: Request, user=Depends(get_verified_user)):
|
async def get_note_list(
|
||||||
|
request: Request, page: Optional[int] = None, user=Depends(get_verified_user)
|
||||||
|
):
|
||||||
if user.role != "admin" and not has_permission(
|
if user.role != "admin" and not has_permission(
|
||||||
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
|
user.id, "features.notes", request.app.state.config.USER_PERMISSIONS
|
||||||
):
|
):
|
||||||
|
|
@ -72,9 +73,15 @@ async def get_note_list(request: Request, user=Depends(get_verified_user)):
|
||||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
limit = None
|
||||||
|
skip = None
|
||||||
|
if page is not None:
|
||||||
|
limit = 60
|
||||||
|
skip = (page - 1) * limit
|
||||||
|
|
||||||
notes = [
|
notes = [
|
||||||
NoteTitleIdResponse(**note.model_dump())
|
NoteTitleIdResponse(**note.model_dump())
|
||||||
for note in Notes.get_notes_by_user_id(user.id, "write")
|
for note in Notes.get_notes_by_user_id(user.id, "write", skip=skip, limit=limit)
|
||||||
]
|
]
|
||||||
|
|
||||||
return notes
|
return notes
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,15 @@ export const getNotes = async (token: string = '', raw: boolean = false) => {
|
||||||
return grouped;
|
return grouped;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNoteList = async (token: string = '') => {
|
export const getNoteList = async (token: string = '', page: number | null = null) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list`, {
|
if (page !== null) {
|
||||||
|
searchParams.append('page', `${page}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list?${searchParams.toString()}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -226,48 +226,6 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- <div slot="content" class=" pl-2 pt-1 flex flex-col gap-0.5">
|
|
||||||
{#if !item.legacy && (item?.files ?? []).length > 0}
|
|
||||||
{#each item?.files ?? [] as file, fileIdx}
|
|
||||||
<button
|
|
||||||
class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center hover:bg-gray-50 dark:hover:bg-gray-850 dark:hover:text-gray-100 selected-command-option-button"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
console.log(file);
|
|
||||||
}}
|
|
||||||
on:mousemove={() => {
|
|
||||||
selectedIdx = idx;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class=" font-medium text-black dark:text-gray-100 flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
|
|
||||||
>
|
|
||||||
File
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="line-clamp-1">
|
|
||||||
{file?.meta?.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
|
|
||||||
{$i18n.t('Updated')}
|
|
||||||
{dayjs(file.updated_at * 1000).fromNow()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{:else}
|
|
||||||
<div class=" text-gray-500 text-xs mt-1 mb-2">
|
|
||||||
{$i18n.t('File not found.')}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div> -->
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')}
|
{#if query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be')}
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[240px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin transition"
|
class="w-full max-w-[260px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin transition"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
alignOffset={-6}
|
alignOffset={-6}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
|
|
@ -422,7 +422,7 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Knowledge />
|
<Knowledge knowledge={$knowledge ?? []} />
|
||||||
</div>
|
</div>
|
||||||
{:else if tab === 'notes'}
|
{:else if tab === 'notes'}
|
||||||
<div in:fly={{ x: 20, duration: 150 }}>
|
<div in:fly={{ x: 20, duration: 150 }}>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import { decodeString } from '$lib/utils';
|
||||||
|
import { getChatList } from '$lib/apis/chats';
|
||||||
|
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import Loader from '$lib/components/common/Loader.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let onSelect = (e) => {};
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
let selectedIdx = 0;
|
||||||
|
|
||||||
|
let page = 1;
|
||||||
|
let itemsLoading = false;
|
||||||
|
let allItemsLoaded = false;
|
||||||
|
|
||||||
|
const loadMoreItems = async () => {
|
||||||
|
if (allItemsLoaded) return;
|
||||||
|
page += 1;
|
||||||
|
await getItemsPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemsPage = async () => {
|
||||||
|
itemsLoading = true;
|
||||||
|
let res = await getChatList(localStorage.token, page).catch(() => {
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((res ?? []).length === 0) {
|
||||||
|
allItemsLoaded = true;
|
||||||
|
} else {
|
||||||
|
allItemsLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
items = [
|
||||||
|
...items,
|
||||||
|
...res.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
type: 'chat',
|
||||||
|
name: item.title,
|
||||||
|
description: dayjs(item.updated_at * 1000).fromNow()
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
itemsLoading = false;
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await getItemsPage();
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
{#if items.length === 0}
|
||||||
|
<div class="text-center text-xs text-gray-500 py-3">{$i18n.t('No chats found')}</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
{#each items as item, idx}
|
||||||
|
<button
|
||||||
|
class=" px-2.5 py-1 rounded-xl w-full text-left flex justify-between items-center text-sm {idx ===
|
||||||
|
selectedIdx
|
||||||
|
? ' bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button'
|
||||||
|
: ''}"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
onSelect(item);
|
||||||
|
}}
|
||||||
|
on:mousemove={() => {
|
||||||
|
selectedIdx = idx;
|
||||||
|
}}
|
||||||
|
on:mouseleave={() => {
|
||||||
|
if (idx === 0) {
|
||||||
|
selectedIdx = -1;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-selected={idx === selectedIdx}
|
||||||
|
>
|
||||||
|
<div class="text-black dark:text-gray-100 flex items-center gap-1.5">
|
||||||
|
<Tooltip content={item.description || decodeString(item?.name)} placement="top-start">
|
||||||
|
<div class="line-clamp-1 flex-1">
|
||||||
|
{decodeString(item?.name)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if !allItemsLoaded}
|
||||||
|
<Loader
|
||||||
|
on:visible={(e) => {
|
||||||
|
if (!itemsLoading) {
|
||||||
|
loadMoreItems();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="w-full flex justify-center py-4 text-xs animate-pulse items-center gap-2">
|
||||||
|
<Spinner className=" size-4" />
|
||||||
|
<div class=" ">{$i18n.t('Loading...')}</div>
|
||||||
|
</div>
|
||||||
|
</Loader>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="py-5">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import { decodeString } from '$lib/utils';
|
||||||
|
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import Database from '$lib/components/icons/Database.svelte';
|
||||||
|
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let knowledge = [];
|
||||||
|
export let onSelect = (e) => {};
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
let selectedIdx = 0;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
let legacy_documents = knowledge
|
||||||
|
.filter((item) => item?.meta?.document)
|
||||||
|
.map((item) => ({
|
||||||
|
...item,
|
||||||
|
type: 'file'
|
||||||
|
}));
|
||||||
|
|
||||||
|
let legacy_collections =
|
||||||
|
legacy_documents.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'All Documents',
|
||||||
|
legacy: true,
|
||||||
|
type: 'collection',
|
||||||
|
description: 'Deprecated (legacy collection), please create a new knowledge base.',
|
||||||
|
title: $i18n.t('All Documents'),
|
||||||
|
collection_names: legacy_documents.map((item) => item.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
...legacy_documents
|
||||||
|
.reduce((a, item) => {
|
||||||
|
return [...new Set([...a, ...(item?.meta?.tags ?? []).map((tag) => tag.name)])];
|
||||||
|
}, [])
|
||||||
|
.map((tag) => ({
|
||||||
|
name: tag,
|
||||||
|
legacy: true,
|
||||||
|
type: 'collection',
|
||||||
|
description: 'Deprecated (legacy collection), please create a new knowledge base.',
|
||||||
|
collection_names: legacy_documents
|
||||||
|
.filter((item) => (item?.meta?.tags ?? []).map((tag) => tag.name).includes(tag))
|
||||||
|
.map((item) => item.id)
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
let collections = knowledge
|
||||||
|
.filter((item) => !item?.meta?.document)
|
||||||
|
.map((item) => ({
|
||||||
|
...item,
|
||||||
|
type: 'collection'
|
||||||
|
}));
|
||||||
|
let collection_files =
|
||||||
|
knowledge.length > 0
|
||||||
|
? [
|
||||||
|
...knowledge
|
||||||
|
.reduce((a, item) => {
|
||||||
|
return [
|
||||||
|
...new Set([
|
||||||
|
...a,
|
||||||
|
...(item?.files ?? []).map((file) => ({
|
||||||
|
...file,
|
||||||
|
collection: { name: item.name, description: item.description } // DO NOT REMOVE, USED IN FILE DESCRIPTION/ATTACHMENT
|
||||||
|
}))
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}, [])
|
||||||
|
.map((file) => ({
|
||||||
|
...file,
|
||||||
|
name: file?.meta?.name,
|
||||||
|
description: `${file?.collection?.name} - ${file?.collection?.description}`,
|
||||||
|
knowledge: true, // DO NOT REMOVE, USED TO INDICATE KNOWLEDGE BASE FILE
|
||||||
|
type: 'file'
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map(
|
||||||
|
(item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
{#each items as item, idx}
|
||||||
|
<button
|
||||||
|
class=" px-2.5 py-1 rounded-xl w-full text-left flex justify-between items-center text-sm {idx ===
|
||||||
|
selectedIdx
|
||||||
|
? ' bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button'
|
||||||
|
: ''}"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
console.log(item);
|
||||||
|
onSelect(item);
|
||||||
|
}}
|
||||||
|
on:mousemove={() => {
|
||||||
|
selectedIdx = idx;
|
||||||
|
}}
|
||||||
|
on:mouseleave={() => {
|
||||||
|
if (idx === 0) {
|
||||||
|
selectedIdx = -1;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-selected={idx === selectedIdx}
|
||||||
|
>
|
||||||
|
<div class=" text-black dark:text-gray-100 flex items-center gap-1">
|
||||||
|
<Tooltip
|
||||||
|
content={item?.legacy
|
||||||
|
? $i18n.t('Legacy')
|
||||||
|
: item?.type === 'file'
|
||||||
|
? $i18n.t('File')
|
||||||
|
: item?.type === 'collection'
|
||||||
|
? $i18n.t('Collection')
|
||||||
|
: ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
{#if item?.type === 'collection'}
|
||||||
|
<Database className="size-4" />
|
||||||
|
{:else}
|
||||||
|
<DocumentPage className="size-4" />
|
||||||
|
{/if}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={item.description || decodeString(item?.name)} placement="top-start">
|
||||||
|
<div class="line-clamp-1 flex-1">
|
||||||
|
{decodeString(item?.name)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
|
import { decodeString } from '$lib/utils';
|
||||||
|
import { getNoteList } from '$lib/apis/notes';
|
||||||
|
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import PageEdit from '$lib/components/icons/PageEdit.svelte';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import Loader from '$lib/components/common/Loader.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
export let onSelect = (e) => {};
|
||||||
|
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
let selectedIdx = 0;
|
||||||
|
|
||||||
|
let page = 1;
|
||||||
|
let itemsLoading = false;
|
||||||
|
let allItemsLoaded = false;
|
||||||
|
|
||||||
|
const loadMoreItems = async () => {
|
||||||
|
if (allItemsLoaded) return;
|
||||||
|
page += 1;
|
||||||
|
await getItemsPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemsPage = async () => {
|
||||||
|
itemsLoading = true;
|
||||||
|
let res = await getNoteList(localStorage.token, page).catch(() => {
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((res ?? []).length === 0) {
|
||||||
|
allItemsLoaded = true;
|
||||||
|
} else {
|
||||||
|
allItemsLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
items = [
|
||||||
|
...items,
|
||||||
|
...res.map((note) => {
|
||||||
|
return {
|
||||||
|
...note,
|
||||||
|
type: 'note',
|
||||||
|
name: note.title,
|
||||||
|
description: dayjs(note.updated_at / 1000000).fromNow()
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
itemsLoading = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await getItemsPage();
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
{#if items.length === 0}
|
||||||
|
<div class="text-center text-xs text-gray-500 py-3">{$i18n.t('No notes found')}</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
{#each items as item, idx}
|
||||||
|
<button
|
||||||
|
class=" px-2.5 py-1 rounded-xl w-full text-left flex justify-between items-center text-sm {idx ===
|
||||||
|
selectedIdx
|
||||||
|
? ' bg-gray-50 dark:bg-gray-800 dark:text-gray-100 selected-command-option-button'
|
||||||
|
: ''}"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
onSelect(item);
|
||||||
|
}}
|
||||||
|
on:mousemove={() => {
|
||||||
|
selectedIdx = idx;
|
||||||
|
}}
|
||||||
|
on:mouseleave={() => {
|
||||||
|
if (idx === 0) {
|
||||||
|
selectedIdx = -1;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-selected={idx === selectedIdx}
|
||||||
|
>
|
||||||
|
<div class="text-black dark:text-gray-100 flex items-center gap-1.5">
|
||||||
|
<Tooltip content={$i18n.t('Note')} placement="top">
|
||||||
|
<PageEdit className="size-4" />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={item.description || decodeString(item?.name)} placement="top-start">
|
||||||
|
<div class="line-clamp-1 flex-1">
|
||||||
|
{decodeString(item?.name)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if !allItemsLoaded}
|
||||||
|
<Loader
|
||||||
|
on:visible={(e) => {
|
||||||
|
if (!itemsLoading) {
|
||||||
|
loadMoreItems();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="w-full flex justify-center py-4 text-xs animate-pulse items-center gap-2">
|
||||||
|
<Spinner className=" size-4" />
|
||||||
|
<div class=" ">{$i18n.t('Loading...')}</div>
|
||||||
|
</div>
|
||||||
|
</Loader>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="py-5">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[240px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin"
|
class="w-full max-w-[260px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
alignOffset={-6}
|
alignOffset={-6}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
|
|
|
||||||
|
|
@ -1546,3 +1546,11 @@ export const convertHeicToJpeg = async (file: File) => {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const decodeString = (str: string) => {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(str);
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue