Merge branch 'dev' into dev
This commit is contained in:
@@ -651,8 +651,8 @@
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={textSplitter}
|
||||
>
|
||||
<option value="">{$i18n.t('Default (Character)')} </option>
|
||||
<option value="token">{$i18n.t('Token (Tiktoken)')}</option>
|
||||
<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
|
||||
<option value="token">{$i18n.t('Token')} ({$i18n.t('Tiktoken')})</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -184,7 +184,13 @@
|
||||
|
||||
const onDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
dragged = true;
|
||||
|
||||
// Check if a file is being dragged.
|
||||
if (e.dataTransfer?.types?.includes('Files')) {
|
||||
dragged = true;
|
||||
} else {
|
||||
dragged = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
@@ -200,8 +206,6 @@
|
||||
if (inputFiles && inputFiles.length > 0) {
|
||||
console.log(inputFiles);
|
||||
inputFilesHandler(inputFiles);
|
||||
} else {
|
||||
toast.error($i18n.t(`File not found.`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -269,6 +269,13 @@
|
||||
await mediaRecorder.stop();
|
||||
}
|
||||
clearInterval(durationCounter);
|
||||
|
||||
if (stream) {
|
||||
const tracks = stream.getTracks();
|
||||
tracks.forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
stream = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { config } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -50,15 +51,15 @@
|
||||
loadReasons();
|
||||
});
|
||||
|
||||
const submitHandler = () => {
|
||||
console.log('submitHandler');
|
||||
const saveHandler = () => {
|
||||
console.log('saveHandler');
|
||||
|
||||
if (!selectedReason) {
|
||||
toast.error($i18n.t('Please select a reason'));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch('submit', {
|
||||
dispatch('save', {
|
||||
reason: selectedReason,
|
||||
comment: comment
|
||||
});
|
||||
@@ -69,7 +70,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850"
|
||||
class=" my-2.5 rounded-xl px-4 py-3 border border-gray-50 dark:border-gray-850"
|
||||
id="message-feedback-{message.id}"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -97,7 +98,7 @@
|
||||
<div class="flex flex-wrap gap-2 text-sm mt-2.5">
|
||||
{#each reasons as reason}
|
||||
<button
|
||||
class="px-3.5 py-1 border dark:border-gray-850 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason ===
|
||||
class="px-3.5 py-1 border border-gray-50 dark:border-gray-850 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason ===
|
||||
reason
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ''} transition rounded-lg"
|
||||
@@ -120,14 +121,26 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-end">
|
||||
<div class="mt-2 gap-1.5 flex justify-end">
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<button
|
||||
class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Share to OpenWebUI Community')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class=" bg-emerald-700 text-white text-sm font-medium rounded-lg px-3.5 py-1.5"
|
||||
class=" bg-emerald-700 hover:bg-emerald-800 transition text-white text-sm font-medium rounded-xl px-3.5 py-1.5"
|
||||
on:click={() => {
|
||||
submitHandler();
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Submit')}
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1103,7 +1103,7 @@
|
||||
<RateComment
|
||||
bind:message
|
||||
bind:show={showRateComment}
|
||||
on:submit={(e) => {
|
||||
on:save={(e) => {
|
||||
dispatch('save', {
|
||||
...message,
|
||||
annotation: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
addTagById,
|
||||
deleteTagById,
|
||||
getAllChatTags,
|
||||
getAllTags,
|
||||
getChatList,
|
||||
getChatListByTagName,
|
||||
getTagsById,
|
||||
@@ -37,7 +37,10 @@
|
||||
tags: tags
|
||||
});
|
||||
|
||||
_tags.set(await getAllChatTags(localStorage.token));
|
||||
await _tags.set(await getAllTags(localStorage.token));
|
||||
dispatch('add', {
|
||||
name: tagName
|
||||
});
|
||||
};
|
||||
|
||||
const deleteTag = async (tagName) => {
|
||||
@@ -47,7 +50,7 @@
|
||||
tags: tags
|
||||
});
|
||||
|
||||
await _tags.set(await getAllChatTags(localStorage.token));
|
||||
await _tags.set(await getAllTags(localStorage.token));
|
||||
dispatch('delete', {
|
||||
name: tagName
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
$: dispatch('change', open);
|
||||
|
||||
import { slide } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
|
||||
@@ -7,26 +12,26 @@
|
||||
|
||||
export let open = false;
|
||||
export let className = '';
|
||||
export let buttonClassName = 'w-fit';
|
||||
export let title = null;
|
||||
|
||||
let contentHeight = 0;
|
||||
let contentElement: HTMLElement;
|
||||
|
||||
function handleClick(event) {
|
||||
if (!event.target.closest('.no-toggle')) {
|
||||
open = !open;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (contentElement) {
|
||||
contentHeight = open ? contentElement.scrollHeight : 0;
|
||||
}
|
||||
export let disabled = false;
|
||||
export let hide = false;
|
||||
</script>
|
||||
|
||||
<div class={className}>
|
||||
{#if title !== null}
|
||||
<button class="w-full" on:click={handleClick}>
|
||||
<div class="w-full font-medium transition flex items-center justify-between gap-2">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={buttonClassName}
|
||||
on:pointerup={() => {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" w-fit font-medium transition flex items-center justify-between gap-2">
|
||||
<div>
|
||||
{title}
|
||||
</div>
|
||||
@@ -39,23 +44,28 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleClick}
|
||||
class="flex w-full items-center gap-2 text-left text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={buttonClassName}
|
||||
on:pointerup={() => {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
<div
|
||||
class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
bind:this={contentElement}
|
||||
class="overflow-hidden transition-all duration-300 ease-in-out"
|
||||
style="max-height: {contentHeight}px;"
|
||||
>
|
||||
<div>
|
||||
{#if open && !hide}
|
||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
30
src/lib/components/common/DragGhost.svelte
Normal file
30
src/lib/components/common/DragGhost.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
export let x;
|
||||
export let y;
|
||||
|
||||
let popupElement = null;
|
||||
|
||||
onMount(() => {
|
||||
document.body.appendChild(popupElement);
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
document.body.removeChild(popupElement);
|
||||
document.body.style.overflow = 'unset';
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
<div
|
||||
bind:this={popupElement}
|
||||
class="fixed top-0 left-0 w-screen h-[100dvh] z-50 touch-none pointer-events-none"
|
||||
>
|
||||
<div class=" absolute text-white z-[99999]" style="top: {y + 10}px; left: {x + 10}px;">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<slot name="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700 z-50 bg-gray-850 text-white"
|
||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-900 z-50 bg-gray-850 text-white"
|
||||
sideOffset={8}
|
||||
side="bottom"
|
||||
align="start"
|
||||
|
||||
108
src/lib/components/common/Folder.svelte
Normal file
108
src/lib/components/common/Folder.svelte
Normal file
@@ -0,0 +1,108 @@
|
||||
<script>
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||
import Collapsible from './Collapsible.svelte';
|
||||
|
||||
export let open = true;
|
||||
|
||||
export let id = '';
|
||||
export let name = '';
|
||||
export let collapsible = true;
|
||||
|
||||
export let className = '';
|
||||
|
||||
let folderElement;
|
||||
|
||||
let draggedOver = false;
|
||||
|
||||
const onDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
draggedOver = true;
|
||||
};
|
||||
|
||||
const onDrop = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (folderElement.contains(e.target)) {
|
||||
console.log('Dropped on the Button');
|
||||
|
||||
try {
|
||||
// get data from the drag event
|
||||
const dataTransfer = e.dataTransfer.getData('text/plain');
|
||||
const data = JSON.parse(dataTransfer);
|
||||
console.log(data);
|
||||
dispatch('drop', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
draggedOver = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
draggedOver = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
folderElement.addEventListener('drop', onDrop);
|
||||
folderElement.addEventListener('dragleave', onDragLeave);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('drop', onDrop);
|
||||
folderElement.removeEventListener('dragleave', onDragLeave);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={folderElement} class="relative {className}">
|
||||
{#if draggedOver}
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full rounded-sm bg-[hsla(258,88%,66%,0.1)] bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
{#if collapsible}
|
||||
<Collapsible
|
||||
bind:open
|
||||
className="w-full "
|
||||
buttonClassName="w-full"
|
||||
on:change={(e) => {
|
||||
dispatch('change', e.detail);
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="w-full">
|
||||
<button
|
||||
class="w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
>
|
||||
<div class="text-gray-300 dark:text-gray-600">
|
||||
{#if open}
|
||||
<ChevronDown className=" size-3" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<ChevronRight className=" size-3" strokeWidth="2.5" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="translate-y-[0.5px]">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div slot="content" class="w-full">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Collapsible>
|
||||
{:else}
|
||||
<slot></slot>
|
||||
{/if}
|
||||
</div>
|
||||
19
src/lib/components/icons/Document.svelte
Normal file
19
src/lib/components/icons/Document.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -14,28 +14,23 @@
|
||||
pinnedChats,
|
||||
scrollPaginationEnabled,
|
||||
currentChatPage,
|
||||
temporaryChatEnabled,
|
||||
showArtifacts,
|
||||
showOverview,
|
||||
showControls
|
||||
temporaryChatEnabled
|
||||
} from '$lib/stores';
|
||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
import {
|
||||
deleteChatById,
|
||||
getChatList,
|
||||
getChatById,
|
||||
getChatListByTagName,
|
||||
updateChatById,
|
||||
getAllChatTags,
|
||||
archiveChatById,
|
||||
cloneChatById,
|
||||
getAllTags,
|
||||
getChatListBySearchText,
|
||||
createNewChat,
|
||||
getPinnedChatList
|
||||
getPinnedChatList,
|
||||
toggleChatPinnedStatusById,
|
||||
getChatPinnedStatusById,
|
||||
getChatById,
|
||||
updateChatFolderIdById
|
||||
} from '$lib/apis/chats';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
@@ -45,9 +40,13 @@
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import Loader from '../common/Loader.svelte';
|
||||
import FilesOverlay from '../chat/MessageInput/FilesOverlay.svelte';
|
||||
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
|
||||
import { select } from 'd3-selection';
|
||||
import SearchInput from './Sidebar/SearchInput.svelte';
|
||||
import Folder from '../common/Folder.svelte';
|
||||
import Plus from '../icons/Plus.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders';
|
||||
import Folders from './Sidebar/Folders.svelte';
|
||||
|
||||
const BREAKPOINT = 768;
|
||||
|
||||
@@ -64,12 +63,82 @@
|
||||
|
||||
let selectedTagName = null;
|
||||
|
||||
let showPinnedChat = true;
|
||||
|
||||
// Pagination variables
|
||||
let chatListLoading = false;
|
||||
let allChatsLoaded = false;
|
||||
|
||||
let folders = {};
|
||||
|
||||
const initFolders = async () => {
|
||||
const folderList = await getFolders(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
folders = {};
|
||||
|
||||
// First pass: Initialize all folder entries
|
||||
for (const folder of folderList) {
|
||||
// Ensure folder is added to folders with its data
|
||||
folders[folder.id] = { ...(folders[folder.id] || {}), ...folder };
|
||||
}
|
||||
|
||||
// Second pass: Tie child folders to their parents
|
||||
for (const folder of folderList) {
|
||||
if (folder.parent_id) {
|
||||
// Ensure the parent folder is initialized if it doesn't exist
|
||||
if (!folders[folder.parent_id]) {
|
||||
folders[folder.parent_id] = {}; // Create a placeholder if not already present
|
||||
}
|
||||
|
||||
// Initialize childrenIds array if it doesn't exist and add the current folder id
|
||||
folders[folder.parent_id].childrenIds = folders[folder.parent_id].childrenIds
|
||||
? [...folders[folder.parent_id].childrenIds, folder.id]
|
||||
: [folder.id];
|
||||
|
||||
// Sort the children by updated_at field
|
||||
folders[folder.parent_id].childrenIds.sort((a, b) => {
|
||||
return folders[b].updated_at - folders[a].updated_at;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createFolder = async (name = 'Untitled') => {
|
||||
if (name === '') {
|
||||
toast.error($i18n.t('Folder name cannot be empty.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const rootFolders = Object.values(folders).filter((folder) => folder.parent_id === null);
|
||||
if (rootFolders.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) {
|
||||
// If a folder with the same name already exists, append a number to the name
|
||||
let i = 1;
|
||||
while (
|
||||
rootFolders.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase())
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
|
||||
name = `${name} ${i}`;
|
||||
}
|
||||
|
||||
const res = await createNewFolder(localStorage.token, name).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
await initFolders();
|
||||
}
|
||||
};
|
||||
|
||||
const initChatList = async () => {
|
||||
// Reset pagination variables
|
||||
tags.set(await getAllTags(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
allChatsLoaded = false;
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
@@ -93,7 +162,7 @@
|
||||
|
||||
// once the bottom of the list has been reached (no results) there is no need to continue querying
|
||||
allChatsLoaded = newChatList.length === 0;
|
||||
await chats.set([...$chats, ...newChatList]);
|
||||
await chats.set([...($chats ? $chats : []), ...newChatList]);
|
||||
|
||||
chatListLoading = false;
|
||||
};
|
||||
@@ -116,6 +185,10 @@
|
||||
searchDebounceTimeout = setTimeout(async () => {
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatListBySearchText(localStorage.token, search));
|
||||
|
||||
if ($chats.length === 0) {
|
||||
tags.set(await getAllTags(localStorage.token));
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
@@ -127,6 +200,8 @@
|
||||
});
|
||||
|
||||
if (res) {
|
||||
tags.set(await getAllTags(localStorage.token));
|
||||
|
||||
if ($chatId === id) {
|
||||
await chatId.set('');
|
||||
await tick();
|
||||
@@ -136,7 +211,6 @@
|
||||
allChatsLoaded = false;
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
}
|
||||
};
|
||||
@@ -171,14 +245,11 @@
|
||||
const tagEventHandler = async (type, tagName, chatId) => {
|
||||
console.log(type, tagName, chatId);
|
||||
if (type === 'delete') {
|
||||
if (selectedTagName === tagName) {
|
||||
if ($tags.map((t) => t.name).includes(tagName)) {
|
||||
await chats.set(await getChatListByTagName(localStorage.token, tagName));
|
||||
} else {
|
||||
selectedTagName = null;
|
||||
await initChatList();
|
||||
}
|
||||
}
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatListBySearchText(localStorage.token, search, $currentChatPage));
|
||||
} else if (type === 'add') {
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatListBySearchText(localStorage.token, search, $currentChatPage));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -186,7 +257,13 @@
|
||||
|
||||
const onDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
dragged = true;
|
||||
|
||||
// Check if a file is being dragged.
|
||||
if (e.dataTransfer?.types?.includes('Files')) {
|
||||
dragged = true;
|
||||
} else {
|
||||
dragged = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
@@ -195,19 +272,19 @@
|
||||
|
||||
const onDrop = async (e) => {
|
||||
e.preventDefault();
|
||||
console.log(e);
|
||||
console.log(e); // Log the drop event
|
||||
|
||||
// Perform file drop check and handle it accordingly
|
||||
if (e.dataTransfer?.files) {
|
||||
const inputFiles = Array.from(e.dataTransfer?.files);
|
||||
|
||||
if (inputFiles && inputFiles.length > 0) {
|
||||
console.log(inputFiles);
|
||||
inputFilesHandler(inputFiles);
|
||||
} else {
|
||||
toast.error($i18n.t(`File not found.`));
|
||||
console.log(inputFiles); // Log the dropped files
|
||||
inputFilesHandler(inputFiles); // Handle the dropped files
|
||||
}
|
||||
}
|
||||
|
||||
dragged = false;
|
||||
dragged = false; // Reset dragged status after drop
|
||||
};
|
||||
|
||||
let touchstart;
|
||||
@@ -256,6 +333,8 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
showPinnedChat = localStorage?.showPinnedChat ? localStorage.showPinnedChat === 'true' : true;
|
||||
|
||||
mobile.subscribe((e) => {
|
||||
if ($showSidebar && e) {
|
||||
showSidebar.set(false);
|
||||
@@ -271,6 +350,7 @@
|
||||
localStorage.sidebar = value;
|
||||
});
|
||||
|
||||
await initFolders();
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
await initChatList();
|
||||
|
||||
@@ -311,7 +391,8 @@
|
||||
<ArchivedChatsModal
|
||||
bind:show={$showArchivedChats}
|
||||
on:change={async () => {
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
await initChatList();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -342,8 +423,8 @@
|
||||
bind:this={navElement}
|
||||
id="sidebar"
|
||||
class="h-screen max-h-[100dvh] min-h-screen select-none {$showSidebar
|
||||
? 'md:relative w-[260px]'
|
||||
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0
|
||||
? 'md:relative w-[260px] max-w-[260px]'
|
||||
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0 overflow-x-hidden
|
||||
"
|
||||
data-state={$showSidebar}
|
||||
>
|
||||
@@ -360,14 +441,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar
|
||||
class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] overflow-x-hidden z-50 {$showSidebar
|
||||
? ''
|
||||
: 'invisible'}"
|
||||
>
|
||||
<div class="px-2.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400">
|
||||
<a
|
||||
id="sidebar-new-chat-button"
|
||||
class="flex flex-1 justify-between rounded-xl px-2 h-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
class="flex flex-1 justify-between rounded-lg px-2 h-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
href="/"
|
||||
draggable="false"
|
||||
on:click={async () => {
|
||||
@@ -411,7 +492,7 @@
|
||||
</a>
|
||||
|
||||
<button
|
||||
class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
class=" cursor-pointer px-2 py-2 flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
showSidebar.set(!$showSidebar);
|
||||
}}
|
||||
@@ -438,7 +519,7 @@
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class="px-2.5 flex justify-center text-gray-800 dark:text-gray-200">
|
||||
<a
|
||||
class="flex-grow flex space-x-3 rounded-xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
class="flex-grow flex space-x-3 rounded-lg px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
href="/workspace"
|
||||
on:click={() => {
|
||||
selectedChatId = null;
|
||||
@@ -474,6 +555,31 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="relative {$temporaryChatEnabled ? 'opacity-20' : ''}">
|
||||
{#if $temporaryChatEnabled}
|
||||
<div class="absolute z-40 w-full h-full flex justify-center"></div>
|
||||
{/if}
|
||||
|
||||
<div class="absolute z-40 right-4 top-1">
|
||||
<Tooltip content={$i18n.t('New folder')}>
|
||||
<button
|
||||
class="p-1 rounded-lg bg-gray-50 hover:bg-gray-100 dark:bg-gray-950 dark:hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
createFolder();
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<SearchInput
|
||||
bind:value={search}
|
||||
on:input={searchDebounceHandler}
|
||||
placeholder={$i18n.t('Search')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative flex flex-col flex-1 overflow-y-auto {$temporaryChatEnabled
|
||||
? 'opacity-20'
|
||||
@@ -483,120 +589,169 @@
|
||||
<div class="absolute z-40 w-full h-full flex justify-center"></div>
|
||||
{/if}
|
||||
|
||||
<div class="px-2 mt-0.5 mb-2 flex justify-center space-x-2">
|
||||
<div class="flex w-full rounded-xl" id="chat-search">
|
||||
<div class="self-center pl-3 py-2 rounded-l-xl bg-transparent">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{#if !search && $pinnedChats.length > 0}
|
||||
<div class="flex flex-col space-y-1 rounded-xl">
|
||||
<Folder
|
||||
className="px-2"
|
||||
bind:open={showPinnedChat}
|
||||
on:change={(e) => {
|
||||
localStorage.setItem('showPinnedChat', e.detail);
|
||||
console.log(e.detail);
|
||||
}}
|
||||
on:drop={async (e) => {
|
||||
const { type, id } = e.detail;
|
||||
|
||||
<input
|
||||
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none"
|
||||
placeholder={$i18n.t('Search')}
|
||||
bind:value={search}
|
||||
on:input={() => {
|
||||
searchDebounceHandler();
|
||||
if (type === 'chat') {
|
||||
const chat = await getChatById(localStorage.token, id);
|
||||
|
||||
if (chat) {
|
||||
console.log(chat);
|
||||
if (chat.folder_id) {
|
||||
const res = await updateChatFolderIdById(
|
||||
localStorage.token,
|
||||
chat.id,
|
||||
null
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
initChatList();
|
||||
await initFolders();
|
||||
}
|
||||
}
|
||||
|
||||
if (!chat.pinned) {
|
||||
const res = await toggleChatPinnedStatusById(localStorage.token, id);
|
||||
|
||||
if (res) {
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
initChatList();
|
||||
await initFolders();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
name={$i18n.t('Pinned')}
|
||||
>
|
||||
<div
|
||||
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
|
||||
>
|
||||
{#each $pinnedChats as chat, idx}
|
||||
<ChatItem
|
||||
className=""
|
||||
id={chat.id}
|
||||
title={chat.title}
|
||||
{shiftKey}
|
||||
selected={selectedChatId === chat.id}
|
||||
on:select={() => {
|
||||
selectedChatId = chat.id;
|
||||
}}
|
||||
on:unselect={() => {
|
||||
selectedChatId = null;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
if ((e?.detail ?? '') === 'shift') {
|
||||
deleteChatHandler(chat.id);
|
||||
} else {
|
||||
deleteChat = chat;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
}}
|
||||
on:change={async () => {
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
initChatList();
|
||||
}}
|
||||
on:tag={(e) => {
|
||||
const { type, name } = e.detail;
|
||||
tagEventHandler(type, name, chat.id);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Folder>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" flex-1 flex flex-col overflow-y-auto scrollbar-hidden">
|
||||
{#if !search && folders}
|
||||
<Folders
|
||||
{folders}
|
||||
on:update={async (e) => {
|
||||
initChatList();
|
||||
await initFolders();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $tags.length > 0}
|
||||
<div class="px-3.5 mb-2.5 flex gap-0.5 flex-wrap">
|
||||
<button
|
||||
class="px-2.5 py-[1px] text-xs transition {selectedTagName === null
|
||||
? 'bg-gray-100 dark:bg-gray-900'
|
||||
: ' '} rounded-md font-medium"
|
||||
on:click={async () => {
|
||||
selectedTagName = null;
|
||||
await initChatList();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('all')}
|
||||
</button>
|
||||
{#each $tags as tag}
|
||||
<button
|
||||
class="px-2.5 py-[1px] text-xs transition {selectedTagName === tag.name
|
||||
? 'bg-gray-100 dark:bg-gray-900'
|
||||
: ''} rounded-md font-medium"
|
||||
on:click={async () => {
|
||||
selectedTagName = tag.name;
|
||||
scrollPaginationEnabled.set(false);
|
||||
<Folder
|
||||
collapsible={!search}
|
||||
className="px-2"
|
||||
name={$i18n.t('All chats')}
|
||||
on:drop={async (e) => {
|
||||
const { type, id } = e.detail;
|
||||
|
||||
let taggedChatList = await getChatListByTagName(localStorage.token, tag.name);
|
||||
if (taggedChatList.length === 0) {
|
||||
await tags.set(await getAllChatTags(localStorage.token));
|
||||
// if the tag we deleted is no longer a valid tag, return to main chat list view
|
||||
await initChatList();
|
||||
} else {
|
||||
await chats.set(taggedChatList);
|
||||
}
|
||||
chatListLoading = false;
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
if (type === 'chat') {
|
||||
const chat = await getChatById(localStorage.token, id);
|
||||
|
||||
{#if !search && $pinnedChats.length > 0}
|
||||
<div class="pl-2 pb-2 flex flex-col space-y-1">
|
||||
<div class="">
|
||||
<div class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium pb-1.5">
|
||||
{$i18n.t('Pinned')}
|
||||
</div>
|
||||
if (chat) {
|
||||
console.log(chat);
|
||||
if (chat.folder_id) {
|
||||
const res = await updateChatFolderIdById(localStorage.token, chat.id, null).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
{#each $pinnedChats as chat, idx}
|
||||
<ChatItem
|
||||
{chat}
|
||||
{shiftKey}
|
||||
selected={selectedChatId === chat.id}
|
||||
on:select={() => {
|
||||
selectedChatId = chat.id;
|
||||
}}
|
||||
on:unselect={() => {
|
||||
selectedChatId = null;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
if ((e?.detail ?? '') === 'shift') {
|
||||
deleteChatHandler(chat.id);
|
||||
} else {
|
||||
deleteChat = chat;
|
||||
showDeleteConfirm = true;
|
||||
if (res) {
|
||||
initChatList();
|
||||
await initFolders();
|
||||
}
|
||||
}}
|
||||
on:tag={(e) => {
|
||||
const { type, name } = e.detail;
|
||||
tagEventHandler(type, name, chat.id);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
}
|
||||
|
||||
<div class="pl-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
|
||||
{#if $chats}
|
||||
{#each $chats as chat, idx}
|
||||
{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
|
||||
<div
|
||||
class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
|
||||
? ''
|
||||
: 'pt-5'} pb-0.5"
|
||||
>
|
||||
{$i18n.t(chat.time_range)}
|
||||
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
|
||||
if (chat.pinned) {
|
||||
const res = await toggleChatPinnedStatusById(localStorage.token, id);
|
||||
|
||||
if (res) {
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
initChatList();
|
||||
await initFolders();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type === 'folder') {
|
||||
if (folders[id].parent_id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await updateFolderParentIdById(localStorage.token, id, null).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
await initFolders();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="pt-1.5">
|
||||
{#if $chats}
|
||||
{#each $chats as chat, idx}
|
||||
{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
|
||||
<div
|
||||
class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx ===
|
||||
0
|
||||
? ''
|
||||
: 'pt-5'} pb-1.5"
|
||||
>
|
||||
{$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')}
|
||||
@@ -614,60 +769,68 @@
|
||||
{$i18n.t('November')}
|
||||
{$i18n.t('December')}
|
||||
-->
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ChatItem
|
||||
{chat}
|
||||
{shiftKey}
|
||||
selected={selectedChatId === chat.id}
|
||||
on:select={() => {
|
||||
selectedChatId = chat.id;
|
||||
}}
|
||||
on:unselect={() => {
|
||||
selectedChatId = null;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
if ((e?.detail ?? '') === 'shift') {
|
||||
deleteChatHandler(chat.id);
|
||||
} else {
|
||||
deleteChat = chat;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
}}
|
||||
on:tag={(e) => {
|
||||
const { type, name } = e.detail;
|
||||
tagEventHandler(type, name, chat.id);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
<ChatItem
|
||||
className=""
|
||||
id={chat.id}
|
||||
title={chat.title}
|
||||
{shiftKey}
|
||||
selected={selectedChatId === chat.id}
|
||||
on:select={() => {
|
||||
selectedChatId = chat.id;
|
||||
}}
|
||||
on:unselect={() => {
|
||||
selectedChatId = null;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
if ((e?.detail ?? '') === 'shift') {
|
||||
deleteChatHandler(chat.id);
|
||||
} else {
|
||||
deleteChat = chat;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
}}
|
||||
on:change={async () => {
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
initChatList();
|
||||
}}
|
||||
on:tag={(e) => {
|
||||
const { type, name } = e.detail;
|
||||
tagEventHandler(type, name, chat.id);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if $scrollPaginationEnabled && !allChatsLoaded}
|
||||
<Loader
|
||||
on:visible={(e) => {
|
||||
if (!chatListLoading) {
|
||||
loadMoreChats();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $scrollPaginationEnabled && !allChatsLoaded}
|
||||
<Loader
|
||||
on:visible={(e) => {
|
||||
if (!chatListLoading) {
|
||||
loadMoreChats();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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}
|
||||
{:else}
|
||||
<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}
|
||||
{:else}
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Folder>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-2.5 pb-safe-bottom">
|
||||
<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
|
||||
|
||||
<div class="px-2">
|
||||
<div class="flex flex-col font-primary">
|
||||
{#if $user !== undefined}
|
||||
<UserMenu
|
||||
@@ -679,7 +842,7 @@
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
class=" flex items-center rounded-xl py-2.5 px-2.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
showDropdown = !showDropdown;
|
||||
}}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
getArchivedChatList
|
||||
} from '$lib/apis/chats';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
@@ -30,7 +29,6 @@
|
||||
});
|
||||
|
||||
chats = await getArchivedChatList(localStorage.token);
|
||||
|
||||
dispatch('change');
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto, invalidate, invalidateAll } from '$app/navigation';
|
||||
import { onMount, getContext, createEventDispatcher, tick } from 'svelte';
|
||||
import { onMount, getContext, createEventDispatcher, tick, onDestroy } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -10,6 +10,7 @@
|
||||
archiveChatById,
|
||||
cloneChatById,
|
||||
deleteChatById,
|
||||
getAllTags,
|
||||
getChatList,
|
||||
getChatListByTagName,
|
||||
getPinnedChatList,
|
||||
@@ -22,7 +23,8 @@
|
||||
mobile,
|
||||
pinnedChats,
|
||||
showSidebar,
|
||||
currentChatPage
|
||||
currentChatPage,
|
||||
tags
|
||||
} from '$lib/stores';
|
||||
|
||||
import ChatMenu from './ChatMenu.svelte';
|
||||
@@ -30,8 +32,16 @@
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import DragGhost from '$lib/components/common/DragGhost.svelte';
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Document from '$lib/components/icons/Document.svelte';
|
||||
|
||||
export let className = '';
|
||||
|
||||
export let id;
|
||||
export let title;
|
||||
|
||||
export let chat;
|
||||
export let selected = false;
|
||||
export let shiftKey = false;
|
||||
|
||||
@@ -40,7 +50,7 @@
|
||||
let showShareChatModal = false;
|
||||
let confirmEdit = false;
|
||||
|
||||
let chatTitle = chat.title;
|
||||
let chatTitle = title;
|
||||
|
||||
const editChatTitle = async (id, title) => {
|
||||
if (title === '') {
|
||||
@@ -77,6 +87,7 @@
|
||||
|
||||
const archiveChatHandler = async (id) => {
|
||||
await archiveChatById(localStorage.token, id);
|
||||
tags.set(await getAllTags(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
@@ -86,14 +97,89 @@
|
||||
const focusEdit = async (node: HTMLInputElement) => {
|
||||
node.focus();
|
||||
};
|
||||
|
||||
let itemElement;
|
||||
|
||||
let dragged = false;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
const dragImage = new Image();
|
||||
dragImage.src =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
|
||||
const onDragStart = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
event.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
|
||||
// Set the data to be transferred
|
||||
event.dataTransfer.setData(
|
||||
'text/plain',
|
||||
JSON.stringify({
|
||||
type: 'chat',
|
||||
id: id
|
||||
})
|
||||
);
|
||||
|
||||
dragged = true;
|
||||
itemElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
|
||||
};
|
||||
|
||||
const onDrag = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
};
|
||||
|
||||
const onDragEnd = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
itemElement.style.opacity = '1'; // Reset visual cue after drag
|
||||
dragged = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (itemElement) {
|
||||
// Event listener for when dragging starts
|
||||
itemElement.addEventListener('dragstart', onDragStart);
|
||||
// Event listener for when dragging occurs (optional)
|
||||
itemElement.addEventListener('drag', onDrag);
|
||||
// Event listener for when dragging ends
|
||||
itemElement.addEventListener('dragend', onDragEnd);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (itemElement) {
|
||||
itemElement.removeEventListener('dragstart', onDragStart);
|
||||
itemElement.removeEventListener('drag', onDrag);
|
||||
itemElement.removeEventListener('dragend', onDragEnd);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={chat.id} />
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={id} />
|
||||
|
||||
<div class=" w-full pr-2 relative group">
|
||||
{#if dragged && x && y}
|
||||
<DragGhost {x} {y}>
|
||||
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
|
||||
<div class="flex items-center gap-1">
|
||||
<Document className=" size-[18px]" strokeWidth="2" />
|
||||
<div class=" text-xs text-white line-clamp-1">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DragGhost>
|
||||
{/if}
|
||||
|
||||
<div bind:this={itemElement} class=" w-full {className} relative group" draggable="true">
|
||||
{#if confirmEdit}
|
||||
<div
|
||||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
|
||||
class=" w-full flex justify-between rounded-lg px-[11px] py-[7px] {id === $chatId ||
|
||||
confirmEdit
|
||||
? 'bg-gray-200 dark:bg-gray-900'
|
||||
: selected
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
@@ -107,12 +193,13 @@
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
|
||||
class=" w-full flex justify-between rounded-lg px-[11px] py-[7px] {id === $chatId ||
|
||||
confirmEdit
|
||||
? 'bg-gray-200 dark:bg-gray-900'
|
||||
: selected
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||
href="/c/{chat.id}"
|
||||
href="/c/{id}"
|
||||
on:click={() => {
|
||||
dispatch('select');
|
||||
|
||||
@@ -121,7 +208,7 @@
|
||||
}
|
||||
}}
|
||||
on:dblclick={() => {
|
||||
chatTitle = chat.title;
|
||||
chatTitle = title;
|
||||
confirmEdit = true;
|
||||
}}
|
||||
on:mouseenter={(e) => {
|
||||
@@ -135,7 +222,7 @@
|
||||
>
|
||||
<div class=" flex self-center flex-1 w-full">
|
||||
<div class=" text-left self-center overflow-hidden w-full h-[20px]">
|
||||
{chat.title}
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -144,12 +231,14 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="
|
||||
{chat.id === $chatId || confirmEdit
|
||||
{id === $chatId || confirmEdit
|
||||
? 'from-gray-200 dark:from-gray-900'
|
||||
: selected
|
||||
? 'from-gray-100 dark:from-gray-950'
|
||||
: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
|
||||
absolute right-[10px] top-[6px] py-1 pr-2 pl-5 bg-gradient-to-l from-80%
|
||||
absolute {className === 'pr-2'
|
||||
? 'right-[8px]'
|
||||
: 'right-0'} top-[5px] py-1 pr-0.5 mr-2 pl-5 bg-gradient-to-l from-80%
|
||||
|
||||
to-transparent"
|
||||
on:mouseenter={(e) => {
|
||||
@@ -160,28 +249,19 @@
|
||||
}}
|
||||
>
|
||||
{#if confirmEdit}
|
||||
<div class="flex self-center space-x-1.5 z-10">
|
||||
<div
|
||||
class="flex self-center items-center space-x-1.5 z-10 translate-y-[0.5px] -translate-x-[0.5px]"
|
||||
>
|
||||
<Tooltip content={$i18n.t('Confirm')}>
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
editChatTitle(chat.id, chatTitle);
|
||||
editChatTitle(id, chatTitle);
|
||||
confirmEdit = false;
|
||||
chatTitle = '';
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Check className=" size-3.5" strokeWidth="2.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -193,16 +273,7 @@
|
||||
chatTitle = '';
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<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>
|
||||
<XMark strokeWidth="2.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -212,7 +283,7 @@
|
||||
<button
|
||||
class=" self-center dark:hover:text-white transition"
|
||||
on:click={() => {
|
||||
archiveChatHandler(chat.id);
|
||||
archiveChatHandler(id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
@@ -235,18 +306,18 @@
|
||||
{:else}
|
||||
<div class="flex self-center space-x-1 z-10">
|
||||
<ChatMenu
|
||||
chatId={chat.id}
|
||||
chatId={id}
|
||||
cloneChatHandler={() => {
|
||||
cloneChatHandler(chat.id);
|
||||
cloneChatHandler(id);
|
||||
}}
|
||||
shareHandler={() => {
|
||||
showShareChatModal = true;
|
||||
}}
|
||||
archiveChatHandler={() => {
|
||||
archiveChatHandler(chat.id);
|
||||
archiveChatHandler(id);
|
||||
}}
|
||||
renameHandler={() => {
|
||||
chatTitle = chat.title;
|
||||
chatTitle = title;
|
||||
|
||||
confirmEdit = true;
|
||||
}}
|
||||
@@ -257,7 +328,7 @@
|
||||
dispatch('unselect');
|
||||
}}
|
||||
on:change={async () => {
|
||||
await pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
dispatch('change');
|
||||
}}
|
||||
on:tag={(e) => {
|
||||
dispatch('tag', e.detail);
|
||||
@@ -283,7 +354,7 @@
|
||||
</button>
|
||||
</ChatMenu>
|
||||
|
||||
{#if chat.id === $chatId}
|
||||
{#if id === $chatId}
|
||||
<!-- Shortcut support using "delete-chat-button" id -->
|
||||
<button
|
||||
id="delete-chat-button"
|
||||
|
||||
@@ -15,13 +15,8 @@
|
||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||
import Bookmark from '$lib/components/icons/Bookmark.svelte';
|
||||
import BookmarkSlash from '$lib/components/icons/BookmarkSlash.svelte';
|
||||
import {
|
||||
addTagById,
|
||||
deleteTagById,
|
||||
getChatPinnedStatusById,
|
||||
getTagsById,
|
||||
toggleChatPinnedStatusById
|
||||
} from '$lib/apis/chats';
|
||||
import { getChatPinnedStatusById, toggleChatPinnedStatusById } from '$lib/apis/chats';
|
||||
import { chats } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -65,14 +60,14 @@
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
|
||||
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl"
|
||||
sideOffset={-2}
|
||||
side="bottom"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
pinHandler();
|
||||
}}
|
||||
@@ -87,7 +82,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
renameHandler();
|
||||
}}
|
||||
@@ -97,7 +92,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
cloneChatHandler();
|
||||
}}
|
||||
@@ -107,7 +102,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
archiveChatHandler();
|
||||
}}
|
||||
@@ -117,7 +112,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
shareHandler();
|
||||
}}
|
||||
@@ -127,7 +122,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
deleteHandler();
|
||||
}}
|
||||
@@ -136,16 +131,25 @@
|
||||
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
|
||||
<hr class="border-gray-100 dark:border-gray-800 mt-1 mb-1" />
|
||||
|
||||
<div class="flex p-1">
|
||||
<Tags
|
||||
{chatId}
|
||||
on:add={(e) => {
|
||||
dispatch('tag', {
|
||||
type: 'add',
|
||||
name: e.detail.name
|
||||
});
|
||||
|
||||
show = false;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
dispatch('tag', {
|
||||
type: 'delete',
|
||||
name: e.detail.name
|
||||
});
|
||||
|
||||
show = false;
|
||||
}}
|
||||
on:close={() => {
|
||||
|
||||
29
src/lib/components/layout/Sidebar/Folders.svelte
Normal file
29
src/lib/components/layout/Sidebar/Folders.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
import RecursiveFolder from './RecursiveFolder.svelte';
|
||||
export let folders = {};
|
||||
|
||||
let folderList = [];
|
||||
// Get the list of folders that have no parent, sorted by name alphabetically
|
||||
$: folderList = Object.keys(folders)
|
||||
.filter((key) => folders[key].parent_id === null)
|
||||
.sort((a, b) =>
|
||||
folders[a].name.localeCompare(folders[b].name, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
{#each folderList as folderId (folderId)}
|
||||
<RecursiveFolder
|
||||
className="px-2"
|
||||
{folders}
|
||||
{folderId}
|
||||
on:update={(e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
58
src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte
Normal file
58
src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
bind:show
|
||||
on:change={(e) => {
|
||||
if (e.detail === false) {
|
||||
dispatch('close');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip content={$i18n.t('More')}>
|
||||
<slot />
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[160px] rounded-lg px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-xl"
|
||||
sideOffset={-2}
|
||||
side="bottom"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('rename');
|
||||
}}
|
||||
>
|
||||
<Pencil strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Rename')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('delete');
|
||||
}}
|
||||
>
|
||||
<GarbageBin strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
397
src/lib/components/layout/Sidebar/RecursiveFolder.svelte
Normal file
397
src/lib/components/layout/Sidebar/RecursiveFolder.svelte
Normal file
@@ -0,0 +1,397 @@
|
||||
<script>
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import ChevronDown from '../../icons/ChevronDown.svelte';
|
||||
import ChevronRight from '../../icons/ChevronRight.svelte';
|
||||
import Collapsible from '../../common/Collapsible.svelte';
|
||||
import DragGhost from '$lib/components/common/DragGhost.svelte';
|
||||
|
||||
import FolderOpen from '$lib/components/icons/FolderOpen.svelte';
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
import {
|
||||
deleteFolderById,
|
||||
updateFolderIsExpandedById,
|
||||
updateFolderNameById,
|
||||
updateFolderParentIdById
|
||||
} from '$lib/apis/folders';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { updateChatFolderIdById } from '$lib/apis/chats';
|
||||
import ChatItem from './ChatItem.svelte';
|
||||
import FolderMenu from './Folders/FolderMenu.svelte';
|
||||
|
||||
export let open = false;
|
||||
|
||||
export let folders;
|
||||
export let folderId;
|
||||
|
||||
export let className = '';
|
||||
|
||||
export let parentDragged = false;
|
||||
|
||||
let folderElement;
|
||||
|
||||
let edit = false;
|
||||
|
||||
let draggedOver = false;
|
||||
let dragged = false;
|
||||
|
||||
let name = '';
|
||||
|
||||
const onDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (dragged || parentDragged) {
|
||||
return;
|
||||
}
|
||||
draggedOver = true;
|
||||
};
|
||||
|
||||
const onDrop = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (dragged || parentDragged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (folderElement.contains(e.target)) {
|
||||
console.log('Dropped on the Button');
|
||||
|
||||
try {
|
||||
// get data from the drag event
|
||||
const dataTransfer = e.dataTransfer.getData('text/plain');
|
||||
const data = JSON.parse(dataTransfer);
|
||||
console.log(data);
|
||||
|
||||
const { type, id } = data;
|
||||
|
||||
if (type === 'folder') {
|
||||
open = true;
|
||||
if (id === folderId) {
|
||||
return;
|
||||
}
|
||||
// Move the folder
|
||||
const res = await updateFolderParentIdById(localStorage.token, id, folderId).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
dispatch('update');
|
||||
}
|
||||
} else if (type === 'chat') {
|
||||
open = true;
|
||||
|
||||
// Move the chat
|
||||
const res = await updateChatFolderIdById(localStorage.token, id, folderId).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
dispatch('update');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
draggedOver = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
if (dragged || parentDragged) {
|
||||
return;
|
||||
}
|
||||
|
||||
draggedOver = false;
|
||||
};
|
||||
|
||||
const dragImage = new Image();
|
||||
dragImage.src =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
|
||||
let x;
|
||||
let y;
|
||||
|
||||
const onDragStart = (event) => {
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
|
||||
// Set the data to be transferred
|
||||
event.dataTransfer.setData(
|
||||
'text/plain',
|
||||
JSON.stringify({
|
||||
type: 'folder',
|
||||
id: folderId
|
||||
})
|
||||
);
|
||||
|
||||
dragged = true;
|
||||
folderElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
|
||||
};
|
||||
|
||||
const onDrag = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
};
|
||||
|
||||
const onDragEnd = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
folderElement.style.opacity = '1'; // Reset visual cue after drag
|
||||
dragged = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
open = folders[folderId].is_expanded;
|
||||
if (folderElement) {
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
folderElement.addEventListener('drop', onDrop);
|
||||
folderElement.addEventListener('dragleave', onDragLeave);
|
||||
|
||||
// Event listener for when dragging starts
|
||||
folderElement.addEventListener('dragstart', onDragStart);
|
||||
// Event listener for when dragging occurs (optional)
|
||||
folderElement.addEventListener('drag', onDrag);
|
||||
// Event listener for when dragging ends
|
||||
folderElement.addEventListener('dragend', onDragEnd);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (folderElement) {
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('drop', onDrop);
|
||||
folderElement.removeEventListener('dragleave', onDragLeave);
|
||||
|
||||
folderElement.removeEventListener('dragstart', onDragStart);
|
||||
folderElement.removeEventListener('drag', onDrag);
|
||||
folderElement.removeEventListener('dragend', onDragEnd);
|
||||
}
|
||||
});
|
||||
|
||||
const deleteHandler = async () => {
|
||||
const res = await deleteFolderById(localStorage.token, folderId).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Folder deleted successfully'));
|
||||
dispatch('update');
|
||||
}
|
||||
};
|
||||
|
||||
const nameUpdateHandler = async () => {
|
||||
if (name === '') {
|
||||
toast.error($i18n.t('Folder name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === folders[folderId].name) {
|
||||
edit = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentName = folders[folderId].name;
|
||||
|
||||
name = name.trim();
|
||||
folders[folderId].name = name;
|
||||
|
||||
const res = await updateFolderNameById(localStorage.token, folderId, name).catch((error) => {
|
||||
toast.error(error);
|
||||
|
||||
folders[folderId].name = currentName;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
folders[folderId].name = name;
|
||||
toast.success($i18n.t('Folder name updated successfully'));
|
||||
dispatch('update');
|
||||
}
|
||||
};
|
||||
|
||||
const isExpandedUpdateHandler = async () => {
|
||||
const res = await updateFolderIsExpandedById(localStorage.token, folderId, open).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let isExpandedUpdateTimeout;
|
||||
|
||||
const isExpandedUpdateDebounceHandler = (open) => {
|
||||
clearTimeout(isExpandedUpdateTimeout);
|
||||
isExpandedUpdateTimeout = setTimeout(() => {
|
||||
isExpandedUpdateHandler();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
$: isExpandedUpdateDebounceHandler(open);
|
||||
|
||||
const editHandler = async () => {
|
||||
console.log('Edit');
|
||||
await tick();
|
||||
name = folders[folderId].name;
|
||||
edit = true;
|
||||
|
||||
await tick();
|
||||
|
||||
// focus on the input
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById(`folder-${folderId}-input`);
|
||||
input.focus();
|
||||
}, 100);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if dragged && x && y}
|
||||
<DragGhost {x} {y}>
|
||||
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
|
||||
<div class="flex items-center gap-1">
|
||||
<FolderOpen className="size-3.5" strokeWidth="2" />
|
||||
<div class=" text-xs text-white line-clamp-1">
|
||||
{folders[folderId].name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DragGhost>
|
||||
{/if}
|
||||
|
||||
<div bind:this={folderElement} class="relative {className}" draggable="true">
|
||||
{#if draggedOver}
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full rounded-sm bg-[hsla(258,88%,66%,0.1)] bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
<Collapsible
|
||||
bind:open
|
||||
className="w-full"
|
||||
buttonClassName="w-full"
|
||||
hide={(folders[folderId]?.childrenIds ?? []).length === 0 &&
|
||||
(folders[folderId].items?.chats ?? []).length === 0}
|
||||
on:change={(e) => {
|
||||
dispatch('open', e.detail);
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="w-full group">
|
||||
<button
|
||||
id="folder-{folderId}-button"
|
||||
class="relative w-full py-1.5 px-2 rounded-md flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:dblclick={() => {
|
||||
editHandler();
|
||||
}}
|
||||
>
|
||||
<div class="text-gray-300 dark:text-gray-600">
|
||||
{#if open}
|
||||
<ChevronDown className=" size-3" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<ChevronRight className=" size-3" strokeWidth="2.5" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="translate-y-[0.5px] flex-1 justify-start text-start line-clamp-1">
|
||||
{#if edit}
|
||||
<input
|
||||
id="folder-{folderId}-input"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
on:blur={() => {
|
||||
nameUpdateHandler();
|
||||
edit = false;
|
||||
}}
|
||||
on:click={(e) => {
|
||||
// Prevent accidental collapse toggling when clicking inside input
|
||||
e.stopPropagation();
|
||||
}}
|
||||
on:mousedown={(e) => {
|
||||
// Prevent accidental collapse toggling when clicking inside input
|
||||
e.stopPropagation();
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
edit = false;
|
||||
}
|
||||
}}
|
||||
class="w-full h-full bg-transparent text-gray-500 dark:text-gray-500 outline-none"
|
||||
/>
|
||||
{:else}
|
||||
{folders[folderId].name}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
|
||||
on:pointerup={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<FolderMenu
|
||||
on:rename={() => {
|
||||
editHandler();
|
||||
}}
|
||||
on:delete={() => {
|
||||
deleteHandler();
|
||||
}}
|
||||
>
|
||||
<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" on:click={(e) => {}}>
|
||||
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
|
||||
</button>
|
||||
</FolderMenu>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div slot="content" class="w-full">
|
||||
{#if (folders[folderId]?.childrenIds ?? []).length > 0 || (folders[folderId].items?.chats ?? []).length > 0}
|
||||
<div
|
||||
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
|
||||
>
|
||||
{#if folders[folderId]?.childrenIds}
|
||||
{@const children = folders[folderId]?.childrenIds
|
||||
.map((id) => folders[id])
|
||||
.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
})
|
||||
)}
|
||||
|
||||
{#each children as childFolder (`${folderId}-${childFolder.id}`)}
|
||||
<svelte:self
|
||||
{folders}
|
||||
folderId={childFolder.id}
|
||||
parentDragged={dragged}
|
||||
on:update={(e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if folders[folderId].items?.chats}
|
||||
{#each folders[folderId].items.chats as chat (chat.id)}
|
||||
<ChatItem id={chat.id} title={chat.title} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
208
src/lib/components/layout/Sidebar/SearchInput.svelte
Normal file
208
src/lib/components/layout/Sidebar/SearchInput.svelte
Normal file
@@ -0,0 +1,208 @@
|
||||
<script lang="ts">
|
||||
import { tags } from '$lib/stores';
|
||||
import { stringify } from 'postcss';
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let placeholder = '';
|
||||
export let value = '';
|
||||
|
||||
let selectedIdx = 0;
|
||||
|
||||
let lastWord = '';
|
||||
$: lastWord = value ? value.split(' ').at(-1) : value;
|
||||
|
||||
let focused = false;
|
||||
let options = [
|
||||
{
|
||||
name: 'tag:',
|
||||
description: $i18n.t('search for tags')
|
||||
}
|
||||
];
|
||||
|
||||
let filteredOptions = options;
|
||||
$: filteredOptions = options.filter((option) => {
|
||||
return option.name.startsWith(lastWord);
|
||||
});
|
||||
|
||||
let filteredTags = [];
|
||||
$: filteredTags = lastWord.startsWith('tag:')
|
||||
? $tags.filter((tag) => {
|
||||
const tagName = lastWord.slice(4);
|
||||
if (tagName) {
|
||||
const tagId = tagName.replace(' ', '_').toLowerCase();
|
||||
|
||||
if (tag.id !== tagId) {
|
||||
return tag.id.startsWith(tagId);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
: [];
|
||||
|
||||
const documentClickHandler = (e) => {
|
||||
const searchContainer = document.getElementById('search-container');
|
||||
const chatSearch = document.getElementById('chat-search');
|
||||
|
||||
if (!searchContainer.contains(e.target) && !chatSearch.contains(e.target)) {
|
||||
if (e.target.id.startsWith('search-tag-') || e.target.id.startsWith('search-option-')) {
|
||||
return;
|
||||
}
|
||||
focused = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('click', documentClickHandler);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
document.removeEventListener('click', documentClickHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="px-2 mb-1 flex justify-center space-x-2 relative z-10" id="search-container">
|
||||
<div class="flex w-full rounded-xl" id="chat-search">
|
||||
<div class="self-center pl-3 py-2 rounded-l-xl bg-transparent">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none"
|
||||
placeholder={placeholder ? placeholder : $i18n.t('Search')}
|
||||
bind:value
|
||||
on:input={() => {
|
||||
dispatch('input');
|
||||
}}
|
||||
on:focus={() => {
|
||||
focused = true;
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (filteredTags.length > 0) {
|
||||
const tagElement = document.getElementById(`search-tag-${selectedIdx}`);
|
||||
tagElement.click();
|
||||
return;
|
||||
}
|
||||
|
||||
if (filteredOptions.length > 0) {
|
||||
const optionElement = document.getElementById(`search-option-${selectedIdx}`);
|
||||
optionElement.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
selectedIdx = Math.max(0, selectedIdx - 1);
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
|
||||
if (filteredTags.length > 0) {
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredTags.length - 1);
|
||||
} else {
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredOptions.length - 1);
|
||||
}
|
||||
} else {
|
||||
// if the user types something, reset to the top selection.
|
||||
selectedIdx = 0;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if focused && (filteredOptions.length > 0 || filteredTags.length > 0)}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="absolute top-0 mt-8 left-0 right-1 border dark:border-gray-900 bg-gray-50 dark:bg-gray-950 rounded-lg z-10 shadow-lg"
|
||||
in:fade={{ duration: 50 }}
|
||||
on:mouseenter={() => {
|
||||
selectedIdx = null;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
selectedIdx = 0;
|
||||
}}
|
||||
>
|
||||
<div class="px-2 py-2 text-xs group">
|
||||
{#if filteredTags.length > 0}
|
||||
<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Tags</div>
|
||||
|
||||
<div class="">
|
||||
{#each filteredTags as tag, tagIdx}
|
||||
<button
|
||||
class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
|
||||
tagIdx
|
||||
? 'bg-gray-100 dark:bg-gray-900'
|
||||
: ''}"
|
||||
id="search-tag-{tagIdx}"
|
||||
on:click|stopPropagation={async () => {
|
||||
const words = value.split(' ');
|
||||
|
||||
words.pop();
|
||||
words.push(`tag:${tag.id} `);
|
||||
|
||||
value = words.join(' ');
|
||||
|
||||
dispatch('input');
|
||||
}}
|
||||
>
|
||||
<div class="dark:text-gray-300 text-gray-700 font-medium">{tag.name}</div>
|
||||
|
||||
<div class=" text-gray-500 line-clamp-1">
|
||||
{tag.id}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if filteredOptions.length > 0}
|
||||
<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Search options</div>
|
||||
|
||||
<div class="">
|
||||
{#each filteredOptions as option, optionIdx}
|
||||
<button
|
||||
class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx ===
|
||||
optionIdx
|
||||
? 'bg-gray-100 dark:bg-gray-900'
|
||||
: ''}"
|
||||
id="search-option-{optionIdx}"
|
||||
on:click|stopPropagation={async () => {
|
||||
const words = value.split(' ');
|
||||
|
||||
words.pop();
|
||||
words.push('tag:');
|
||||
|
||||
value = words.join(' ');
|
||||
|
||||
dispatch('input');
|
||||
}}
|
||||
>
|
||||
<div class="dark:text-gray-300 text-gray-700 font-medium">{option.name}</div>
|
||||
|
||||
<div class=" text-gray-500 line-clamp-1">
|
||||
{option.description}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -47,6 +47,14 @@
|
||||
|
||||
let showDeleteConfirm = false;
|
||||
|
||||
let filteredItems = [];
|
||||
$: filteredItems = $functions.filter(
|
||||
(f) =>
|
||||
query === '' ||
|
||||
f.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
f.id.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
const shareHandler = async (func) => {
|
||||
const item = await getFunctionById(localStorage.token, func.id).catch((error) => {
|
||||
toast.error(error);
|
||||
@@ -174,17 +182,7 @@
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Functions')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$functions.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class=" flex w-full space-x-2 mb-2.5">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<svg
|
||||
@@ -225,12 +223,21 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5" />
|
||||
|
||||
<div class="mb-3.5">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-base font-medium px-0.5">
|
||||
{$i18n.t('Functions')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-base font-medium text-gray-500 dark:text-gray-300"
|
||||
>{filteredItems.length}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 mb-5">
|
||||
{#each $functions.filter((f) => query === '' || f.name
|
||||
.toLowerCase()
|
||||
.includes(query.toLowerCase()) || f.id.toLowerCase().includes(query.toLowerCase())) as func}
|
||||
{#each filteredItems as func}
|
||||
<div
|
||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||
>
|
||||
|
||||
@@ -72,17 +72,7 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Knowledge')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$knowledge.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class=" flex w-full space-x-2 mb-2.5">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<svg
|
||||
@@ -127,12 +117,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5" />
|
||||
<div class="mb-3.5">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-base font-medium px-0.5">
|
||||
{$i18n.t('Knowledge')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-base font-medium text-gray-500 dark:text-gray-300"
|
||||
>{filteredItems.length}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 mb-5 grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||
<div class="my-3 mb-5 grid md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{#each filteredItems as item}
|
||||
<button
|
||||
class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-xl"
|
||||
class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-xl"
|
||||
on:click={() => {
|
||||
if (item?.meta?.document) {
|
||||
toast.error(
|
||||
|
||||
@@ -553,37 +553,7 @@
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full max-h-[100dvh] h-full">
|
||||
<div class="flex items-center justify-between">
|
||||
<button
|
||||
class="flex space-x-1 w-fit"
|
||||
on:click={() => {
|
||||
goto('/workspace/knowledge');
|
||||
}}
|
||||
>
|
||||
<div class=" self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
|
||||
</button>
|
||||
|
||||
<div class=" flex-shrink-0">
|
||||
<div>
|
||||
<Badge type="success" content="Collection" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col my-2 flex-1 overflow-auto h-0">
|
||||
<div class="flex flex-col mb-2 flex-1 overflow-auto h-0">
|
||||
{#if id && knowledge}
|
||||
<div class="flex flex-row h-0 flex-1 overflow-auto">
|
||||
<div
|
||||
|
||||
@@ -35,8 +35,16 @@
|
||||
let modelsImportInputElement: HTMLInputElement;
|
||||
|
||||
let _models = [];
|
||||
|
||||
let filteredModels = [];
|
||||
let selectedModel = null;
|
||||
|
||||
$: if (_models) {
|
||||
filteredModels = _models.filter(
|
||||
(m) => searchValue === '' || m.name.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
let sortable = null;
|
||||
let searchValue = '';
|
||||
|
||||
@@ -294,17 +302,7 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Models')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$models.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class=" flex w-full space-x-2 mb-2.5">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<svg
|
||||
@@ -329,7 +327,7 @@
|
||||
|
||||
<div>
|
||||
<a
|
||||
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
|
||||
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition font-medium text-sm flex items-center space-x-1"
|
||||
href="/workspace/models/create"
|
||||
>
|
||||
<svg
|
||||
@@ -346,12 +344,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5" />
|
||||
<div class="mb-3.5">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-base font-medium px-0.5">
|
||||
{$i18n.t('Models')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-850" />
|
||||
<span class="text-base font-medium text-gray-500 dark:text-gray-300"
|
||||
>{filteredModels.length}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-1" href="/workspace/models/create">
|
||||
<div class=" self-center w-10 flex-shrink-0">
|
||||
<div class=" self-center w-8 flex-shrink-0">
|
||||
<div
|
||||
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
class="w-full h-8 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
|
||||
<path
|
||||
@@ -365,16 +373,14 @@
|
||||
|
||||
<div class=" self-center">
|
||||
<div class=" font-semibold line-clamp-1">{$i18n.t('Create a model')}</div>
|
||||
<div class=" text-sm line-clamp-1">{$i18n.t('Customize models for a specific purpose')}</div>
|
||||
<div class=" text-sm line-clamp-1 text-gray-500">
|
||||
{$i18n.t('Customize models for a specific purpose')}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5" />
|
||||
|
||||
<div class=" my-2 mb-5" id="model-list">
|
||||
{#each _models.filter((m) => searchValue === '' || m.name
|
||||
.toLowerCase()
|
||||
.includes(searchValue.toLowerCase())) as model}
|
||||
{#each filteredModels as model}
|
||||
<div
|
||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||
id="model-item-{model.id}"
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
let showDeleteConfirm = false;
|
||||
let deletePrompt = null;
|
||||
|
||||
let filteredItems = [];
|
||||
$: filteredItems = $prompts.filter((p) => query === '' || p.command.includes(query));
|
||||
|
||||
const shareHandler = async (prompt) => {
|
||||
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
|
||||
|
||||
@@ -64,17 +67,7 @@
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Prompts')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$prompts.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class=" flex w-full space-x-2 mb-2.5">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<svg
|
||||
@@ -116,10 +109,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5" />
|
||||
<div class="mb-3.5">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-base font-medium px-0.5">
|
||||
{$i18n.t('Prompts')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-base font-medium text-gray-500 dark:text-gray-300"
|
||||
>{filteredItems.length}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 mb-5">
|
||||
{#each $prompts.filter((p) => query === '' || p.command.includes(query)) as prompt}
|
||||
{#each filteredItems as prompt}
|
||||
<div
|
||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||
>
|
||||
|
||||
@@ -42,6 +42,14 @@
|
||||
|
||||
let showDeleteConfirm = false;
|
||||
|
||||
let filteredItems = [];
|
||||
$: filteredItems = $tools.filter(
|
||||
(t) =>
|
||||
query === '' ||
|
||||
t.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
t.id.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
const shareHandler = async (tool) => {
|
||||
const item = await getToolById(localStorage.token, tool.id).catch((error) => {
|
||||
toast.error(error);
|
||||
@@ -146,17 +154,7 @@
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Tools')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$tools.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class=" flex w-full space-x-2 mb-2.5">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<svg
|
||||
@@ -198,12 +196,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5" />
|
||||
<div class="mb-3.5">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex md:self-center text-base font-medium px-0.5">
|
||||
{$i18n.t('Tools')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-base font-medium text-gray-500 dark:text-gray-300"
|
||||
>{filteredItems.length}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 mb-5">
|
||||
{#each $tools.filter((t) => query === '' || t.name
|
||||
.toLowerCase()
|
||||
.includes(query.toLowerCase()) || t.id.toLowerCase().includes(query.toLowerCase())) as tool}
|
||||
{#each filteredItems as tool}
|
||||
<div
|
||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user