mirror of
https://github.com/open-webui/open-webui
synced 2025-06-11 00:49:44 +00:00
refac: user chat list modal
This commit is contained in:
parent
44e7e09784
commit
75208935d7
@ -417,6 +417,7 @@ class ChatTable:
|
||||
self,
|
||||
user_id: str,
|
||||
include_archived: bool = False,
|
||||
filter: Optional[dict] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> list[ChatModel]:
|
||||
@ -425,7 +426,23 @@ class ChatTable:
|
||||
if not include_archived:
|
||||
query = query.filter_by(archived=False)
|
||||
|
||||
query = query.order_by(Chat.updated_at.desc())
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
if query_key:
|
||||
query = query.filter(Chat.title.ilike(f"%{query_key}%"))
|
||||
|
||||
order_by = filter.get("order_by")
|
||||
direction = filter.get("direction")
|
||||
|
||||
if order_by and direction and getattr(Chat, order_by):
|
||||
if direction.lower() == "asc":
|
||||
query = query.order_by(getattr(Chat, order_by).asc())
|
||||
elif direction.lower() == "desc":
|
||||
query = query.order_by(getattr(Chat, order_by).desc())
|
||||
else:
|
||||
raise ValueError("Invalid direction for ordering")
|
||||
else:
|
||||
query = query.order_by(Chat.updated_at.desc())
|
||||
|
||||
if skip:
|
||||
query = query.offset(skip)
|
||||
@ -566,7 +583,9 @@ class ChatTable:
|
||||
search_text = search_text.lower().strip()
|
||||
|
||||
if not search_text:
|
||||
return self.get_chat_list_by_user_id(user_id, include_archived, skip, limit)
|
||||
return self.get_chat_list_by_user_id(
|
||||
user_id, include_archived, filter={}, skip=skip, limit=limit
|
||||
)
|
||||
|
||||
search_text_words = search_text.split(" ")
|
||||
|
||||
|
@ -76,17 +76,34 @@ async def delete_all_user_chats(request: Request, user=Depends(get_verified_user
|
||||
@router.get("/list/user/{user_id}", response_model=list[ChatTitleIdResponse])
|
||||
async def get_user_chat_list_by_user_id(
|
||||
user_id: str,
|
||||
page: Optional[int] = None,
|
||||
query: Optional[str] = None,
|
||||
order_by: Optional[str] = None,
|
||||
direction: Optional[str] = None,
|
||||
user=Depends(get_admin_user),
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
):
|
||||
if not ENABLE_ADMIN_CHAT_ACCESS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
if page is None:
|
||||
page = 1
|
||||
|
||||
limit = 60
|
||||
skip = (page - 1) * limit
|
||||
|
||||
filter = {}
|
||||
if query:
|
||||
filter["query"] = query
|
||||
if order_by:
|
||||
filter["order_by"] = order_by
|
||||
if direction:
|
||||
filter["direction"] = direction
|
||||
|
||||
return Chats.get_chat_list_by_user_id(
|
||||
user_id, include_archived=True, skip=skip, limit=limit
|
||||
user_id, include_archived=True, filter=filter, skip=skip, limit=limit
|
||||
)
|
||||
|
||||
|
||||
|
@ -111,17 +111,37 @@ export const getChatList = async (token: string = '', page: number | null = null
|
||||
}));
|
||||
};
|
||||
|
||||
export const getChatListByUserId = async (token: string = '', userId: string) => {
|
||||
export const getChatListByUserId = async (
|
||||
token: string = '',
|
||||
userId: string,
|
||||
page: number = 1,
|
||||
filter?: object
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/list/user/${userId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.append('page', `${page}`);
|
||||
|
||||
if (filter) {
|
||||
Object.entries(filter).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
searchParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${WEBUI_API_BASE_URL}/chats/list/user/${userId}?${searchParams.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
@ -188,7 +208,10 @@ export const getArchivedChatList = async (
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
return res.map((chat) => ({
|
||||
...chat,
|
||||
time_range: getTimeRange(chat.updated_at)
|
||||
}));
|
||||
};
|
||||
|
||||
export const getAllChats = async (token: string) => {
|
||||
|
@ -165,7 +165,10 @@
|
||||
getUserList();
|
||||
}}
|
||||
/>
|
||||
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
|
||||
|
||||
{#if selectedUser}
|
||||
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
|
||||
{/if}
|
||||
|
||||
{#if ($config?.license_metadata?.seats ?? null) !== null && total && total > $config?.license_metadata?.seats}
|
||||
<div class=" mt-1 mb-2 text-xs text-red-500">
|
||||
|
@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
|
||||
@ -12,191 +12,106 @@
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
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 ChatsModal from '$lib/components/layout/ChatsModal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
export let user;
|
||||
|
||||
let chats = null;
|
||||
let showDeleteConfirmDialog = false;
|
||||
let chatToDelete = null;
|
||||
let chatList = null;
|
||||
let page = 1;
|
||||
|
||||
const deleteChatHandler = async (chatId) => {
|
||||
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
});
|
||||
let query = '';
|
||||
let orderBy = 'updated_at';
|
||||
let direction = 'desc';
|
||||
|
||||
chats = await getChatListByUserId(localStorage.token, user.id);
|
||||
let filter = {};
|
||||
$: filter = {
|
||||
...(query ? { query } : {}),
|
||||
...(orderBy ? { order_by: orderBy } : {}),
|
||||
...(direction ? { direction } : {})
|
||||
};
|
||||
|
||||
$: if (filter !== null) {
|
||||
searchHandler();
|
||||
}
|
||||
|
||||
let allChatsLoaded = false;
|
||||
let chatListLoading = false;
|
||||
|
||||
let searchDebounceTimeout;
|
||||
|
||||
const searchHandler = async () => {
|
||||
console.log('search', query);
|
||||
|
||||
if (searchDebounceTimeout) {
|
||||
clearTimeout(searchDebounceTimeout);
|
||||
}
|
||||
|
||||
page = 1;
|
||||
chatList = null;
|
||||
|
||||
if (query === '') {
|
||||
chatList = await getChatListByUserId(localStorage.token, user.id, page, filter);
|
||||
} else {
|
||||
searchDebounceTimeout = setTimeout(async () => {
|
||||
chatList = await getChatListByUserId(localStorage.token, user.id, page, filter);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
if ((chatList ?? []).length === 0) {
|
||||
allChatsLoaded = true;
|
||||
} else {
|
||||
allChatsLoaded = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMoreChats = async () => {
|
||||
chatListLoading = true;
|
||||
page += 1;
|
||||
|
||||
let newChatList = [];
|
||||
|
||||
newChatList = await getChatListByUserId(localStorage.token, user.id, page, filter);
|
||||
|
||||
// once the bottom of the list has been reached (no results) there is no need to continue querying
|
||||
allChatsLoaded = newChatList.length === 0;
|
||||
|
||||
if (newChatList.length > 0) {
|
||||
chatList = [...chatList, ...newChatList];
|
||||
}
|
||||
|
||||
chatListLoading = false;
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
chatList = await getChatListByUserId(localStorage.token, user.id, page, filter);
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
(async () => {
|
||||
if (user.id) {
|
||||
chats = await getChatListByUserId(localStorage.token, user.id);
|
||||
}
|
||||
})();
|
||||
init();
|
||||
} else {
|
||||
chats = null;
|
||||
}
|
||||
chatList = null;
|
||||
page = 1;
|
||||
|
||||
let sortKey = 'updated_at'; // default sort key
|
||||
let sortOrder = 'desc'; // default sort order
|
||||
function setSortKey(key) {
|
||||
if (sortKey === key) {
|
||||
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortOrder = 'asc';
|
||||
}
|
||||
allChatsLoaded = false;
|
||||
chatListLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:show={showDeleteConfirmDialog}
|
||||
on:confirm={() => {
|
||||
if (chatToDelete) {
|
||||
deleteChatHandler(chatToDelete);
|
||||
chatToDelete = null;
|
||||
}
|
||||
<ChatsModal
|
||||
bind:show
|
||||
bind:query
|
||||
bind:orderBy
|
||||
bind:direction
|
||||
title={$i18n.t("{{user}}'s Chats", { user: user.name })}
|
||||
emptyPlaceholder={$i18n.t('No chats found for this user.')}
|
||||
{chatList}
|
||||
{allChatsLoaded}
|
||||
{chatListLoading}
|
||||
onUpdate={() => {
|
||||
init();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal size="lg" bind:show>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
|
||||
<div class=" text-lg font-medium self-center capitalize">
|
||||
{$i18n.t("{{user}}'s Chats", { user: user.name })}
|
||||
</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 flex-col md:flex-row w-full px-5 pt-2 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
{#if chats}
|
||||
{#if chats.length > 0}
|
||||
<div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll">
|
||||
<div class="relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
|
||||
<thead
|
||||
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-850"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-2 cursor-pointer select-none"
|
||||
on:click={() => setSortKey('title')}
|
||||
>
|
||||
{$i18n.t('Title')}
|
||||
{#if sortKey === 'title'}
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
{:else}
|
||||
<span class="invisible">▲</span>
|
||||
{/if}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-2 hidden md:flex cursor-pointer select-none justify-end"
|
||||
on:click={() => setSortKey('updated_at')}
|
||||
>
|
||||
{$i18n.t('Updated at')}
|
||||
{#if sortKey === 'updated_at'}
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
{:else}
|
||||
<span class="invisible">▲</span>
|
||||
{/if}
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2 text-right" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each chats.sort((a, b) => {
|
||||
if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1;
|
||||
if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
}) as chat, idx}
|
||||
<tr
|
||||
class="bg-transparent {idx !== chats.length - 1 &&
|
||||
'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
|
||||
>
|
||||
<td class="px-3 py-1">
|
||||
<a href="/s/{chat.id}" target="_blank">
|
||||
<div class=" underline line-clamp-1 max-w-96">
|
||||
{chat.title}
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td class=" px-3 py-1 hidden md:flex h-[2.5rem] justify-end">
|
||||
<div class="my-auto shrink-0">
|
||||
{dayjs(chat.updated_at * 1000).format('LLL')}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-1 text-right">
|
||||
<div class="flex justify-end w-full">
|
||||
<Tooltip content={$i18n.t('Delete Chat')}>
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
on:click={async () => {
|
||||
chatToDelete = chat.id;
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
>
|
||||
<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="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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- {#each chats as chat}
|
||||
<div>
|
||||
{JSON.stringify(chat)}
|
||||
</div>
|
||||
{/each} -->
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-left text-sm w-full mb-8">
|
||||
{user.name}
|
||||
{$i18n.t('has no conversations.')}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Spinner />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
loadHandler={loadMoreChats}
|
||||
></ChatsModal>
|
||||
|
@ -140,7 +140,7 @@
|
||||
{#if chatList}
|
||||
<div class="w-full">
|
||||
{#if chatList.length > 0}
|
||||
<div class="flex text-xs font-medium">
|
||||
<div class="flex text-xs font-medium mb-1.5">
|
||||
<button
|
||||
class="px-1.5 py-1 cursor-pointer select-none basis-3/5"
|
||||
on:click={() => setSortKey('title')}
|
||||
|
Loading…
Reference in New Issue
Block a user