diff --git a/src/lib/components/workspace/Knowledge.svelte b/src/lib/components/workspace/Knowledge.svelte index b979c30966..700659c587 100644 --- a/src/lib/components/workspace/Knowledge.svelte +++ b/src/lib/components/workspace/Knowledge.svelte @@ -10,7 +10,7 @@ const i18n = getContext>('i18n'); - import { WEBUI_NAME, knowledge, user } from '$lib/stores'; + import { WEBUI_NAME, knowledge, user, workspaceActions } from '$lib/stores'; import { deleteKnowledgeById, searchKnowledgeBases, @@ -24,7 +24,6 @@ import ItemMenu from './Knowledge/ItemMenu.svelte'; import Badge from '../common/Badge.svelte'; import Search from '../icons/Search.svelte'; - import Plus from '../icons/Plus.svelte'; import Spinner from '../common/Spinner.svelte'; import Tooltip from '../common/Tooltip.svelte'; import XMark from '../icons/XMark.svelte'; @@ -63,6 +62,16 @@ let allItemsLoaded = false; let itemsLoading = false; + $: if (loaded) { + workspaceActions.set([ + { + id: 'knowledge-new', + label: $i18n.t('Create'), + href: '/workspace/knowledge/create' + } + ]); + } + const handleSearchInput = () => { clearTimeout(searchDebounceTimer); searchDebounceTimer = setTimeout(() => { @@ -188,31 +197,6 @@ }} /> -
-
-
-
- {$i18n.t('Knowledge')} -
- -
- {total} -
-
- - -
-
-
diff --git a/src/lib/components/workspace/Models.svelte b/src/lib/components/workspace/Models.svelte index 8f58910550..e2719f9695 100644 --- a/src/lib/components/workspace/Models.svelte +++ b/src/lib/components/workspace/Models.svelte @@ -11,7 +11,15 @@ import { goto } from '$app/navigation'; const i18n = getContext('i18n'); - import { WEBUI_NAME, config, mobile, models as _models, settings, user } from '$lib/stores'; + import { + WEBUI_NAME, + config, + mobile, + models as _models, + settings, + user, + workspaceActions + } from '$lib/stores'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { createNewModel, @@ -36,7 +44,6 @@ import Tooltip from '../common/Tooltip.svelte'; import GarbageBin from '../icons/GarbageBin.svelte'; import Search from '../icons/Search.svelte'; - import Plus from '../icons/Plus.svelte'; import Switch from '../common/Switch.svelte'; import Spinner from '../common/Spinner.svelte'; import XMark from '../icons/XMark.svelte'; @@ -78,6 +85,30 @@ let searchDebounceTimer; + $: if (loaded) { + workspaceActions.set([ + { + id: 'models-new', + label: $i18n.t('Create'), + href: '/workspace/models/create' + }, + { + id: 'models-import', + label: $i18n.t('Import JSON'), + onClick: () => modelsImportInputElement?.click(), + visible: $user?.role === 'admin' || $user?.permissions?.workspace?.models_import + }, + { + id: 'models-export', + label: $i18n.t('Export JSON'), + onClick: async () => { + await downloadModels(models); + }, + visible: $user?.role === 'admin' || $user?.permissions?.workspace?.models_export + } + ]); + } + $: if (loaded && page !== undefined && selectedTag !== undefined && viewOption !== undefined) { getModelList(); } @@ -374,113 +405,64 @@ }} /> -
- { - console.log(importFiles); + { + console.log(importFiles); - let reader = new FileReader(); - reader.onload = async (event) => { - let savedModels = []; - try { - savedModels = JSON.parse(event.target.result); - console.log(savedModels); - } catch (e) { - toast.error($i18n.t('Invalid JSON file')); - return; - } + let reader = new FileReader(); + reader.onload = async (event) => { + let savedModels = []; + try { + savedModels = JSON.parse(event.target.result); + console.log(savedModels); + } catch (e) { + toast.error($i18n.t('Invalid JSON file')); + return; + } - for (const model of savedModels) { - if (model?.info ?? false) { - if ($_models.find((m) => m.id === model.id)) { - await updateModelById(localStorage.token, model.id, model.info).catch((error) => { - toast.error(`${error}`); - return null; - }); - } else { - await createNewModel(localStorage.token, model.info).catch((error) => { - toast.error(`${error}`); - return null; - }); - } + for (const model of savedModels) { + if (model?.info ?? false) { + if ($_models.find((m) => m.id === model.id)) { + await updateModelById(localStorage.token, model.id, model.info).catch((error) => { + toast.error(`${error}`); + return null; + }); } else { - if (model?.id && model?.name) { - await createNewModel(localStorage.token, model).catch((error) => { - toast.error(`${error}`); - return null; - }); - } + await createNewModel(localStorage.token, model.info).catch((error) => { + toast.error(`${error}`); + return null; + }); + } + } else { + if (model?.id && model?.name) { + await createNewModel(localStorage.token, model).catch((error) => { + toast.error(`${error}`); + return null; + }); } } + } - await _models.set( - await getModels( - localStorage.token, - $config?.features?.enable_direct_connections && ($settings?.directConnections ?? null) - ) - ); + await _models.set( + await getModels( + localStorage.token, + $config?.features?.enable_direct_connections && ($settings?.directConnections ?? null) + ) + ); - page = 1; - getModelList(); - }; + page = 1; + getModelList(); + }; - reader.readAsText(importFiles[0]); - }} - /> -
-
-
- {$i18n.t('Models')} -
- -
- {total} -
-
- -
- {#if $user?.role === 'admin' || $user?.permissions?.workspace?.models_import} - - {/if} - - {#if total && ($user?.role === 'admin' || $user?.permissions?.workspace?.models_export)} - - {/if} - - - - - -
-
-
+ reader.readAsText(importFiles[0]); + }} + />
diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index 973945b1ee..b9b5ab017d 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -5,7 +5,7 @@ import { goto } from '$app/navigation'; import { onMount, getContext, tick, onDestroy } from 'svelte'; - import { WEBUI_NAME, config, user } from '$lib/stores'; + import { WEBUI_NAME, config, user, workspaceActions } from '$lib/stores'; import { createNewPrompt, @@ -22,7 +22,6 @@ import Check from '../icons/Check.svelte'; import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import Search from '../icons/Search.svelte'; - import Plus from '../icons/Plus.svelte'; import Spinner from '../common/Spinner.svelte'; import Tooltip from '../common/Tooltip.svelte'; import XMark from '../icons/XMark.svelte'; @@ -59,6 +58,33 @@ let page = 1; + $: if (loaded) { + workspaceActions.set([ + { + id: 'prompts-new', + label: $i18n.t('Create'), + href: '/workspace/prompts/create' + }, + { + id: 'prompts-import', + label: $i18n.t('Import JSON'), + onClick: () => promptsImportInputElement?.click(), + visible: $user?.role === 'admin' || $user?.permissions?.workspace?.prompts_import + }, + { + id: 'prompts-export', + label: $i18n.t('Export JSON'), + onClick: async () => { + let blob = new Blob([JSON.stringify(prompts)], { + type: 'application/json' + }); + saveAs(blob, `prompts-export-${Date.now()}.json`); + }, + visible: $user?.role === 'admin' || $user?.permissions?.workspace?.prompts_export + } + ]); + } + const handleSearchInput = () => { loading = true; clearTimeout(searchDebounceTimer); @@ -231,97 +257,45 @@
-
- { - console.log(importFiles); - if (!importFiles || importFiles.length === 0) return; + { + console.log(importFiles); + if (!importFiles || importFiles.length === 0) return; - const reader = new FileReader(); - reader.onload = async (event) => { - const savedPrompts = JSON.parse(event.target.result); - console.log(savedPrompts); + const reader = new FileReader(); + reader.onload = async (event) => { + const savedPrompts = JSON.parse(event.target.result); + console.log(savedPrompts); - try { - for (const prompt of savedPrompts) { - await createNewPrompt(localStorage.token, { - command: prompt.command, - name: prompt.name, - content: prompt.content - }).catch((error) => { - toast.error(typeof error === 'string' ? error : JSON.stringify(error)); - return null; - }); - } - - page = 1; - await getPromptList(); - } finally { - importFiles = null; - promptsImportInputElement.value = ''; + try { + for (const prompt of savedPrompts) { + await createNewPrompt(localStorage.token, { + command: prompt.command, + name: prompt.name, + content: prompt.content + }).catch((error) => { + toast.error(typeof error === 'string' ? error : JSON.stringify(error)); + return null; + }); } - }; - reader.readAsText(importFiles[0]); - }} - /> -
-
-
- {$i18n.t('Prompts')} -
+ page = 1; + await getPromptList(); + } finally { + importFiles = null; + promptsImportInputElement.value = ''; + } + }; -
- {total ?? ''} -
-
- -
- {#if $user?.role === 'admin' || $user?.permissions?.workspace?.prompts_import} - - {/if} - - {#if total && ($user?.role === 'admin' || $user?.permissions?.workspace?.prompts_export)} - - {/if} - - - - - -
-
-
+ reader.readAsText(importFiles[0]); + }} + />
diff --git a/src/lib/components/workspace/Skills.svelte b/src/lib/components/workspace/Skills.svelte index d14b83daea..9b198b6d0d 100644 --- a/src/lib/components/workspace/Skills.svelte +++ b/src/lib/components/workspace/Skills.svelte @@ -6,7 +6,7 @@ import { onMount, getContext, tick, onDestroy } from 'svelte'; const i18n = getContext('i18n'); - import { WEBUI_NAME, user, skills as _skills } from '$lib/stores'; + import { WEBUI_NAME, user, skills as _skills, workspaceActions } from '$lib/stores'; import { goto } from '$app/navigation'; import { getSkills, @@ -26,7 +26,6 @@ import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte'; import GarbageBin from '../icons/GarbageBin.svelte'; import Search from '../icons/Search.svelte'; - import Plus from '../icons/Plus.svelte'; import XMark from '../icons/XMark.svelte'; import Spinner from '../common/Spinner.svelte'; import ViewSelector from './common/ViewSelector.svelte'; @@ -55,6 +54,40 @@ let viewOption = ''; let page = 1; + $: if (loaded) { + workspaceActions.set([ + { + id: 'skills-new', + label: $i18n.t('Create'), + href: '/workspace/skills/create', + visible: $user?.role === 'admin' || $user?.permissions?.workspace?.skills + }, + { + id: 'skills-import', + label: $i18n.t('Import JSON'), + onClick: () => importInputElement?.click(), + visible: $user?.role === 'admin' || $user?.permissions?.workspace?.skills_import + }, + { + id: 'skills-export', + label: $i18n.t('Export JSON'), + onClick: async () => { + const _skills = await exportSkills(localStorage.token).catch((error) => { + toast.error(`${error}`); + return null; + }); + if (_skills) { + let blob = new Blob([JSON.stringify(_skills)], { + type: 'application/json' + }); + saveAs(blob, `skills-export-${Date.now()}.json`); + } + }, + visible: $user?.role === 'admin' || $user?.permissions?.workspace?.skills_export + } + ]); + } + const loadSkillItems = async () => { if (!loaded) return; @@ -184,133 +217,71 @@ {#if loaded} -
-
-
-
- {$i18n.t('Skills')} -
+ { + if (importFiles && importFiles.length > 0) { + const file = importFiles[0]; + const ext = file.name.split('.').pop()?.toLowerCase(); -
- {total ?? ''} -
-
+ if (ext === 'json') { + // JSON import: create skills via API + const reader = new FileReader(); + reader.onload = async (event) => { + try { + const content = event.target?.result; + if (typeof content !== 'string') return; -
- { - if (importFiles && importFiles.length > 0) { - const file = importFiles[0]; - const ext = file.name.split('.').pop()?.toLowerCase(); + const parsedSkills = JSON.parse(content); + const items = Array.isArray(parsedSkills) ? parsedSkills : [parsedSkills]; - if (ext === 'json') { - // JSON import: create skills via API - const reader = new FileReader(); - reader.onload = async (event) => { - try { - const content = event.target?.result; - if (typeof content !== 'string') return; - - const parsedSkills = JSON.parse(content); - const items = Array.isArray(parsedSkills) ? parsedSkills : [parsedSkills]; - - for (const skill of items) { - await createNewSkill(localStorage.token, skill).catch((error) => { - toast.error(`${error}`); - }); - } - - toast.success($i18n.t('Skill imported successfully')); - page = 1; - loadSkillItems(); - _skills.set(await getSkills(localStorage.token)); - } catch (e) { - toast.error($i18n.t('Invalid JSON file')); - } - }; - reader.readAsText(file); - } else { - // Markdown import: parse frontmatter and open in editor - const reader = new FileReader(); - reader.onload = (event) => { - const mdContent = event.target?.result; - if (typeof mdContent === 'string') { - const fm = parseFrontmatter(mdContent); - const fileName = file.name.replace(/\.md$/, ''); - const rawName = fm.name || fileName; - const displayName = formatSkillName(rawName); - sessionStorage.skill = JSON.stringify({ - name: displayName, - id: fm.name || '', - description: fm.description || '', - content: mdContent, - is_active: true, - access_grants: [] - }); - goto('/workspace/skills/create'); - } - }; - reader.readAsText(file); - } - - importInputElement.value = ''; - } - }} - /> - - {#if $user?.role === 'admin' || $user?.permissions?.workspace?.skills_import} - - {/if} - - {#if total && ($user?.role === 'admin' || $user?.permissions?.workspace?.skills_export)} - - {/if} - {#if $user?.role === 'admin' || $user?.permissions?.workspace?.skills} - - + toast.success($i18n.t('Skill imported successfully')); + page = 1; + loadSkillItems(); + _skills.set(await getSkills(localStorage.token)); + } catch (e) { + toast.error($i18n.t('Invalid JSON file')); + } + }; + reader.readAsText(file); + } else { + // Markdown import: parse frontmatter and open in editor + const reader = new FileReader(); + reader.onload = (event) => { + const mdContent = event.target?.result; + if (typeof mdContent === 'string') { + const fm = parseFrontmatter(mdContent); + const fileName = file.name.replace(/\.md$/, ''); + const rawName = fm.name || fileName; + const displayName = formatSkillName(rawName); + sessionStorage.skill = JSON.stringify({ + name: displayName, + id: fm.name || '', + description: fm.description || '', + content: mdContent, + is_active: true, + access_grants: [] + }); + goto('/workspace/skills/create'); + } + }; + reader.readAsText(file); + } - - - {/if} -
-
-
+ importInputElement.value = ''; + } + }} + />
diff --git a/src/lib/components/workspace/Tools.svelte b/src/lib/components/workspace/Tools.svelte index bd7b2cdc23..af794e1c43 100644 --- a/src/lib/components/workspace/Tools.svelte +++ b/src/lib/components/workspace/Tools.svelte @@ -6,7 +6,7 @@ import { onMount, getContext, tick, onDestroy } from 'svelte'; const i18n = getContext('i18n'); - import { WEBUI_NAME, config, tools as _tools, user } from '$lib/stores'; + import { WEBUI_NAME, config, tools as _tools, user, workspaceActions } from '$lib/stores'; import { goto } from '$app/navigation'; import { @@ -30,10 +30,8 @@ import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import GarbageBin from '../icons/GarbageBin.svelte'; import Search from '../icons/Search.svelte'; - import Plus from '../icons/Plus.svelte'; import Spinner from '../common/Spinner.svelte'; import XMark from '../icons/XMark.svelte'; - import AddToolMenu from './Tools/AddToolMenu.svelte'; import ImportModal from '../ImportModal.svelte'; import ViewSelector from './common/ViewSelector.svelte'; import CommunityDiscover from './common/CommunityDiscover.svelte'; @@ -63,6 +61,48 @@ let showImportModal = false; + $: if (loaded) { + workspaceActions.set([ + { + id: 'tools-new', + label: $i18n.t('Create'), + href: '/workspace/tools/create' + }, + { + id: 'tools-import-link', + label: $i18n.t('Import From Link'), + onClick: () => { + showImportModal = true; + }, + visible: $user?.role === 'admin' + }, + { + id: 'tools-import', + label: $i18n.t('Import JSON'), + onClick: () => toolsImportInputElement?.click(), + visible: $user?.role === 'admin' || $user?.permissions?.workspace?.tools_import + }, + { + id: 'tools-export', + label: $i18n.t('Export JSON'), + onClick: async () => { + const _tools = await exportTools(localStorage.token).catch((error) => { + toast.error(`${error}`); + return null; + }); + + if (_tools) { + let blob = new Blob([JSON.stringify(_tools)], { + type: 'application/json' + }); + saveAs(blob, `tools-export-${Date.now()}.json`); + } + }, + visible: $user?.role === 'admin' || $user?.permissions?.workspace?.tools_export + } + ]); + } + const handleSearchInput = () => { clearTimeout(searchDebounceTimer); searchDebounceTimer = setTimeout(() => { @@ -220,98 +260,18 @@ /> {#if loaded} -
- { - console.log(importFiles); - showConfirm = true; - }} - /> - -
-
-
- {$i18n.t('Tools')} -
- -
- {filteredItems.length} -
-
- -
- {#if $user?.role === 'admin' || $user?.permissions?.workspace?.tools_import} - - {/if} - - {#if tools.length && ($user?.role === 'admin' || $user?.permissions?.workspace?.tools_export)} - - {/if} - - {#if $user?.role === 'admin'} - { - goto('/workspace/tools/create'); - }} - importFromLinkHandler={() => { - showImportModal = true; - }} - > -
- - - -
-
- {:else} - - - - - {/if} -
-
-
+ { + console.log(importFiles); + showConfirm = true; + }} + />
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 76b43ff224..d535515356 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -72,6 +72,24 @@ export const tools = writable(null); export const skills = writable(null); export const functions = writable(null); +export type WorkspaceSection = 'models' | 'knowledge' | 'prompts' | 'skills' | 'tools'; +export type WorkspaceAction = { + id: string; + label: string; + href?: string; + onClick?: () => void | Promise; + visible?: boolean; +}; + +export const workspaceCounts: Writable> = writable({ + models: null, + knowledge: null, + prompts: null, + skills: null, + tools: null +}); +export const workspaceActions: Writable = writable([]); + export const toolServers = writable([]); export const terminalServers = writable([]); diff --git a/src/routes/(app)/workspace/+layout.svelte b/src/routes/(app)/workspace/+layout.svelte index 96719b3bf1..9d2e3f49d8 100644 --- a/src/routes/(app)/workspace/+layout.svelte +++ b/src/routes/(app)/workspace/+layout.svelte @@ -1,24 +1,106 @@