refac
This commit is contained in:
1 parent
05e3f71317
commit
f8350360df
13 files changed
+492
-249
No files matched your search
@@ -18,7 +18,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="w-full h-full pb-2 px-[16px]">
|
<div class="w-full h-full pb-2 px-3">
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, tick, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { user } from '$lib/stores';
|
import { user } from '$lib/stores';
|
||||||
@@ -55,41 +54,43 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col w-full h-full pb-2">
|
{#if loaded}
|
||||||
<div
|
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
||||||
id="users-tabs-container"
|
<div
|
||||||
class="mx-2 sm:mx-[16px] flex flex-row overflow-x-auto gap-2.5 max-w-full dark:text-gray-200 text-sm font-normal text-left scrollbar-none"
|
id="users-tabs-container"
|
||||||
>
|
class="tabs mx-2 sm:mx-[16px] lg:mx-0 lg:px-[16px] flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-50 dark:text-gray-200 text-sm font-normal text-left scrollbar-none"
|
||||||
<a
|
|
||||||
id="overview"
|
|
||||||
href="/admin/users/overview"
|
|
||||||
draggable="false"
|
|
||||||
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition select-none {selectedTab ===
|
|
||||||
'overview'
|
|
||||||
? ''
|
|
||||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
|
||||||
>
|
>
|
||||||
<div class=" self-center">{$i18n.t('Overview')}</div>
|
<a
|
||||||
</a>
|
id="overview"
|
||||||
|
href="/admin/users/overview"
|
||||||
|
draggable="false"
|
||||||
|
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition select-none {selectedTab ===
|
||||||
|
'overview'
|
||||||
|
? ''
|
||||||
|
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||||
|
>
|
||||||
|
<div class=" self-center">{$i18n.t('Overview')}</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
id="groups"
|
id="groups"
|
||||||
href="/admin/users/groups"
|
href="/admin/users/groups"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition select-none {selectedTab ===
|
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition select-none {selectedTab ===
|
||||||
'groups'
|
'groups'
|
||||||
? ''
|
? ''
|
||||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||||
>
|
>
|
||||||
<div class=" self-center">{$i18n.t('Groups')}</div>
|
<div class=" self-center">{$i18n.t('Groups')}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 mt-1 px-[16px] overflow-y-scroll">
|
<div class="flex-1 mt-1 lg:mt-0 px-[16px] lg:pr-[16px] lg:pl-0 overflow-y-scroll">
|
||||||
{#if selectedTab === 'overview'}
|
{#if selectedTab === 'overview'}
|
||||||
<UserList />
|
<UserList />
|
||||||
{:else if selectedTab === 'groups'}
|
{:else if selectedTab === 'groups'}
|
||||||
<Groups />
|
<Groups />
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
onSubmit={addGroupHandler}
|
onSubmit={addGroupHandler}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
|
<div class="flex flex-col gap-1 mt-0.5 mb-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center md:self-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
<div class="flex items-center md:self-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
||||||
<div>
|
<div>
|
||||||
@@ -145,8 +145,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1">
|
<div
|
||||||
<div class="flex h-8 flex-1 items-center w-full gap-2">
|
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100/30 dark:border-gray-850/30"
|
||||||
|
>
|
||||||
|
<div class="px-2.5 flex h-8 flex-1 items-center w-full gap-2">
|
||||||
<div class="flex min-w-0 flex-1">
|
<div class="flex min-w-0 flex-1">
|
||||||
<div class="self-center ml-1 mr-3">
|
<div class="self-center ml-1 mr-3">
|
||||||
<Search className="size-3.5" />
|
<Search className="size-3.5" />
|
||||||
@@ -199,7 +201,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if filteredGroups.length !== 0}
|
{#if filteredGroups.length !== 0}
|
||||||
<div class="my-1 grid grid-cols-1 gap-y-0.5">
|
<div class="mt-1 grid grid-cols-1 px-2">
|
||||||
{#each filteredGroups as group}
|
{#each filteredGroups as group}
|
||||||
<GroupItem {group} {setGroups} {defaultPermissions} />
|
<GroupItem {group} {setGroups} {defaultPermissions} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="group flex cursor-pointer text-left w-full px-2.5 py-2 rounded-2xl border-b border-gray-100/60 last:border-b-0 hover:bg-gray-50/70 dark:border-gray-850/60 dark:hover:bg-gray-850/50 transition"
|
class="group flex cursor-pointer text-left w-full px-2.5 py-2 rounded-xl border-b border-gray-100/60 last:border-b-0 hover:bg-gray-50/70 dark:border-gray-850/60 dark:hover:bg-gray-850/50 transition"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showEdit = true;
|
showEdit = true;
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
import { WEBUI_NAME, config, user, showSidebar } from '$lib/stores';
|
import { config, user } from '$lib/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { getContext, onDestroy } from 'svelte';
|
||||||
import { onMount, getContext, onDestroy } from 'svelte';
|
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
@@ -12,7 +11,7 @@
|
|||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users';
|
import { getUsers, deleteUserById } from '$lib/apis/users';
|
||||||
|
|
||||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||||
import ChatBubbles from '$lib/components/icons/ChatBubbles.svelte';
|
import ChatBubbles from '$lib/components/icons/ChatBubbles.svelte';
|
||||||
@@ -23,13 +22,11 @@
|
|||||||
import AddUserModal from '$lib/components/admin/Users/UserList/AddUserModal.svelte';
|
import AddUserModal from '$lib/components/admin/Users/UserList/AddUserModal.svelte';
|
||||||
|
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import RoleUpdateConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
|
||||||
|
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
import About from '$lib/components/chat/Settings/About.svelte';
|
|
||||||
import Banner from '$lib/components/common/Banner.svelte';
|
import Banner from '$lib/components/common/Banner.svelte';
|
||||||
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
@@ -169,7 +166,7 @@
|
|||||||
<div
|
<div
|
||||||
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
||||||
>
|
>
|
||||||
<div class="flex md:self-center text-lg font-normal px-0.5 gap-2">
|
<div class="flex items-center md:self-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
{$i18n.t('Users')}
|
{$i18n.t('Users')}
|
||||||
</div>
|
</div>
|
||||||
@@ -194,7 +191,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<div class=" flex w-full space-x-2">
|
<div class="flex w-full space-x-2">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div class=" self-center ml-1 mr-3">
|
<div class=" self-center ml-1 mr-3">
|
||||||
<svg
|
<svg
|
||||||
@@ -356,11 +353,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col" class="px-2.5 py-2 text-right" />
|
<th scope="col" class="px-2.5 py-2 text-right"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="">
|
<tbody class="">
|
||||||
{#each users as user, userIdx (user.id)}
|
{#each users as user (user.id)}
|
||||||
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
|
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
|
||||||
<td class="px-3 py-1 min-w-[7rem] w-28">
|
<td class="px-3 py-1 min-w-[7rem] w-28">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import type { i18n as i18nType } from 'i18next';
|
||||||
|
|
||||||
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
|
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
||||||
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
|
import DocumentArrowDown from '$lib/components/icons/DocumentArrowDown.svelte';
|
||||||
|
import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
|
||||||
|
import Link from '$lib/components/icons/Link.svelte';
|
||||||
|
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||||
|
|
||||||
|
type SplitCreateAction = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
onClick?: () => void | Promise<void>;
|
||||||
|
visible?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let actions: SplitCreateAction[] = [];
|
||||||
|
export let label = 'Create';
|
||||||
|
|
||||||
|
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||||
|
|
||||||
|
let showMenu = false;
|
||||||
|
$: visibleActions = actions.filter((action) => action.visible ?? true);
|
||||||
|
$: primaryAction =
|
||||||
|
visibleActions.find((action) => action.id.endsWith('-new')) ?? visibleActions[0] ?? null;
|
||||||
|
|
||||||
|
const runAction = async (action: SplitCreateAction | null) => {
|
||||||
|
if (!action) return;
|
||||||
|
if (action.href) {
|
||||||
|
await goto(action.href);
|
||||||
|
} else {
|
||||||
|
await action.onClick?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionIcon = (id: string) => {
|
||||||
|
if (id.endsWith('-new')) return Pencil;
|
||||||
|
if (id.includes('-import-link')) return Link;
|
||||||
|
if (id.includes('-import')) return DocumentArrowUp;
|
||||||
|
if (id.includes('-export')) return DocumentArrowDown;
|
||||||
|
return Pencil;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visibleActions.length}
|
||||||
|
<div
|
||||||
|
class="ml-1 flex overflow-hidden rounded-lg bg-gray-50 text-xs text-gray-900 transition ring-1 ring-gray-200 dark:bg-gray-850 dark:text-gray-100 dark:ring-gray-800"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-1 transition hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||||
|
on:click={() => {
|
||||||
|
runAction(primaryAction);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t(label)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Dropdown bind:show={showMenu} align="end" sideOffset={6}>
|
||||||
|
<button
|
||||||
|
class="flex items-center border-l border-gray-200 px-1.5 py-1 transition hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-800"
|
||||||
|
aria-label={$i18n.t('Open create menu')}
|
||||||
|
>
|
||||||
|
<ChevronDown className="size-2.5" strokeWidth="2.5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div slot="content">
|
||||||
|
<DropdownMenu className="min-w-[170px]">
|
||||||
|
{#each visibleActions as action (action.id)}
|
||||||
|
{@const Icon = getActionIcon(action.id)}
|
||||||
|
{#if action.href}
|
||||||
|
<a
|
||||||
|
href={action.href}
|
||||||
|
on:click={() => {
|
||||||
|
showMenu = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon className="size-3.5" />
|
||||||
|
<span class="self-center truncate">{action.label}</span>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
on:click={async () => {
|
||||||
|
await action.onClick?.();
|
||||||
|
showMenu = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon className="size-3.5" />
|
||||||
|
<span class="self-center truncate">{action.label}</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
import {
|
import {
|
||||||
createNewNote,
|
createNewNote,
|
||||||
deleteNoteById,
|
deleteNoteById,
|
||||||
|
getNotes,
|
||||||
getNoteById,
|
getNoteById,
|
||||||
getNoteList,
|
getNoteList,
|
||||||
searchNotes,
|
searchNotes,
|
||||||
@@ -46,7 +47,6 @@
|
|||||||
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import Search from '../icons/Search.svelte';
|
import Search from '../icons/Search.svelte';
|
||||||
import Plus from '../icons/Plus.svelte';
|
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import NoteMenu from './Notes/NoteMenu.svelte';
|
import NoteMenu from './Notes/NoteMenu.svelte';
|
||||||
@@ -55,10 +55,13 @@
|
|||||||
import DropdownOptions from '../common/DropdownOptions.svelte';
|
import DropdownOptions from '../common/DropdownOptions.svelte';
|
||||||
import Loader from '../common/Loader.svelte';
|
import Loader from '../common/Loader.svelte';
|
||||||
import SidebarIcon from '../icons/Sidebar.svelte';
|
import SidebarIcon from '../icons/Sidebar.svelte';
|
||||||
|
import SplitCreateButton from '../common/SplitCreateButton.svelte';
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
|
||||||
let importFiles = '';
|
let importFiles = '';
|
||||||
|
let importDocumentFiles: FileList | null = null;
|
||||||
|
let notesImportInputElement: HTMLInputElement;
|
||||||
let selectedNote = null;
|
let selectedNote = null;
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
|
|
||||||
@@ -80,6 +83,15 @@
|
|||||||
let itemsLoading = false;
|
let itemsLoading = false;
|
||||||
let allItemsLoaded = false;
|
let allItemsLoaded = false;
|
||||||
|
|
||||||
|
type NoteExportItem = {
|
||||||
|
title?: string;
|
||||||
|
data?: {
|
||||||
|
content?: {
|
||||||
|
md?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const downloadHandler = async (type) => {
|
const downloadHandler = async (type) => {
|
||||||
// Fetch the full note since the list response may not contain full content
|
// Fetch the full note since the list response may not contain full content
|
||||||
const note = await getNoteById(localStorage.token, selectedNote.id).catch((error) => {
|
const note = await getNoteById(localStorage.token, selectedNote.id).catch((error) => {
|
||||||
@@ -116,49 +128,76 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputFilesHandler = async (inputFiles) => {
|
const inputFilesHandler = async (inputFiles: File[]) => {
|
||||||
// Check if all the file is a markdown file and extract name and content
|
let imported = false;
|
||||||
|
|
||||||
for (const file of inputFiles) {
|
for (const file of inputFiles) {
|
||||||
if (file.type !== 'text/markdown') {
|
const isSupportedFile =
|
||||||
toast.error($i18n.t('Only markdown files are allowed'));
|
file.type === 'text/markdown' ||
|
||||||
|
file.type === 'text/plain' ||
|
||||||
|
/\.(md|txt)$/i.test(file.name);
|
||||||
|
|
||||||
|
if (!isSupportedFile) {
|
||||||
|
toast.error($i18n.t('Only TXT and Markdown files are allowed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new FileReader();
|
const content = await file.text();
|
||||||
reader.onload = async (event) => {
|
const name = file.name.replace(/\.(md|txt)$/i, '');
|
||||||
const content = event.target.result;
|
|
||||||
let name = file.name.replace(/\.md$/, '');
|
|
||||||
|
|
||||||
if (typeof content !== 'string') {
|
const res = await createNewNote(localStorage.token, {
|
||||||
toast.error($i18n.t('Invalid file content'));
|
title: name,
|
||||||
return;
|
data: {
|
||||||
}
|
content: {
|
||||||
|
json: null,
|
||||||
|
html: marked.parse(content ?? ''),
|
||||||
|
md: content
|
||||||
|
}
|
||||||
|
},
|
||||||
|
meta: null,
|
||||||
|
access_grants: []
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
// Create a new note with the content
|
if (res) {
|
||||||
const res = await createNewNote(localStorage.token, {
|
imported = true;
|
||||||
title: name,
|
}
|
||||||
data: {
|
|
||||||
content: {
|
|
||||||
json: null,
|
|
||||||
html: marked.parse(content ?? ''),
|
|
||||||
md: content
|
|
||||||
}
|
|
||||||
},
|
|
||||||
meta: null,
|
|
||||||
access_grants: []
|
|
||||||
}).catch((error) => {
|
|
||||||
toast.error(`${error}`);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imported) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNoteExportContent = (notes: NoteExportItem[], type: 'md' | 'txt') => {
|
||||||
|
return notes
|
||||||
|
.map((note) => {
|
||||||
|
const title = note.title ?? $i18n.t('Untitled');
|
||||||
|
const content = note.data?.content?.md ?? '';
|
||||||
|
|
||||||
|
if (type === 'md') {
|
||||||
|
return `# ${title}\n\n${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${title}\n\n${content}`;
|
||||||
|
})
|
||||||
|
.join(type === 'md' ? '\n\n---\n\n' : '\n\n-----\n\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportNotes = async (type: 'md' | 'txt') => {
|
||||||
|
const allNotes = await getNotes(localStorage.token, true).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allNotes) return;
|
||||||
|
|
||||||
|
const blob = new Blob([getNoteExportContent(allNotes, type)], {
|
||||||
|
type: type === 'md' ? 'text/markdown' : 'text/plain'
|
||||||
|
});
|
||||||
|
saveAs(blob, `notes-export-${Date.now()}.${type}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
@@ -326,6 +365,29 @@
|
|||||||
|
|
||||||
<div id="notes-container" class="w-full min-h-full h-full">
|
<div id="notes-container" class="w-full min-h-full h-full">
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
|
<input
|
||||||
|
id="notes-import-input"
|
||||||
|
bind:this={notesImportInputElement}
|
||||||
|
bind:files={importDocumentFiles}
|
||||||
|
type="file"
|
||||||
|
accept=".txt,.md,text/plain,text/markdown"
|
||||||
|
multiple
|
||||||
|
hidden
|
||||||
|
on:change={async () => {
|
||||||
|
if (!importDocumentFiles || importDocumentFiles.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await inputFilesHandler(Array.from(importDocumentFiles));
|
||||||
|
toast.success($i18n.t('Imported notes successfully'));
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
} finally {
|
||||||
|
importDocumentFiles = null;
|
||||||
|
notesImportInputElement.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
<DeleteConfirmDialog
|
||||||
bind:show={showDeleteConfirm}
|
bind:show={showDeleteConfirm}
|
||||||
title={$i18n.t('Delete note?')}
|
title={$i18n.t('Delete note?')}
|
||||||
@@ -370,20 +432,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-1">
|
<div class="ml-auto flex items-center gap-1">
|
||||||
<button
|
<SplitCreateButton
|
||||||
class="ml-1 px-2 py-1.5 text-xs gap-1 rounded-xl bg-black text-white dark:bg-white dark:text-black transition flex items-center"
|
actions={[
|
||||||
on:click={async () => {
|
{
|
||||||
const res = await createNoteHandler(dayjs().format('YYYY-MM-DD'));
|
id: 'notes-new',
|
||||||
|
label: $i18n.t('Create'),
|
||||||
|
onClick: async () => {
|
||||||
|
const res = await createNoteHandler(dayjs().format('YYYY-MM-DD'));
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
goto(`/notes/${res.id}`);
|
goto(`/notes/${res.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notes-import',
|
||||||
|
label: $i18n.t('Import TXT/MD'),
|
||||||
|
onClick: () => notesImportInputElement?.click()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notes-export-md',
|
||||||
|
label: $i18n.t('Export MD'),
|
||||||
|
onClick: () => exportNotes('md')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notes-export-txt',
|
||||||
|
label: $i18n.t('Export TXT'),
|
||||||
|
onClick: () => exportNotes('txt')
|
||||||
}
|
}
|
||||||
}}
|
]}
|
||||||
>
|
/>
|
||||||
<Plus className="size-3" strokeWidth="2.5" />
|
|
||||||
|
|
||||||
<div class="hidden md:block md:ml-1 text-xs">{$i18n.t('New Note')}</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -699,14 +777,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="w-full h-full flex flex-col items-center justify-center">
|
<div class="flex min-h-[calc(100dvh-13rem)] w-full flex-col items-center justify-center">
|
||||||
<div class="py-20 text-center">
|
<div class="text-center">
|
||||||
<div class=" text-sm text-gray-400 dark:text-gray-600">
|
<div class=" text-sm text-gray-400 dark:text-gray-600">
|
||||||
{$i18n.t('No Notes')}
|
{$i18n.t('No Notes')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-1 text-xs text-gray-300 dark:text-gray-700">
|
<div class="mt-1 text-xs text-gray-300 dark:text-gray-700">
|
||||||
{$i18n.t('Create your first note by clicking on the plus button below.')}
|
{$i18n.t('Create your first note from the Create menu.')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
|
|
||||||
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||||
import ItemMenu from './Knowledge/ItemMenu.svelte';
|
import ItemMenu from './Knowledge/ItemMenu.svelte';
|
||||||
|
import CreateKnowledgeBase from './Knowledge/CreateKnowledgeBase.svelte';
|
||||||
import Badge from '../common/Badge.svelte';
|
import Badge from '../common/Badge.svelte';
|
||||||
|
import Modal from '../common/Modal.svelte';
|
||||||
import Search from '../icons/Search.svelte';
|
import Search from '../icons/Search.svelte';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
|
let showCreateModal = false;
|
||||||
let tagsContainerElement: HTMLDivElement;
|
let tagsContainerElement: HTMLDivElement;
|
||||||
|
|
||||||
let selectedItem: KnowledgeListItem | null = null;
|
let selectedItem: KnowledgeListItem | null = null;
|
||||||
@@ -67,7 +70,9 @@
|
|||||||
{
|
{
|
||||||
id: 'knowledge-new',
|
id: 'knowledge-new',
|
||||||
label: $i18n.t('Create'),
|
label: $i18n.t('Create'),
|
||||||
href: '/workspace/knowledge/create'
|
onClick: () => {
|
||||||
|
showCreateModal = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -197,6 +202,15 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal bind:show={showCreateModal} size="sm">
|
||||||
|
<CreateKnowledgeBase
|
||||||
|
modal={true}
|
||||||
|
onBack={() => {
|
||||||
|
showCreateModal = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="flex h-8 w-full items-center gap-2">
|
<div class="flex h-8 w-full items-center gap-2">
|
||||||
<div class="flex min-w-0 flex-1">
|
<div class="flex min-w-0 flex-1">
|
||||||
|
|||||||
@@ -10,11 +10,19 @@
|
|||||||
|
|
||||||
import AccessControl from '../common/AccessControl.svelte';
|
import AccessControl from '../common/AccessControl.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
|
export let modal = false;
|
||||||
|
/** @type {() => void | Promise<void>} */
|
||||||
|
export let onBack = () => goto('/workspace/knowledge');
|
||||||
|
/** @type {(knowledge: { id: string }) => void | Promise<void>} */
|
||||||
|
export let onCreated = (knowledge) => goto(`/workspace/knowledge/${knowledge.id}`);
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
let name = '';
|
let name = '';
|
||||||
let description = '';
|
let description = '';
|
||||||
|
/** @type {{ id?: string; principal_type: 'user' | 'group'; principal_id: string; permission: 'read' | 'write' }[]} */
|
||||||
let accessGrants = [];
|
let accessGrants = [];
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
@@ -36,7 +44,7 @@
|
|||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t('Knowledge created successfully.'));
|
toast.success($i18n.t('Knowledge created successfully.'));
|
||||||
goto(`/workspace/knowledge/${res.id}`);
|
await onCreated(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
@@ -44,39 +52,57 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full max-h-full">
|
<div class="w-full max-h-full">
|
||||||
<button
|
{#if modal}
|
||||||
class="flex space-x-1"
|
<div class="flex justify-between items-center dark:text-gray-100 px-5 pt-4 pb-2">
|
||||||
on:click={() => {
|
<h3 class="text-base font-normal">{$i18n.t('Create a knowledge base')}</h3>
|
||||||
goto('/workspace/knowledge');
|
<button
|
||||||
}}
|
class="self-center shrink-0 ml-2"
|
||||||
>
|
aria-label={$i18n.t('Close')}
|
||||||
<div class=" self-center">
|
type="button"
|
||||||
<svg
|
on:click={() => {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
onBack();
|
||||||
viewBox="0 0 20 20"
|
}}
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
>
|
||||||
<path
|
<XMark className="size-5" />
|
||||||
fill-rule="evenodd"
|
</button>
|
||||||
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>
|
||||||
<div class=" self-center font-normal text-sm">{$i18n.t('Back')}</div>
|
{:else}
|
||||||
</button>
|
<button
|
||||||
|
class="flex space-x-1"
|
||||||
|
on:click={() => {
|
||||||
|
onBack();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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-normal text-sm">{$i18n.t('Back')}</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="flex flex-col max-w-lg mx-auto mt-10 mb-10"
|
class="flex flex-col {modal ? 'px-5 pb-3' : 'max-w-lg mx-auto mt-10 mb-10'}"
|
||||||
on:submit|preventDefault={() => {
|
on:submit|preventDefault={() => {
|
||||||
submitHandler();
|
submitHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" w-full flex flex-col justify-center">
|
<div class=" w-full flex flex-col justify-center">
|
||||||
<div class=" text-2xl font-normal mb-2.5">
|
{#if !modal}
|
||||||
{$i18n.t('Create a knowledge base')}
|
<div class=" text-2xl font-normal mb-2.5">
|
||||||
</div>
|
{$i18n.t('Create a knowledge base')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="w-full flex flex-col gap-2.5">
|
<div class="w-full flex flex-col gap-2.5">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
@@ -103,7 +129,7 @@
|
|||||||
bind:value={description}
|
bind:value={description}
|
||||||
placeholder={$i18n.t('Describe your knowledge base and objectives')}
|
placeholder={$i18n.t('Describe your knowledge base and objectives')}
|
||||||
required
|
required
|
||||||
/>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,12 +146,24 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end mt-2">
|
<div class="flex justify-end {modal ? 'pt-3 gap-2' : 'mt-2'}">
|
||||||
|
{#if modal}
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
onBack();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Cancel')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class=" text-sm px-4 py-2 transition rounded-lg {loading
|
class="{modal
|
||||||
? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'
|
? `px-3.5 py-1.5 text-sm bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full ${loading ? 'cursor-not-allowed' : ''}`
|
||||||
: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800'} flex"
|
: `text-sm px-4 py-2 transition rounded-lg ${loading ? 'cursor-not-allowed bg-gray-100 dark:bg-gray-800' : 'bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800'}`} flex"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div
|
<div
|
||||||
class="flex gap-0.5 md:gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-normal rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
|
class="flex gap-0.5 md:gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-normal rounded-full bg-transparent pt-1 touch-auto pointer-events-auto"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
draggable="false"
|
draggable="false"
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
import { WEBUI_NAME, mobile, showSidebar, user, config } from '$lib/stores';
|
import { WEBUI_NAME, mobile, showSidebar, user, config } from '$lib/stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createAutomation,
|
||||||
getAutomationItems,
|
getAutomationItems,
|
||||||
toggleAutomationById,
|
toggleAutomationById,
|
||||||
runAutomationById,
|
runAutomationById,
|
||||||
deleteAutomationById,
|
deleteAutomationById,
|
||||||
|
type AutomationForm,
|
||||||
type AutomationResponse
|
type AutomationResponse
|
||||||
} from '$lib/apis/automations';
|
} from '$lib/apis/automations';
|
||||||
|
import fileSaver from 'file-saver';
|
||||||
|
|
||||||
import AutomationModal from '$lib/components/AutomationModal.svelte';
|
import AutomationModal from '$lib/components/AutomationModal.svelte';
|
||||||
import AutomationMenu from '$lib/components/automations/AutomationMenu.svelte';
|
import AutomationMenu from '$lib/components/automations/AutomationMenu.svelte';
|
||||||
@@ -19,7 +22,7 @@
|
|||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import SplitCreateButton from '$lib/components/common/SplitCreateButton.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import SidebarIcon from '$lib/components/icons/Sidebar.svelte';
|
import SidebarIcon from '$lib/components/icons/Sidebar.svelte';
|
||||||
import Search from '$lib/components/icons/Search.svelte';
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
@@ -33,6 +36,8 @@
|
|||||||
import CheckCircle from '$lib/components/icons/CheckCircle.svelte';
|
import CheckCircle from '$lib/components/icons/CheckCircle.svelte';
|
||||||
import Minus from '$lib/components/icons/Minus.svelte';
|
import Minus from '$lib/components/icons/Minus.svelte';
|
||||||
|
|
||||||
|
const { saveAs } = fileSaver;
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
@@ -51,6 +56,8 @@
|
|||||||
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
let importFiles: FileList | null = null;
|
||||||
|
let automationsImportInputElement: HTMLInputElement;
|
||||||
|
|
||||||
const handleSearchInput = () => {
|
const handleSearchInput = () => {
|
||||||
if (!loaded) return;
|
if (!loaded) return;
|
||||||
@@ -157,6 +164,69 @@
|
|||||||
showCreateModal = true;
|
showCreateModal = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllAutomations = async () => {
|
||||||
|
let currentPage = 1;
|
||||||
|
let allAutomations: AutomationResponse[] = [];
|
||||||
|
let totalAutomations = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const res = await getAutomationItems(localStorage.token, null, 'all', currentPage);
|
||||||
|
const pageItems = res?.items ?? [];
|
||||||
|
totalAutomations = res?.total ?? pageItems.length;
|
||||||
|
allAutomations = [...allAutomations, ...pageItems];
|
||||||
|
currentPage += 1;
|
||||||
|
|
||||||
|
if (pageItems.length === 0) break;
|
||||||
|
} while (allAutomations.length < totalAutomations);
|
||||||
|
|
||||||
|
return allAutomations;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toAutomationForm = (automation: AutomationResponse): AutomationForm => ({
|
||||||
|
name: automation.name,
|
||||||
|
data: automation.data,
|
||||||
|
meta: automation.meta ?? undefined,
|
||||||
|
is_active: automation.is_active
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportAutomations = async () => {
|
||||||
|
const allAutomations = await getAllAutomations().catch((err) => {
|
||||||
|
toast.error(`${err}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allAutomations) return;
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(allAutomations.map(toAutomationForm), null, 2)], {
|
||||||
|
type: 'application/json'
|
||||||
|
});
|
||||||
|
saveAs(blob, `automations-export-${Date.now()}.json`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const importAutomations = async (file: File) => {
|
||||||
|
const text = await file.text();
|
||||||
|
const savedAutomations = JSON.parse(text);
|
||||||
|
const automationItems = Array.isArray(savedAutomations)
|
||||||
|
? savedAutomations
|
||||||
|
: (savedAutomations?.automations ?? []);
|
||||||
|
|
||||||
|
if (!Array.isArray(automationItems)) {
|
||||||
|
throw new Error($i18n.t('Invalid JSON format'));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const automation of automationItems) {
|
||||||
|
await createAutomation(localStorage.token, {
|
||||||
|
name: automation.name,
|
||||||
|
data: automation.data,
|
||||||
|
meta: automation.meta ?? undefined,
|
||||||
|
is_active: automation.is_active ?? true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
page = 1;
|
||||||
|
await getAutomationList();
|
||||||
|
};
|
||||||
|
|
||||||
const formatRRule = (rrule: string): string => {
|
const formatRRule = (rrule: string): string => {
|
||||||
// Detect one-time schedule (ONCE)
|
// Detect one-time schedule (ONCE)
|
||||||
if (rrule.includes('COUNT=1')) {
|
if (rrule.includes('COUNT=1')) {
|
||||||
@@ -240,6 +310,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</DeleteConfirmDialog>
|
</DeleteConfirmDialog>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="automations-import-input"
|
||||||
|
bind:this={automationsImportInputElement}
|
||||||
|
bind:files={importFiles}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
hidden
|
||||||
|
on:change={async () => {
|
||||||
|
if (!importFiles || importFiles.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await importAutomations(importFiles[0]);
|
||||||
|
toast.success($i18n.t('Imported automations successfully'));
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
} finally {
|
||||||
|
importFiles = null;
|
||||||
|
automationsImportInputElement.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<AutomationModal
|
<AutomationModal
|
||||||
bind:show={showCreateModal}
|
bind:show={showCreateModal}
|
||||||
automation={null}
|
automation={null}
|
||||||
@@ -291,18 +383,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-1">
|
<div class="ml-auto flex items-center gap-1">
|
||||||
<button
|
<SplitCreateButton
|
||||||
class="ml-1 px-2 py-1.5 text-xs gap-1 rounded-xl bg-black text-white dark:bg-white dark:text-black transition flex items-center"
|
actions={[
|
||||||
on:click={() => {
|
{
|
||||||
cloneFrom = null;
|
id: 'automations-new',
|
||||||
showCreateModal = true;
|
label: $i18n.t('Create'),
|
||||||
}}
|
onClick: () => {
|
||||||
>
|
cloneFrom = null;
|
||||||
<Plus className="size-3" strokeWidth="2.5" />
|
showCreateModal = true;
|
||||||
<div class="hidden md:block md:ml-1 text-xs">
|
}
|
||||||
{$i18n.t('New Automation')}
|
},
|
||||||
</div>
|
{
|
||||||
</button>
|
id: 'automations-import',
|
||||||
|
label: $i18n.t('Import JSON'),
|
||||||
|
onClick: () => automationsImportInputElement?.click()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'automations-export',
|
||||||
|
label: $i18n.t('Export JSON'),
|
||||||
|
onClick: exportAutomations
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -417,11 +519,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if automations === null || loading}
|
{#if automations === null || loading}
|
||||||
<div class="w-full h-full flex justify-center items-center my-16 mb-24">
|
<div class="flex min-h-[calc(100dvh-13rem)] w-full items-center justify-center">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{:else if (automations ?? []).length === 0}
|
{:else if (automations ?? []).length === 0}
|
||||||
<div class="w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
<div
|
||||||
|
class="flex min-h-[calc(100dvh-13rem)] w-full flex-col items-center justify-center"
|
||||||
|
>
|
||||||
<div class="max-w-md text-center">
|
<div class="max-w-md text-center">
|
||||||
<div class="text-3xl mb-3">⚡</div>
|
<div class="text-3xl mb-3">⚡</div>
|
||||||
<div class="text-lg font-normal mb-1">
|
<div class="text-lg font-normal mb-1">
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
import CalendarEventModal from '$lib/components/calendar/CalendarEventModal.svelte';
|
import CalendarEventModal from '$lib/components/calendar/CalendarEventModal.svelte';
|
||||||
import CreateCalendarModal from '$lib/components/calendar/CreateCalendarModal.svelte';
|
import CreateCalendarModal from '$lib/components/calendar/CreateCalendarModal.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import SidebarIcon from '$lib/components/icons/Sidebar.svelte';
|
import SidebarIcon from '$lib/components/icons/Sidebar.svelte';
|
||||||
import Select from '$lib/components/common/Select.svelte';
|
import Select from '$lib/components/common/Select.svelte';
|
||||||
@@ -331,24 +330,10 @@
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="ml-1 px-2 py-1.5 text-xs gap-1 rounded-xl bg-black text-white dark:bg-white dark:text-black transition text-sm flex items-center"
|
class="ml-1 rounded-lg bg-gray-50 px-2.5 py-1 text-xs text-gray-900 transition ring-1 ring-gray-200 hover:bg-gray-100 dark:bg-gray-850 dark:text-gray-100 dark:ring-gray-800 dark:hover:bg-gray-800"
|
||||||
on:click={handleNewEvent}
|
on:click={handleNewEvent}
|
||||||
>
|
>
|
||||||
<svg
|
{$i18n.t('Create')}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-3"
|
|
||||||
><path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M12 4.5v15m7.5-7.5h-15"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
|
|
||||||
<span class="hidden sm:inline">{$i18n.t('New Event')}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,23 +19,15 @@
|
|||||||
import { getSkillItems } from '$lib/apis/skills';
|
import { getSkillItems } from '$lib/apis/skills';
|
||||||
import { getToolList } from '$lib/apis/tools';
|
import { getToolList } from '$lib/apis/tools';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
|
||||||
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
|
||||||
import Sidebar from '$lib/components/icons/Sidebar.svelte';
|
import Sidebar from '$lib/components/icons/Sidebar.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import SplitCreateButton from '$lib/components/common/SplitCreateButton.svelte';
|
||||||
import DocumentArrowDown from '$lib/components/icons/DocumentArrowDown.svelte';
|
|
||||||
import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
|
|
||||||
import Link from '$lib/components/icons/Link.svelte';
|
|
||||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
|
||||||
|
|
||||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
let lastPath = '';
|
let lastPath = '';
|
||||||
let activeWorkspaceSection = '';
|
let activeWorkspaceSection = '';
|
||||||
let showCreateMenu = false;
|
|
||||||
let visibleActions = [];
|
let visibleActions = [];
|
||||||
let primaryCreateAction = null;
|
|
||||||
|
|
||||||
$: if ($page.url.pathname !== lastPath) {
|
$: if ($page.url.pathname !== lastPath) {
|
||||||
lastPath = $page.url.pathname;
|
lastPath = $page.url.pathname;
|
||||||
@@ -48,28 +40,9 @@
|
|||||||
|
|
||||||
$: activeWorkspaceSection = $page.url.pathname.split('/')[2] ?? '';
|
$: activeWorkspaceSection = $page.url.pathname.split('/')[2] ?? '';
|
||||||
$: visibleActions = $workspaceActions.filter((action) => action.visible ?? true);
|
$: visibleActions = $workspaceActions.filter((action) => action.visible ?? true);
|
||||||
$: primaryCreateAction =
|
|
||||||
visibleActions.find((action) => action.id.endsWith('-new')) ?? visibleActions[0] ?? null;
|
|
||||||
|
|
||||||
const getCount = (res: any) => res?.total ?? (Array.isArray(res) ? res.length : null);
|
const getCount = (res: any) => res?.total ?? (Array.isArray(res) ? res.length : null);
|
||||||
|
|
||||||
const runWorkspaceAction = async (action) => {
|
|
||||||
if (!action) return;
|
|
||||||
if (action.href) {
|
|
||||||
await goto(action.href);
|
|
||||||
} else {
|
|
||||||
await action.onClick?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getActionIcon = (id: string) => {
|
|
||||||
if (id.endsWith('-new')) return Pencil;
|
|
||||||
if (id.endsWith('-import-link')) return Link;
|
|
||||||
if (id.endsWith('-import')) return DocumentArrowUp;
|
|
||||||
if (id.endsWith('-export')) return DocumentArrowDown;
|
|
||||||
return Pencil;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadWorkspaceCounts = async () => {
|
const loadWorkspaceCounts = async () => {
|
||||||
const canViewModels = $user?.role === 'admin' || $user?.permissions?.workspace?.models;
|
const canViewModels = $user?.role === 'admin' || $user?.permissions?.workspace?.models;
|
||||||
const canViewKnowledge = $user?.role === 'admin' || $user?.permissions?.workspace?.knowledge;
|
const canViewKnowledge = $user?.role === 'admin' || $user?.permissions?.workspace?.knowledge;
|
||||||
@@ -168,7 +141,7 @@
|
|||||||
|
|
||||||
<div class="flex w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div
|
<div
|
||||||
class="flex min-w-0 items-center gap-0.5 md:gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-normal rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
|
class="flex min-w-0 mr-1.5 items-center gap-0.5 md:gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-normal rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
|
||||||
>
|
>
|
||||||
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models}
|
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models}
|
||||||
<a
|
<a
|
||||||
@@ -257,58 +230,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex shrink-0 items-center gap-1">
|
<div class="ml-auto flex shrink-0 items-center gap-1">
|
||||||
{#if visibleActions.length}
|
<SplitCreateButton actions={visibleActions} />
|
||||||
<div
|
|
||||||
class="ml-1 flex overflow-hidden rounded-lg bg-gray-50 text-xs text-gray-900 transition ring-1 ring-gray-200 dark:bg-gray-850 dark:text-gray-100 dark:ring-gray-800"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="px-2.5 py-1 transition hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
||||||
on:click={() => {
|
|
||||||
runWorkspaceAction(primaryCreateAction);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$i18n.t('Create')}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Dropdown bind:show={showCreateMenu} align="end" sideOffset={6}>
|
|
||||||
<button
|
|
||||||
class="flex items-center border-l border-gray-200 px-1.5 py-1 transition hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-800"
|
|
||||||
aria-label={$i18n.t('Open create menu')}
|
|
||||||
>
|
|
||||||
<ChevronDown className="size-2.5" strokeWidth="2.5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div slot="content">
|
|
||||||
<DropdownMenu className="min-w-[170px]">
|
|
||||||
{#each visibleActions as action (action.id)}
|
|
||||||
{@const Icon = getActionIcon(action.id)}
|
|
||||||
{#if action.href}
|
|
||||||
<a
|
|
||||||
href={action.href}
|
|
||||||
on:click={() => {
|
|
||||||
showCreateMenu = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon className="size-3.5" />
|
|
||||||
<span class="self-center truncate">{action.label}</span>
|
|
||||||
</a>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
on:click={async () => {
|
|
||||||
await action.onClick?.();
|
|
||||||
showCreateMenu = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon className="size-3.5" />
|
|
||||||
<span class="self-center truncate">{action.label}</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user