mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-07 02:58:21 +00:00
refac
This commit is contained in:
parent
d3913657df
commit
106aae70e9
16 changed files with 711 additions and 85 deletions
|
|
@ -263,6 +263,7 @@ from open_webui.utils.models import (
|
|||
get_all_models,
|
||||
get_filtered_models,
|
||||
)
|
||||
from open_webui.utils.payload import apply_model_controls
|
||||
from open_webui.utils.oauth import (
|
||||
OAuthClientInformationFull,
|
||||
OAuthClientManager,
|
||||
|
|
@ -1192,7 +1193,19 @@ async def chat_completion(
|
|||
default_model_params,
|
||||
model_info.params.model_dump() if model_info and model_info.params else {},
|
||||
)
|
||||
model_info_params.pop('model_controls', None)
|
||||
request_params = {key: value for key, value in (form_data.get('params') or {}).items() if value is not None}
|
||||
model_controls = request_params.pop('model_controls', {})
|
||||
if not isinstance(model_controls, dict):
|
||||
raise HTTPException(400, 'Model control selections must be keyed by model.')
|
||||
model_controls = {} if form_data.get('automation_id') else model_controls
|
||||
if any(model_controls.values()) and user.role != 'admin':
|
||||
permissions = await Config.get('user.permissions')
|
||||
for permission in ('chat.controls', 'chat.params'):
|
||||
if not await has_permission(user.id, permission, permissions):
|
||||
raise HTTPException(403, 'You cannot change model parameters.')
|
||||
if missing_base_model and model_controls.get(model_id):
|
||||
raise HTTPException(400, 'Model control selections cannot be applied to the fallback model.')
|
||||
if model_info_params or request_params:
|
||||
form_data['params'] = merge_model_params(model_info_params, request_params)
|
||||
|
||||
|
|
@ -1313,6 +1326,7 @@ async def chat_completion(
|
|||
or 'native'
|
||||
),
|
||||
'tool_approval_mode': tool_approval_mode,
|
||||
'model_controls': model_controls,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1670,6 +1684,15 @@ async def chat_completion(
|
|||
error_detail = None
|
||||
try:
|
||||
try:
|
||||
if not metadata.get('direct'):
|
||||
target = (
|
||||
model_info if form_data['model'] == model_id
|
||||
else await Models.get_model_by_id(form_data['model'])
|
||||
)
|
||||
controls = target.params.model_dump().get('model_controls', {}) if target else {}
|
||||
form_data['params'] = apply_model_controls(
|
||||
copy.deepcopy(form_data.get('params') or {}), controls, model_controls.get(form_data['model'], {})
|
||||
)
|
||||
ctx = None
|
||||
# Saved chats load the message after approved tool calls run, so their results are kept
|
||||
if metadata.get('assistant_message_id') and not is_saved_chat_id(metadata.get('chat_id')):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
from typing import Annotated, Any
|
||||
|
||||
from open_webui.internal.db import Base, JSONField, get_async_db_context
|
||||
from open_webui.models.access_grants import AccessGrantModel, AccessGrants
|
||||
|
|
@ -11,7 +12,7 @@ from open_webui.models.groups import Groups
|
|||
from open_webui.models.users import User, UserModel, UserResponse, Users
|
||||
from open_webui.utils.misc import json_text_variants
|
||||
from open_webui.utils.validate import validate_image_url
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, JsonValue, ValidationInfo, field_validator, model_validator
|
||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, cast, delete, func, or_, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
|
@ -65,11 +66,38 @@ def strip_extracted_content_from_model_knowledge(knowledge: Any) -> Any:
|
|||
# --- Models DB Schema ---
|
||||
|
||||
|
||||
ModelControlKey = Annotated[str, Field(pattern=re.compile(r'^(?!(?:constructor|prototype)\Z)[a-zA-Z][a-zA-Z0-9_-]*\Z'))]
|
||||
|
||||
|
||||
class ModelControlOption(BaseModel):
|
||||
model_config = ConfigDict(allow_inf_nan=False)
|
||||
|
||||
label: str = Field(pattern=r'\S')
|
||||
params: dict[str, JsonValue]
|
||||
|
||||
|
||||
class ModelControl(BaseModel):
|
||||
label: str = Field(pattern=r'\S')
|
||||
description: str | None = Field(default=None, exclude_if=lambda value: value is None)
|
||||
default: str | None = Field(default=None, exclude_if=lambda value: value is None)
|
||||
options: dict[ModelControlKey, ModelControlOption] = Field(min_length=1)
|
||||
|
||||
@model_validator(mode='after')
|
||||
def check_default(self):
|
||||
if self.default is not None and self.default not in self.options:
|
||||
raise ValueError('Default must name an approved option.')
|
||||
return self
|
||||
|
||||
|
||||
class ModelParams(BaseModel):
|
||||
"""Parameters for model inference (temperature, top_p, etc.)."""
|
||||
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
model_controls: dict[ModelControlKey, ModelControl] = Field(
|
||||
default_factory=dict, exclude_if=lambda value: not value
|
||||
)
|
||||
|
||||
|
||||
class ModelMeta(BaseModel):
|
||||
"""Metadata for a workspace model entry (profile, description, tags, capabilities)."""
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from open_webui.utils.access_control import filter_allowed_access_grants, has_ac
|
|||
from open_webui.utils.access_control.files import has_access_to_file
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.chat_variables import get_chat_variables_schema
|
||||
from open_webui.utils.json_codec import JSONCodec
|
||||
from open_webui.utils.models import get_all_models
|
||||
from open_webui.utils.validate import BACKGROUND_IMAGE_MAX_BYTES, validate_background_image
|
||||
from pydantic import BaseModel, Field
|
||||
|
|
@ -57,6 +58,32 @@ log = logging.getLogger(__name__)
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
def model_response(model, user):
|
||||
if model is None:
|
||||
return None
|
||||
data = model.model_dump()
|
||||
if user.role != 'admin':
|
||||
data.get('params', {}).pop('model_controls', None)
|
||||
return data
|
||||
|
||||
|
||||
async def _check_model_controls(form, previous, user, request):
|
||||
old = previous.params.model_dump().get('model_controls', {}) if previous else {}
|
||||
if 'model_controls' not in form.params.model_fields_set:
|
||||
if old:
|
||||
form.params.model_controls = previous.params.model_controls
|
||||
return
|
||||
controls = form.params.model_dump().get('model_controls', {})
|
||||
if user.role != 'admin' and JSONCodec.dumps(controls) != JSONCodec.dumps(old):
|
||||
raise HTTPException(403, 'Only admins can change model controls.')
|
||||
if controls:
|
||||
if not request.app.state.MODELS:
|
||||
await get_all_models(request, user=user)
|
||||
base = request.app.state.MODELS.get(form.base_model_id or form.id, {})
|
||||
if 'pipe' in base or base.get('direct') or base.get('owned_by') == 'arena':
|
||||
raise HTTPException(400, 'Model controls require a server-managed provider model.')
|
||||
|
||||
|
||||
def add_chat_variables_schema(model_dict: dict) -> dict:
|
||||
system = (model_dict.get('params') or {}).get('system') if isinstance(model_dict.get('params'), dict) else None
|
||||
schema = get_chat_variables_schema(system)
|
||||
|
|
@ -217,7 +244,7 @@ async def get_models(
|
|||
# Strip profile_image_url from meta — images are served via /model/profile/image.
|
||||
items = []
|
||||
for model in result.items:
|
||||
data = add_chat_variables_schema(model.model_dump())
|
||||
data = add_chat_variables_schema(model_response(model, user))
|
||||
if data.get('meta'):
|
||||
data['meta'].pop('profile_image_url', None)
|
||||
write_access = (
|
||||
|
|
@ -352,6 +379,7 @@ async def create_new_model(
|
|||
'sharing.public_models',
|
||||
)
|
||||
|
||||
await _check_model_controls(form_data, None, user, request)
|
||||
model = await Models.insert_new_model(form_data, user.id, db=db)
|
||||
if not model:
|
||||
raise HTTPException(
|
||||
|
|
@ -366,7 +394,7 @@ async def create_new_model(
|
|||
subject_id=model.id,
|
||||
data={'name': model.name},
|
||||
)
|
||||
return model
|
||||
return model_response(model, user)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -406,7 +434,7 @@ async def export_models(
|
|||
raise HTTPException(status_code=403, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
||||
exported = []
|
||||
for model in models:
|
||||
data = model.model_dump()
|
||||
data = model_response(model, user)
|
||||
url = model.meta.background_image_url
|
||||
if url:
|
||||
try:
|
||||
|
|
@ -602,6 +630,7 @@ async def import_models(
|
|||
)
|
||||
imported_model = new_model
|
||||
|
||||
await _check_model_controls(imported_model, existing_model, user, request)
|
||||
uploaded = None
|
||||
try:
|
||||
encoded = model_data.pop('background_image_data', None)
|
||||
|
|
@ -692,6 +721,7 @@ async def sync_models(
|
|||
existing = {model.id: model for model in await Models.get_models_by_ids([m.id for m in form_data.models], db=db)}
|
||||
for model in form_data.models:
|
||||
previous = existing.get(model.id)
|
||||
await _check_model_controls(model, previous, user, request)
|
||||
if previous and 'background_image_url' not in model.meta.model_fields_set:
|
||||
model.meta.background_image_url = previous.meta.background_image_url
|
||||
await _verify_background_image(
|
||||
|
|
@ -741,7 +771,7 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes
|
|||
permission='read',
|
||||
db=db,
|
||||
):
|
||||
model_dict = model.model_dump()
|
||||
model_dict = model_response(model, user)
|
||||
model_dict = add_chat_variables_schema(model_dict)
|
||||
# Strip params (system prompt and other admin-curated config)
|
||||
# for read-only callers — matches the params strip already
|
||||
|
|
@ -909,7 +939,7 @@ async def toggle_model_by_id(
|
|||
subject_type='model',
|
||||
data={'name': model.name},
|
||||
)
|
||||
return model
|
||||
return model_response(model, user)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -1012,6 +1042,7 @@ async def update_model_by_id(
|
|||
'sharing.public_models',
|
||||
)
|
||||
|
||||
await _check_model_controls(form_data, model, user, request)
|
||||
model = await Models.update_model_by_id(form_data.id, ModelForm(**form_data.model_dump()), db=db)
|
||||
if model:
|
||||
await publish_event(
|
||||
|
|
@ -1021,7 +1052,7 @@ async def update_model_by_id(
|
|||
subject_id=model.id,
|
||||
data={'name': model.name},
|
||||
)
|
||||
return model
|
||||
return model_response(model, user)
|
||||
|
||||
|
||||
############################
|
||||
|
|
@ -1103,7 +1134,7 @@ async def update_model_access_by_id(
|
|||
actor=user,
|
||||
subject_id=form_data.id,
|
||||
)
|
||||
return model
|
||||
return model_response(model, user)
|
||||
|
||||
|
||||
############################
|
||||
|
|
|
|||
|
|
@ -32,6 +32,23 @@ log = logging.getLogger(__name__)
|
|||
BASE_MODELS_CACHE_KEY = f'{REDIS_KEY_PREFIX}:models:base'
|
||||
|
||||
|
||||
def public_model_params(params):
|
||||
controls = params.get('model_controls', {})
|
||||
return (
|
||||
{
|
||||
'model_controls': {
|
||||
key: {
|
||||
**{field: control[field] for field in ('label', 'description', 'default') if field in control},
|
||||
'options': {key: {'label': option['label']} for key, option in control['options'].items()},
|
||||
}
|
||||
for key, control in controls.items()
|
||||
}
|
||||
}
|
||||
if controls
|
||||
else {}
|
||||
)
|
||||
|
||||
|
||||
async def fetch_ollama_models(request: Request, user: UserModel = None):
|
||||
raw_ollama_models = await ollama.get_all_models(request, user=user)
|
||||
return [
|
||||
|
|
@ -209,8 +226,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
|||
action_ids.extend(model['info']['meta'].get('actionIds', []))
|
||||
filter_ids.extend(model['info']['meta'].get('filterIds', []))
|
||||
|
||||
if 'params' in model['info']:
|
||||
del model['info']['params']
|
||||
model['info']['params'] = public_model_params(model['info'].get('params', {}))
|
||||
|
||||
model['action_ids'] = action_ids
|
||||
model['filter_ids'] = filter_ids
|
||||
|
|
@ -253,9 +269,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
|||
info.setdefault('meta', {})['chat_variables_schema'] = schema
|
||||
elif isinstance(info.get('meta'), dict):
|
||||
info['meta'].pop('chat_variables_schema', None)
|
||||
if 'params' in info:
|
||||
# Remove params to avoid exposing sensitive info
|
||||
del info['params']
|
||||
info['params'] = public_model_params(info.get('params', {}))
|
||||
|
||||
model['info'] = info
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Callable, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from open_webui.utils.chat_variables import render_chat_variables, render_user_variables
|
||||
from open_webui.utils.json_codec import JSONCodec
|
||||
from open_webui.utils.misc import (
|
||||
|
|
@ -14,6 +17,22 @@ from open_webui.utils.task import prompt_template, prompt_variables_template
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def apply_model_controls(params: dict, controls: dict, selections: dict) -> dict:
|
||||
"""Expand approved choices into ordinary Custom Params before normal request processing."""
|
||||
if not isinstance(selections, dict):
|
||||
raise HTTPException(400, 'Model control selections must be an object.')
|
||||
for key, choice in selections.items():
|
||||
if key not in controls or not isinstance(choice, str) or choice not in controls[key]['options']:
|
||||
raise HTTPException(400, f'Model control {key}: the selected option is no longer available.')
|
||||
for key, control in controls.items():
|
||||
choice = selections.get(key, control.get('default'))
|
||||
if choice is not None:
|
||||
params['custom_params'] = deep_update(
|
||||
deepcopy(params.get('custom_params') or {}), deepcopy(control['options'][choice]['params'])
|
||||
)
|
||||
return params
|
||||
|
||||
|
||||
async def resolve_system_prompt(
|
||||
system: Optional[str],
|
||||
metadata: Optional[dict] = None,
|
||||
|
|
@ -96,6 +115,7 @@ def apply_params_to_form_data(form_data: dict, model: dict, params: dict | None
|
|||
'system': str,
|
||||
'note_id': str,
|
||||
'tool_approval_mode': str,
|
||||
'model_controls': dict,
|
||||
}
|
||||
|
||||
for key in list(params.keys()):
|
||||
|
|
@ -151,6 +171,7 @@ def remove_open_webui_params(params: dict) -> dict:
|
|||
'system': str,
|
||||
'note_id': str,
|
||||
'tool_approval_mode': str,
|
||||
'model_controls': dict,
|
||||
}
|
||||
|
||||
for key in list(params.keys()):
|
||||
|
|
|
|||
|
|
@ -1790,4 +1790,13 @@ export interface ModelMeta {
|
|||
background_image_url?: string | null;
|
||||
}
|
||||
|
||||
export interface ModelParams {}
|
||||
export interface ModelControl {
|
||||
label: string;
|
||||
description?: string;
|
||||
default?: string | null;
|
||||
options: Record<string, { label: string; params?: Record<string, any> }>;
|
||||
}
|
||||
|
||||
export interface ModelParams {
|
||||
model_controls?: Record<string, ModelControl>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1268,6 +1268,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<ModelEditor
|
||||
admin
|
||||
edit
|
||||
model={models.find((m) => m.id === selectedModelId)}
|
||||
preset={false}
|
||||
|
|
|
|||
294
src/lib/components/admin/Settings/Models/ModelControls.svelte
Normal file
294
src/lib/components/admin/Settings/Models/ModelControls.svelte
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
<script lang="ts">
|
||||
import { getContext, tick } from 'svelte';
|
||||
// @ts-expect-error The existing SortableJS dependency does not ship declarations.
|
||||
import Sortable from 'sortablejs';
|
||||
import type { ModelControl } from '$lib/apis';
|
||||
import CustomParams from '$lib/components/chat/Settings/Advanced/CustomParams.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import NativeSelect from '$lib/components/common/NativeSelect.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||
|
||||
const i18n: any = getContext('i18n');
|
||||
export let controls: Record<string, ModelControl> = {};
|
||||
let show = false;
|
||||
let editingKey = '';
|
||||
let label = '';
|
||||
let description = '';
|
||||
let defaultOption = '';
|
||||
let options: { id: string; key?: string; label: string; params: Record<string, any> }[] = [];
|
||||
let nameInput: HTMLInputElement;
|
||||
const actionClass =
|
||||
'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';
|
||||
const inputClass =
|
||||
'w-full min-w-0 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';
|
||||
|
||||
const keyFor = (label: string, items: object) => {
|
||||
let base = label
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_|_$/g, '');
|
||||
if (!/^[a-z]/.test(base) || ['constructor', 'prototype'].includes(base))
|
||||
base = `option_${base}`;
|
||||
let key = base;
|
||||
let index = 2;
|
||||
while (key in items) key = `${base}_${index++}`;
|
||||
return key;
|
||||
};
|
||||
const focus = (node: HTMLInputElement) => node.focus();
|
||||
const addOption = () => {
|
||||
options = [...options, { id: crypto.randomUUID(), label: '', params: {} }];
|
||||
};
|
||||
const edit = async (key = '') => {
|
||||
editingKey = key;
|
||||
const control = controls?.[key];
|
||||
label = control?.label ?? '';
|
||||
description = control?.description ?? '';
|
||||
defaultOption = control?.default ?? '';
|
||||
options = Object.entries(control?.options ?? {}).map(([key, option]) => ({
|
||||
id: key,
|
||||
key,
|
||||
label: option.label,
|
||||
params: structuredClone(option.params ?? {})
|
||||
}));
|
||||
if (!options.length) addOption();
|
||||
show = true;
|
||||
await tick();
|
||||
nameInput?.focus();
|
||||
};
|
||||
const apply = () => {
|
||||
const saved: ModelControl['options'] = {};
|
||||
// Reserve existing keys so a new option cannot overwrite a renamed option.
|
||||
const keys = Object.fromEntries(
|
||||
options.filter((option) => option.key).map((option) => [option.key!, true])
|
||||
);
|
||||
let selected: string | null = null;
|
||||
for (const option of options) {
|
||||
const key = option.key ?? keyFor(option.label, keys);
|
||||
keys[key] = true;
|
||||
saved[key] = { label: option.label.trim(), params: option.params };
|
||||
if (option.id === defaultOption) selected = key;
|
||||
}
|
||||
controls = {
|
||||
...controls,
|
||||
[editingKey || keyFor(label, controls ?? {})]: {
|
||||
label: label.trim(),
|
||||
...(description.trim() ? { description: description.trim() } : {}),
|
||||
default: selected,
|
||||
options: saved
|
||||
}
|
||||
};
|
||||
show = false;
|
||||
};
|
||||
const move = <T,>(items: T[], from: number, to: number): T[] => {
|
||||
if (to < 0 || to >= items.length) return items;
|
||||
const result = [...items];
|
||||
result.splice(to, 0, result.splice(from, 1)[0]);
|
||||
return result;
|
||||
};
|
||||
const moveControl = (from: number, to: number) => {
|
||||
controls = Object.fromEntries(move(Object.entries(controls ?? {}), from, to));
|
||||
};
|
||||
const moveOption = (from: number, to: number) => {
|
||||
options = move(options, from, to);
|
||||
};
|
||||
const keyboardMove = (
|
||||
event: KeyboardEvent,
|
||||
index: number,
|
||||
reorder: (from: number, to: number) => void
|
||||
) => {
|
||||
if (!['ArrowUp', 'ArrowDown'].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
reorder(index, index + (event.key === 'ArrowUp' ? -1 : 1));
|
||||
};
|
||||
const sortable = (node: HTMLElement, reorder: (from: number, to: number) => void) => {
|
||||
let nextSibling: ChildNode | null;
|
||||
const instance = new Sortable(node, {
|
||||
animation: 150,
|
||||
handle: '.sort-handle',
|
||||
onStart: ({ item }: { item: HTMLElement }) => (nextSibling = item.nextSibling),
|
||||
onEnd: ({
|
||||
item,
|
||||
from,
|
||||
oldIndex,
|
||||
newIndex
|
||||
}: {
|
||||
item: HTMLElement;
|
||||
from: HTMLElement;
|
||||
oldIndex: number;
|
||||
newIndex: number;
|
||||
}) => {
|
||||
// Let Svelte reconcile the keyed rows in their original DOM order.
|
||||
from.insertBefore(item, nextSibling);
|
||||
reorder(oldIndex, newIndex);
|
||||
}
|
||||
});
|
||||
return { destroy: () => instance.destroy() };
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex h-7 w-full items-center justify-between">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Model controls')}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 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"
|
||||
aria-label={$i18n.t('Add control')}
|
||||
title={$i18n.t('Add control')}
|
||||
on:click={() => edit()}><Plus className="size-3" /></button
|
||||
>
|
||||
</div>
|
||||
<p class="mb-1 text-xs leading-4 text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Let people choose approved parameter presets in chat.')}
|
||||
</p>
|
||||
<div use:sortable={moveControl}>
|
||||
{#each Object.entries(controls ?? {}) as [key, control], index (key)}
|
||||
<div class="flex items-center gap-1 py-0.5">
|
||||
<button
|
||||
type="button"
|
||||
class={`${actionClass} sort-handle cursor-grab`}
|
||||
aria-label={`${$i18n.t('Reorder')} ${control.label}`}
|
||||
title={$i18n.t('Drag or use the up and down arrow keys to reorder')}
|
||||
on:keydown={(event) => keyboardMove(event, index, moveControl)}
|
||||
><EllipsisVertical className="size-4" /></button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 flex-1 items-center justify-between gap-3 py-1 text-left text-[0.8125rem] text-gray-700 dark:text-gray-300"
|
||||
aria-label={`${$i18n.t('Edit')} ${control.label}`}
|
||||
on:click={() => edit(key)}
|
||||
>
|
||||
<span class="truncate">{control.label}</span>
|
||||
<span class="flex min-w-0 items-center gap-3 text-xs">
|
||||
<span class="truncate text-gray-400 dark:text-gray-600"
|
||||
>{$i18n.t('Default option')}: {control.options[control.default ?? '']?.label ??
|
||||
$i18n.t('None')}</span
|
||||
>
|
||||
<span class="text-gray-500 dark:text-gray-400">{$i18n.t('Edit')}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 flex shrink-0 items-center justify-center rounded text-gray-400 transition hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
aria-label={`${$i18n.t('Remove')} ${control.label}`}
|
||||
on:click={() => {
|
||||
delete controls[key];
|
||||
controls = { ...controls };
|
||||
}}><XMark className="size-3" /></button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<div class="px-5 pb-5 pt-4 dark:text-gray-200">
|
||||
<div class="mb-3 flex items-center justify-between gap-3">
|
||||
<div class="font-primary text-lg font-medium">
|
||||
{$i18n.t(editingKey ? 'Edit control' : 'Add control')}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class={actionClass}
|
||||
aria-label={$i18n.t('Close')}
|
||||
on:click={() => (show = false)}><XMark className="size-5" /></button
|
||||
>
|
||||
</div>
|
||||
<form class="space-y-2.5" on:submit|preventDefault|stopPropagation={apply}>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_minmax(0,8rem)] gap-4">
|
||||
<label class="block min-w-0 text-xs leading-4">
|
||||
<span class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Name')}</span>
|
||||
<input
|
||||
bind:this={nameInput}
|
||||
class={inputClass}
|
||||
aria-label={$i18n.t('Control name')}
|
||||
placeholder={$i18n.t('e.g. Thinking')}
|
||||
bind:value={label}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="block min-w-0 text-xs leading-4">
|
||||
<span class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Default option')}</span>
|
||||
<NativeSelect
|
||||
value={defaultOption}
|
||||
options={[
|
||||
{ value: '', label: $i18n.t('None') },
|
||||
...options
|
||||
.filter((option) => option.label.trim())
|
||||
.map((option) => ({ value: option.id, label: option.label }))
|
||||
]}
|
||||
className={`${inputClass} block`}
|
||||
on:change={(event) => (defaultOption = event.detail)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label class="block text-xs leading-4">
|
||||
<span class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Description')}</span>
|
||||
<input class={inputClass} placeholder={$i18n.t('Optional')} bind:value={description} />
|
||||
</label>
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Options')}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 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"
|
||||
aria-label={$i18n.t('Add option')}
|
||||
title={$i18n.t('Add option')}
|
||||
on:click={addOption}><Plus className="size-3" /></button
|
||||
>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100/60 dark:divide-gray-850/60" use:sortable={moveOption}>
|
||||
{#each options as option, index (option.id)}
|
||||
<div class="py-1.5">
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class={`${actionClass} sort-handle cursor-grab`}
|
||||
aria-label={`${$i18n.t('Reorder')} ${option.label || $i18n.t('option')}`}
|
||||
title={$i18n.t('Drag or use the up and down arrow keys to reorder')}
|
||||
on:keydown={(event) => keyboardMove(event, index, moveOption)}
|
||||
><EllipsisVertical className="size-4" /></button
|
||||
>
|
||||
<input
|
||||
class={inputClass}
|
||||
aria-label={$i18n.t('Option name')}
|
||||
use:focus
|
||||
placeholder={$i18n.t('Option name')}
|
||||
bind:value={option.label}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="flex shrink-0 items-center justify-center rounded text-gray-400 transition hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
aria-label={`${$i18n.t('Remove')} ${option.label || $i18n.t('option')}`}
|
||||
on:click={() => {
|
||||
options = options.filter((item) => item.id !== option.id);
|
||||
if (defaultOption === option.id) defaultOption = '';
|
||||
}}><XMark className="size-3" /></button
|
||||
>
|
||||
</div>
|
||||
<div class="pl-8"><CustomParams compact bind:value={option.params} /></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-1.5 pt-1 text-sm font-medium">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full px-3.5 py-1.5 transition hover:bg-gray-100 dark:hover:bg-gray-850"
|
||||
on:click={() => (show = false)}>{$i18n.t('Cancel')}</button
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!label.trim() ||
|
||||
!options.length ||
|
||||
options.some((option) => !option.label.trim())}
|
||||
class="rounded-full bg-black px-3.5 py-1.5 text-white transition hover:bg-gray-950 disabled:opacity-40 dark:bg-white dark:text-black dark:hover:bg-gray-100"
|
||||
>{$i18n.t('Apply')}</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -4504,6 +4504,7 @@
|
|||
class=" pb-2 {dragged ? 'z-0' : 'z-10'}"
|
||||
>
|
||||
<MessageInput
|
||||
bind:params
|
||||
bind:this={messageInput}
|
||||
{history}
|
||||
{taskIds}
|
||||
|
|
@ -4596,6 +4597,7 @@
|
|||
{/if}
|
||||
<div id={embedded ? messageInputDropzoneId : undefined} class="pb-2 z-10">
|
||||
<MessageInput
|
||||
bind:params
|
||||
bind:this={messageInput}
|
||||
{history}
|
||||
{taskIds}
|
||||
|
|
@ -4657,6 +4659,7 @@
|
|||
{:else}
|
||||
<div class="flex items-center h-full">
|
||||
<Placeholder
|
||||
bind:params
|
||||
bind:selectedModelIdx
|
||||
{history}
|
||||
bind:selectedModels
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@
|
|||
import InputMenu from './MessageInput/InputMenu.svelte';
|
||||
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
||||
import ModelSelector from './ModelSelector.svelte';
|
||||
import ModelControls from './MessageInput/ModelControls.svelte';
|
||||
|
||||
import ToolServersModal from './ToolServersModal.svelte';
|
||||
import SkillsModal from './SkillsModal.svelte';
|
||||
|
|
@ -141,6 +142,7 @@
|
|||
export let statusHandler: Function = () => {};
|
||||
export let forkHandler: Function = () => {};
|
||||
export let chatId = '';
|
||||
export let params = {};
|
||||
export let contextUsage = null;
|
||||
export let contextCompactionEnabled = false;
|
||||
export let embedded = false;
|
||||
|
|
@ -2616,6 +2618,7 @@
|
|||
triggerClassName="items-center gap-1.5 rounded-lg pl-2 pr-1.5 py-1 text-[0.8125rem] font-normal text-gray-600 transition-colors duration-100 hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-300 dark:hover:bg-gray-800/40 dark:hover:text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<ModelControls selectedModels={selectedModelIds} bind:params />
|
||||
|
||||
{#if isActive && prompt === '' && files.length === 0}
|
||||
<div class=" flex items-center">
|
||||
|
|
|
|||
159
src/lib/components/chat/MessageInput/ModelControls.svelte
Normal file
159
src/lib/components/chat/MessageInput/ModelControls.svelte
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<script lang="ts">
|
||||
import { getContext, tick } from 'svelte';
|
||||
import { models, user, mobile, settings, type Model } from '$lib/stores';
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
||||
import DropdownSub from '$lib/components/common/DropdownSub.svelte';
|
||||
import type { ModelControl } from '$lib/apis';
|
||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Knobs from '$lib/components/icons/Knobs.svelte';
|
||||
import { resolveLocalizedModelName } from '$lib/utils/localizedContent';
|
||||
|
||||
const i18n: any = getContext('i18n');
|
||||
export let selectedModels: string[] = [];
|
||||
export let params: Record<string, any> = {};
|
||||
let active: {
|
||||
model: Model;
|
||||
key: string;
|
||||
control: ModelControl;
|
||||
trigger: HTMLButtonElement;
|
||||
} | null = null;
|
||||
const back = async () => {
|
||||
const trigger = active?.trigger;
|
||||
active = null;
|
||||
await tick();
|
||||
if (trigger) document.getElementById(trigger.id)?.focus();
|
||||
};
|
||||
$: available = selectedModels
|
||||
.map((id) => $models.find((model) => model.id === id))
|
||||
.filter(
|
||||
(model): model is Model =>
|
||||
!!model &&
|
||||
!('direct' in model && model.direct) &&
|
||||
!('pipe' in model && model.pipe) &&
|
||||
Object.keys(model.info?.params?.model_controls ?? {}).length > 0
|
||||
);
|
||||
$: permitted =
|
||||
$user?.role === 'admin' ||
|
||||
(($user?.permissions?.chat?.controls ?? true) && ($user?.permissions?.chat?.params ?? true));
|
||||
$: rowClass = `focus-ring flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-xl px-2 text-left text-[0.8125rem] font-normal text-gray-700 outline-hidden transition-colors duration-75 dark:text-gray-100 ${$settings?.highContrastMode ? 'hover:bg-gray-200! dark:hover:bg-gray-800!' : 'hover:bg-gray-50/40! dark:hover:bg-gray-800/40!'}`;
|
||||
$: selectedClass = $settings?.highContrastMode
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: 'bg-gray-50/70 dark:bg-gray-800/60';
|
||||
const select = (modelId: string, controlId: string, value: string) => {
|
||||
const selections = { ...params.model_controls?.[modelId] };
|
||||
if (value) selections[controlId] = value;
|
||||
else delete selections[controlId];
|
||||
params = { ...params, model_controls: { ...params.model_controls, [modelId]: selections } };
|
||||
};
|
||||
</script>
|
||||
|
||||
{#snippet summary(model: Model, key: string, control: ModelControl)}
|
||||
<span class="min-w-0 flex-1 truncate text-left">{control.label}</span>
|
||||
<span class="max-w-[55%] truncate text-gray-500 dark:text-gray-400">
|
||||
{control.options[params.model_controls?.[model.id]?.[key] ?? control.default ?? '']?.label ??
|
||||
$i18n.t('Default')}
|
||||
</span>
|
||||
<ChevronRight className="size-3.5 shrink-0 text-gray-400" />
|
||||
{/snippet}
|
||||
|
||||
{#snippet choices(model: Model, key: string, control: ModelControl)}
|
||||
{#if control.description}
|
||||
<p class="px-2 py-1.5 text-xs leading-4 text-gray-500 dark:text-gray-400">
|
||||
{control.description}
|
||||
</p>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={!params.model_controls?.[model.id]?.[key]}
|
||||
class={`${rowClass} ${!params.model_controls?.[model.id]?.[key] ? selectedClass : ''}`}
|
||||
on:click={() => select(model.id, key, '')}
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate text-left">
|
||||
{$i18n.t('Default')}{#if control.options[control.default ?? '']?.label}{' · '}{control
|
||||
.options[control.default ?? ''].label}{/if}
|
||||
</span>
|
||||
{#if !params.model_controls?.[model.id]?.[key]}<Check className="size-3! shrink-0" />{/if}
|
||||
</button>
|
||||
{#each Object.entries(control.options) as [value, option] (value)}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={params.model_controls?.[model.id]?.[key] === value}
|
||||
class={`${rowClass} ${params.model_controls?.[model.id]?.[key] === value ? selectedClass : ''}`}
|
||||
on:click={() => select(model.id, key, value)}
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate text-left">{option.label}</span>
|
||||
{#if params.model_controls?.[model.id]?.[key] === value}<Check
|
||||
className="size-3! shrink-0"
|
||||
/>{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
{#if permitted && available.length}
|
||||
<Dropdown align="end" visualViewportAware onOpenChange={() => (active = null)}>
|
||||
<Tooltip content={$i18n.t('Model controls')} placement="top">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={$i18n.t('Model controls')}
|
||||
class="mr-2 flex shrink-0 items-center justify-center text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
<Knobs className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div slot="content">
|
||||
<DropdownMenu className="w-60 max-w-[calc(100vw-2rem)]">
|
||||
{#if $mobile && active}
|
||||
<button type="button" aria-label={$i18n.t('Back')} on:click={back}>
|
||||
<ChevronLeft className="size-3.5" />
|
||||
<span>{active.control.label}</span>
|
||||
</button>
|
||||
{@render choices(active.model, active.key, active.control)}
|
||||
{:else}
|
||||
{#each available as model (model.id)}
|
||||
{#if selectedModels.length > 1}
|
||||
<div class="truncate px-2 pt-1.5 pb-0.5 text-xs text-gray-400 dark:text-gray-500">
|
||||
{resolveLocalizedModelName(model, $i18n.language)}
|
||||
</div>
|
||||
{/if}
|
||||
{#each Object.entries(model.info?.params?.model_controls ?? {}) as [key, control] (key)}
|
||||
{#if $mobile}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={control.label}
|
||||
id={`model-control-${model.id}-${key}`}
|
||||
class={rowClass}
|
||||
on:click={(event) =>
|
||||
(active = { model, key, control, trigger: event.currentTarget })}
|
||||
>
|
||||
{@render summary(model, key, control)}
|
||||
</button>
|
||||
{:else}
|
||||
<DropdownSub
|
||||
maxWidth={240}
|
||||
contentClass="w-60 max-h-[calc(100dvh-2rem)] overflow-y-auto scrollbar-thin"
|
||||
>
|
||||
<button
|
||||
slot="trigger"
|
||||
type="button"
|
||||
aria-label={control.label}
|
||||
aria-haspopup="menu"
|
||||
class={rowClass}
|
||||
>
|
||||
{@render summary(model, key, control)}
|
||||
</button>
|
||||
{@render choices(model, key, control)}
|
||||
</DropdownSub>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Dropdown>
|
||||
{/if}
|
||||
|
|
@ -47,6 +47,7 @@
|
|||
export let history;
|
||||
|
||||
export let prompt = '';
|
||||
export let params = {};
|
||||
export let files = [];
|
||||
export let messageInput = null;
|
||||
|
||||
|
|
@ -241,6 +242,7 @@
|
|||
<div class="text-base font-normal @md:max-w-3xl w-full py-3 {atSelectedModel ? 'mt-2' : ''}">
|
||||
{#if !($selectedFolder && folderReadOnly)}
|
||||
<MessageInput
|
||||
bind:params
|
||||
bind:this={messageInput}
|
||||
{history}
|
||||
bind:selectedModels
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import CustomParams from './CustomParams.svelte';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
|
@ -1490,72 +1490,7 @@
|
|||
</div>
|
||||
|
||||
{#if custom && admin}
|
||||
<div class="flex flex-col justify-center">
|
||||
{#each Object.keys(params?.custom_params ?? {}) as key}
|
||||
<div class=" py-0.5 w-full justify-between mb-1">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<input
|
||||
type="text"
|
||||
class=" text-xs w-full bg-transparent outline-none"
|
||||
aria-label={$i18n.t('Custom Parameter Name')}
|
||||
placeholder={$i18n.t('Custom Parameter Name')}
|
||||
value={key}
|
||||
on:change={(e) => {
|
||||
const newKey = e.currentTarget.value.trim();
|
||||
if (newKey && newKey !== key) {
|
||||
params.custom_params[newKey] = params.custom_params[key];
|
||||
delete params.custom_params[key];
|
||||
params = {
|
||||
...params,
|
||||
custom_params: { ...params.custom_params }
|
||||
};
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
delete params.custom_params[key];
|
||||
params = {
|
||||
...params,
|
||||
custom_params: { ...params.custom_params }
|
||||
};
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Remove')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
bind:value={params.custom_params[key]}
|
||||
type="text"
|
||||
class="text-sm w-full bg-transparent outline-hidden outline-none"
|
||||
aria-label={$i18n.t('Custom Parameter Value')}
|
||||
placeholder={$i18n.t('Custom Parameter Value')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<button
|
||||
class=" flex gap-2 items-center w-full text-center justify-center mt-1 mb-5"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
params.custom_params = (params?.custom_params ?? {}) || {};
|
||||
params.custom_params['custom_param_name'] = 'custom_param_value';
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Plus />
|
||||
</div>
|
||||
<div>{$i18n.t('Add Custom Parameter')}</div>
|
||||
</button>
|
||||
</div>
|
||||
<CustomParams bind:value={params.custom_params} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
const i18n: any = getContext('i18n');
|
||||
export let compact = false;
|
||||
export let value: Record<string, any> = {};
|
||||
const focusNew = (node: HTMLInputElement, empty: boolean) => {
|
||||
if (empty) node.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="min-w-0 flex-1">
|
||||
{#each Object.keys(value ?? {}) as key}
|
||||
<div
|
||||
class={compact
|
||||
? 'mb-1 grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] items-start gap-3 py-1'
|
||||
: 'mb-1 grid grid-cols-[1fr_auto] items-center gap-y-0.5 py-0.5'}
|
||||
>
|
||||
<label class="min-w-0">
|
||||
{#if compact}<span class="block text-xs text-gray-400 dark:text-gray-600"
|
||||
>{$i18n.t('Parameter')}</span
|
||||
>{/if}
|
||||
<input
|
||||
type="text"
|
||||
class={compact
|
||||
? 'w-full min-w-0 bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-none placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700'
|
||||
: 'min-w-0 w-full bg-transparent text-xs text-gray-400 dark:text-gray-600 outline-none'}
|
||||
aria-label={$i18n.t('Custom Parameter Name')}
|
||||
placeholder={compact ? $i18n.t('e.g. temperature') : $i18n.t('Custom Parameter Name')}
|
||||
required={compact}
|
||||
pattern={compact ? '.*\\S.*' : undefined}
|
||||
use:focusNew={compact && key === ''}
|
||||
title={key}
|
||||
value={key}
|
||||
on:change={(event) => {
|
||||
const name = event.currentTarget.value.trim();
|
||||
if (name && name !== key) {
|
||||
value[name] = value[key];
|
||||
delete value[key];
|
||||
value = { ...value };
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label class={compact ? 'min-w-0' : 'order-last col-span-2'}>
|
||||
{#if compact}<span class="block text-xs text-gray-400 dark:text-gray-600"
|
||||
>{$i18n.t('Value')}</span
|
||||
>{/if}
|
||||
<input
|
||||
type="text"
|
||||
class={compact
|
||||
? 'w-full min-w-0 bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-none placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700'
|
||||
: 'w-full min-w-0 bg-transparent text-[0.8125rem] text-gray-700 dark:text-gray-300 outline-none'}
|
||||
aria-label={$i18n.t('Custom Parameter Value')}
|
||||
placeholder={compact ? $i18n.t('e.g. 0.7') : $i18n.t('Custom Parameter Value')}
|
||||
value={typeof value[key] === 'object' ? JSON.stringify(value[key]) : value[key]}
|
||||
on:input={(event) => (value = { ...value, [key]: event.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class={compact
|
||||
? 'mt-4 flex size-5 items-center justify-center rounded text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300'
|
||||
: 'flex shrink-0 rounded-sm p-1 px-3 text-xs outline-hidden transition'}
|
||||
aria-label={`${$i18n.t('Remove')} ${key || $i18n.t('parameter')}`}
|
||||
on:click={() => {
|
||||
delete value[key];
|
||||
value = { ...value };
|
||||
}}
|
||||
>
|
||||
{#if compact}<XMark className="size-3" />{:else}{$i18n.t('Remove')}{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class={compact
|
||||
? 'flex w-fit items-center gap-1.5 whitespace-nowrap py-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 disabled:opacity-40'
|
||||
: 'mt-1 mb-5 flex w-full items-center justify-center gap-2 text-center'}
|
||||
aria-label={$i18n.t('Add Custom Parameter')}
|
||||
title={$i18n.t('Add Custom Parameter')}
|
||||
disabled={compact && Object.hasOwn(value ?? {}, '')}
|
||||
on:click={() => {
|
||||
value = value ?? {};
|
||||
let key = compact ? '' : 'custom_param_name';
|
||||
let index = 2;
|
||||
while (key in value) key = `custom_param_name_${index++}`;
|
||||
value = { ...value, [key]: '' };
|
||||
}}
|
||||
>
|
||||
<Plus className={compact ? 'size-3' : 'size-4'} /><span
|
||||
>{$i18n.t(compact ? 'Add parameter' : 'Add Custom Parameter')}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
bind:value
|
||||
{required}
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
dispatch('change', value);
|
||||
}}
|
||||
>
|
||||
{#if placeholder}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import { uploadFile, deleteFileById } from '$lib/apis/files';
|
||||
|
||||
import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
|
||||
import ModelControls from '$lib/components/admin/Settings/Models/ModelControls.svelte';
|
||||
import ModelSelector from '$lib/components/chat/ModelSelector/Selector.svelte';
|
||||
import Tags from '$lib/components/common/Tags.svelte';
|
||||
import Knowledge from '$lib/components/workspace/Models/Knowledge.svelte';
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
|
||||
export let model = null;
|
||||
export let edit = false;
|
||||
export let admin = false;
|
||||
|
||||
export let preset = true;
|
||||
|
||||
|
|
@ -111,7 +113,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
let params = {
|
||||
let params: Record<string, any> = {
|
||||
system: ''
|
||||
};
|
||||
|
||||
|
|
@ -1155,6 +1157,9 @@
|
|||
<AdvancedParams admin={true} custom={true} layout="grid" bind:params />
|
||||
</div>
|
||||
{/if}
|
||||
{#if admin}
|
||||
<ModelControls bind:controls={params.model_controls} />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -1330,7 +1335,7 @@
|
|||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
const copied = await copyToClipboard(JSON.stringify(info, null, 2));
|
||||
const copied = await copyToClipboard(JSON.stringify(modelInfo, null, 2));
|
||||
if (copied) {
|
||||
toast.success($i18n.t('Copied to clipboard'));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue