enh: folders
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import { getTimeRange } from '$lib/utils';
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export let chats = [];
|
||||
|
||||
let chatList = null;
|
||||
|
||||
const init = async () => {
|
||||
if (chats.length === 0) {
|
||||
chatList = [];
|
||||
} else {
|
||||
chatList = chats.map((chat) => ({
|
||||
...chat,
|
||||
time_range: getTimeRange(chat.updated_at)
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
$: if (chats) {
|
||||
init();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if chatList}
|
||||
<div class="text-left text-sm w-full mb-3">
|
||||
{#if chatList.length === 0}
|
||||
<div
|
||||
class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 min-h-20 w-full h-full flex justify-center items-center"
|
||||
>
|
||||
{$i18n.t('No chats found')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each chatList as chat, idx (chat.id)}
|
||||
{#if (idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)) && chat?.time_range}
|
||||
<div
|
||||
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
|
||||
? ''
|
||||
: 'pt-5'} pb-2 px-2"
|
||||
>
|
||||
{$i18n.t(chat.time_range)}
|
||||
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
|
||||
{$i18n.t('Today')}
|
||||
{$i18n.t('Yesterday')}
|
||||
{$i18n.t('Previous 7 days')}
|
||||
{$i18n.t('Previous 30 days')}
|
||||
{$i18n.t('January')}
|
||||
{$i18n.t('February')}
|
||||
{$i18n.t('March')}
|
||||
{$i18n.t('April')}
|
||||
{$i18n.t('May')}
|
||||
{$i18n.t('June')}
|
||||
{$i18n.t('July')}
|
||||
{$i18n.t('August')}
|
||||
{$i18n.t('September')}
|
||||
{$i18n.t('October')}
|
||||
{$i18n.t('November')}
|
||||
{$i18n.t('December')}
|
||||
-->
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850"
|
||||
draggable="false"
|
||||
href={`/c/${chat.id}`}
|
||||
on:click={() => (show = false)}
|
||||
>
|
||||
<div class="text-ellipsis line-clamp-1 w-full sm:basis-3/5">
|
||||
{chat?.title}
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex sm:basis-2/5 items-center justify-end">
|
||||
<div class=" text-gray-500 dark:text-gray-400 text-xs">
|
||||
{dayjs(chat?.updated_at * 1000).calendar()}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
<!-- {#if !allChatsLoaded && loadHandler}
|
||||
<Loader
|
||||
on:visible={(e) => {
|
||||
if (!chatListLoading) {
|
||||
loadHandler();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
|
||||
<Spinner className=" size-4" />
|
||||
<div class=" ">Loading...</div>
|
||||
</div>
|
||||
</Loader>
|
||||
{/if} -->
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import ChatList from './ChatList.svelte';
|
||||
import FolderKnowledge from './FolderKnowledge.svelte';
|
||||
|
||||
export let folder = null;
|
||||
|
||||
let selectedTab = 'chats';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<!-- <div class="mb-1">
|
||||
<div
|
||||
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
|
||||
>
|
||||
<button
|
||||
class="min-w-fit p-1.5 {selectedTab === 'knowledge'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
selectedTab = 'knowledge';
|
||||
}}>{$i18n.t('Knowledge')}</button
|
||||
>
|
||||
|
||||
<button
|
||||
class="min-w-fit p-1.5 {selectedTab === 'chats'
|
||||
? ''
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
selectedTab = 'chats';
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Chats')}
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="">
|
||||
{#if selectedTab === 'knowledge'}
|
||||
<FolderKnowledge />
|
||||
{:else if selectedTab === 'chats'}
|
||||
<ChatList chats={folder?.items?.chats ?? []} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,147 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { selectedFolder } from '$lib/stores';
|
||||
|
||||
import { deleteFolderById, updateFolderById } from '$lib/apis/folders';
|
||||
import { getChatsByFolderId } from '$lib/apis/chats';
|
||||
|
||||
import FolderModal from '$lib/components/layout/Sidebar/Folders/FolderModal.svelte';
|
||||
|
||||
import Folder from '$lib/components/icons/Folder.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import FolderMenu from '$lib/components/layout/Sidebar/Folders/FolderMenu.svelte';
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
export let folder = null;
|
||||
|
||||
export let onUpdate: Function = (folderId) => {};
|
||||
export let onDelete: Function = (folderId) => {};
|
||||
|
||||
let showFolderModal = false;
|
||||
let showDeleteConfirm = false;
|
||||
|
||||
const updateHandler = async ({ name, data }) => {
|
||||
if (name === '') {
|
||||
toast.error($i18n.t('Folder name cannot be empty.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentName = folder.name;
|
||||
|
||||
name = name.trim();
|
||||
folder.name = name;
|
||||
|
||||
const res = await updateFolderById(localStorage.token, folder.id, {
|
||||
name,
|
||||
...(data ? { data } : {})
|
||||
}).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
|
||||
folder.name = currentName;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
folder.name = name;
|
||||
if (data) {
|
||||
folder.data = data;
|
||||
}
|
||||
|
||||
toast.success($i18n.t('Folder updated successfully'));
|
||||
selectedFolder.set(folder);
|
||||
onUpdate(folder);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteHandler = async () => {
|
||||
const res = await deleteFolderById(localStorage.token, folder.id).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Folder deleted successfully'));
|
||||
onDelete(folder);
|
||||
}
|
||||
};
|
||||
|
||||
const exportHandler = async () => {
|
||||
const chats = await getChatsByFolderId(localStorage.token, folder.id).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
if (!chats) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(chats)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
|
||||
saveAs(blob, `folder-${folder.name}-export-${Date.now()}.json`);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if folder}
|
||||
<FolderModal bind:show={showFolderModal} edit={true} {folder} onSubmit={updateHandler} />
|
||||
|
||||
<DeleteConfirmDialog
|
||||
bind:show={showDeleteConfirm}
|
||||
title={$i18n.t('Delete folder?')}
|
||||
on:confirm={() => {
|
||||
deleteHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-700 dark:text-gray-300 flex-1 line-clamp-3">
|
||||
{@html DOMPurify.sanitize(
|
||||
$i18n.t(
|
||||
'This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.',
|
||||
{
|
||||
NAME: folder.name
|
||||
}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
<div class="mb-3 px-6 @md:max-w-3xl justify-between w-full flex relative group items-center">
|
||||
<div class="text-center flex gap-3.5 items-center">
|
||||
<div class=" rounded-full bg-gray-50 dark:bg-gray-800 p-3 w-fit">
|
||||
<Folder className="size-4.5" strokeWidth="2" />
|
||||
</div>
|
||||
|
||||
<div class="text-3xl">
|
||||
{folder.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<FolderMenu
|
||||
align="end"
|
||||
onEdit={() => {
|
||||
showFolderModal = true;
|
||||
}}
|
||||
onDelete={() => {
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onExport={() => {
|
||||
exportHandler();
|
||||
}}
|
||||
>
|
||||
<button class="p-1.5 dark:hover:bg-gray-850 rounded-full touch-auto" on:click={(e) => {}}>
|
||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
||||
</button>
|
||||
</FolderMenu>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user