mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-08 03:08:02 +00:00
refac
This commit is contained in:
parent
7b6562e335
commit
858ab727d2
39 changed files with 1033 additions and 345 deletions
|
|
@ -2163,6 +2163,7 @@ else:
|
|||
|
||||
|
||||
class BannerModel(BaseModel):
|
||||
i18n: dict[str, dict[str, str]] | None = None
|
||||
id: str
|
||||
type: str
|
||||
title: str | None = None
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class Skill(Base):
|
|||
|
||||
|
||||
class SkillMeta(BaseModel):
|
||||
i18n: dict[str, dict[str, str]] | None = None
|
||||
tags: Optional[list[str]] = []
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class Tool(Base): # database table definition
|
|||
|
||||
|
||||
class ToolMeta(BaseModel):
|
||||
i18n: dict[str, dict[str, str]] | None = None
|
||||
description: str | None = None
|
||||
manifest: dict | None = {}
|
||||
has_user_valves: bool = False
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
|
@ -116,6 +117,9 @@
|
|||
(selectedType !== '' ? f.type === selectedType : true) &&
|
||||
(query === '' ||
|
||||
f.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
resolveLocalizedResource(f, $i18n.language)
|
||||
.toLowerCase()
|
||||
.includes(query.toLowerCase()) ||
|
||||
f.id.toLowerCase().includes(query.toLowerCase()) ||
|
||||
(f.user?.name || '').toLowerCase().includes(query.toLowerCase()) ||
|
||||
(f.user?.email || '').toLowerCase().includes(query.toLowerCase()) ||
|
||||
|
|
@ -522,7 +526,7 @@
|
|||
<div
|
||||
class="truncate text-[0.8125rem] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{func.name}
|
||||
{resolveLocalizedResource(func, $i18n.language)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -544,16 +548,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{#if func?.meta?.description}
|
||||
{#if resolveLocalizedResource(func, $i18n.language, 'description')}
|
||||
<Tooltip
|
||||
content={func?.meta?.description}
|
||||
content={resolveLocalizedResource(func, $i18n.language, 'description')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 truncate text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{func?.meta?.description}
|
||||
{resolveLocalizedResource(func, $i18n.language, 'description')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
|
@ -754,7 +758,9 @@
|
|||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-500 truncate">
|
||||
{$i18n.t('This will delete')} <span class=" font-normal">{selectedFunction.name}</span>.
|
||||
{$i18n.t('This will delete')}
|
||||
<span class=" font-normal">{resolveLocalizedResource(selectedFunction, $i18n.language)}</span
|
||||
>.
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
|
|
@ -763,6 +769,7 @@
|
|||
bind:show={showValvesModal}
|
||||
type="function"
|
||||
id={selectedFunction?.id ?? null}
|
||||
meta={selectedFunction?.meta}
|
||||
on:save={async () => {
|
||||
await tick();
|
||||
models.set(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@
|
|||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import LanguageModeSelect from '$lib/components/common/LanguageModeSelect.svelte';
|
||||
import LocalizedField from '$lib/components/common/LocalizedField.svelte';
|
||||
import PluginTranslations from '$lib/components/workspace/common/PluginTranslations.svelte';
|
||||
import { pruneEmptyLocaleEntries } from '$lib/utils/localizedContent';
|
||||
let locale = '';
|
||||
|
||||
let formElement = null;
|
||||
let loading = false;
|
||||
|
|
@ -22,6 +27,7 @@
|
|||
|
||||
export let id = '';
|
||||
export let name = '';
|
||||
/** @type {{description: string, i18n?: Record<string, Record<string, string>>, manifest?: {translations?: Record<string, Record<string, string>>}}} */
|
||||
export let meta = {
|
||||
description: ''
|
||||
};
|
||||
|
|
@ -320,12 +326,17 @@ class Pipe:
|
|||
`;
|
||||
|
||||
const saveHandler = async () => {
|
||||
if (!name.trim() || !meta.description?.trim()) {
|
||||
locale = '';
|
||||
toast.error($i18n.t('Name and description are required'));
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
try {
|
||||
await onSave({
|
||||
id,
|
||||
name,
|
||||
meta,
|
||||
meta: { ...meta, i18n: pruneEmptyLocaleEntries(meta.i18n) },
|
||||
content
|
||||
});
|
||||
} finally {
|
||||
|
|
@ -376,15 +387,14 @@ class Pipe:
|
|||
<span>{$i18n.t('Back')}</span>
|
||||
</button>
|
||||
|
||||
<div class="flex shrink-0 items-start gap-2 pb-2 px-1">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex shrink-0 flex-col gap-2 pb-2 px-1 sm:flex-row sm:items-start">
|
||||
<div class="min-w-0 w-full flex-1">
|
||||
<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
|
||||
<input
|
||||
class="w-full bg-transparent text-sm outline-hidden"
|
||||
type="text"
|
||||
<LocalizedField
|
||||
placeholder={$i18n.t('Function Name')}
|
||||
aria-label={$i18n.t('Function Name')}
|
||||
bind:value={name}
|
||||
bind:translations={meta.i18n}
|
||||
{locale}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
@ -417,12 +427,12 @@ class Pipe:
|
|||
content={$i18n.t('e.g. A filter to remove profanity from text')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden"
|
||||
type="text"
|
||||
<LocalizedField
|
||||
placeholder={$i18n.t('Function Description')}
|
||||
aria-label={$i18n.t('Function Description')}
|
||||
bind:value={meta.description}
|
||||
bind:translations={meta.i18n}
|
||||
{locale}
|
||||
field="description"
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
@ -430,6 +440,10 @@ class Pipe:
|
|||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<LanguageModeSelect
|
||||
bind:value={locale}
|
||||
translatedLocales={Object.keys(pruneEmptyLocaleEntries(meta.i18n))}
|
||||
/>
|
||||
{#if !edit}
|
||||
<select
|
||||
class="h-7 rounded-lg border border-gray-100 bg-transparent px-2 text-xs outline-hidden dark:border-gray-800"
|
||||
|
|
@ -444,32 +458,42 @@ class Pipe:
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-hidden rounded-lg">
|
||||
<CodeEditor
|
||||
bind:this={codeEditor}
|
||||
value={content}
|
||||
lang="python"
|
||||
{boilerplate}
|
||||
className="text-[0.6875rem]"
|
||||
onChange={(e) => {
|
||||
_content = e;
|
||||
if (!edit) {
|
||||
const fm = extractFrontmatter(e);
|
||||
if (fm.title && !name) {
|
||||
name = formatSkillName(fm.title);
|
||||
id = nameToId(fm.title);
|
||||
<div class="min-h-0 flex-1 overflow-hidden rounded-lg flex flex-col">
|
||||
{#if locale}
|
||||
<PluginTranslations
|
||||
id={edit ? id : ''}
|
||||
kind="function"
|
||||
{locale}
|
||||
bind:translations={meta.i18n}
|
||||
/>
|
||||
{/if}
|
||||
<div class={locale ? 'hidden' : 'h-full'}>
|
||||
<CodeEditor
|
||||
bind:this={codeEditor}
|
||||
value={content}
|
||||
lang="python"
|
||||
{boilerplate}
|
||||
className="text-[0.6875rem]"
|
||||
onChange={(e) => {
|
||||
_content = e;
|
||||
if (!edit) {
|
||||
const fm = extractFrontmatter(e);
|
||||
if (fm.title && !name) {
|
||||
name = formatSkillName(fm.title);
|
||||
id = nameToId(fm.title);
|
||||
}
|
||||
if (fm.description && !meta.description) {
|
||||
meta = { ...meta, description: fm.description };
|
||||
}
|
||||
}
|
||||
if (fm.description && !meta.description) {
|
||||
meta = { ...meta, description: fm.description };
|
||||
}}
|
||||
onSave={async () => {
|
||||
if (formElement) {
|
||||
formElement.requestSubmit();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onSave={async () => {
|
||||
if (formElement) {
|
||||
formElement.requestSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 py-2 text-xs text-gray-500">
|
||||
|
|
|
|||
|
|
@ -21,13 +21,14 @@
|
|||
let evaluationConfig = null;
|
||||
let showAddModel = false;
|
||||
|
||||
const submitHandler = async () => {
|
||||
evaluationConfig = await updateConfig(localStorage.token, evaluationConfig).catch((err) => {
|
||||
const submitHandler = async (nextConfig = evaluationConfig) => {
|
||||
const result = await updateConfig(localStorage.token, nextConfig).catch((err) => {
|
||||
toast.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (evaluationConfig) {
|
||||
if (result) {
|
||||
evaluationConfig = result;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
models.set(
|
||||
await getModels(
|
||||
|
|
@ -38,32 +39,23 @@
|
|||
)
|
||||
);
|
||||
}
|
||||
return !!result;
|
||||
};
|
||||
|
||||
const addModelHandler = async (model) => {
|
||||
evaluationConfig.EVALUATION_ARENA_MODELS.push(model);
|
||||
evaluationConfig.EVALUATION_ARENA_MODELS = [...evaluationConfig.EVALUATION_ARENA_MODELS];
|
||||
|
||||
await submitHandler();
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections ? ($settings?.directConnections ?? null) : null
|
||||
)
|
||||
);
|
||||
return submitHandler({
|
||||
...evaluationConfig,
|
||||
EVALUATION_ARENA_MODELS: [...evaluationConfig.EVALUATION_ARENA_MODELS, model]
|
||||
});
|
||||
};
|
||||
|
||||
const editModelHandler = async (model, modelIdx) => {
|
||||
evaluationConfig.EVALUATION_ARENA_MODELS[modelIdx] = model;
|
||||
evaluationConfig.EVALUATION_ARENA_MODELS = [...evaluationConfig.EVALUATION_ARENA_MODELS];
|
||||
|
||||
await submitHandler();
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections ? ($settings?.directConnections ?? null) : null
|
||||
return submitHandler({
|
||||
...evaluationConfig,
|
||||
EVALUATION_ARENA_MODELS: evaluationConfig.EVALUATION_ARENA_MODELS.map((item, index) =>
|
||||
index === modelIdx ? model : item
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteModelHandler = async (modelIdx) => {
|
||||
|
|
@ -90,12 +82,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<ArenaModelModal
|
||||
bind:show={showAddModel}
|
||||
on:submit={async (e) => {
|
||||
addModelHandler(e.detail);
|
||||
}}
|
||||
/>
|
||||
<ArenaModelModal bind:show={showAddModel} onSubmit={addModelHandler} />
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between text-sm"
|
||||
|
|
@ -146,9 +133,7 @@
|
|||
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
|
||||
<Model
|
||||
{model}
|
||||
on:edit={(e) => {
|
||||
editModelHandler(e.detail, index);
|
||||
}}
|
||||
onEdit={(model) => editModelHandler(model, index)}
|
||||
on:delete={(e) => {
|
||||
deleteModelHandler(index);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,19 @@
|
|||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import LanguageModeSelect from '$lib/components/common/LanguageModeSelect.svelte';
|
||||
import LocalizedField from '$lib/components/common/LocalizedField.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let edit = false;
|
||||
|
||||
export let model = null;
|
||||
export let onSubmit = async (_model) => true;
|
||||
|
||||
let name = '';
|
||||
let locale = '';
|
||||
let translations = {};
|
||||
let originalMeta = {};
|
||||
let id = '';
|
||||
|
||||
$: if (name) {
|
||||
|
|
@ -60,7 +66,7 @@
|
|||
selectedModelId = '';
|
||||
};
|
||||
|
||||
const submitHandler = () => {
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
|
||||
if (!name || !id) {
|
||||
|
|
@ -72,7 +78,6 @@
|
|||
if (!edit) {
|
||||
if ($models.find((model) => model.name === name)) {
|
||||
loading = false;
|
||||
name = '';
|
||||
toast.error($i18n.t('Model name already exists, please choose a different one'));
|
||||
return;
|
||||
}
|
||||
|
|
@ -82,6 +87,8 @@
|
|||
id: id,
|
||||
name: name,
|
||||
meta: {
|
||||
...originalMeta,
|
||||
i18n: translations,
|
||||
profile_image_url: profileImageUrl,
|
||||
description: description || null,
|
||||
model_ids: modelIds.length > 0 ? modelIds : null,
|
||||
|
|
@ -90,8 +97,14 @@
|
|||
}
|
||||
};
|
||||
|
||||
dispatch('submit', model);
|
||||
loading = false;
|
||||
try {
|
||||
if ((await onSubmit(model)) === false) return;
|
||||
} catch (error) {
|
||||
toast.error(String(error));
|
||||
return;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
show = false;
|
||||
|
||||
name = '';
|
||||
|
|
@ -106,6 +119,16 @@
|
|||
};
|
||||
|
||||
const initModel = () => {
|
||||
locale = '';
|
||||
originalMeta = structuredClone(model?.meta ?? {});
|
||||
translations = structuredClone(model?.meta?.i18n ?? {});
|
||||
if (!model) {
|
||||
name = '';
|
||||
id = '';
|
||||
description = '';
|
||||
modelIds = [];
|
||||
accessGrants = [];
|
||||
}
|
||||
if (model) {
|
||||
name = model.name;
|
||||
id = model.id;
|
||||
|
|
@ -163,6 +186,12 @@
|
|||
}}
|
||||
>
|
||||
<div class="px-1">
|
||||
<div class="flex justify-end mb-3">
|
||||
<LanguageModeSelect
|
||||
bind:value={locale}
|
||||
translatedLocales={Object.keys(translations)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center pb-3">
|
||||
<input
|
||||
bind:this={imageInputElement}
|
||||
|
|
@ -254,12 +283,12 @@
|
|||
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
<LocalizedField
|
||||
bind:value={name}
|
||||
bind:translations
|
||||
{locale}
|
||||
field="name"
|
||||
placeholder={$i18n.t('Model Name')}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -286,12 +315,12 @@
|
|||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Description')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
type="text"
|
||||
<LocalizedField
|
||||
bind:value={description}
|
||||
bind:translations
|
||||
{locale}
|
||||
field="description"
|
||||
placeholder={$i18n.t('Enter description')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
|
@ -7,6 +8,7 @@
|
|||
import ArenaModelModal from './ArenaModelModal.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
export let model;
|
||||
export let onEdit = async (_model) => true;
|
||||
|
||||
let showModel = false;
|
||||
</script>
|
||||
|
|
@ -15,9 +17,7 @@
|
|||
bind:show={showModel}
|
||||
edit={true}
|
||||
{model}
|
||||
on:submit={async (e) => {
|
||||
dispatch('edit', e.detail);
|
||||
}}
|
||||
onSubmit={onEdit}
|
||||
on:delete={async () => {
|
||||
dispatch('delete');
|
||||
}}
|
||||
|
|
@ -29,14 +29,14 @@
|
|||
<div class="flex gap-2.5 items-center">
|
||||
<img
|
||||
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}`}
|
||||
alt={model.name}
|
||||
alt={resolveLocalizedResource(model, $i18n.language)}
|
||||
class="size-8 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
|
||||
<div class="w-full flex flex-col">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class=" line-clamp-1">
|
||||
{model.name}
|
||||
{resolveLocalizedResource(model, $i18n.language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { getBanners, setBanners } from '$lib/apis/configs';
|
||||
import InterfaceSettings from '$lib/components/common/InterfaceSettings.svelte';
|
||||
import SettingsSelect from '$lib/components/common/SettingsSelect.svelte';
|
||||
import LanguageModeSelect from '$lib/components/common/LanguageModeSelect.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { WEBUI_BUILD_HASH, WEBUI_VERSION } from '$lib/constants';
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
let showUserUiDefaults = false;
|
||||
|
||||
let banners: Banner[] = [];
|
||||
let bannerLocale = '';
|
||||
const inputClass =
|
||||
'w-full h-7 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const textareaClass =
|
||||
|
|
@ -454,39 +456,40 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 flex w-full items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Banners')}</div>
|
||||
<div class="mt-1.5 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Create announcements shown to users in the app.')}
|
||||
</div>
|
||||
<div class="mb-1 flex min-h-7 w-full items-center justify-between gap-2">
|
||||
<div class="min-w-0 text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Banners')}</div>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
{#if banners.length > 0}
|
||||
<LanguageModeSelect bind:value={bannerLocale} className="w-fit" />
|
||||
{/if}
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Add banner')}
|
||||
on:click={() => {
|
||||
if (banners.length === 0 || banners[banners.length - 1]?.content !== '') {
|
||||
banners = [
|
||||
...banners,
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: '',
|
||||
title: '',
|
||||
content: '',
|
||||
dismissible: true,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Add banner')}
|
||||
on:click={() => {
|
||||
if (banners.length === 0 || banners[banners.length - 1]?.content !== '') {
|
||||
banners = [
|
||||
...banners,
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: '',
|
||||
title: '',
|
||||
content: '',
|
||||
dismissible: true,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Banners bind:banners />
|
||||
<div class="mb-2 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Create announcements shown to users in the app.')}
|
||||
</div>
|
||||
<Banners bind:banners locale={bannerLocale} />
|
||||
</div>
|
||||
</AdminSettingSection>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import LocalizedField from '$lib/components/common/LocalizedField.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
|
@ -10,11 +10,9 @@
|
|||
const i18n = getContext('i18n');
|
||||
|
||||
export let banners = [];
|
||||
export let locale = '';
|
||||
|
||||
let sortable = null;
|
||||
let bannerListElement = null;
|
||||
|
||||
const positionChangeHandler = () => {
|
||||
const positionChangeHandler = (bannerListElement: HTMLElement) => {
|
||||
const bannerIdOrder = Array.from(bannerListElement.children).map((child) =>
|
||||
child.id.replace('banner-item-', '')
|
||||
);
|
||||
|
|
@ -26,72 +24,76 @@
|
|||
});
|
||||
};
|
||||
|
||||
const classNames: Record<string, string> = {
|
||||
info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ',
|
||||
success: 'bg-green-500/20 text-green-700 dark:text-green-200',
|
||||
warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200',
|
||||
error: 'bg-red-500/20 text-red-700 dark:text-red-200'
|
||||
};
|
||||
|
||||
$: if (banners) {
|
||||
init();
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
if (sortable) {
|
||||
sortable.destroy();
|
||||
}
|
||||
|
||||
if (bannerListElement) {
|
||||
sortable = new Sortable(bannerListElement, {
|
||||
animation: 150,
|
||||
handle: '.item-handle',
|
||||
onUpdate: async (event) => {
|
||||
positionChangeHandler();
|
||||
}
|
||||
});
|
||||
}
|
||||
const init = (node: HTMLElement) => {
|
||||
const sortable = new Sortable(node, {
|
||||
animation: 150,
|
||||
handle: '.item-handle',
|
||||
onUpdate: () => positionChangeHandler(node)
|
||||
});
|
||||
return { destroy: () => sortable.destroy() };
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class=" flex flex-col gap-3 {banners?.length > 0 ? 'mt-2' : ''}" bind:this={bannerListElement}>
|
||||
<div class="flex flex-col gap-1.5" use:init>
|
||||
{#each banners as banner, bannerIdx (banner.id)}
|
||||
<div class=" flex justify-between items-start -ml-1" id="banner-item-{banner.id}">
|
||||
<EllipsisVertical className="size-4 cursor-move item-handle" />
|
||||
|
||||
<div class="flex flex-row flex-1 gap-2 items-start">
|
||||
<SettingsSelect bind:value={banner.type} required selectClassName="capitalize">
|
||||
<div
|
||||
class="flex items-start gap-1 rounded-lg border border-gray-100/40 bg-transparent px-2 py-1 transition focus-within:border-gray-300 dark:border-gray-850/50 dark:focus-within:border-gray-600"
|
||||
id="banner-item-{banner.id}"
|
||||
>
|
||||
<Tooltip content={$i18n.t('Reorder')}>
|
||||
<div
|
||||
class="item-handle flex h-6 w-3 shrink-0 cursor-move items-center justify-center text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<EllipsisVertical className="size-3.5" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-0.5 sm:flex-row sm:items-start sm:gap-2">
|
||||
<SettingsSelect
|
||||
bind:value={banner.type}
|
||||
required
|
||||
ariaLabel={$i18n.t('Type')}
|
||||
className="w-24 max-w-full shrink-0"
|
||||
selectClassName="!h-6 !border-transparent !bg-transparent !ps-1 capitalize dark:!bg-transparent"
|
||||
>
|
||||
<option value="" disabled hidden class="text-gray-900">{$i18n.t('Type')}</option>
|
||||
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
||||
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
||||
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||
</SettingsSelect>
|
||||
|
||||
<Textarea
|
||||
className="mr-2 text-xs w-full border border-gray-100/50 !bg-gray-50/40 outline-hidden dark:border-white/[0.04] dark:!bg-white/[0.03] dark:text-gray-300 resize-none"
|
||||
placeholder={$i18n.t('Content')}
|
||||
bind:value={banner.content}
|
||||
maxSize={100}
|
||||
/>
|
||||
|
||||
<div class="relative -left-2">
|
||||
<Tooltip content={$i18n.t('Remember Dismissal')} className="flex h-fit items-center">
|
||||
<Switch bind:state={banner.dismissible} />
|
||||
</Tooltip>
|
||||
<div class="w-full min-w-0 flex-1 sm:pt-0.5">
|
||||
<LocalizedField
|
||||
className="block min-h-5 max-h-40 w-full resize-none bg-transparent text-[0.8125rem] leading-5 text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-200 dark:placeholder:text-gray-700 [field-sizing:content]"
|
||||
placeholder={$i18n.t('Content')}
|
||||
bind:value={banner.content}
|
||||
bind:translations={banner.i18n}
|
||||
{locale}
|
||||
field="content"
|
||||
multiline
|
||||
rows={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="pr-3"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
banners.splice(bannerIdx, 1);
|
||||
banners = banners;
|
||||
}}
|
||||
>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
<div class="flex h-6 shrink-0 items-center px-1">
|
||||
<Switch
|
||||
bind:state={banner.dismissible}
|
||||
ariaLabel={$i18n.t('Remember Dismissal')}
|
||||
tooltip={$i18n.t('Remember Dismissal')}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="flex size-6 shrink-0 items-center justify-center text-gray-400 opacity-70 transition hover:text-gray-700 hover:opacity-100 dark:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Delete')}
|
||||
on:click={() => {
|
||||
banners.splice(bannerIdx, 1);
|
||||
banners = banners;
|
||||
}}
|
||||
>
|
||||
<XMark className="size-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
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/workspace/Models/LanguageModeSelect.svelte';
|
||||
import LanguageModeSelect from '$lib/components/common/LanguageModeSelect.svelte';
|
||||
import LocalizedPromptSuggestions from '$lib/components/workspace/Models/LocalizedPromptSuggestions.svelte';
|
||||
|
||||
export let initHandler = () => {};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { config, functions, models, settings, tools, user } from '$lib/stores';
|
||||
|
|
@ -181,7 +182,9 @@
|
|||
{#each $tools
|
||||
.filter((tool) => !tool?.id?.startsWith('server:'))
|
||||
.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')) as tool, toolIdx}
|
||||
<option value={tool.id} class="bg-gray-100 dark:bg-gray-800">{tool.name}</option>
|
||||
<option value={tool.id} class="bg-gray-100 dark:bg-gray-800"
|
||||
>{resolveLocalizedResource(tool, $i18n.language)}</option
|
||||
>
|
||||
{/each}
|
||||
{:else if tab === 'functions'}
|
||||
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
|
||||
|
|
@ -189,7 +192,9 @@
|
|||
>
|
||||
|
||||
{#each $functions.sort( (a, b) => (a.name ?? '').localeCompare(b.name ?? '') ) as func, funcIdx}
|
||||
<option value={func.id} class="bg-gray-100 dark:bg-gray-800">{func.name}</option>
|
||||
<option value={func.id} class="bg-gray-100 dark:bg-gray-800"
|
||||
>{resolveLocalizedResource(func, $i18n.language)}</option
|
||||
>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
|
|
@ -203,6 +208,10 @@
|
|||
<div class="chat-control-valves">
|
||||
<Valves
|
||||
{valvesSpec}
|
||||
meta={(tab === 'tools' ? $tools : $functions)?.find(
|
||||
(item) => item.id === selectedId
|
||||
)?.meta}
|
||||
userValves
|
||||
bind:valves
|
||||
on:change={() => {
|
||||
debounceSubmitHandler();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedFunction } from '$lib/utils/localizedContent';
|
||||
import { functions as localizedFunctions } from '$lib/stores';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
|
|
@ -2373,7 +2375,14 @@
|
|||
{#each selectedFilterIds as filterId (filterId)}
|
||||
{@const filter = toggleFilters.find((f) => f.id === filterId)}
|
||||
{#if filter}
|
||||
<Tooltip content={filter?.name} placement="top">
|
||||
<Tooltip
|
||||
content={resolveLocalizedFunction(
|
||||
filter,
|
||||
$localizedFunctions,
|
||||
$i18n.language
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
if (
|
||||
|
|
@ -2405,7 +2414,11 @@
|
|||
? 'dark:invert-[80%]'
|
||||
: ''}"
|
||||
style="fill: currentColor;"
|
||||
alt={filter.name}
|
||||
alt={resolveLocalizedFunction(
|
||||
filter,
|
||||
$localizedFunctions,
|
||||
$i18n.language
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { getContext, onDestroy } from 'svelte';
|
||||
import { getSkillItems } from '$lib/apis/skills';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
|
@ -60,8 +61,10 @@
|
|||
.replaceAll("'", ''');
|
||||
|
||||
const getTooltipContent = (skill) => {
|
||||
const name = escapeTooltipText(skill.name);
|
||||
const description = escapeTooltipText(skill.description);
|
||||
const name = escapeTooltipText(resolveLocalizedResource(skill, $i18n.language));
|
||||
const description = escapeTooltipText(
|
||||
resolveLocalizedResource(skill, $i18n.language, 'description')
|
||||
);
|
||||
|
||||
return `<div class="max-w-80 whitespace-normal text-left leading-snug">
|
||||
<span class="break-words font-normal">${name}</span>${description ? `: <span class="break-words opacity-80">${description}</span>` : ''}
|
||||
|
|
@ -100,7 +103,7 @@
|
|||
<Cube className="size-3.5" />
|
||||
</div>
|
||||
<div class="truncate min-w-0 flex-1">
|
||||
{skill.name}
|
||||
{resolveLocalizedResource(skill, $i18n.language)}
|
||||
</div>
|
||||
<div class="ml-2 max-w-24 shrink-0 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{skill.id}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { getContext, onDestroy } from 'svelte';
|
||||
import { getPrompts } from '$lib/apis/prompts';
|
||||
import { getSkillItems } from '$lib/apis/skills';
|
||||
|
|
@ -126,8 +127,10 @@
|
|||
.replaceAll("'", ''');
|
||||
|
||||
const getSkillTooltipContent = (skill) => {
|
||||
const name = escapeTooltipText(skill.name);
|
||||
const description = escapeTooltipText(skill.description);
|
||||
const name = escapeTooltipText(resolveLocalizedResource(skill, $i18n.language));
|
||||
const description = escapeTooltipText(
|
||||
resolveLocalizedResource(skill, $i18n.language, 'description')
|
||||
);
|
||||
|
||||
return `<div class="max-w-80 whitespace-normal text-left leading-snug">
|
||||
<span class="break-words font-normal">${name}</span>${description ? `: <span class="break-words opacity-80">${description}</span>` : ''}
|
||||
|
|
@ -441,7 +444,7 @@
|
|||
<Cube className="size-3.5" />
|
||||
</div>
|
||||
<div class="truncate min-w-0 flex-1">
|
||||
{skill.name}
|
||||
{resolveLocalizedResource(skill, $i18n.language)}
|
||||
</div>
|
||||
<div class="ml-2 max-w-24 shrink-0 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{skill.id}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource, resolveLocalizedFunction } from '$lib/utils/localizedContent';
|
||||
import { functions as localizedFunctions } from '$lib/stores';
|
||||
import { getContext, onDestroy, tick } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
|
|
@ -108,14 +110,22 @@
|
|||
|
||||
const setTools = (toolItems: IntegrationItem[] | null, query = '') => {
|
||||
const q = query.trim().toLowerCase();
|
||||
const items = (toolItems ?? []).reduce<Record<string, IntegrationItem>>((a, tool) => {
|
||||
a[tool.id] = {
|
||||
...tool,
|
||||
name: tool.name,
|
||||
description: tool.meta?.description
|
||||
};
|
||||
return a;
|
||||
}, {});
|
||||
const items = (toolItems ?? [])
|
||||
.filter(
|
||||
(tool) =>
|
||||
!q ||
|
||||
`${tool.name} ${resolveLocalizedResource(tool, $i18n.language)} ${resolveLocalizedResource(tool, $i18n.language, 'description')}`
|
||||
.toLowerCase()
|
||||
.includes(q)
|
||||
)
|
||||
.reduce<Record<string, IntegrationItem>>((a, tool) => {
|
||||
a[tool.id] = {
|
||||
...tool,
|
||||
name: tool.name,
|
||||
description: tool.meta?.description
|
||||
};
|
||||
return a;
|
||||
}, {});
|
||||
|
||||
for (const serverIdx in ($toolServers ?? []) as any[]) {
|
||||
const server = (($toolServers ?? []) as any[])[serverIdx];
|
||||
|
|
@ -142,7 +152,14 @@
|
|||
|
||||
const setSkills = (skillItems: IntegrationItem[] | null, query = '') => {
|
||||
skills = (skillItems ?? [])
|
||||
.filter((skill) => skill.is_active)
|
||||
.filter(
|
||||
(skill) =>
|
||||
skill.is_active &&
|
||||
(!query.trim() ||
|
||||
`${skill.name} ${resolveLocalizedResource(skill, $i18n.language)} ${resolveLocalizedResource(skill, $i18n.language, 'description')}`
|
||||
.toLowerCase()
|
||||
.includes(query.trim().toLowerCase()))
|
||||
)
|
||||
.reduce<Record<string, IntegrationItem>>((a, skill) => {
|
||||
a[skill.id] = {
|
||||
...skill,
|
||||
|
|
@ -162,13 +179,6 @@
|
|||
const q = query.trim();
|
||||
searchedToolQuery = query;
|
||||
|
||||
if (q) {
|
||||
const toolItems = await getTools(localStorage.token, q).catch(() => []);
|
||||
if (requestId !== toolRequestId) return;
|
||||
setTools(toolItems, q);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_tools === null) {
|
||||
await _tools.set(await getTools(localStorage.token));
|
||||
}
|
||||
|
|
@ -181,13 +191,6 @@
|
|||
const q = query.trim();
|
||||
searchedSkillQuery = query;
|
||||
|
||||
if (q) {
|
||||
const skillItems = await getSkills(localStorage.token, q).catch(() => []);
|
||||
if (requestId !== skillRequestId) return;
|
||||
setSkills(skillItems, q);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_skills === null) {
|
||||
await _skills.set(await getSkills(localStorage.token));
|
||||
}
|
||||
|
|
@ -328,7 +331,15 @@
|
|||
|
||||
{#if toggleFilters && toggleFilters.length > 0}
|
||||
{#each toggleFilters.sort( (a, b) => a.name.localeCompare( b.name, undefined, { sensitivity: 'base' } ) ) as filter, filterIdx (filter.id)}
|
||||
<Tooltip content={filter?.description} placement="top-start">
|
||||
<Tooltip
|
||||
content={resolveLocalizedFunction(
|
||||
filter,
|
||||
$localizedFunctions,
|
||||
$i18n.language,
|
||||
'description'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<button
|
||||
class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
||||
aria-pressed={selectedFilterIds.includes(filter.id)}
|
||||
|
|
@ -351,7 +362,11 @@
|
|||
? 'dark:invert-[80%]'
|
||||
: ''}"
|
||||
style="fill: currentColor;"
|
||||
alt={filter.name}
|
||||
alt={resolveLocalizedFunction(
|
||||
filter,
|
||||
$localizedFunctions,
|
||||
$i18n.language
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -359,7 +374,9 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" truncate">{filter?.name}</div>
|
||||
<div class=" truncate">
|
||||
{resolveLocalizedFunction(filter, $localizedFunctions, $i18n.language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -513,13 +530,29 @@
|
|||
{/if}
|
||||
<div class="flex-1 truncate">
|
||||
<div class="flex flex-1 gap-2 items-center">
|
||||
<Tooltip content={tools?.[toolId]?.name ?? ''} placement="top">
|
||||
<Tooltip
|
||||
content={resolveLocalizedResource(
|
||||
tools?.[toolId],
|
||||
$i18n.language,
|
||||
'name'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<div class="shrink-0">
|
||||
<Wrench />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip content={tools?.[toolId]?.description ?? ''} placement="top-start">
|
||||
<div class=" truncate">{tools?.[toolId]?.name}</div>
|
||||
<Tooltip
|
||||
content={resolveLocalizedResource(
|
||||
tools?.[toolId],
|
||||
$i18n.language,
|
||||
'description'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<div class=" truncate">
|
||||
{resolveLocalizedResource(tools?.[toolId], $i18n.language, 'name')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -623,16 +656,29 @@
|
|||
>
|
||||
<div class="flex-1 truncate">
|
||||
<div class="flex flex-1 gap-2 items-center">
|
||||
<Tooltip content={skills?.[skillId]?.name ?? ''} placement="top">
|
||||
<Tooltip
|
||||
content={resolveLocalizedResource(
|
||||
skills?.[skillId],
|
||||
$i18n.language,
|
||||
'name'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<div class="shrink-0">
|
||||
<Cube className="size-3.5" strokeWidth="1.75" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={skills?.[skillId]?.description ?? ''}
|
||||
content={resolveLocalizedResource(
|
||||
skills?.[skillId],
|
||||
$i18n.language,
|
||||
'description'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<div class=" truncate">{skills?.[skillId]?.name}</div>
|
||||
<div class=" truncate">
|
||||
{resolveLocalizedResource(skills?.[skillId], $i18n.language, 'name')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedFunction } from '$lib/utils/localizedContent';
|
||||
import { functions as localizedFunctions } from '$lib/stores';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||
|
|
@ -1469,10 +1471,21 @@
|
|||
{/if}
|
||||
|
||||
{#each model?.actions ?? [] as action}
|
||||
<Tooltip content={action.name} placement="bottom">
|
||||
<Tooltip
|
||||
content={resolveLocalizedFunction(
|
||||
action,
|
||||
$localizedFunctions,
|
||||
$i18n.language
|
||||
)}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={action.name}
|
||||
aria-label={resolveLocalizedFunction(
|
||||
action,
|
||||
$localizedFunctions,
|
||||
$i18n.language
|
||||
)}
|
||||
class="{isLastMessage || ($settings?.highContrastMode ?? false)
|
||||
? 'visible'
|
||||
: 'hover-reveal'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
|
||||
|
|
@ -1488,7 +1501,11 @@
|
|||
? 'dark:invert-[80%]'
|
||||
: ''}"
|
||||
style="fill: currentColor;"
|
||||
alt={action.name}
|
||||
alt={resolveLocalizedFunction(
|
||||
action,
|
||||
$localizedFunctions,
|
||||
$i18n.language
|
||||
)}
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { getContext } from 'svelte';
|
||||
import { skills } from '$lib/stores';
|
||||
|
||||
|
|
@ -42,12 +43,12 @@
|
|||
<Collapsible buttonClassName="w-full mb-0.5">
|
||||
<div class="truncate">
|
||||
<div class="text-sm font-normal dark:text-gray-100 text-gray-800 truncate">
|
||||
{skill?.name}
|
||||
{resolveLocalizedResource(skill, $i18n.language)}
|
||||
</div>
|
||||
|
||||
{#if skill?.description}
|
||||
{#if resolveLocalizedResource(skill, $i18n.language, 'description')}
|
||||
<div class="text-xs text-gray-500">
|
||||
{skill?.description}
|
||||
{resolveLocalizedResource(skill, $i18n.language, 'description')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { getContext } from 'svelte';
|
||||
import { toolServers, tools } from '$lib/stores';
|
||||
|
||||
|
|
@ -66,7 +67,7 @@
|
|||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1 min-w-0">
|
||||
<div class="text-sm font-normal dark:text-gray-100 text-gray-800 truncate">
|
||||
{tool?.name}
|
||||
{resolveLocalizedResource(tool, $i18n.language)}
|
||||
</div>
|
||||
{#if status}
|
||||
<span class="text-[0.6875rem] {status.pill} shrink-0">{status.label}</span>
|
||||
|
|
@ -83,9 +84,9 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if tool?.meta?.description}
|
||||
{#if resolveLocalizedResource(tool, $i18n.language, 'description')}
|
||||
<div class="text-xs text-gray-500 truncate">
|
||||
{tool?.meta?.description}
|
||||
{resolveLocalizedResource(tool, $i18n.language, 'description')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Banner } from '$lib/types';
|
||||
import { resolveLocalizedString } from '$lib/utils/localizedContent';
|
||||
import { onMount, createEventDispatcher, getContext } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
|
@ -100,7 +101,16 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-white max-h-60 overflow-y-auto">
|
||||
{@html DOMPurify.sanitize(marked.parse((banner?.content ?? '').replace(/\n/g, '<br>')))}
|
||||
{@html DOMPurify.sanitize(
|
||||
marked.parse(
|
||||
resolveLocalizedString(
|
||||
banner?.content,
|
||||
banner?.i18n,
|
||||
$i18n.language,
|
||||
'content'
|
||||
).replace(/\n/g, '<br>')
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,17 @@
|
|||
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import availableLanguages from '$lib/i18n/locales/languages.json';
|
||||
|
||||
const i18n = getContext<any>('i18n');
|
||||
|
||||
export let value = '';
|
||||
export let languages: { code: string; title: string }[] = [];
|
||||
export let languages: { code: string; title: string }[] = availableLanguages;
|
||||
export let translatedLocales: string[] = [];
|
||||
export let className = '';
|
||||
export let includeDefault = true;
|
||||
|
||||
$: options = languages.filter((language) => language.code !== 'en-US');
|
||||
$: options = languages;
|
||||
|
||||
const statusFor = (code: string) => (translatedLocales.includes(code) ? ' *' : '');
|
||||
</script>
|
||||
|
|
@ -29,7 +31,7 @@
|
|||
selectClassName="min-w-[8.5rem] !ps-8"
|
||||
ariaLabel={$i18n.t('Editing language')}
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
{#if includeDefault}<option value="">{$i18n.t('Default')}</option>{/if}
|
||||
{#each options as language}
|
||||
<option value={language.code}>{language.title}{statusFor(language.code)}</option>
|
||||
{/each}
|
||||
74
src/lib/components/common/LocalizedField.svelte
Normal file
74
src/lib/components/common/LocalizedField.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { resolveLocalizedString } from '$lib/utils/localizedContent';
|
||||
const i18n = getContext<any>('i18n');
|
||||
export let value = '';
|
||||
export let translations: Record<string, Record<string, string>> = {};
|
||||
export let locale = '';
|
||||
export let field = 'name';
|
||||
export let placeholder = '';
|
||||
export let className = 'w-full bg-transparent text-sm outline-hidden';
|
||||
export let multiline = false;
|
||||
export let rows = 3;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
$: translated = translations?.[locale]?.[field] ?? '';
|
||||
$: fallback = resolveLocalizedString(value, translations, locale, field);
|
||||
const update = (next: string) => {
|
||||
if (!locale) {
|
||||
value = next;
|
||||
return;
|
||||
}
|
||||
const entry = { ...translations?.[locale] };
|
||||
if (next.trim()) entry[field] = next;
|
||||
else delete entry[field];
|
||||
const nextTranslations = { ...translations };
|
||||
if (Object.keys(entry).length) nextTranslations[locale] = entry;
|
||||
else delete nextTranslations[locale];
|
||||
translations = nextTranslations;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
{#if multiline}
|
||||
<textarea
|
||||
class={className}
|
||||
{rows}
|
||||
value={locale ? translated : value}
|
||||
placeholder={locale ? fallback || placeholder : placeholder}
|
||||
aria-label={placeholder || field}
|
||||
required={required && !locale}
|
||||
{disabled}
|
||||
on:input={(e) => update(e.currentTarget.value)}
|
||||
></textarea>
|
||||
{:else}
|
||||
<input
|
||||
class={className}
|
||||
value={locale ? translated : value}
|
||||
placeholder={locale ? fallback || placeholder : placeholder}
|
||||
aria-label={placeholder || field}
|
||||
required={required && !locale}
|
||||
{disabled}
|
||||
on:input={(e) => update(e.currentTarget.value)}
|
||||
/>
|
||||
{/if}
|
||||
{#if locale}
|
||||
<div
|
||||
class="mt-1 flex flex-wrap items-center gap-3 text-[0.6875rem] text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<span>{translated ? $i18n.t('Translated') : $i18n.t('Using default')}</span>
|
||||
{#if translated}<button
|
||||
type="button"
|
||||
class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
{disabled}
|
||||
on:click={() => update('')}>{$i18n.t('Use default')}</button
|
||||
>
|
||||
{:else if fallback}<button
|
||||
type="button"
|
||||
class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
{disabled}
|
||||
on:click={() => update(fallback)}>{$i18n.t('Copy default')}</button
|
||||
>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
188
src/lib/components/common/TranslationTable.svelte
Normal file
188
src/lib/components/common/TranslationTable.svelte
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { validateDictionary } from '$lib/utils/translationDictionary';
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import ArrowUturnLeft from '$lib/components/icons/ArrowUturnLeft.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
const i18n = getContext<any>('i18n');
|
||||
export let value: Record<string, string> = {};
|
||||
export let source: Record<string, string> = {};
|
||||
export let filename = 'translations.json';
|
||||
export let onChange = (_value: Record<string, string>) => {};
|
||||
export let allowNewKeys = false;
|
||||
let adding = false;
|
||||
let newKey = '';
|
||||
const addKey = () => {
|
||||
if (newKey.trim()) {
|
||||
update(newKey.trim(), value?.[newKey.trim()] ?? '');
|
||||
query = newKey.trim();
|
||||
modified = false;
|
||||
adding = false;
|
||||
newKey = '';
|
||||
}
|
||||
};
|
||||
let query = '';
|
||||
let modified = false;
|
||||
let page = 0;
|
||||
let fileInput: HTMLInputElement;
|
||||
const exportFile = () => {
|
||||
const data = Object.fromEntries(Object.entries(value ?? {}).filter(([, text]) => text.trim()));
|
||||
const url = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
};
|
||||
$: keys = [...new Set([...Object.keys(source), ...Object.keys(value ?? {})])]
|
||||
.filter(
|
||||
(key) =>
|
||||
(!modified || value?.[key]?.trim()) &&
|
||||
`${key} ${source[key] ?? ''} ${value?.[key] ?? ''}`
|
||||
.toLocaleLowerCase()
|
||||
.includes(query.toLocaleLowerCase())
|
||||
)
|
||||
.sort();
|
||||
$: (query, modified, (page = 0));
|
||||
$: pages = Math.max(1, Math.ceil(keys.length / 50));
|
||||
$: if (page >= pages) page = pages - 1;
|
||||
const update = (key: string, text: string) => {
|
||||
value = { ...value, [key]: text };
|
||||
onChange(value);
|
||||
};
|
||||
const reset = (key: string) => {
|
||||
const next = { ...value };
|
||||
delete next[key];
|
||||
value = next;
|
||||
onChange(value);
|
||||
};
|
||||
const importFile = async (event: Event) => {
|
||||
const input = event.currentTarget as HTMLInputElement;
|
||||
try {
|
||||
if (input.files?.[0]) {
|
||||
value = {
|
||||
...value,
|
||||
...validateDictionary(JSON.parse(await input.files[0].text()), source)
|
||||
};
|
||||
onChange(value);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(String(error));
|
||||
}
|
||||
input.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-2">
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs">
|
||||
<slot name="language" />
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
hidden
|
||||
on:change={importFile}
|
||||
/>
|
||||
<button type="button" on:click={() => fileInput.click()}>{$i18n.t('Import')}</button>
|
||||
<button type="button" on:click={exportFile}>{$i18n.t('Export')}</button>
|
||||
{#if allowNewKeys}<Tooltip content={$i18n.t('Add translation')}
|
||||
><button
|
||||
class="flex size-6 items-center justify-center"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Add translation')}
|
||||
on:click={() => (adding = !adding)}><Plus className="size-3.5" /></button
|
||||
></Tooltip
|
||||
>{/if}
|
||||
</div>
|
||||
{#if adding}<div class="flex items-center gap-2 text-xs">
|
||||
<input
|
||||
class="min-w-0 flex-1 rounded-lg border border-gray-200 bg-transparent px-2 py-1.5 dark:border-gray-800"
|
||||
aria-label={$i18n.t('Translation key')}
|
||||
placeholder={$i18n.t('Translation key')}
|
||||
bind:value={newKey}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
addKey();
|
||||
}
|
||||
}}
|
||||
/><button
|
||||
type="button"
|
||||
class="flex size-7 items-center justify-center"
|
||||
aria-label={$i18n.t('Add')}
|
||||
on:click={addKey}><Plus className="size-4" /></button
|
||||
>
|
||||
</div>{/if}
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
<input
|
||||
class="min-w-0 flex-1 rounded-lg border border-gray-200 bg-transparent px-2 py-1.5 outline-hidden dark:border-gray-800"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search')}
|
||||
aria-label={$i18n.t('Search translations')}
|
||||
/>
|
||||
<label class="flex shrink-0 items-center gap-1.5"
|
||||
><input type="checkbox" bind:checked={modified} />{$i18n.t('Modified')}</label
|
||||
>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<div
|
||||
class="sticky top-0 z-10 hidden grid-cols-[minmax(0,1fr)_minmax(0,1fr)_1.5rem] gap-3 border-b border-gray-100 bg-white py-2 text-xs text-gray-500 dark:border-gray-800 dark:bg-gray-900 sm:grid"
|
||||
>
|
||||
<span>{$i18n.t('Original')}</span><span>{$i18n.t('Translation')}</span>
|
||||
</div>
|
||||
{#each keys.slice(page * 50, (page + 1) * 50) as key (key)}
|
||||
<div
|
||||
class="grid grid-cols-[minmax(0,1fr)_1.5rem] items-start gap-2 border-b border-gray-100 py-2 dark:border-gray-800 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_1.5rem] sm:gap-3"
|
||||
>
|
||||
<div class="col-span-2 min-w-0 text-xs sm:col-span-1">
|
||||
<div class="break-words">{key}</div>
|
||||
{#if source[key] && source[key] !== key}<div class="mt-1 break-words text-gray-500">
|
||||
{source[key]}
|
||||
</div>{/if}
|
||||
</div>
|
||||
<textarea
|
||||
rows="2"
|
||||
class="w-full min-w-0 resize-y rounded-md border border-gray-100 bg-transparent px-2 py-1 text-xs outline-hidden focus:border-gray-400 dark:border-gray-800"
|
||||
aria-label={key}
|
||||
placeholder={source[key] || key}
|
||||
value={value?.[key] ?? ''}
|
||||
on:input={(event) => update(key, event.currentTarget.value)}
|
||||
></textarea>
|
||||
<Tooltip content={$i18n.t('Use default')}
|
||||
><button
|
||||
type="button"
|
||||
class="flex size-6 items-center justify-center disabled:opacity-30"
|
||||
aria-label={`${$i18n.t('Use default')}: ${key}`}
|
||||
disabled={!Object.hasOwn(value ?? {}, key)}
|
||||
on:click={() => reset(key)}><ArrowUturnLeft className="size-3.5" /></button
|
||||
></Tooltip
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
{#if !keys.length}<div class="py-6 text-center text-xs text-gray-500">
|
||||
{$i18n.t('No results found')}
|
||||
</div>{/if}
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3 text-xs text-gray-500">
|
||||
<span>{page + 1} / {pages}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-7 items-center justify-center disabled:opacity-30"
|
||||
aria-label={$i18n.t('Previous')}
|
||||
disabled={page === 0}
|
||||
on:click={() => page--}><ChevronLeft className="size-4" /></button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-7 items-center justify-center disabled:opacity-30"
|
||||
aria-label={$i18n.t('Next')}
|
||||
disabled={page + 1 >= pages}
|
||||
on:click={() => page++}><ChevronRight className="size-4" /></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { marked } from 'marked';
|
||||
import { localizeValvesSchema, resolveLocalizedString } from '$lib/utils/localizedContent';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
|
@ -14,16 +15,20 @@
|
|||
|
||||
export let valvesSpec = null;
|
||||
export let valves = {};
|
||||
export let meta = {};
|
||||
export let userValves = false;
|
||||
$: prefix = userValves ? 'user_valves' : 'valves';
|
||||
$: displaySpec = localizeValvesSchema(valvesSpec, $i18n.language, meta, prefix);
|
||||
</script>
|
||||
|
||||
{#if valvesSpec && Object.keys(valvesSpec?.properties ?? {}).length}
|
||||
{#each Object.keys(valvesSpec.properties) as property}
|
||||
{#if displaySpec && Object.keys(displaySpec?.properties ?? {}).length}
|
||||
{#each Object.keys(displaySpec.properties) as property}
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">
|
||||
{valvesSpec.properties[property].title}
|
||||
{displaySpec.properties[property].title}
|
||||
|
||||
{#if (valvesSpec?.required ?? []).includes(property)}
|
||||
{#if (displaySpec?.required ?? []).includes(property)}
|
||||
<span class=" text-gray-500">{$i18n.t('*required')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -32,7 +37,7 @@
|
|||
class="px-2 py-1 text-xs flex rounded-lg transition hover:bg-gray-50/70 dark:hover:bg-gray-850/50"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
const propertySpec = valvesSpec.properties[property] ?? {};
|
||||
const propertySpec = displaySpec.properties[property] ?? {};
|
||||
|
||||
if ((valves[property] ?? null) === null) {
|
||||
// Initialize to custom value
|
||||
|
|
@ -55,7 +60,7 @@
|
|||
>
|
||||
{#if (valves[property] ?? null) === null}
|
||||
<span class="ml-2 self-center">
|
||||
{#if (valvesSpec?.required ?? []).includes(property)}
|
||||
{#if (displaySpec?.required ?? []).includes(property)}
|
||||
{$i18n.t('None')}
|
||||
{:else}
|
||||
{$i18n.t('Default')}
|
||||
|
|
@ -71,7 +76,7 @@
|
|||
<!-- {valves[property]} -->
|
||||
<div class="flex mt-0.5 mb-0.5 space-x-2">
|
||||
<div class=" flex-1 min-w-0">
|
||||
{#if valvesSpec.properties[property]?.enum ?? null}
|
||||
{#if displaySpec.properties[property]?.enum ?? null}
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||
bind:value={valves[property]}
|
||||
|
|
@ -79,13 +84,18 @@
|
|||
dispatch('change');
|
||||
}}
|
||||
>
|
||||
{#each valvesSpec.properties[property].enum as option}
|
||||
{#each displaySpec.properties[property].enum as option}
|
||||
<option value={option} selected={option === valves[property]}>
|
||||
{option}
|
||||
{resolveLocalizedString(
|
||||
String(option),
|
||||
meta?.i18n,
|
||||
$i18n.language,
|
||||
`${prefix}.${property}.enum.${option}`
|
||||
)}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if (valvesSpec.properties[property]?.type ?? null) === 'boolean'}
|
||||
{:else if (displaySpec.properties[property]?.type ?? null) === 'boolean'}
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-xs text-gray-500">
|
||||
{valves[property] ? $i18n.t('Enabled') : $i18n.t('Disabled')}
|
||||
|
|
@ -100,21 +110,21 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if valvesSpec.properties[property]?.input?.type === 'multiselect' && valvesSpec.properties[property]?.input?.options}
|
||||
{:else if displaySpec.properties[property]?.input?.type === 'multiselect' && displaySpec.properties[property]?.input?.options}
|
||||
<MultiSelect
|
||||
className="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||
bind:value={valves[property]}
|
||||
options={valvesSpec.properties[property].input.options}
|
||||
options={displaySpec.properties[property].input.options}
|
||||
placeholder={$i18n.t('Select options')}
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
{:else if (valvesSpec.properties[property]?.type ?? null) !== 'string'}
|
||||
{:else if (displaySpec.properties[property]?.type ?? null) !== 'string'}
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||
type="text"
|
||||
placeholder={valvesSpec.properties[property].title}
|
||||
placeholder={displaySpec.properties[property].title}
|
||||
bind:value={valves[property]}
|
||||
autocomplete="off"
|
||||
required
|
||||
|
|
@ -122,33 +132,33 @@
|
|||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
{:else if valvesSpec.properties[property]?.input ?? null}
|
||||
{#if valvesSpec.properties[property]?.input?.type === 'password'}
|
||||
{:else if displaySpec.properties[property]?.input ?? null}
|
||||
{#if displaySpec.properties[property]?.input?.type === 'password'}
|
||||
<div
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100/30 dark:border-gray-850/30"
|
||||
>
|
||||
<SensitiveInput
|
||||
id="valve-{property}"
|
||||
placeholder={valvesSpec.properties[property]?.description ?? ''}
|
||||
placeholder={displaySpec.properties[property]?.description ?? ''}
|
||||
bind:value={valves[property]}
|
||||
required={(valvesSpec?.required ?? []).includes(property)}
|
||||
required={(displaySpec?.required ?? []).includes(property)}
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if valvesSpec.properties[property]?.input?.type === 'select' && valvesSpec.properties[property]?.input?.options}
|
||||
{:else if displaySpec.properties[property]?.input?.type === 'select' && displaySpec.properties[property]?.input?.options}
|
||||
<NativeSelect
|
||||
className="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||
bind:value={valves[property]}
|
||||
options={valvesSpec.properties[property].input.options}
|
||||
placeholder={valvesSpec.properties[property]?.description ??
|
||||
options={displaySpec.properties[property].input.options}
|
||||
placeholder={displaySpec.properties[property]?.description ??
|
||||
$i18n.t('Select an option')}
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
{:else if valvesSpec.properties[property]?.input?.type === 'color'}
|
||||
{:else if displaySpec.properties[property]?.input?.type === 'color'}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative size-6">
|
||||
<input
|
||||
|
|
@ -175,7 +185,7 @@
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if valvesSpec.properties[property]?.input?.type === 'map'}
|
||||
{:else if displaySpec.properties[property]?.input?.type === 'map'}
|
||||
<!-- EXPERIMENTAL INPUT TYPE, DO NOT USE IN PRODUCTION -->
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<MapSelector
|
||||
|
|
@ -205,7 +215,7 @@
|
|||
{:else}
|
||||
<textarea
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
|
||||
placeholder={valvesSpec.properties[property].title}
|
||||
placeholder={displaySpec.properties[property].title}
|
||||
bind:value={valves[property]}
|
||||
autocomplete="off"
|
||||
required
|
||||
|
|
@ -218,11 +228,11 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if (valvesSpec.properties[property]?.description ?? null) !== null}
|
||||
{#if (displaySpec.properties[property]?.description ?? null) !== null}
|
||||
<div class="markdown-prose-xs max-w-full text-gray-500 dark:text-gray-400">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html DOMPurify.sanitize(
|
||||
marked.parse(valvesSpec.properties[property].description ?? '', { async: false })
|
||||
marked.parse(displaySpec.properties[property].description ?? '', { async: false })
|
||||
)}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { getContext } from 'svelte';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
|
@ -65,7 +66,7 @@
|
|||
<div class=" flex items-center gap-2 mr-3">
|
||||
<div class="self-center flex items-center">
|
||||
<Checkbox
|
||||
ariaLabel={action.name}
|
||||
ariaLabel={resolveLocalizedResource(action, $i18n.language)}
|
||||
state="checked"
|
||||
disabled={action.is_global}
|
||||
on:change={(e) => {
|
||||
|
|
@ -77,8 +78,11 @@
|
|||
</div>
|
||||
|
||||
<div class=" py-0.5 text-xs capitalize">
|
||||
<Tooltip content={action.meta?.description ?? action.id}>
|
||||
{action.name}
|
||||
<Tooltip
|
||||
content={resolveLocalizedResource(action, $i18n.language, 'description') ||
|
||||
action.id}
|
||||
>
|
||||
{resolveLocalizedResource(action, $i18n.language)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { getContext } from 'svelte';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
|
@ -67,7 +68,7 @@
|
|||
<div class=" flex items-center gap-2 mr-3">
|
||||
<div class="self-center flex items-center">
|
||||
<Checkbox
|
||||
ariaLabel={filter.name}
|
||||
ariaLabel={resolveLocalizedResource(filter, $i18n.language)}
|
||||
state={isSelected ? 'checked' : 'unchecked'}
|
||||
disabled={filter.is_global}
|
||||
on:change={(e) => {
|
||||
|
|
@ -85,8 +86,11 @@
|
|||
</div>
|
||||
|
||||
<div class=" py-0.5 text-xs capitalize">
|
||||
<Tooltip content={filter.meta?.description ?? filter.id}>
|
||||
{filter.name}
|
||||
<Tooltip
|
||||
content={resolveLocalizedResource(filter, $i18n.language, 'description') ||
|
||||
filter.id}
|
||||
>
|
||||
{resolveLocalizedResource(filter, $i18n.language)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
import DefaultFiltersSelector from './DefaultFiltersSelector.svelte';
|
||||
import DefaultFeatures from './DefaultFeatures.svelte';
|
||||
import BuiltinTools from './BuiltinTools.svelte';
|
||||
import LanguageModeSelect from './LanguageModeSelect.svelte';
|
||||
import LanguageModeSelect from '$lib/components/common/LanguageModeSelect.svelte';
|
||||
import LocalizedPromptSuggestions from './LocalizedPromptSuggestions.svelte';
|
||||
import PromptSuggestions from './PromptSuggestions.svelte';
|
||||
import TerminalSelector from './TerminalSelector.svelte';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import TypeaheadSelector from './TypeaheadSelector.svelte';
|
||||
|
|
@ -58,7 +59,7 @@
|
|||
<div class=" flex items-center gap-2 mr-3">
|
||||
<div class="self-center flex items-center">
|
||||
<Checkbox
|
||||
ariaLabel={skill.name}
|
||||
ariaLabel={resolveLocalizedResource(skill, $i18n.language)}
|
||||
state="checked"
|
||||
on:change={(e) => {
|
||||
if (e.detail === 'unchecked') {
|
||||
|
|
@ -68,9 +69,11 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Tooltip content={skill.description ?? skill.id}>
|
||||
<Tooltip
|
||||
content={resolveLocalizedResource(skill, $i18n.language, 'description') || skill.id}
|
||||
>
|
||||
<div class=" py-0.5 text-xs capitalize">
|
||||
{skill.name}
|
||||
{resolveLocalizedResource(skill, $i18n.language)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import TypeaheadSelector from './TypeaheadSelector.svelte';
|
||||
|
|
@ -56,7 +57,7 @@
|
|||
<div class=" flex items-center gap-2 mr-3">
|
||||
<div class="self-center flex items-center">
|
||||
<Checkbox
|
||||
ariaLabel={tool.name}
|
||||
ariaLabel={resolveLocalizedResource(tool, $i18n.language)}
|
||||
state="checked"
|
||||
on:change={(e) => {
|
||||
if (e.detail === 'unchecked') {
|
||||
|
|
@ -66,9 +67,11 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Tooltip content={tool.meta?.description ?? tool.id}>
|
||||
<Tooltip
|
||||
content={resolveLocalizedResource(tool, $i18n.language, 'description') || tool.id}
|
||||
>
|
||||
<div class=" py-0.5 text-xs capitalize">
|
||||
{tool.name}
|
||||
{resolveLocalizedResource(tool, $i18n.language)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte';
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
|
|
@ -39,7 +40,12 @@
|
|||
const description = (item.description ?? item.meta?.description ?? '').toLowerCase();
|
||||
|
||||
return (
|
||||
query === '' || id.includes(query) || name.includes(query) || description.includes(query)
|
||||
query === '' ||
|
||||
id.includes(query) ||
|
||||
name.includes(query) ||
|
||||
description.includes(query) ||
|
||||
resolveLocalizedResource(item, $i18n.language).toLowerCase().includes(query) ||
|
||||
resolveLocalizedResource(item, $i18n.language, 'description').toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -123,7 +129,9 @@
|
|||
selectItem(item);
|
||||
}}
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate">{item.name || item.id}</span>
|
||||
<span class="min-w-0 flex-1 truncate"
|
||||
>{resolveLocalizedResource(item, $i18n.language)}</span
|
||||
>
|
||||
{#if selectedIds !== null && selectedIds.includes(item.id)}
|
||||
<svg
|
||||
class="size-3.5 shrink-0 text-gray-500 dark:text-gray-400"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
|
@ -453,7 +454,7 @@
|
|||
<div
|
||||
class="truncate text-[0.8125rem] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{skill.name}
|
||||
{resolveLocalizedResource(skill, $i18n.language)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -485,12 +486,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{#if skill.description}
|
||||
<Tooltip content={skill.description} className="min-w-0" placement="top-start">
|
||||
{#if resolveLocalizedResource(skill, $i18n.language, 'description')}
|
||||
<Tooltip
|
||||
content={resolveLocalizedResource(skill, $i18n.language, 'description')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 truncate text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{skill.description}
|
||||
{resolveLocalizedResource(skill, $i18n.language, 'description')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
|
@ -618,7 +623,8 @@
|
|||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-500 truncate">
|
||||
{$i18n.t('This will delete')} <span class=" font-normal">{selectedSkill.name}</span>.
|
||||
{$i18n.t('This will delete')}
|
||||
<span class=" font-normal">{resolveLocalizedResource(selectedSkill, $i18n.language)}</span>.
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
import LanguageModeSelect from '$lib/components/common/LanguageModeSelect.svelte';
|
||||
import LocalizedField from '$lib/components/common/LocalizedField.svelte';
|
||||
let locale = '';
|
||||
let meta: { i18n?: Record<string, Record<string, string>>; [key: string]: any } = {};
|
||||
import { user } from '$lib/stores';
|
||||
import { slugify, parseFrontmatter, formatSkillName } from '$lib/utils';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
|
@ -15,11 +19,11 @@
|
|||
|
||||
export let onSubmit: Function;
|
||||
export let edit = false;
|
||||
export let skill = null;
|
||||
export let skill: any = null;
|
||||
export let clone = false;
|
||||
export let disabled = false;
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const i18n = getContext<any>('i18n');
|
||||
|
||||
let loading = false;
|
||||
|
||||
|
|
@ -51,23 +55,31 @@
|
|||
return;
|
||||
}
|
||||
loading = true;
|
||||
if (!name.trim()) {
|
||||
locale = '';
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
if (!edit) id = slugify(id);
|
||||
|
||||
await onSubmit({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
content,
|
||||
is_active: true,
|
||||
meta: { tags: [] },
|
||||
access_grants: accessGrants
|
||||
});
|
||||
|
||||
loading = false;
|
||||
try {
|
||||
await onSubmit({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
content,
|
||||
is_active: true,
|
||||
meta,
|
||||
access_grants: accessGrants
|
||||
});
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if (skill) {
|
||||
meta = structuredClone(skill.meta ?? {});
|
||||
name = skill.name || '';
|
||||
await tick();
|
||||
id = skill.id || '';
|
||||
|
|
@ -110,15 +122,14 @@
|
|||
<span>{$i18n.t('Back')}</span>
|
||||
</button>
|
||||
|
||||
<div class="flex shrink-0 items-start gap-2 pb-2 px-1">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex shrink-0 flex-col gap-2 pb-2 px-1 sm:flex-row sm:items-start">
|
||||
<div class="min-w-0 w-full flex-1">
|
||||
<Tooltip content={$i18n.t('e.g. Code Review Guidelines')} placement="top-start">
|
||||
<input
|
||||
class="w-full bg-transparent text-sm outline-hidden"
|
||||
type="text"
|
||||
<LocalizedField
|
||||
placeholder={$i18n.t('Skill Name')}
|
||||
aria-label={$i18n.t('Skill Name')}
|
||||
bind:value={name}
|
||||
bind:translations={meta.i18n}
|
||||
{locale}
|
||||
required
|
||||
{disabled}
|
||||
/>
|
||||
|
|
@ -152,12 +163,12 @@
|
|||
content={$i18n.t('e.g. Step-by-step instructions for code reviews')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden"
|
||||
type="text"
|
||||
<LocalizedField
|
||||
placeholder={$i18n.t('Skill Description')}
|
||||
aria-label={$i18n.t('Skill Description')}
|
||||
bind:value={description}
|
||||
bind:translations={meta.i18n}
|
||||
{locale}
|
||||
field="description"
|
||||
{disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
@ -165,6 +176,7 @@
|
|||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-1 pr-0.5">
|
||||
<LanguageModeSelect bind:value={locale} translatedLocales={Object.keys(meta.i18n ?? {})} />
|
||||
{#if !disabled}
|
||||
<AccessButton on:click={() => (showAccessControlModal = true)} />
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
|
@ -141,6 +142,7 @@
|
|||
const lowerQuery = query.toLowerCase();
|
||||
return (
|
||||
((t.name || '').toLowerCase().includes(lowerQuery) ||
|
||||
resolveLocalizedResource(t, $i18n.language).toLowerCase().includes(lowerQuery) ||
|
||||
(t.id || '').toLowerCase().includes(lowerQuery) ||
|
||||
(t.user?.name || '').toLowerCase().includes(lowerQuery) || // Search by user name
|
||||
(t.user?.email || '').toLowerCase().includes(lowerQuery)) && // Search by user email
|
||||
|
|
@ -450,7 +452,7 @@
|
|||
<div
|
||||
class="truncate text-[0.8125rem] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{tool.name}
|
||||
{resolveLocalizedResource(tool, $i18n.language)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -476,16 +478,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{#if tool?.meta?.description}
|
||||
{#if resolveLocalizedResource(tool, $i18n.language, 'description')}
|
||||
<Tooltip
|
||||
content={tool?.meta?.description}
|
||||
content={resolveLocalizedResource(tool, $i18n.language, 'description')}
|
||||
className="min-w-0"
|
||||
placement="top-start"
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 truncate text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
{tool?.meta?.description}
|
||||
{resolveLocalizedResource(tool, $i18n.language, 'description')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
|
@ -648,11 +650,17 @@
|
|||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-500 truncate">
|
||||
{$i18n.t('This will delete')} <span class=" font-normal">{selectedTool.name}</span>.
|
||||
{$i18n.t('This will delete')}
|
||||
<span class=" font-normal">{resolveLocalizedResource(selectedTool, $i18n.language)}</span>.
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
<ValvesModal bind:show={showValvesModal} type="tool" id={selectedTool?.id ?? null} />
|
||||
<ValvesModal
|
||||
bind:show={showValvesModal}
|
||||
type="tool"
|
||||
id={selectedTool?.id ?? null}
|
||||
meta={selectedTool?.meta}
|
||||
/>
|
||||
<ManifestModal bind:show={showManifestModal} manifest={selectedTool?.meta?.manifest ?? {}} />
|
||||
|
||||
<ConfirmDialog
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import AccessControlModal from '../common/AccessControlModal.svelte';
|
||||
import LanguageModeSelect from '$lib/components/common/LanguageModeSelect.svelte';
|
||||
import LocalizedField from '$lib/components/common/LocalizedField.svelte';
|
||||
import PluginTranslations from '$lib/components/workspace/common/PluginTranslations.svelte';
|
||||
import { pruneEmptyLocaleEntries } from '$lib/utils/localizedContent';
|
||||
let locale = '';
|
||||
|
||||
let formElement = null;
|
||||
let loading = false;
|
||||
|
|
@ -30,6 +35,7 @@
|
|||
|
||||
export let id = '';
|
||||
export let name = '';
|
||||
/** @type {{description: string, i18n?: Record<string, Record<string, string>>, manifest?: {translations?: Record<string, Record<string, string>>}}} */
|
||||
export let meta = {
|
||||
description: ''
|
||||
};
|
||||
|
|
@ -159,12 +165,17 @@ class Tools:
|
|||
`;
|
||||
|
||||
const saveHandler = async () => {
|
||||
if (!name.trim() || !meta.description?.trim()) {
|
||||
locale = '';
|
||||
toast.error($i18n.t('Name and description are required'));
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
try {
|
||||
await onSave({
|
||||
id,
|
||||
name,
|
||||
meta,
|
||||
meta: { ...meta, i18n: pruneEmptyLocaleEntries(meta.i18n) },
|
||||
content,
|
||||
access_grants: accessGrants
|
||||
});
|
||||
|
|
@ -235,15 +246,14 @@ class Tools:
|
|||
<span>{$i18n.t('Back')}</span>
|
||||
</button>
|
||||
|
||||
<div class="flex shrink-0 items-start gap-2 pb-2 px-1">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex shrink-0 flex-col gap-2 pb-2 px-1 sm:flex-row sm:items-start">
|
||||
<div class="min-w-0 w-full flex-1">
|
||||
<Tooltip content={$i18n.t('e.g. My Tools')} placement="top-start">
|
||||
<input
|
||||
class="w-full bg-transparent text-sm outline-hidden"
|
||||
type="text"
|
||||
<LocalizedField
|
||||
placeholder={$i18n.t('Tool Name')}
|
||||
aria-label={$i18n.t('Tool Name')}
|
||||
bind:value={name}
|
||||
bind:translations={meta.i18n}
|
||||
{locale}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
@ -276,12 +286,12 @@ class Tools:
|
|||
content={$i18n.t('e.g. Tools for performing various operations')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent outline-hidden"
|
||||
type="text"
|
||||
<LocalizedField
|
||||
placeholder={$i18n.t('Tool Description')}
|
||||
aria-label={$i18n.t('Tool Description')}
|
||||
bind:value={meta.description}
|
||||
bind:translations={meta.i18n}
|
||||
{locale}
|
||||
field="description"
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
@ -289,6 +299,10 @@ class Tools:
|
|||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-1 pr-0.5">
|
||||
<LanguageModeSelect
|
||||
bind:value={locale}
|
||||
translatedLocales={Object.keys(pruneEmptyLocaleEntries(meta.i18n))}
|
||||
/>
|
||||
<AccessButton
|
||||
on:click={() => {
|
||||
showAccessControlModal = true;
|
||||
|
|
@ -297,32 +311,42 @@ class Tools:
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-hidden rounded-lg">
|
||||
<CodeEditor
|
||||
bind:this={codeEditor}
|
||||
value={content}
|
||||
lang="python"
|
||||
{boilerplate}
|
||||
className="text-[0.6875rem]"
|
||||
onChange={(e) => {
|
||||
_content = e;
|
||||
if (!edit) {
|
||||
const fm = extractFrontmatter(e);
|
||||
if (fm.title && !name) {
|
||||
name = formatSkillName(fm.title);
|
||||
id = nameToId(fm.title);
|
||||
<div class="min-h-0 flex-1 overflow-hidden rounded-lg flex flex-col">
|
||||
{#if locale}
|
||||
<PluginTranslations
|
||||
id={edit ? id : ''}
|
||||
kind="tool"
|
||||
{locale}
|
||||
bind:translations={meta.i18n}
|
||||
/>
|
||||
{/if}
|
||||
<div class={locale ? 'hidden' : 'h-full'}>
|
||||
<CodeEditor
|
||||
bind:this={codeEditor}
|
||||
value={content}
|
||||
lang="python"
|
||||
{boilerplate}
|
||||
className="text-[0.6875rem]"
|
||||
onChange={(e) => {
|
||||
_content = e;
|
||||
if (!edit) {
|
||||
const fm = extractFrontmatter(e);
|
||||
if (fm.title && !name) {
|
||||
name = formatSkillName(fm.title);
|
||||
id = nameToId(fm.title);
|
||||
}
|
||||
if (fm.description && !meta.description) {
|
||||
meta = { ...meta, description: fm.description };
|
||||
}
|
||||
}
|
||||
if (fm.description && !meta.description) {
|
||||
meta = { ...meta, description: fm.description };
|
||||
}}
|
||||
onSave={async () => {
|
||||
if (formElement) {
|
||||
formElement.requestSubmit();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onSave={async () => {
|
||||
if (formElement) {
|
||||
formElement.requestSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 py-2 text-xs text-gray-500">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
<script lang="ts">
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import {
|
||||
getToolValvesSpecById,
|
||||
getUserValvesSpecById as getToolUserValvesSpec
|
||||
} from '$lib/apis/tools';
|
||||
import {
|
||||
getFunctionValvesSpecById,
|
||||
getUserValvesSpecById as getFunctionUserValvesSpec
|
||||
} from '$lib/apis/functions';
|
||||
import { valveTranslationSource } from '$lib/utils/localizedContent';
|
||||
import TranslationTable from '$lib/components/common/TranslationTable.svelte';
|
||||
const i18n = getContext<any>('i18n');
|
||||
export let id = '';
|
||||
export let kind: 'tool' | 'function';
|
||||
export let locale: string;
|
||||
export let translations: Record<string, Record<string, string>> = {};
|
||||
let fields: Record<string, string> = {};
|
||||
let loadError = false;
|
||||
const headerFields = ['name', 'description'];
|
||||
$: source = fields;
|
||||
$: values = Object.fromEntries(
|
||||
Object.entries(translations?.[locale] ?? {}).filter(([key]) => !headerFields.includes(key))
|
||||
);
|
||||
const load = async () => {
|
||||
if (!id) return;
|
||||
loadError = false;
|
||||
const loaders =
|
||||
kind === 'tool'
|
||||
? [getToolValvesSpecById, getToolUserValvesSpec]
|
||||
: [getFunctionValvesSpecById, getFunctionUserValvesSpec];
|
||||
const results = await Promise.allSettled(
|
||||
loaders.map((loader) => loader(localStorage.token, id))
|
||||
);
|
||||
fields = Object.assign(
|
||||
{},
|
||||
...results.map((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
loadError = true;
|
||||
return {};
|
||||
}
|
||||
return valveTranslationSource(result.value, index ? 'user_valves' : 'valves');
|
||||
})
|
||||
);
|
||||
};
|
||||
const changed = (next: Record<string, string>) => {
|
||||
translations = {
|
||||
...translations,
|
||||
[locale]: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(translations?.[locale] ?? {}).filter(([key]) => headerFields.includes(key))
|
||||
),
|
||||
...next
|
||||
}
|
||||
};
|
||||
};
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
{#if loadError}<div class="mb-2 text-xs text-red-500" role="alert">
|
||||
{$i18n.t('Failed to load valves')}<button type="button" class="ms-2 underline" on:click={load}
|
||||
>{$i18n.t('Retry')}</button
|
||||
>
|
||||
</div>{/if}
|
||||
<TranslationTable
|
||||
value={values}
|
||||
{source}
|
||||
filename={`${kind}-${id}-${locale}.json`}
|
||||
onChange={changed}
|
||||
allowNewKeys
|
||||
/>
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { tools, functions } from '$lib/stores';
|
||||
import { addUser } from '$lib/apis/auths';
|
||||
|
||||
import Modal from '../../common/Modal.svelte';
|
||||
|
|
@ -38,6 +39,9 @@
|
|||
export let type = 'tool';
|
||||
export let id = null;
|
||||
export let userValves = false;
|
||||
export let meta = null;
|
||||
$: resourceMeta =
|
||||
meta ?? (type === 'tool' ? $tools : $functions)?.find((item) => item.id === id)?.meta ?? {};
|
||||
|
||||
let saving = false;
|
||||
let loading = false;
|
||||
|
|
@ -179,7 +183,7 @@
|
|||
>
|
||||
<div>
|
||||
{#if !loading}
|
||||
<Valves {valvesSpec} bind:valves />
|
||||
<Valves {valvesSpec} bind:valves meta={resourceMeta} {userValves} />
|
||||
{:else}
|
||||
<Spinner className="size-5" />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export type Banner = {
|
||||
i18n?: Record<string, { content: string }>;
|
||||
id: string;
|
||||
type: string;
|
||||
title?: string;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,91 @@ export const resolveLocalizedString = (
|
|||
return fallback ?? '';
|
||||
};
|
||||
|
||||
export const resolveLocalizedResource = (resource: any, locale: string, field = 'name') => {
|
||||
const meta = resource?.meta;
|
||||
const key = resource?.action_id ? `actions.${resource.action_id}.${field}` : field;
|
||||
return resolveLocalizedString(
|
||||
resource?.[field] ?? meta?.[field] ?? (field === 'name' ? resource?.id : ''),
|
||||
meta?.i18n,
|
||||
locale,
|
||||
key
|
||||
);
|
||||
};
|
||||
|
||||
export const resolveLocalizedFunction = (
|
||||
item: any,
|
||||
functions: any[] | null,
|
||||
locale: string,
|
||||
field = 'name'
|
||||
) => {
|
||||
const owner = (functions ?? [])
|
||||
.filter((fn) => item?.id === fn.id || item?.id?.startsWith(`${fn.id}.`))
|
||||
.sort((a, b) => b.id.length - a.id.length)[0];
|
||||
const key =
|
||||
owner && item.id !== owner.id
|
||||
? `actions.${item.id.slice(owner.id.length + 1)}.${field}`
|
||||
: field;
|
||||
return resolveLocalizedString(item?.[field], owner?.meta?.i18n ?? item?.meta?.i18n, locale, key);
|
||||
};
|
||||
|
||||
// This is a display-only copy: identifiers, defaults and submitted values stay untouched.
|
||||
export const localizeValvesSchema = (
|
||||
schema: any,
|
||||
locale: string,
|
||||
meta: any = {},
|
||||
prefix = 'valves'
|
||||
) => {
|
||||
if (!schema) return schema;
|
||||
const translate = (value: string, key: string) =>
|
||||
resolveLocalizedString(value, meta?.i18n, locale, `${prefix}.${key}`);
|
||||
return {
|
||||
...schema,
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(schema.properties ?? {}).map(([key, value]: [string, any]) => [
|
||||
key,
|
||||
{
|
||||
...value,
|
||||
title: translate(value.title ?? key, `${key}.title`),
|
||||
description: translate(value.description ?? '', `${key}.description`),
|
||||
...(Array.isArray(value.input?.options)
|
||||
? {
|
||||
input: {
|
||||
...value.input,
|
||||
options: value.input.options.map((option: any) =>
|
||||
typeof option === 'object'
|
||||
? {
|
||||
...option,
|
||||
label: translate(
|
||||
option.label ?? String(option.value),
|
||||
`${key}.enum.${option.value}`
|
||||
)
|
||||
}
|
||||
: { value: option, label: translate(String(option), `${key}.enum.${option}`) }
|
||||
)
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
])
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
export const valveTranslationSource = (schema: any, prefix: string): Record<string, string> => {
|
||||
const strings: Record<string, string> = {};
|
||||
for (const [key, field] of Object.entries(schema?.properties ?? {}) as [string, any][]) {
|
||||
strings[`${prefix}.${key}.title`] = field.title ?? key;
|
||||
if (field.description) strings[`${prefix}.${key}.description`] = field.description;
|
||||
for (const option of field.enum ?? field.input?.options ?? []) {
|
||||
const value = typeof option === 'object' ? option.value : option;
|
||||
strings[`${prefix}.${key}.enum.${value}`] = String(
|
||||
typeof option === 'object' ? (option.label ?? value) : value
|
||||
);
|
||||
}
|
||||
}
|
||||
return strings;
|
||||
};
|
||||
|
||||
export const resolveLocalizedModelName = (model: any, locale?: string | null) => {
|
||||
const meta = model?.info?.meta ?? model?.meta;
|
||||
const info = model?.info ?? model;
|
||||
|
|
|
|||
25
src/lib/utils/translationDictionary.ts
Normal file
25
src/lib/utils/translationDictionary.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export const placeholders = (text: string) =>
|
||||
[
|
||||
...new Set(
|
||||
Array.from(text.matchAll(/\{\{\s*-?\s*([^},]+)(?:,[^}]+)?\s*\}\}/g), (match) =>
|
||||
match[1].trim()
|
||||
)
|
||||
)
|
||||
].sort();
|
||||
|
||||
export const validateDictionary = (
|
||||
value: unknown,
|
||||
source: Record<string, string> = {}
|
||||
): Record<string, string> => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
throw new Error('Expected a JSON object of translation keys and strings.');
|
||||
for (const [key, text] of Object.entries(value)) {
|
||||
if (typeof text !== 'string') throw new Error(`Translation must be a string: ${key}`);
|
||||
if (
|
||||
text.trim() &&
|
||||
JSON.stringify(placeholders(source[key] || key)) !== JSON.stringify(placeholders(text))
|
||||
)
|
||||
throw new Error(`Interpolation placeholders do not match: ${key}`);
|
||||
}
|
||||
return value as Record<string, string>;
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue