This commit is contained in:
Timothy Jaeryang Baek
2026-09-04 17:39:34 -04:00
parent 323f8da202
commit 7b6562e335
25 changed files with 639 additions and 162 deletions
+2
View File
@@ -1697,6 +1697,7 @@ if default_prompt_suggestions == []:
]
DEFAULT_PROMPT_SUGGESTIONS = default_prompt_suggestions
DEFAULT_PROMPT_SUGGESTIONS_I18N = {}
try:
model_order_list = JSONCodec.loads(os.getenv('MODEL_ORDER_LIST', '[]'))
@@ -3096,6 +3097,7 @@ DEFAULT_CONFIG = {
'ui.default_pinned_models': DEFAULT_PINNED_MODELS,
'ui.default_interface_settings': DEFAULT_INTERFACE_SETTINGS,
'ui.prompt_suggestions': DEFAULT_PROMPT_SUGGESTIONS,
'ui.prompt_suggestions_i18n': DEFAULT_PROMPT_SUGGESTIONS_I18N,
'ui.model_order_list': MODEL_ORDER_LIST,
'models.default_metadata': DEFAULT_MODEL_METADATA,
'models.default_params': DEFAULT_MODEL_PARAMS,
+2
View File
@@ -2261,6 +2261,7 @@ async def get_app_config(request: Request):
'ui.default_pinned_models',
'ui.default_interface_settings',
'ui.prompt_suggestions',
'ui.prompt_suggestions_i18n',
'code_execution.engine',
'code_interpreter.engine',
'audio.tts.engine',
@@ -2361,6 +2362,7 @@ async def get_app_config(request: Request):
'default_models': config.get('ui.default_models'),
'default_pinned_models': config.get('ui.default_pinned_models'),
'default_prompt_suggestions': config.get('ui.prompt_suggestions'),
'default_prompt_suggestions_i18n': config.get('ui.prompt_suggestions_i18n'),
**({'user_count': user_count} if user_count is not None else {}),
'code': {
'engine': config.get('code_execution.engine'),
+1
View File
@@ -80,6 +80,7 @@ class ModelMeta(BaseModel):
profile_image_url: str | None = None
description: str | None = Field(default=None, description='User-facing description of the model.')
i18n: dict[str, Any] | None = None
capabilities: dict | None = None
knowledge: list[Any] | None = None
+11 -5
View File
@@ -1,8 +1,7 @@
from __future__ import annotations
import copy
import logging
from typing import Optional
from typing import Any, Optional
import aiohttp
from fastapi import APIRouter, Depends, HTTPException, Request
@@ -808,17 +807,24 @@ class PromptSuggestion(BaseModel):
class SetDefaultSuggestionsForm(BaseModel):
suggestions: list[PromptSuggestion]
i18n: dict[str, Any] | None = None
@router.post('/suggestions', response_model=list[PromptSuggestion])
@router.post('/suggestions', response_model=dict)
async def set_default_suggestions(
request: Request,
form_data: SetDefaultSuggestionsForm,
user=Depends(get_admin_user),
):
data = form_data.model_dump()
await Config.upsert({'ui.prompt_suggestions': data['suggestions']})
await Config.upsert(
{
'ui.prompt_suggestions': data['suggestions'],
'ui.prompt_suggestions_i18n': data.get('i18n') or {},
}
)
suggestions = await Config.get('ui.prompt_suggestions')
suggestions_i18n = await Config.get('ui.prompt_suggestions_i18n')
await publish_event(
request,
EVENTS.CONFIG_SUGGESTIONS_UPDATED,
@@ -827,7 +833,7 @@ async def set_default_suggestions(
subject_type='config',
data={'count': len(suggestions or [])},
)
return suggestions
return {'suggestions': suggestions, 'i18n': suggestions_i18n}
############################
+7 -2
View File
@@ -742,7 +742,11 @@ export const setSubagentsConfig = async (token: string, config: object) => {
return res.json();
};
export const setDefaultPromptSuggestions = async (token: string, promptSuggestions: string) => {
export const setDefaultPromptSuggestions = async (
token: string,
promptSuggestions: any[],
promptSuggestionsI18n: Record<string, any> = {}
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/suggestions`, {
@@ -752,7 +756,8 @@ export const setDefaultPromptSuggestions = async (token: string, promptSuggestio
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
suggestions: promptSuggestions
suggestions: promptSuggestions,
i18n: promptSuggestionsI18n
})
})
.then(async (res) => {
+1
View File
@@ -1759,6 +1759,7 @@ export interface ModelConfig {
export interface ModelMeta {
toolIds: never[];
description?: string;
i18n?: Record<string, Record<string, any>>;
hidden?: boolean;
capabilities?: object;
profile_image_url?: string;
@@ -8,12 +8,14 @@
import { DEFAULT_CAPABILITIES } from '$lib/constants';
import { getModelsConfig, setModelsConfig, setDefaultPromptSuggestions } from '$lib/apis/configs';
import { getBackendConfig } from '$lib/apis';
import { getLanguages } from '$lib/i18n';
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 PromptSuggestions from '$lib/components/workspace/Models/PromptSuggestions.svelte';
import LanguageModeSelect from '$lib/components/workspace/Models/LanguageModeSelect.svelte';
import LocalizedPromptSuggestions from '$lib/components/workspace/Models/LocalizedPromptSuggestions.svelte';
export let initHandler = () => {};
export let dirty = false;
@@ -32,6 +34,9 @@
let defaultParams = {};
let builtinTools = {};
let promptSuggestions = [];
let promptSuggestionsI18n = {};
let languages = [];
let editingLocale = '';
$: configuredParams = Object.entries(defaultParams ?? {}).filter(
([_, value]) => value !== null && value !== '' && value !== undefined
@@ -40,6 +45,10 @@
$: 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);
$: editingLocaleLabel = languages.find((language) => language.code === editingLocale)?.title;
const getSnapshot = () =>
JSON.stringify({
@@ -47,7 +56,8 @@
defaultFeatureIds,
defaultParams: Object.fromEntries(configuredParams),
builtinTools,
promptSuggestions: promptSuggestions.filter((p) => p.content !== '')
promptSuggestions: promptSuggestions.filter((p) => p.content !== ''),
promptSuggestionsI18n
});
const updateDirty = async () => {
@@ -74,6 +84,8 @@
defaultParams = config?.DEFAULT_MODEL_PARAMS ?? {};
promptSuggestions = $appConfig?.default_prompt_suggestions ?? [];
promptSuggestionsI18n = $appConfig?.default_prompt_suggestions_i18n ?? {};
languages = await getLanguages();
savedSnapshot = getSnapshot();
dirty = false;
loading = false;
@@ -104,7 +116,13 @@
if (res) {
config = res;
promptSuggestions = promptSuggestions.filter((p) => p.content !== '');
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
const suggestionsRes = await setDefaultPromptSuggestions(
localStorage.token,
promptSuggestions,
promptSuggestionsI18n
);
promptSuggestions = suggestionsRes?.suggestions ?? promptSuggestions;
promptSuggestionsI18n = suggestionsRes?.i18n ?? promptSuggestionsI18n;
await appConfig.set(await getBackendConfig());
savedSnapshot = getSnapshot();
dirty = false;
@@ -236,12 +254,26 @@
{#if showPromptSuggestions}
<div
class="max-h-[24rem] overflow-y-auto pb-2 pr-1 scrollbar-hover"
class="max-h-[24rem] space-y-2 overflow-y-auto pb-2 pr-1 scrollbar-hover"
on:click={updateDirty}
on:change={updateDirty}
on:input={updateDirty}
>
<PromptSuggestions bind:promptSuggestions />
<LocalizedPromptSuggestions
bind:promptSuggestions
bind:localizedPromptSuggestions={promptSuggestionsI18n}
locale={editingLocale}
localeLabel={editingLocaleLabel}
onChange={updateDirty}
>
<LanguageModeSelect
slot="language"
bind:value={editingLocale}
{languages}
translatedLocales={translatedPromptLocales}
className="w-fit max-w-[10rem]"
/>
</LocalizedPromptSuggestions>
</div>
{/if}
</div>
+27 -13
View File
@@ -10,6 +10,12 @@
import Suggestions from './Suggestions.svelte';
import { sanitizeResponseContent } from '$lib/utils';
import {
resolveLocalizedModelDescription,
resolveLocalizedModelName,
resolveLocalizedModelPromptSuggestions,
resolveLocalizedPromptSuggestions
} from '$lib/utils/localizedContent';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
@@ -23,12 +29,27 @@
let mounted = false;
let selectedModelIdx = 0;
let selectedModel;
let selectedModelName = '';
let selectedModelDescription = '';
let selectedSuggestionPrompts = [];
$: if (modelIds.length > 0) {
selectedModelIdx = models.length - 1;
}
$: models = modelIds.map((id) => $_models.find((m) => m.id === id));
$: selectedModel = atSelectedModel ?? models[selectedModelIdx];
$: selectedModelName = resolveLocalizedModelName(selectedModel, $i18n.language);
$: selectedModelDescription = resolveLocalizedModelDescription(selectedModel, $i18n.language);
$: selectedSuggestionPrompts =
resolveLocalizedModelPromptSuggestions(atSelectedModel, $i18n.language) ??
resolveLocalizedModelPromptSuggestions(models[selectedModelIdx], $i18n.language) ??
resolveLocalizedPromptSuggestions(
$config?.default_prompt_suggestions ?? [],
$config?.default_prompt_suggestions_i18n ?? {},
$i18n.language
);
onMount(() => {
mounted = true;
@@ -48,9 +69,7 @@
<Tooltip
content={DOMPurify.sanitize(
marked.parse(
sanitizeResponseContent(
models[selectedModelIdx]?.info?.meta?.description ?? ''
).replaceAll('\n', '<br>')
sanitizeResponseContent(selectedModelDescription).replaceAll('\n', '<br>')
)
)}
placement="right"
@@ -90,23 +109,21 @@
>
<div>
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
{#if models[selectedModelIdx]?.name}
{models[selectedModelIdx]?.name}
{#if selectedModelName}
{selectedModelName}
{:else}
{$i18n.t('Hello, {{name}}', { name: $user?.name })}
{/if}
</div>
<div in:fade={{ duration: 200, delay: 200 }}>
{#if models[selectedModelIdx]?.info?.meta?.description ?? null}
{#if selectedModelDescription}
<div
class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown"
>
{@html DOMPurify.sanitize(
marked.parse(
sanitizeResponseContent(
models[selectedModelIdx]?.info?.meta?.description
).replaceAll('\n', '<br>')
sanitizeResponseContent(selectedModelDescription).replaceAll('\n', '<br>')
)
)}
</div>
@@ -138,10 +155,7 @@
<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
<Suggestions
className="grid grid-cols-2"
suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config?.default_prompt_suggestions ??
[]}
suggestionPrompts={selectedSuggestionPrompts}
{onSelect}
/>
</div>
+3 -1
View File
@@ -68,6 +68,7 @@
import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
import { matchKeybinding, Shortcut } from '$lib/shortcuts';
import { resolveLocalizedModelName } from '$lib/utils/localizedContent';
import { createNoteHandler } from '../notes/utils';
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
@@ -673,6 +674,7 @@
$: showCommands =
['/', '#', '@', '$', ':'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
let suggestions = null;
$: atSelectedModelName = resolveLocalizedModelName(atSelectedModel, $i18n.language);
let showTools = false;
let showSkills = false;
@@ -1874,7 +1876,7 @@
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${$models.find((model) => model.id === atSelectedModel.id).id}&lang=${$i18n.language}`}
/>
<div class="translate-y-[0.5px]">
<span class="">{atSelectedModel.name}</span>
<span class="">{atSelectedModelName}</span>
</div>
</div>
<div>
@@ -15,6 +15,10 @@
import { searchKnowledgeBases, searchKnowledgeFiles } from '$lib/apis/knowledge';
import { searchFiles } from '$lib/apis/terminal';
import { decodeString, isValidHttpUrl, isYoutubeUrl } from '$lib/utils';
import {
resolveLocalizedModelDescription,
resolveLocalizedModelName
} from '$lib/utils/localizedContent';
import { isTemporaryChatId } from '$lib/utils/chatId';
import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -46,9 +50,9 @@
.filter((model) => !model?.info?.meta?.hidden)
.map((model) => ({
...model,
modelName: model?.name,
modelName: resolveLocalizedModelName(model, $i18n.language),
tags: model?.info?.meta?.tags?.map((tag: any) => tag.name).join(' '),
desc: model?.info?.meta?.description
desc: resolveLocalizedModelDescription(model, $i18n.language)
}));
$: fuse = new Fuse(modelItems, {
@@ -341,7 +345,7 @@
<div class="flex min-w-0 items-center text-black dark:text-gray-100">
<img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
alt={model?.name ?? model.id}
alt={resolveLocalizedModelName(model, $i18n.language) ?? model.id}
class="mr-2 size-4.5 rounded-full object-cover"
on:error={(e) => {
// LICENSE covers this Open WebUI fallback logo.
@@ -351,7 +355,7 @@
}}
/>
<div class="min-w-0 truncate">
{model.name}
{resolveLocalizedModelName(model, $i18n.language)}
</div>
</div>
</button>
@@ -7,6 +7,10 @@
import { models } from '$lib/stores';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import {
resolveLocalizedModelDescription,
resolveLocalizedModelName
} from '$lib/utils/localizedContent';
const i18n = getContext('i18n');
@@ -22,9 +26,9 @@
.map((model) => {
const _item = {
...model,
modelName: model?.name,
modelName: resolveLocalizedModelName(model, $i18n.language),
tags: model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
desc: model?.info?.meta?.description
desc: resolveLocalizedModelDescription(model, $i18n.language)
};
return _item;
}),
@@ -85,7 +89,7 @@
<div class="flex min-w-0 items-center text-black dark:text-gray-100">
<img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
alt={model?.name ?? model.id}
alt={resolveLocalizedModelName(model, $i18n.language) ?? model.id}
class="mr-2 size-4.5 rounded-full object-cover"
on:error={(e) => {
// LICENSE covers this Open WebUI fallback logo.
@@ -95,7 +99,7 @@
}}
/>
<div class="min-w-0 truncate">
{model.name}
{resolveLocalizedModelName(model, $i18n.language)}
</div>
</div>
</button>
@@ -37,6 +37,7 @@
removeAllDetails
} from '$lib/utils';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { resolveLocalizedModelName } from '$lib/utils/localizedContent';
import equal from 'fast-deep-equal';
import Name from './Name.svelte';
@@ -181,6 +182,7 @@
let showDeleteConfirm = false;
let model = null;
$: localizedModelName = model ? resolveLocalizedModelName(model, $i18n.language) : message.model;
$: model = $models.find((m) => m.id === message.model);
$: statusEntries = message?.statusHistory ?? [...(message?.status ? [message?.status] : [])];
@@ -672,9 +674,9 @@
<div class="flex-auto w-0 pl-1 relative">
{#if !compactPreview}
<Name>
<Tooltip content={model?.name ?? message.model} placement="top-start">
<Tooltip content={localizedModelName} placement="top-start">
<span id="response-message-model-name" class="line-clamp-1 text-black dark:text-white">
{model?.name ?? message.model}
{localizedModelName}
</span>
</Tooltip>
</Name>
+2 -1
View File
@@ -5,6 +5,7 @@
import Selector from './ModelSelector/Selector.svelte';
import { updateUserSettings } from '$lib/apis/users';
import { resolveLocalizedModelName } from '$lib/utils/localizedContent';
import equal from 'fast-deep-equal';
const i18n = getContext('i18n');
@@ -69,7 +70,7 @@
placeholder={$i18n.t('Select a model')}
items={$models.map((model) => ({
value: model.id,
label: model.name,
label: resolveLocalizedModelName(model, $i18n.language),
model: model
}))}
{pinModelHandler}
@@ -9,6 +9,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { copyToClipboard, sanitizeResponseContent } from '$lib/utils';
import { resolveLocalizedModelDescription } from '$lib/utils/localizedContent';
import ArrowUpTray from '$lib/components/icons/ArrowUpTray.svelte';
import Check from '$lib/components/icons/Check.svelte';
import ModelItemMenu from './ModelItemMenu.svelte';
@@ -33,6 +34,8 @@
export let onClick: () => void = () => {};
$: localizedDescription = resolveLocalizedModelDescription(item.model, $i18n.language);
const copyLinkHandler = async (model) => {
const baseUrl = window.location.origin;
const res = await copyToClipboard(`${baseUrl}/?model=${encodeURIComponent(model.id)}`);
@@ -247,10 +250,10 @@
</Tooltip>
{/if}
{#if item.model?.info?.meta?.description}
{#if localizedDescription}
<Tooltip
content={`${marked.parse(
sanitizeResponseContent(item.model?.info?.meta?.description).replaceAll('\n', '<br>')
sanitizeResponseContent(localizedDescription).replaceAll('\n', '<br>')
)}`}
>
<div class=" translate-y-[1px]">
@@ -34,6 +34,10 @@
} from '$lib/stores';
import { toast } from 'svelte-sonner';
import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
import {
resolveLocalizedModelDescription,
resolveLocalizedModelName
} from '$lib/utils/localizedContent';
import { getModels } from '$lib/apis';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
@@ -288,9 +292,9 @@
items.map((item) => {
const _item = {
...item,
modelName: item.model?.name,
modelName: resolveLocalizedModelName(item.model, $i18n.language),
tags: (item.model?.tags ?? []).map((tag) => tag.name).join(' '),
desc: item.model?.info?.meta?.description
desc: resolveLocalizedModelDescription(item.model, $i18n.language)
};
return _item;
}),
@@ -306,9 +310,9 @@
items.map((item) => {
const _item = {
...item,
modelName: item.model?.name,
modelName: resolveLocalizedModelName(item.model, $i18n.language),
tags: (item.model?.tags ?? []).map((tag) => tag.name).join(' '),
desc: item.model?.info?.meta?.description
desc: resolveLocalizedModelDescription(item.model, $i18n.language)
};
return _item;
})
+5 -1
View File
@@ -7,6 +7,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Heart from '$lib/components/icons/Heart.svelte';
import { getOutputText } from '../Messages/structuredOutput';
import { resolveLocalizedModelName } from '$lib/utils/localizedContent';
const i18n = getContext('i18n');
@@ -17,6 +18,9 @@
getOutputText(nodeData?.message?.output) || nodeData?.message?.content || '';
$: messageContent = getMessageContent(data);
$: modelName = data?.model
? resolveLocalizedModelName(data.model, $i18n.language)
: (data?.message?.model ?? 'Assistant');
</script>
<div
@@ -57,7 +61,7 @@
<div class="ml-2 flex-1 min-w-0">
<div class=" flex justify-between items-center">
<div class="text-xs text-black dark:text-white font-normal line-clamp-1">
{data?.model?.name ?? data?.message?.model ?? 'Assistant'}
{modelName}
</div>
<button
+28 -22
View File
@@ -19,6 +19,12 @@
} from '$lib/stores';
import { refreshChatList, refreshFolderChatLists } from '$lib/stores/chatList';
import { sanitizeResponseContent, extractCurlyBraceWords } from '$lib/utils';
import {
resolveLocalizedModelDescription,
resolveLocalizedModelName,
resolveLocalizedModelPromptSuggestions,
resolveLocalizedPromptSuggestions
} from '$lib/utils/localizedContent';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import Suggestions from './Suggestions.svelte';
@@ -80,12 +86,27 @@
let models = [];
let selectedModelIdx = 0;
let selectedModel;
let selectedModelName = '';
let selectedModelDescription = '';
let selectedSuggestionPrompts = [];
$: if (selectedModels.length > 0) {
selectedModelIdx = models.length - 1;
}
$: models = selectedModels.map((id) => $_models.find((m) => m.id === id));
$: selectedModel = atSelectedModel ?? models[selectedModelIdx];
$: selectedModelName = resolveLocalizedModelName(selectedModel, $i18n.language);
$: selectedModelDescription = resolveLocalizedModelDescription(selectedModel, $i18n.language);
$: selectedSuggestionPrompts =
resolveLocalizedModelPromptSuggestions(atSelectedModel, $i18n.language) ??
resolveLocalizedModelPromptSuggestions(models[selectedModelIdx], $i18n.language) ??
resolveLocalizedPromptSuggestions(
$config?.default_prompt_suggestions ?? [],
$config?.default_prompt_suggestions_i18n ?? {},
$i18n.language
);
// True when viewing a shared folder the current user doesn't own AND lacks write access
$: folderReadOnly =
@@ -164,14 +185,10 @@
class=" text-2xl @sm:text-2xl line-clamp-1 flex items-center"
in:fade={{ duration: 100 }}
>
{#if models[selectedModelIdx]?.name}
<Tooltip
content={models[selectedModelIdx]?.name}
placement="top"
className=" flex items-center "
>
{#if selectedModelName}
<Tooltip content={selectedModelName} placement="top" className=" flex items-center ">
<span class="line-clamp-1">
{models[selectedModelIdx]?.name}
{selectedModelName}
</span>
</Tooltip>
{:else}
@@ -182,14 +199,12 @@
<div class="flex mt-1 mb-2">
<div in:fade={{ duration: 100, delay: 50 }}>
{#if models[selectedModelIdx]?.info?.meta?.description ?? null}
{#if selectedModelDescription}
<Tooltip
className=" w-fit"
content={DOMPurify.sanitize(
marked.parse(
sanitizeResponseContent(
models[selectedModelIdx]?.info?.meta?.description ?? ''
).replaceAll('\n', '<br>')
sanitizeResponseContent(selectedModelDescription).replaceAll('\n', '<br>')
)
)}
placement="top"
@@ -199,9 +214,7 @@
>
{@html DOMPurify.sanitize(
marked.parse(
sanitizeResponseContent(
models[selectedModelIdx]?.info?.meta?.description ?? ''
).replaceAll('\n', '<br>')
sanitizeResponseContent(selectedModelDescription).replaceAll('\n', '<br>')
)
)}
</div>
@@ -279,14 +292,7 @@
{:else}
<div class="mx-auto max-w-2xl mt-2" in:fade={{ duration: 200, delay: 200 }}>
<div class="mx-5">
<Suggestions
suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config?.default_prompt_suggestions ??
[]}
inputValue={prompt}
{onSelect}
/>
<Suggestions suggestionPrompts={selectedSuggestionPrompts} inputValue={prompt} {onSelect} />
</div>
</div>
{/if}
@@ -4,6 +4,7 @@
const i18n = getContext('i18n');
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { resolveLocalizedModelName } from '$lib/utils/localizedContent';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import PinSlash from './icons/PinSlash.svelte';
@@ -14,6 +15,7 @@
export let onUnpin = () => {};
let mouseOver = false;
$: localizedModelName = resolveLocalizedModelName(model, $i18n.language);
</script>
{#if model}
@@ -50,7 +52,7 @@
<div class="flex self-center translate-y-[0.5px]">
<div class=" self-center text-[0.8125rem] leading-5 line-clamp-1">
{model?.name ?? model.id}
{localizedModelName}
</div>
</div>
</a>
+15 -4
View File
@@ -39,6 +39,10 @@
import { updateUserSettings } from '$lib/apis/users';
import { capitalizeFirstLetter, copyToClipboard } from '$lib/utils';
import {
resolveLocalizedModelDescription,
resolveLocalizedModelName
} from '$lib/utils/localizedContent';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
import CheckCircle from '../icons/CheckCircle.svelte';
@@ -691,6 +695,7 @@
<div class="grid gap-y-0.5">
{#each models as model (model.id)}
{@const localizedModelName = resolveLocalizedModelName(model, $i18n.language)}
<div
class="group flex min-h-8 w-full items-center gap-2 overflow-hidden rounded-xl px-2 py-1 text-left {model.write_access
? 'cursor-pointer'
@@ -732,12 +737,16 @@
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
<div class="flex min-w-0 items-center gap-2 overflow-hidden">
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
<Tooltip content={model.name} className="min-w-0" placement="top-start">
<Tooltip
content={localizedModelName}
className="min-w-0"
placement="top-start"
>
<a
href={`/?model=${encodeURIComponent(model.id)}`}
class="block truncate text-[0.8125rem] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
>
{model.name}
{localizedModelName}
</a>
</Tooltip>
@@ -762,7 +771,9 @@
</div>
<Tooltip
content={(model?.meta?.description ?? '').trim() ||
content={(
resolveLocalizedModelDescription(model, $i18n.language) ?? ''
).trim() ||
model.base_model_id ||
$i18n.t('No description')}
className="min-w-0"
@@ -771,7 +782,7 @@
<div
class="truncate text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-600"
>
{(model?.meta?.description ?? '').trim() ||
{(resolveLocalizedModelDescription(model, $i18n.language) ?? '').trim() ||
model.base_model_id ||
$i18n.t('No description')}
</div>
@@ -0,0 +1,37 @@
<script lang="ts">
import SettingsSelect from '$lib/components/common/SettingsSelect.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { getContext } from 'svelte';
const i18n = getContext<any>('i18n');
export let value = '';
export let languages: { code: string; title: string }[] = [];
export let translatedLocales: string[] = [];
export let className = '';
$: options = languages.filter((language) => language.code !== 'en-US');
const statusFor = (code: string) => (translatedLocales.includes(code) ? ' *' : '');
</script>
<Tooltip
content={$i18n.t('Editing language')}
className="relative {className || 'w-full sm:w-fit'}"
>
<GlobeAlt
className="pointer-events-none absolute start-2.5 top-1/2 z-10 size-3.5 -translate-y-1/2 text-gray-400 dark:text-gray-500"
/>
<SettingsSelect
bind:value
className="w-full"
selectClassName="min-w-[8.5rem] !ps-8"
ariaLabel={$i18n.t('Editing language')}
>
<option value="">{$i18n.t('Default')}</option>
{#each options as language}
<option value={language.code}>{language.title}{statusFor(language.code)}</option>
{/each}
</SettingsSelect>
</Tooltip>
@@ -0,0 +1,81 @@
<script lang="ts">
import { getContext } from 'svelte';
import PromptSuggestions from './PromptSuggestions.svelte';
const i18n = getContext<any>('i18n');
export let promptSuggestions = [];
export let localizedPromptSuggestions = {};
export let locale = '';
export let localeLabel = '';
export let onChange = () => {};
const clone = (value) => JSON.parse(JSON.stringify(value ?? []));
$: hasCustomLocalePrompts =
locale !== '' && Array.isArray(localizedPromptSuggestions?.[locale]?.suggestion_prompts);
$: localePrompts = clone(
hasCustomLocalePrompts
? localizedPromptSuggestions[locale].suggestion_prompts
: promptSuggestions
);
const updateLocalePrompts = (suggestions) => {
localizedPromptSuggestions = {
...(localizedPromptSuggestions ?? {}),
[locale]: {
...(localizedPromptSuggestions?.[locale] ?? {}),
suggestion_prompts: suggestions
}
};
onChange();
};
const useDefault = () => {
const next = { ...(localizedPromptSuggestions ?? {}) };
if (next[locale]) {
next[locale] = { ...next[locale] };
delete next[locale].suggestion_prompts;
if (Object.keys(next[locale]).length === 0) {
delete next[locale];
}
}
localizedPromptSuggestions = next;
onChange();
};
</script>
{#if !locale}
<PromptSuggestions bind:promptSuggestions onChange={() => onChange()}>
<span slot="label"></span>
<svelte:fragment slot="actions"><slot name="language" /></svelte:fragment>
</PromptSuggestions>
{:else}
<PromptSuggestions
promptSuggestions={localePrompts}
inherited={!hasCustomLocalePrompts}
onChange={updateLocalePrompts}
>
<span
slot="label"
title={localeLabel}
class="flex flex-wrap items-center gap-x-3 gap-y-1 text-[0.6875rem] text-gray-400 dark:text-gray-600"
>
{hasCustomLocalePrompts ? $i18n.t('Translated') : $i18n.t('Using default')}
{#if hasCustomLocalePrompts}
<button
type="button"
class="text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
aria-label={$i18n.t('Use default')}
on:click={useDefault}
>
{$i18n.t('Use default')}
</button>
{/if}
</span>
<svelte:fragment slot="actions">
<slot name="language" />
</svelte:fragment>
</PromptSuggestions>
{/if}
@@ -9,6 +9,7 @@
import { getSkills } from '$lib/apis/skills';
import { getFunctions } from '$lib/apis/functions';
import { getModelsDefaults } from '$lib/apis/configs';
import { getLanguages } from '$lib/i18n';
import { getBaseModelTags, getModelTags } from '$lib/apis/models';
import { getVoices } from '$lib/apis/audio';
@@ -28,12 +29,15 @@
import DefaultFiltersSelector from './DefaultFiltersSelector.svelte';
import DefaultFeatures from './DefaultFeatures.svelte';
import BuiltinTools from './BuiltinTools.svelte';
import LanguageModeSelect from './LanguageModeSelect.svelte';
import LocalizedPromptSuggestions from './LocalizedPromptSuggestions.svelte';
import PromptSuggestions from './PromptSuggestions.svelte';
import TerminalSelector from './TerminalSelector.svelte';
import TTSVoiceInput from './TTSVoiceInput.svelte';
import AccessControlModal from '../common/AccessControlModal.svelte';
import AccessButton from '$lib/components/common/AccessButton.svelte';
import { extractInputVariables } from '$lib/utils';
import { pruneEmptyLocaleEntries } from '$lib/utils/localizedContent';
const i18n: any = getContext('i18n');
@@ -63,6 +67,8 @@
let id = '';
let name = '';
let languages: { code: string; title: string }[] = [];
let editingLocale = '';
let enableDescription = true;
@@ -86,6 +92,7 @@
// https://docs.openwebui.com/license.
profile_image_url: `${WEBUI_BASE_URL}/static/favicon.png`,
description: '',
i18n: {},
suggestion_prompts: null,
tags: []
},
@@ -117,6 +124,42 @@
export let suggestionTags: { name: string }[] = [];
let voices: { id: string; name?: string }[] = [];
$: translatedLocales = Object.entries(info?.meta?.i18n ?? {})
.filter(([_, value]: [string, any]) => Object.keys(value ?? {}).length > 0)
.map(([locale]) => locale);
$: editingLocaleLabel = languages.find((language) => language.code === editingLocale)?.title;
const localizedField = (field: string) => info?.meta?.i18n?.[editingLocale]?.[field] ?? '';
const setLocalizedField = (field: string, value: string) => {
if (!editingLocale) return;
info.meta.i18n = {
...(info.meta.i18n ?? {}),
[editingLocale]: {
...(info.meta.i18n?.[editingLocale] ?? {}),
[field]: value
}
};
info = info;
};
const clearLocalizedField = (field: string) => {
if (!editingLocale || !info.meta.i18n?.[editingLocale]) return;
const nextLocale = { ...info.meta.i18n[editingLocale] };
delete nextLocale[field];
const nextI18n = { ...(info.meta.i18n ?? {}) };
if (Object.keys(nextLocale).length === 0) {
delete nextI18n[editingLocale];
} else {
nextI18n[editingLocale] = nextLocale;
}
info.meta.i18n = nextI18n;
info = info;
};
const chatVariableKeyRegex = /^[a-z][a-z0-9_]*$/;
const getChatVariablesPreview = (prompt: string) => {
const variables = extractInputVariables(prompt);
@@ -336,6 +379,11 @@
}
}
info.meta.i18n = pruneEmptyLocaleEntries(info.meta.i18n);
if (Object.keys(info.meta.i18n).length === 0) {
delete info.meta.i18n;
}
if (terminalId) {
info.meta.terminalId = terminalId;
} else {
@@ -375,6 +423,7 @@
};
onMount(async () => {
languages = await getLanguages();
await tools.set((await getTools(localStorage.token).catch(() => null)) ?? []);
skillsList = (await getSkills(localStorage.token).catch(() => null)) ?? [];
if (!$functions) {
@@ -483,6 +532,7 @@
)
)
};
info.meta.i18n = info.meta.i18n ?? {};
console.log(model);
}
@@ -676,11 +726,28 @@
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-2">
<input
class="min-w-0 flex-1 bg-transparent text-base leading-tight text-gray-900 outline-hidden placeholder:text-gray-300 dark:text-white dark:placeholder:text-gray-700 md:text-lg"
placeholder={$i18n.t('Model Name')}
bind:value={name}
required
{#if editingLocale}
<input
class="min-w-0 flex-1 bg-transparent text-base leading-tight text-gray-900 outline-hidden placeholder:text-gray-300 dark:text-white dark:placeholder:text-gray-700 md:text-lg"
placeholder={name || $i18n.t('Model Name')}
value={localizedField('name')}
on:input={(e) =>
setLocalizedField('name', (e.currentTarget as HTMLInputElement).value)}
/>
{:else}
<input
class="min-w-0 flex-1 bg-transparent text-base leading-tight text-gray-900 outline-hidden placeholder:text-gray-300 dark:text-white dark:placeholder:text-gray-700 md:text-lg"
placeholder={$i18n.t('Model Name')}
bind:value={name}
required
/>
{/if}
<LanguageModeSelect
bind:value={editingLocale}
{languages}
{translatedLocales}
className="hidden w-fit sm:inline-flex"
/>
<AccessButton
@@ -690,6 +757,28 @@
/>
</div>
{#if editingLocale}
<div class="mt-1 flex items-center justify-between gap-3 text-[0.6875rem]">
<span class="truncate text-gray-400 dark:text-gray-600">
{localizedField('name')
? $i18n.t('Translated')
: $i18n.t('Using default')}
</span>
<div
class="flex shrink-0 items-center gap-2 text-gray-500 dark:text-gray-400"
>
<button type="button" on:click={() => setLocalizedField('name', name)}>
{$i18n.t('Copy default')}
</button>
{#if localizedField('name')}
<button type="button" on:click={() => clearLocalizedField('name')}>
{$i18n.t('Use default')}
</button>
{/if}
</div>
</div>
{/if}
<input
class="block w-full bg-transparent py-0.5 text-xs text-gray-500 outline-hidden placeholder:text-gray-300 dark:text-gray-500 dark:placeholder:text-gray-700"
placeholder={$i18n.t('Model ID')}
@@ -697,6 +786,14 @@
disabled={edit}
required
/>
<div class="mt-1 sm:hidden">
<LanguageModeSelect
bind:value={editingLocale}
{languages}
{translatedLocales}
/>
</div>
</div>
</div>
</div>
@@ -723,29 +820,63 @@
<div>
<div class="mb-1 flex w-full items-center justify-between">
<div class="self-center text-xs text-gray-400 dark:text-gray-600">
{$i18n.t('Description')}
{editingLocale
? $i18n.t('Description ({{language}})', {
language: editingLocaleLabel || editingLocale
})
: $i18n.t('Description')}
</div>
<button
class="text-xs text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
type="button"
aria-pressed={enableDescription ? 'true' : 'false'}
aria-label={enableDescription
? $i18n.t('Custom description enabled')
: $i18n.t('Default description enabled')}
on:click={() => {
enableDescription = !enableDescription;
}}
>
{#if !enableDescription}
<span>{$i18n.t('Default')}</span>
{:else}
<span>{$i18n.t('Custom')}</span>
{/if}
</button>
{#if editingLocale}
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
<button
type="button"
on:click={() =>
setLocalizedField('description', info.meta.description ?? '')}
>
{$i18n.t('Copy default')}
</button>
{#if localizedField('description')}
<button type="button" on:click={() => clearLocalizedField('description')}>
{$i18n.t('Use default')}
</button>
{/if}
</div>
{:else}
<button
class="text-xs text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
type="button"
aria-pressed={enableDescription ? 'true' : 'false'}
aria-label={enableDescription
? $i18n.t('Custom description enabled')
: $i18n.t('Default description enabled')}
on:click={() => {
enableDescription = !enableDescription;
}}
>
{#if !enableDescription}
<span>{$i18n.t('Default')}</span>
{:else}
<span>{$i18n.t('Custom')}</span>
{/if}
</button>
{/if}
</div>
{#if enableDescription}
{#if editingLocale}
<Textarea
className="w-full resize-none overflow-y-hidden bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
placeholder={info.meta.description ||
$i18n.t('Add a short description about what this model does')}
minSize={32}
value={localizedField('description')}
onInput={(e) =>
setLocalizedField(
'description',
(e.currentTarget as HTMLTextAreaElement).value
)}
/>
{:else if enableDescription}
<Textarea
className="w-full resize-none overflow-y-hidden bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
placeholder={$i18n.t('Add a short description about what this model does')}
@@ -889,26 +1020,35 @@
{$i18n.t('Prompts')}
</div>
<button
class="text-xs text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
type="button"
on:click={() => {
if ((info?.meta?.suggestion_prompts ?? null) === null) {
info.meta.suggestion_prompts = [{ content: '', title: ['', ''] }];
} else {
info.meta.suggestion_prompts = null;
}
}}
>
{#if (info?.meta?.suggestion_prompts ?? null) === null}
<span>{$i18n.t('Default')}</span>
{:else}
<span>{$i18n.t('Custom')}</span>
{/if}
</button>
{#if !editingLocale}
<button
class="text-xs text-gray-500 transition hover:text-gray-700 dark:hover:text-gray-300"
type="button"
on:click={() => {
if ((info?.meta?.suggestion_prompts ?? null) === null) {
info.meta.suggestion_prompts = [{ content: '', title: ['', ''] }];
} else {
info.meta.suggestion_prompts = null;
}
}}
>
{#if (info?.meta?.suggestion_prompts ?? null) === null}
<span>{$i18n.t('Default')}</span>
{:else}
<span>{$i18n.t('Custom')}</span>
{/if}
</button>
{/if}
</div>
{#if info?.meta?.suggestion_prompts}
{#if editingLocale}
<LocalizedPromptSuggestions
promptSuggestions={info.meta.suggestion_prompts ?? []}
bind:localizedPromptSuggestions={info.meta.i18n}
locale={editingLocale}
localeLabel={editingLocaleLabel}
/>
{:else if info?.meta?.suggestion_prompts}
<PromptSuggestions bind:promptSuggestions={info.meta.suggestion_prompts} />
{/if}
</section>
@@ -8,18 +8,23 @@
const i18n = getContext('i18n');
export let promptSuggestions = [];
export let onChange = (suggestions) => {};
export let inherited = false;
let _promptSuggestions = [];
let importInput: HTMLInputElement;
const toolClass =
'flex size-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition hover:bg-black/5 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-200 disabled:opacity-40';
const updateSuggestions = (suggestions) => {
promptSuggestions = suggestions;
onChange(suggestions);
};
const setPromptSuggestions = () => {
_promptSuggestions = promptSuggestions.map((s) => {
if (typeof s.title === 'string') {
s.title = [s.title, ''];
} else if (!Array.isArray(s.title)) {
s.title = ['', ''];
}
return s;
});
_promptSuggestions = promptSuggestions.map((s) => ({
...s,
title: typeof s.title === 'string' ? [s.title, ''] : [...(s.title ?? ['', ''])]
}));
};
const autosize = (node: HTMLTextAreaElement) => {
@@ -45,14 +50,15 @@
</script>
<div class="space-y-2">
<div class="mb-1 flex h-6 w-full items-center justify-between">
<div class="min-w-0 flex-1 self-center text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Default Prompt Suggestions')}
<div class="mb-1 flex min-h-7 w-full flex-wrap items-center justify-between gap-x-2 gap-y-1">
<div class="min-w-fit flex-1 self-center text-xs text-gray-500 dark:text-gray-400">
<slot name="label">{$i18n.t('Default Prompt Suggestions')}</slot>
</div>
<div class="flex shrink-0 items-center justify-end gap-1.5">
<div class="ms-auto flex max-w-full shrink-0 items-center justify-end gap-1">
<slot name="actions" />
<input
id="prompt-suggestions-import-input"
bind:this={importInput}
type="file"
accept=".json"
hidden
@@ -62,12 +68,22 @@
return;
}
console.log(files);
let reader = new FileReader();
reader.onload = async (event) => {
try {
let suggestions = JSON.parse(event.target.result);
if (
!Array.isArray(suggestions) ||
suggestions.some(
(s) =>
!s ||
typeof s.content !== 'string' ||
(s.title != null &&
typeof s.title !== 'string' &&
(!Array.isArray(s.title) || s.title.some((part) => typeof part !== 'string')))
)
)
throw new Error('Invalid prompt suggestions');
suggestions = suggestions.map((s) => {
if (typeof s.title === 'string') {
@@ -79,7 +95,7 @@
return s;
});
promptSuggestions = [...promptSuggestions, ...suggestions];
updateSuggestions([...promptSuggestions, ...suggestions]);
} catch (error) {
toast.error($i18n.t('Invalid JSON file'));
return;
@@ -93,24 +109,20 @@
/>
<button
class="flex items-center rounded-xl bg-transparent px-1 py-0.5 text-xs text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
class="shrink-0 px-1 py-0.5 text-xs text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
type="button"
on:click={() => {
const input = document.getElementById('prompt-suggestions-import-input');
if (input) {
input.click();
}
}}
aria-label={$i18n.t('Import')}
on:click={() => importInput.click()}
>
<div class="line-clamp-1 self-center font-normal">
{$i18n.t('Import')}
</div>
{$i18n.t('Import')}
</button>
{#if promptSuggestions.length}
<button
class="flex items-center rounded-xl bg-transparent px-1 py-0.5 text-xs text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
class="shrink-0 px-1 py-0.5 text-xs text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
type="button"
aria-label={$i18n.t('Export')}
disabled={!promptSuggestions.length}
on:click={async () => {
let blob = new Blob([JSON.stringify(promptSuggestions)], {
type: 'application/json'
@@ -118,24 +130,24 @@
saveAs(blob, `prompt-suggestions-export-${Date.now()}.json`);
}}
>
<div class="line-clamp-1 self-center font-normal">
{$i18n.t('Export')}
</div>
{$i18n.t('Export')}
</button>
{/if}
<button
class="flex size-6 items-center justify-center text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
type="button"
aria-label={$i18n.t('Add prompt suggestion')}
on:click={() => {
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
}
}}
>
<Plus className="size-3.5" strokeWidth="2.25" />
</button>
<Tooltip content={$i18n.t('Add prompt suggestion')}>
<button
class={toolClass}
type="button"
aria-label={$i18n.t('Add prompt suggestion')}
on:click={() => {
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
updateSuggestions([...promptSuggestions, { content: '', title: ['', ''] }]);
}
}}
>
<Plus className="size-3.5" strokeWidth="2.25" />
</button>
</Tooltip>
</div>
</div>
@@ -143,7 +155,9 @@
<div class="flex flex-col gap-1.5">
{#each _promptSuggestions as prompt, promptIdx}
<div
class="flex gap-1 rounded-lg border border-gray-100/40 bg-transparent px-2 py-1 dark:border-gray-850/50"
class="flex 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 {inherited
? 'opacity-60 focus-within:opacity-100'
: ''}"
>
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<div class="grid min-w-0 gap-1 md:grid-cols-2 md:gap-1.5">
@@ -152,7 +166,11 @@
class="w-full 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"
placeholder={$i18n.t('Title')}
aria-label={$i18n.t('Title')}
bind:value={prompt.title[0]}
value={prompt.title[0]}
on:input={(e) => {
prompt.title[0] = e.currentTarget.value;
updateSuggestions(_promptSuggestions);
}}
/>
</Tooltip>
@@ -161,7 +179,11 @@
class="w-full bg-transparent text-[0.8125rem] leading-5 text-gray-500 outline-hidden placeholder:text-gray-300 dark:text-gray-500 dark:placeholder:text-gray-700"
placeholder={$i18n.t('Subtitle')}
aria-label={$i18n.t('Subtitle')}
bind:value={prompt.title[1]}
value={prompt.title[1]}
on:input={(e) => {
prompt.title[1] = e.currentTarget.value;
updateSuggestions(_promptSuggestions);
}}
/>
</Tooltip>
</div>
@@ -177,8 +199,12 @@
aria-label={$i18n.t('Content')}
rows="1"
use:autosize={prompt.content}
bind:value={prompt.content}
/>
value={prompt.content}
on:input={(e) => {
prompt.content = e.currentTarget.value;
updateSuggestions(_promptSuggestions);
}}
></textarea>
</Tooltip>
</div>
@@ -187,8 +213,7 @@
type="button"
aria-label={$i18n.t('Remove prompt suggestion')}
on:click={() => {
promptSuggestions.splice(promptIdx, 1);
promptSuggestions = promptSuggestions;
updateSuggestions(promptSuggestions.filter((_, index) => index !== promptIdx));
}}
>
<XMark className="size-3.5" />
+1
View File
@@ -332,6 +332,7 @@ type Config = {
default_models: string;
default_pinned_models?: string | null;
default_prompt_suggestions: PromptSuggestion[];
default_prompt_suggestions_i18n?: Record<string, { suggestion_prompts: PromptSuggestion[] }>;
features: {
auth: boolean;
auth_trusted_header: boolean;
+87
View File
@@ -0,0 +1,87 @@
type LocaleEntry = Record<string, any>;
type LocalizedMap = Record<string, LocaleEntry>;
export type PromptSuggestion = {
title: [string, string] | string[];
content: string;
[key: string]: any;
};
const isPresent = (value: unknown) => typeof value === 'string' && value.trim() !== '';
export const getLocaleCandidates = (locale?: string | null) => {
if (!locale) return [];
const normalized = locale.trim();
if (!normalized) return [];
const base = normalized.split('-')[0];
return base && base !== normalized ? [normalized, base] : [normalized];
};
export const resolveLocalizedString = (
fallback: string | null | undefined,
i18n: LocalizedMap | null | undefined,
locale: string | null | undefined,
key: string
) => {
for (const candidate of getLocaleCandidates(locale)) {
const value = i18n?.[candidate]?.[key];
if (isPresent(value)) return value;
}
return fallback ?? '';
};
export const resolveLocalizedModelName = (model: any, locale?: string | null) => {
const meta = model?.info?.meta ?? model?.meta;
const info = model?.info ?? model;
return resolveLocalizedString(model?.name ?? info?.name ?? model?.id, meta?.i18n, locale, 'name');
};
export const resolveLocalizedModelDescription = (model: any, locale?: string | null) => {
const meta = model?.info?.meta ?? model?.meta;
return resolveLocalizedString(meta?.description, meta?.i18n, locale, 'description');
};
export const resolveLocalizedPromptSuggestions = (
fallback: PromptSuggestion[] | null | undefined,
i18n: LocalizedMap | null | undefined,
locale?: string | null
) => {
for (const candidate of getLocaleCandidates(locale)) {
const prompts = i18n?.[candidate]?.suggestion_prompts ?? i18n?.[candidate];
if (Array.isArray(prompts)) return prompts;
}
return fallback ?? [];
};
export const resolveLocalizedModelPromptSuggestions = (model: any, locale?: string | null) => {
const meta = model?.info?.meta ?? model?.meta;
for (const candidate of getLocaleCandidates(locale)) {
const prompts = meta?.i18n?.[candidate]?.suggestion_prompts;
if (Array.isArray(prompts)) return prompts;
}
return meta?.suggestion_prompts ?? null;
};
export const pruneEmptyLocaleEntries = (i18n: LocalizedMap | null | undefined) => {
if (!i18n) return {};
return Object.fromEntries(
Object.entries(i18n)
.map(([locale, entry]) => [
locale,
Object.fromEntries(
Object.entries(entry ?? {}).filter(([_, value]) => {
if (typeof value === 'string') return value.trim() !== '';
if (Array.isArray(value)) return true;
return value !== null && value !== undefined;
})
)
])
.filter(([_, entry]) => Object.keys(entry as LocaleEntry).length > 0)
);
};