mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-17 04:34:13 +00:00
refac
This commit is contained in:
parent
b0ba137551
commit
fe0e725997
11 changed files with 62 additions and 33 deletions
|
|
@ -176,6 +176,8 @@ async def export_skills(
|
|||
raise HTTPException(403, 'Export permission required')
|
||||
if version_id and (not ids or len(ids) != 1):
|
||||
raise HTTPException(400, 'Select one skill to export a historical version')
|
||||
if format == 'skill' and (not ids or len(ids) != 1):
|
||||
raise HTTPException(400, 'Select one skill to export as .skill')
|
||||
skills = (
|
||||
[await authorized_skill(id, user, db=db) for id in ids]
|
||||
if ids is not None
|
||||
|
|
@ -195,14 +197,15 @@ async def export_skills(
|
|||
}
|
||||
)
|
||||
try:
|
||||
if format == 'zip':
|
||||
if format in ('zip', 'skill'):
|
||||
filename = f'{skills[0].id}.skill' if format == 'skill' else 'skills.zip'
|
||||
return Response(
|
||||
zip_export(packages),
|
||||
media_type='application/zip',
|
||||
headers={'Content-Disposition': 'attachment; filename="skills.zip"'},
|
||||
headers={'Content-Disposition': f'attachment; filename="{filename}"'},
|
||||
)
|
||||
if format != 'json':
|
||||
raise ValueError('Export format must be json or zip')
|
||||
raise ValueError('Export format must be json, zip, or skill')
|
||||
return packages[0] if ids and len(ids) == 1 else packages
|
||||
except ValueError as error:
|
||||
raise HTTPException(400, str(error))
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ def parse_import(
|
|||
if filename.lower().endswith('.json'):
|
||||
value = json.loads(data)
|
||||
packages = value if isinstance(value, list) else [value]
|
||||
elif filename.lower().endswith('.zip'):
|
||||
elif filename.lower().endswith(('.zip', '.skill')):
|
||||
entries = {}
|
||||
links = []
|
||||
total = 0
|
||||
|
|
@ -388,7 +388,12 @@ def parse_import(
|
|||
total += info.file_size
|
||||
if info.file_size > MAX_FILE_BYTES or total > MAX_IMPORT_BYTES or len(entries) >= 10000:
|
||||
raise ValueError('Archive exceeds import limits')
|
||||
entries[path] = archive.read(info)
|
||||
if info.flag_bits & 1:
|
||||
raise ValueError('Encrypted skill archives are not supported')
|
||||
try:
|
||||
entries[path] = archive.read(info)
|
||||
except NotImplementedError as error:
|
||||
raise ValueError('Skill archive uses an unsupported compression method') from error
|
||||
roots = (
|
||||
[''] if 'SKILL.md' in entries else sorted(p[: -len('SKILL.md')] for p in entries if p.endswith('/SKILL.md'))
|
||||
)
|
||||
|
|
@ -406,7 +411,7 @@ def parse_import(
|
|||
elif filename.lower().endswith('.md'):
|
||||
packages = [{'files': [encode_file('SKILL.md', data)]}]
|
||||
else:
|
||||
raise ValueError('Use JSON, ZIP, or Markdown')
|
||||
raise ValueError('Use JSON, ZIP, .skill, or Markdown')
|
||||
total = 0
|
||||
result = []
|
||||
for package in packages:
|
||||
|
|
@ -500,7 +505,7 @@ async def load_skill_from_url(url: str) -> list[dict]:
|
|||
raise ValueError('Could not download the skill. Check that the URL is accessible.')
|
||||
content_type = response.content_type
|
||||
if content_type == 'text/html':
|
||||
raise ValueError('The URL returned a web page. Use a raw skill file or a ZIP download link.')
|
||||
raise ValueError('The URL returned a web page. Use a raw skill file or a ZIP/.skill download link.')
|
||||
data = bytearray()
|
||||
async for chunk in response.content.iter_chunked(64 * 1024):
|
||||
data.extend(chunk)
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ export const cloneSkill = async (
|
|||
).json();
|
||||
export const exportSkillBundle = async (
|
||||
token: string,
|
||||
format: 'json' | 'zip',
|
||||
format: 'json' | 'zip' | 'skill',
|
||||
ids: string[] = [],
|
||||
versionId?: string
|
||||
) => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount, onDestroy, tick } from 'svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import type { i18n as I18n } from 'i18next';
|
||||
import {
|
||||
terminalServers,
|
||||
settings,
|
||||
|
|
@ -62,7 +64,7 @@
|
|||
import PortPreview from './FileNav/PortPreview.svelte';
|
||||
import TerminalDock from './TerminalDock.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const i18n = getContext<Readable<I18n>>('i18n');
|
||||
|
||||
export let overlay = false;
|
||||
export let chatId: string | null = null;
|
||||
|
|
@ -1441,6 +1443,11 @@
|
|||
|
||||
// Click outside panel to clear selection
|
||||
const handleWindowClick = (e: MouseEvent) => {
|
||||
// A close click may reach the window after its modal has already been destroyed.
|
||||
if (
|
||||
e.composedPath().some((node) => node instanceof Element && node.classList.contains('modal'))
|
||||
)
|
||||
return;
|
||||
if (directoryMenu) directoryMenu = null;
|
||||
if (
|
||||
!comparison &&
|
||||
|
|
@ -1821,22 +1828,6 @@
|
|||
</Tooltip>
|
||||
</FileNavToolbar>
|
||||
|
||||
{#if pinnedComparison}
|
||||
<div class="flex shrink-0 items-center gap-2 px-3 py-1.5 text-xs text-gray-500">
|
||||
<Tooltip content={pinnedComparison.path}>
|
||||
<span class="block max-w-[16rem] truncate"
|
||||
>{$i18n.t('Comparing with {{name}}', { name: pinnedComparison.name })}</span
|
||||
>
|
||||
</Tooltip>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 hover:text-gray-900 dark:hover:text-white"
|
||||
aria-label={$i18n.t('Clear comparison selection')}
|
||||
on:click={() => (pinnedComparison = null)}><Icon name="xmark" size={12} /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !selectedFile}
|
||||
<div
|
||||
class="flex h-8 shrink-0 items-center gap-1.5 border-b border-gray-50 px-3 dark:border-gray-850/30"
|
||||
|
|
@ -1860,6 +1851,22 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if pinnedComparison}
|
||||
<div class="flex shrink-0 items-center gap-2 px-3 py-1.5 text-xs text-gray-500">
|
||||
<Tooltip content={pinnedComparison.path}>
|
||||
<span class="block max-w-[16rem] truncate"
|
||||
>{$i18n.t('Comparing with {{name}}', { name: pinnedComparison.name })}</span
|
||||
>
|
||||
</Tooltip>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 hover:text-gray-900 dark:hover:text-white"
|
||||
aria-label={$i18n.t('Clear comparison selection')}
|
||||
on:click={() => (pinnedComparison = null)}><Icon name="xmark" size={12} /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Bulk action bar -->
|
||||
{#if selectedCount > 0 && !isSearching}
|
||||
<BulkActionBar
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="shrink-0 text-gray-500 hover:text-gray-900 dark:hover:text-white"
|
||||
on:click={onClose}
|
||||
on:click|stopPropagation={onClose}
|
||||
aria-label={$i18n.t('Close')}><Icon name="xmark" size={16} /></button
|
||||
>
|
||||
</div>
|
||||
|
|
@ -245,7 +245,7 @@
|
|||
...new Set([...(result.original.notices ?? []), ...(result.revised.notices ?? [])])
|
||||
].join('\n')}
|
||||
>
|
||||
<span tabindex="0">{$i18n.t('Text only')}</span>
|
||||
<button type="button" class="cursor-help">{$i18n.t('Text only')}</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@
|
|||
type="button"
|
||||
class="shrink-0 text-gray-500 hover:text-gray-900 dark:hover:text-white"
|
||||
aria-label={$i18n.t('Close')}
|
||||
on:click={close}><XMark className="size-4" /></button
|
||||
on:click|stopPropagation={close}><XMark className="size-4" /></button
|
||||
>
|
||||
</header>
|
||||
<div class="grid grid-cols-2 gap-4 px-4 py-2">
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@
|
|||
type="button"
|
||||
class="comparison-close shrink-0 text-gray-500 hover:text-gray-900 dark:hover:text-white"
|
||||
aria-label={$i18n.t('Close')}
|
||||
on:click={requestClose}><XMark className="size-4" /></button
|
||||
on:click|stopPropagation={requestClose}><XMark className="size-4" /></button
|
||||
>
|
||||
</header>
|
||||
{#if error}<p class="px-4 py-2 text-red-600" role="alert">{error}</p>{/if}
|
||||
|
|
@ -643,7 +643,7 @@
|
|||
{applyDescription ??
|
||||
$i18n.t('Apply uses the proposed revision on the right. Save your draft afterward.')}
|
||||
</p>
|
||||
<button type="button" class="secondary" on:click={requestClose}
|
||||
<button type="button" class="secondary" on:click|stopPropagation={requestClose}
|
||||
>{$i18n.t('Cancel')}</button
|
||||
>
|
||||
<button type="button" class="primary" disabled={loading} on:click={apply}
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
const exportHandler = async (skill, format: 'json' | 'zip' = 'json') => {
|
||||
const exportHandler = async (skill, format: 'json' | 'zip' | 'skill' = 'json') => {
|
||||
try {
|
||||
saveAs(
|
||||
await exportSkillBundle(localStorage.token, format, [skill.id]),
|
||||
|
|
@ -367,7 +367,7 @@
|
|||
<input
|
||||
bind:this={importInputElement}
|
||||
type="file"
|
||||
accept=".md,.json,.zip"
|
||||
accept=".md,.json,.zip,.skill"
|
||||
multiple
|
||||
hidden
|
||||
on:change={() => {
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@
|
|||
loading = false;
|
||||
}
|
||||
};
|
||||
const exportVersion = async (format: 'json' | 'zip') => {
|
||||
const exportVersion = async (format: 'json' | 'zip' | 'skill') => {
|
||||
try {
|
||||
saveAs(
|
||||
await exportSkillBundle(localStorage.token, format, [id], versionId!),
|
||||
|
|
@ -569,6 +569,9 @@
|
|||
{/if}
|
||||
{/snippet}
|
||||
{#snippet exports()}
|
||||
<button type="button" on:click={() => exportVersion('skill')}
|
||||
><Download className="size-3.5" />{$i18n.t('Export .skill')}</button
|
||||
>
|
||||
<button type="button" on:click={() => exportVersion('json')}
|
||||
><Download className="size-3.5" />{$i18n.t('Export JSON')}</button
|
||||
>
|
||||
|
|
|
|||
|
|
@ -108,6 +108,16 @@
|
|||
</button>
|
||||
|
||||
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.skills_export}
|
||||
<button
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => {
|
||||
exportHandler('skill');
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<Download className="size-3.5" />
|
||||
<div class="flex items-center">{$i18n.t('Export .skill')}</div>
|
||||
</button>
|
||||
<button
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
on:click={() => {
|
||||
|
|
|
|||
|
|
@ -1072,6 +1072,7 @@
|
|||
"Explored": "",
|
||||
"Exploring": "",
|
||||
"Export": "",
|
||||
"Export .skill": "",
|
||||
"Export as CSV": "",
|
||||
"Export as JSON": "",
|
||||
"Export chat (.json)": "",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue