diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index 8e85b05dab..79525198a1 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -34,10 +34,11 @@ from open_webui.utils.skill_files import ( MAX_IMPORT_BYTES, file_bytes, file_summaries, + load_skill_from_url, parse_import, zip_export, ) -from pydantic import BaseModel +from pydantic import BaseModel, HttpUrl from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -794,6 +795,23 @@ async def require_import(user, db): raise HTTPException(403, 'Import permission required') +class LoadUrlForm(BaseModel): + url: HttpUrl + + +@router.post('/load/url') +async def load_skill_by_url( + form_data: LoadUrlForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await require_import(user, db) + try: + return await load_skill_from_url(str(form_data.url)) + except (ValueError, UnicodeError, zipfile.BadZipFile) as error: + raise HTTPException(400, str(error)) + + @router.post('/import/preview') async def preview_skill_import( files: list[UploadFile] = File(...), user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) diff --git a/backend/open_webui/utils/skill_files.py b/backend/open_webui/utils/skill_files.py index f03c1c1766..9ec201dfbc 100644 --- a/backend/open_webui/utils/skill_files.py +++ b/backend/open_webui/utils/skill_files.py @@ -1,5 +1,6 @@ """Portable skill snapshots. Paths are virtual; packages are never extracted to disk.""" +import asyncio import base64 import io import json @@ -8,7 +9,9 @@ import stat import zipfile from pathlib import PurePosixPath from typing import Literal +from urllib.parse import quote, unquote, urljoin, urlsplit +import aiohttp import yaml from pydantic import BaseModel, ConfigDict @@ -168,7 +171,9 @@ def file_summaries(files: list[dict]) -> list[dict]: return [{'path': f['path'], 'size': len(file_bytes(f)), 'encoding': f.get('encoding')} for f in files] -def parse_import(data: bytes, filename: str) -> list[dict]: +def parse_import( + data: bytes, filename: str, *, discover_skills: bool = False, directory: str | None = None +) -> list[dict]: if len(data) > MAX_IMPORT_BYTES: raise ValueError('Import exceeds 200 MiB') if filename.lower().endswith('.json'): @@ -185,6 +190,10 @@ def parse_import(data: bytes, filename: str) -> list[dict]: raise ValueError('Archives cannot contain links or special files') if info.is_dir(): continue + if directory is not None: + path = path.partition('/')[2] + if not path.startswith(directory): + continue if path in entries: raise ValueError(f'Duplicate archive path: {path}') total += info.file_size @@ -197,7 +206,7 @@ def parse_import(data: bytes, filename: str) -> list[dict]: if not roots: raise ValueError('Archive contains no SKILL.md') roots = [root for root in roots if not any(root != parent and root.startswith(parent) for parent in roots)] - if any(not any(p.startswith(root) for root in roots) for p in entries): + if not discover_skills and any(not any(p.startswith(root) for root in roots) for p in entries): raise ValueError('Archive contains files outside skill directories') packages = [ {'files': [encode_file(p[len(root) :], value) for p, value in entries.items() if p.startswith(root)]} @@ -249,3 +258,72 @@ def zip_export(packages: list[dict]) -> bytes: for file in package['files']: archive.writestr(root + '/' + validate_path(file['path']), file_bytes(file)) return output.getvalue() + + +def skill_import_source(url: str) -> tuple[str, str | None]: + parsed = urlsplit(url.strip()) + if parsed.scheme not in ('http', 'https') or not parsed.hostname or parsed.username or parsed.password: + raise ValueError('Use an HTTP(S) URL without embedded credentials') + if parsed.hostname.lower() != 'github.com': + return url.strip(), None + parts = parsed.path.strip('/').split('/') + if len(parts) < 2 or any(not re.fullmatch(r'[\w.-]+', part) or part in ('.', '..') for part in parts[:2]): + raise ValueError('Invalid GitHub repository URL') + owner, repo = parts[:2] + repo = repo.removesuffix('.git') + if not repo: + raise ValueError('Invalid GitHub repository URL') + ref, directory = 'HEAD', '' + if len(parts) > 2: + if parts[2] in ('archive', 'releases', 'raw'): + return url.strip(), None + if len(parts) < 4 or parts[2] not in ('tree', 'blob'): + raise ValueError('Use a GitHub repository, folder, or SKILL.md URL') + # shortcut: slash-containing refs must be URL-encoded; use a commit permalink otherwise. + ref = validate_path(unquote(parts[3])) + path = '/'.join(unquote(part) for part in parts[4:]) + if parts[2] == 'blob': + if not path or path.split('/')[-1] != 'SKILL.md': + raise ValueError('Select a SKILL.md file or a skill folder') + path = path.removesuffix('SKILL.md').rstrip('/') + directory = validate_path(path) + '/' if path else '' + return f'https://codeload.github.com/{owner}/{repo}/zip/{quote(ref, safe="")}', directory + + +async def load_skill_from_url(url: str) -> list[dict]: + from open_webui.retrieval.web.utils import get_ssrf_safe_session, validate_url + + url, directory = skill_import_source(url) + try: + async with asyncio.timeout(60), get_ssrf_safe_session(trust_env=False, store_cookies=False) as session: + for _ in range(6): + target = urlsplit(url) + if target.username or target.password: + raise ValueError('Use a URL without embedded credentials') + await asyncio.to_thread(validate_url, url) + async with session.get(url, allow_redirects=False) as response: + if response.status in (301, 302, 303, 307, 308): + url = urljoin(url, response.headers.get('Location', '')) + continue + if response.status != 200: + 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.') + data = bytearray() + async for chunk in response.content.iter_chunked(64 * 1024): + data.extend(chunk) + if len(data) > MAX_IMPORT_BYTES: + raise ValueError('Import exceeds 200 MiB') + filename = { + 'application/json': 'skills.json', + 'text/markdown': 'SKILL.md', + }.get(content_type, unquote(urlsplit(url).path)) + if data.startswith(b'PK'): + filename = 'skills.zip' + return await asyncio.to_thread( + parse_import, bytes(data), filename, discover_skills=True, directory=directory + ) + raise ValueError('Too many redirects while downloading the skill') + except (TimeoutError, aiohttp.ClientError) as error: + raise ValueError('Could not download the skill. Please try again.') from error diff --git a/src/lib/apis/skills/index.ts b/src/lib/apis/skills/index.ts index a5bea20233..d4ae83e064 100644 --- a/src/lib/apis/skills/index.ts +++ b/src/lib/apis/skills/index.ts @@ -416,6 +416,13 @@ export const importSkillBundles = async (token: string, files: File[], decisions await skillRequest(token, decisions ? '/import' : '/import/preview', { method: 'POST', body }) ).json(); }; +export const loadSkillByUrl = async (token: string, url: string) => + ( + await skillRequest(token, '/load/url', { + method: 'POST', + body: JSON.stringify({ url }) + }) + ).json(); export const skillError = (error: unknown): string => error instanceof Error ? error.message diff --git a/src/lib/components/ImportModal.svelte b/src/lib/components/ImportModal.svelte index e0192e165d..5f743d1b4d 100644 --- a/src/lib/components/ImportModal.svelte +++ b/src/lib/components/ImportModal.svelte @@ -15,6 +15,16 @@ export let loadUrlHandler: Function = () => {}; export let successMessage: string = ''; + export let transformResult = (res) => { + const func = { ...res, id: res.id || nameToId(res.name) }; + const frontmatter = extractFrontmatter(res.content); + if (frontmatter?.title) func.name = frontmatter.title; + func.meta = { + ...(func.meta ?? {}), + description: frontmatter?.description ?? func.name + }; + return func; + }; let loading = false; let url = ''; @@ -41,23 +51,10 @@ toast.success(successMessage); - let func = res; - func.id = func.id || nameToId(func.name); - - const frontmatter = extractFrontmatter(res.content); // Ensure frontmatter is extracted - - if (frontmatter?.title) { - func.name = frontmatter.title; - } - - func.meta = { - ...(func.meta ?? {}), - description: frontmatter?.description ?? func.name - }; - - onImport(func); + onImport(transformResult(res)); show = false; } + loading = false; }; diff --git a/src/lib/components/admin/Settings/Models/ModelDefaultsPanel.svelte b/src/lib/components/admin/Settings/Models/ModelDefaultsPanel.svelte index f7f8a51147..f0718c0347 100644 --- a/src/lib/components/admin/Settings/Models/ModelDefaultsPanel.svelte +++ b/src/lib/components/admin/Settings/Models/ModelDefaultsPanel.svelte @@ -13,8 +13,6 @@ import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte'; import Capabilities from '$lib/components/workspace/Models/Capabilities.svelte'; - import DefaultFeatures from '$lib/components/workspace/Models/DefaultFeatures.svelte'; - import BuiltinTools from '$lib/components/workspace/Models/BuiltinTools.svelte'; import LanguageModeSelect from '$lib/components/common/LanguageModeSelect.svelte'; import LocalizedPromptSuggestions from '$lib/components/workspace/Models/LocalizedPromptSuggestions.svelte'; @@ -43,9 +41,6 @@ ([_, value]) => value !== null && value !== '' && value !== undefined ); $: enabledCapabilities = Object.entries(defaultCapabilities ?? {}).filter(([_, value]) => value); - $: availableFeatures = enabledCapabilities - .filter(([key]) => ['web_search', 'code_interpreter', 'image_generation'].includes(key)) - .map(([key]) => key); $: translatedPromptLocales = Object.entries(promptSuggestionsI18n ?? {}) .filter(([_, value]) => Array.isArray(value?.suggestion_prompts)) .map(([locale]) => locale); @@ -204,19 +199,11 @@ on:click={updateDirty} on:change={updateDirty} > - - - {#if availableFeatures.length > 0} -
- -
- {/if} - - {#if defaultCapabilities.builtin_tools} -
- -
- {/if} + {/if} diff --git a/src/lib/components/chat/Settings/Advanced/CustomParams.svelte b/src/lib/components/chat/Settings/Advanced/CustomParams.svelte index a76b199be0..2c6a8b9d99 100644 --- a/src/lib/components/chat/Settings/Advanced/CustomParams.svelte +++ b/src/lib/components/chat/Settings/Advanced/CustomParams.svelte @@ -151,7 +151,7 @@ value = { ...value, '': '' }; }} > - {compact ? $i18n.t('Add parameter') : $i18n.t('Add Custom Parameter')} diff --git a/src/lib/components/common/Dropdown.svelte b/src/lib/components/common/Dropdown.svelte index 286c34a975..ef83b07f01 100644 --- a/src/lib/components/common/Dropdown.svelte +++ b/src/lib/components/common/Dropdown.svelte @@ -12,6 +12,9 @@ /** Alignment: 'start' | 'end' */ export let align = 'start'; + /** Optional positioning anchor within a larger clickable trigger. */ + export let anchorElement: HTMLElement | null = null; + /** Close when clicking outside */ export let closeOnOutsideClick = true; @@ -114,7 +117,7 @@ function positionContentDefault() { if (!triggerEl || !contentEl) return; - const rect = triggerEl.getBoundingClientRect(); + const rect = (anchorElement ?? triggerEl).getBoundingClientRect(); resolvedMaxHeight = maxHeight; contentEl.style.position = 'fixed'; @@ -163,7 +166,7 @@ function positionContentVisualViewport() { if (!triggerEl || !contentEl) return; - const rect = triggerEl.getBoundingClientRect(); + const rect = (anchorElement ?? triggerEl).getBoundingClientRect(); const viewport = visualViewportRect(); const viewportRight = viewport.left + viewport.width; const viewportBottom = viewport.top + viewport.height; diff --git a/src/lib/components/workspace/Models/ActionsSelector.svelte b/src/lib/components/workspace/Models/ActionsSelector.svelte index d7d94c5224..57296525d5 100644 --- a/src/lib/components/workspace/Models/ActionsSelector.svelte +++ b/src/lib/components/workspace/Models/ActionsSelector.svelte @@ -1,10 +1,7 @@ -{#if actions.length > 0} -
-
-
- {$i18n.t('Actions')} -
- - {#if selectableActions.length > 0} - ({ - ...action, - description: action.meta?.description - }))} - selectedIds={selectedActionIds} - placeholder={$i18n.t('Search actions')} - triggerLabel={$i18n.t('Select Action')} - emptyLabel={$i18n.t('No actions found')} - variant="dropdown" - on:select={(e) => { - toggleAction(e.detail); - }} - on:enableall={(e) => { - selectedActionIds = [ - ...new Set([...selectedActionIds, ...e.detail.map((action) => action.id)]) - ]; - }} - /> - {/if} -
- -
-
- {#each selectedActions as action, actionIdx (action.id)} -
-
- { - if (!action.is_global && e.detail === 'unchecked') { - selectedActionIds = selectedActionIds.filter((id) => id !== action.id); - } - }} - /> -
- -
- - {resolveLocalizedResource(action, $i18n.language)} - -
-
- {/each} - - {#if selectedActionIds.length > 0} - - {/if} -
-
- -
- {$i18n.t('To select actions here, add them to the "Functions" workspace first.')} -
-
-{/if} + { + selectedActionIds = selectedActionIds.includes(e.detail.id) + ? selectedActionIds.filter((id) => id !== e.detail.id) + : [...selectedActionIds, e.detail.id]; + }} + on:enableall={(e) => { + selectedActionIds = [...new Set([...selectedActionIds, ...e.detail.map((item) => item.id)])]; + }} + on:clear={() => (selectedActionIds = [])} +/> diff --git a/src/lib/components/workspace/Models/BuiltinTools.svelte b/src/lib/components/workspace/Models/BuiltinTools.svelte index e379d38946..1398cdff97 100644 --- a/src/lib/components/workspace/Models/BuiltinTools.svelte +++ b/src/lib/components/workspace/Models/BuiltinTools.svelte @@ -2,9 +2,8 @@ import { getContext } from 'svelte'; import type { Writable } from 'svelte/store'; import type { i18n as i18nType } from 'i18next'; - import Checkbox from '$lib/components/common/Checkbox.svelte'; - import Tooltip from '$lib/components/common/Tooltip.svelte'; - import { marked } from 'marked'; + import ModelSettingToggle from './ModelSettingToggle.svelte'; + import ModelSettingsSection from './ModelSettingsSection.svelte'; const i18n: Writable = getContext('i18n'); @@ -83,6 +82,8 @@ $: allTools = Object.keys(toolLabels ?? {}) as Array; export let builtinTools: Record = {}; + export let disabled = false; + $: enabledTools = allTools.filter((key) => builtinTools[key] !== false); const setBuiltinTool = (tool: keyof typeof toolLabels, checked: boolean) => { if (checked) { @@ -94,34 +95,27 @@ }; -
-
- {$i18n.t('settings.admin.models.builtinTools.title')} -
-
- {#each allTools as tool} -
- { - setBuiltinTool(tool, e.detail === 'checked'); - }} - /> - -
+ + + {enabledTools + .slice(0, 3) + .map((key) => toolLabels[key].label) + .join(', ') || $i18n.t('None')} + {#if enabledTools.length > 3}+{enabledTools.length - 3}{/if} + +
+ {#each allTools as key} + setBuiltinTool(key, e.detail)} + /> {/each}
-
+ diff --git a/src/lib/components/workspace/Models/Capabilities.svelte b/src/lib/components/workspace/Models/Capabilities.svelte index 9ca4410273..9af4368b0b 100644 --- a/src/lib/components/workspace/Models/Capabilities.svelte +++ b/src/lib/components/workspace/Models/Capabilities.svelte @@ -2,9 +2,9 @@ import { getContext } from 'svelte'; import type { Writable } from 'svelte/store'; import type { i18n as i18nType } from 'i18next'; - import Checkbox from '$lib/components/common/Checkbox.svelte'; - import Tooltip from '$lib/components/common/Tooltip.svelte'; - import { marked } from 'marked'; + import ModelSettingsSection from './ModelSettingsSection.svelte'; + import ModelSettingToggle from './ModelSettingToggle.svelte'; + import BuiltinTools from './BuiltinTools.svelte'; const i18n: Writable = getContext('i18n'); @@ -64,48 +64,78 @@ export let capabilities: Partial> = {}; - const setCapability = (capability: Capability, checked: boolean) => { - capabilities[capability] = checked; - capabilities = capabilities; + export let defaultFeatureIds: string[] = []; + export let builtinTools: Record = {}; + export let disabled = false; + const featureKeys: Capability[] = ['web_search', 'image_generation', 'code_interpreter']; + $: visibleCapabilities = (Object.keys(capabilityLabels) as Capability[]).filter( + (key) => key !== 'file_context' || capabilities.file_upload + ); + $: enabledCapabilityIds = visibleCapabilities.filter((key) => capabilities[key]); + $: availableFeatures = featureKeys.filter((key) => capabilities[key]); + $: enabledDefaultIds = availableFeatures.filter((key) => defaultFeatureIds.includes(key)); + const setDefault = (key: string, enabled: boolean) => { + defaultFeatureIds = enabled + ? [...new Set([...defaultFeatureIds, key])] + : defaultFeatureIds.filter((id) => id !== key); }; - - // Hide file_context when file_upload is disabled - $: visibleCapabilities = (Object.keys(capabilityLabels) as Capability[]).filter((cap) => { - if (cap === 'file_context' && !capabilities.file_upload) { - return false; - } - return true; - }); -
-
- {$i18n.t('settings.admin.models.capabilities.title')} -
-
- {#each visibleCapabilities as capability} -
- { - setCapability(capability, e.detail === 'checked'); - }} +
+ + + {enabledCapabilityIds + .slice(0, 3) + .map((key) => capabilityLabels[key].label) + .join(', ') || $i18n.t('None')} + {#if enabledCapabilityIds.length > 3}+{enabledCapabilityIds.length - 3}{/if} + +
+ {#each visibleCapabilities as key} + -
+
+ + {#if availableFeatures.length} + + + {enabledDefaultIds + .slice(0, 3) + .map((key) => capabilityLabels[key].label) + .join(', ') || $i18n.t('None')} - - {$i18n.t(capabilityLabels[capability].label)} - - + {#if enabledDefaultIds.length > 3}+{enabledDefaultIds.length - 3}{/if} + +

+ {$i18n.t('Start enabled in new chats')} +

+
+ {#each availableFeatures as key} + setDefault(key, e.detail)} + /> + {/each}
- {/each} -
+ + {/if} + + {#if capabilities.builtin_tools} + + {/if}
diff --git a/src/lib/components/workspace/Models/DefaultFeatures.svelte b/src/lib/components/workspace/Models/DefaultFeatures.svelte deleted file mode 100644 index f132c839d4..0000000000 --- a/src/lib/components/workspace/Models/DefaultFeatures.svelte +++ /dev/null @@ -1,75 +0,0 @@ - - -
-
- {$i18n.t('settings.admin.models.defaultFeatures.title')} -
-
- {#each availableFeatures as feature} -
- { - setFeature(feature, e.detail === 'checked'); - }} - /> - -
- {/each} -
-
diff --git a/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte b/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte deleted file mode 100644 index 409c980bfc..0000000000 --- a/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte +++ /dev/null @@ -1,107 +0,0 @@ - - -
-
-
- {$i18n.t('Default Filters')} -
- - {#if filters.length > 0} - ({ - ...filter, - description: filter.meta?.description - }))} - selectedIds={selectedFilterIds} - placeholder={$i18n.t('Search filters')} - triggerLabel={$i18n.t('Select Filter')} - emptyLabel={$i18n.t('No filters found')} - variant="dropdown" - on:select={(e) => { - toggleFilter(e.detail); - }} - on:enableall={(e) => { - selectedFilterIds = [ - ...new Set([...selectedFilterIds, ...e.detail.map((filter) => filter.id)]) - ]; - }} - /> - {/if} -
- -
- {#if filters.length > 0} -
- {#each selectedFilters as filter (filter.id)} - {@const isSelected = selectedFilterIds.includes(filter.id)} -
-
- { - if (e.detail === 'checked') { - if (!selectedFilterIds.includes(filter.id)) { - selectedFilterIds = [...selectedFilterIds, filter.id]; - } - } else { - selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id); - } - }} - /> -
- -
- - {filter.name} - -
-
- {/each} - - {#if selectedFilters.length > 0} - - {/if} -
- {/if} -
- -
- {$i18n.t('To select default filters here, enable toggleable filters for this model first.')} -
-
diff --git a/src/lib/components/workspace/Models/FiltersSelector.svelte b/src/lib/components/workspace/Models/FiltersSelector.svelte index 853d7fb243..b7d7920d6b 100644 --- a/src/lib/components/workspace/Models/FiltersSelector.svelte +++ b/src/lib/components/workspace/Models/FiltersSelector.svelte @@ -1,119 +1,42 @@ -{#if filters.length > 0} -
-
-
- {$i18n.t('Filters')} -
- - {#if selectableFilters.length > 0} - ({ - ...filter, - description: filter.meta?.description - }))} - selectedIds={selectedFilterIds} - placeholder={$i18n.t('Search filters')} - triggerLabel={$i18n.t('Select Filter')} - emptyLabel={$i18n.t('No filters found')} - variant="dropdown" - on:select={(e) => { - toggleFilter(e.detail); - }} - on:enableall={(e) => { - selectedFilterIds = [ - ...new Set([...selectedFilterIds, ...e.detail.map((filter) => filter.id)]) - ]; - }} - /> - {/if} -
- - -
-
- {#each selectedFilters as filter (filter.id)} - {@const isSelected = filter.is_global || selectedFilterIds.includes(filter.id)} -
-
- { - if (filter.is_global) return; - - if (e.detail === 'checked') { - if (!selectedFilterIds.includes(filter.id)) { - selectedFilterIds = [...selectedFilterIds, filter.id]; - } - } else { - selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id); - } - }} - /> -
- -
- - {resolveLocalizedResource(filter, $i18n.language)} - -
-
- {/each} - - {#if selectedFilterIds.length > 0} - - {/if} -
-
- -
- {$i18n.t('To select filters here, add them to the "Functions" workspace first.')} -
-
-{/if} + { + selectedFilterIds = selectedFilterIds.includes(e.detail.id) + ? selectedFilterIds.filter((id) => id !== e.detail.id) + : [...selectedFilterIds, e.detail.id]; + }} + on:enableall={(e) => { + selectedFilterIds = [...new Set([...selectedFilterIds, ...e.detail.map((item) => item.id)])]; + }} + on:clear={() => (selectedFilterIds = [])} +/> diff --git a/src/lib/components/workspace/Models/Knowledge.svelte b/src/lib/components/workspace/Models/Knowledge.svelte index ed51e515cd..ad1bea4a70 100644 --- a/src/lib/components/workspace/Models/Knowledge.svelte +++ b/src/lib/components/workspace/Models/Knowledge.svelte @@ -1,6 +1,6 @@ { - if (e.detail === false) { + onOpenChange={async (state) => { + if (disabled) { + show = false; + return; + } + if (state) { + await tick(); + inputElement?.focus(); + } else { onClose(); query = ''; handleSearchInput(); @@ -115,101 +129,71 @@ }} > - -
-
-
-
-
- -
- +
+
+ + +
+
+ {#if selectedItems.length} +
+ {$i18n.t('Selected')}
-
- -
- {#if items.length === 0} -
- {$i18n.t('No knowledge found')} -
- {:else} - {#each items as item, i} - {#if i === 0 || item?.type !== items[i - 1]?.type} -
- {#if item?.type === 'note'} - {$i18n.t('Notes')} - {:else if item?.type === 'collection'} - {$i18n.t('Collections')} - {:else if item?.type === 'file'} - {$i18n.t('Files')} - {/if} -
- {/if} - -
- + + {/if} + {#if items.length === 0 && (query || !selectedItems.length)} +
+ {$i18n.t('No knowledge found')} + {#if !query}

+ {$i18n.t('Upload files or add sources in the Knowledge workspace.')} +

{/if} +
+ {:else} + {#each items as item, i} + {#if i === 0 || item.type !== items[i - 1].type} +
+ {item.type === 'note' + ? $i18n.t('Notes') + : item.type === 'collection' + ? $i18n.t('Collections') + : $i18n.t('Files')}
- {/each} - {/if} -
+ {/if} + + {/each} + {/if} +
+
+
diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index cca734c781..004cf0e843 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -37,9 +37,7 @@ import AccessControl from '../common/AccessControl.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte'; - import DefaultFiltersSelector from './DefaultFiltersSelector.svelte'; - import DefaultFeatures from './DefaultFeatures.svelte'; - import BuiltinTools from './BuiltinTools.svelte'; + import ModelSettingsSection from './ModelSettingsSection.svelte'; import LanguageModeSelect from '$lib/components/common/LanguageModeSelect.svelte'; import LocalizedPromptSuggestions from './LocalizedPromptSuggestions.svelte'; import PromptSuggestions from './PromptSuggestions.svelte'; @@ -964,7 +962,7 @@
-
-
{#if editingLocale} @@ -1440,130 +1436,155 @@ {/if}
-
- +
+ +
+ + + + {#if ($functions ?? []).some((func) => func.type === 'filter')} + func.type === 'filter')} + disabled={readOnly} + /> + {/if} + {#if ($functions ?? []).some((func) => func.type === 'action')} + func.type === 'action')} + disabled={readOnly} + /> + {/if}
- -
- -
- -
- -
- - {#if ($functions ?? []).filter((func) => func.type === 'filter').length > 0 || ($functions ?? []).filter((func) => func.type === 'action').length > 0} -
- - {#if ($functions ?? []).filter((func) => func.type === 'filter').length > 0} -
- func.type === 'filter')} - /> -
- - {@const toggleableFilters = $functions.filter( - (func) => - func.type === 'filter' && - (filterIds.includes(func.id) || func?.is_global) && - func?.meta?.toggle - )} - - {#if toggleableFilters.length > 0} -
- + + + {#if $config?.audio?.realtime?.enabled} +
+
+ +
+
{/if} - {/if} - - {#if ($functions ?? []).filter((func) => func.type === 'action').length > 0} -
- func.type === 'action')} + {#if $config?.audio?.realtime?.enabled || voiceAvatar} + + {/if} +
+
+
+ {$i18n.t('TTS Voice')} +
+
+
- {/if} - {/if} - -
- -
- -
- - {#if Object.keys(capabilities).filter((key) => capabilities[key]).length > 0} - {@const availableFeatures = Object.entries(capabilities) - .filter( - ([key, value]) => - value && - ['web_search', 'code_interpreter', 'image_generation'].includes(key) - ) - .map(([key, value]) => key)} - - {#if availableFeatures.length > 0} -
- -
- {/if} - {/if} - - {#if capabilities.builtin_tools} -
- -
- {/if} - - {#if capabilities.terminal} -
+ + {#if capabilities.terminal} -
- {/if} + {/if} +
+
- {#if $config?.audio?.realtime?.enabled} -
-
- -
- +
+ + +
+ {#if showPreview} +
+