diff --git a/core/http/react-ui/public/locales/de/home.json b/core/http/react-ui/public/locales/de/home.json index 02dab04ae3..0fffa69d35 100644 --- a/core/http/react-ui/public/locales/de/home.json +++ b/core/http/react-ui/public/locales/de/home.json @@ -139,5 +139,16 @@ }, "complete": "ergänzen" }, - "notice": { "appUnavailable": "Die App ist nicht mehr verfügbar." } + "notice": { "appUnavailable": "Die App ist nicht mehr verfügbar." }, + "picker": { + "aliasOf": "Alias von {{target}}", + "aliasBroken": "defekter Alias", + "disabled": "deaktiviert", + "pinned": "angeheftet", + "useTyped": "\"{{name}}\" verwenden", + "empty": "Keine Modelle verfügbar", + "add": "Modell hinzufügen", + "remove": "{{name}} entfernen", + "clear": "Leeren" + } } diff --git a/core/http/react-ui/public/locales/en/home.json b/core/http/react-ui/public/locales/en/home.json index a3a5e4f6ea..2ceeb1b6c3 100644 --- a/core/http/react-ui/public/locales/en/home.json +++ b/core/http/react-ui/public/locales/en/home.json @@ -171,7 +171,16 @@ "cold": "not loaded", "loadedNow": "Loaded now", "installed": "Installed", - "vision": "Understands images" + "vision": "Understands images", + "aliasOf": "alias of {{target}}", + "aliasBroken": "broken alias", + "disabled": "disabled", + "pinned": "pinned", + "useTyped": "Use \"{{name}}\"", + "empty": "No models available", + "add": "Add a model", + "remove": "Remove {{name}}", + "clear": "Clear" }, "slash": { "label": "Actions", diff --git a/core/http/react-ui/public/locales/es/home.json b/core/http/react-ui/public/locales/es/home.json index 8afe400e0f..3ab7c3897a 100644 --- a/core/http/react-ui/public/locales/es/home.json +++ b/core/http/react-ui/public/locales/es/home.json @@ -139,5 +139,16 @@ }, "complete": "completar" }, - "notice": { "appUnavailable": "La aplicación ya no está disponible." } + "notice": { "appUnavailable": "La aplicación ya no está disponible." }, + "picker": { + "aliasOf": "alias de {{target}}", + "aliasBroken": "alias roto", + "disabled": "desactivado", + "pinned": "fijado", + "useTyped": "Usar \"{{name}}\"", + "empty": "No hay modelos disponibles", + "add": "Añadir un modelo", + "remove": "Quitar {{name}}", + "clear": "Borrar" + } } diff --git a/core/http/react-ui/public/locales/fr/home.json b/core/http/react-ui/public/locales/fr/home.json index 663711b09e..ca7e4e48e4 100644 --- a/core/http/react-ui/public/locales/fr/home.json +++ b/core/http/react-ui/public/locales/fr/home.json @@ -142,5 +142,16 @@ }, "complete": "compléter" }, - "notice": { "appUnavailable": "L’application n’est plus disponible." } + "notice": { "appUnavailable": "L’application n’est plus disponible." }, + "picker": { + "aliasOf": "alias de {{target}}", + "aliasBroken": "alias cassé", + "disabled": "désactivé", + "pinned": "épinglé", + "useTyped": "Utiliser \"{{name}}\"", + "empty": "Aucun modèle disponible", + "add": "Ajouter un modèle", + "remove": "Retirer {{name}}", + "clear": "Effacer" + } } diff --git a/core/http/react-ui/public/locales/id/home.json b/core/http/react-ui/public/locales/id/home.json index b8573dbbc9..c3aaec60ef 100644 --- a/core/http/react-ui/public/locales/id/home.json +++ b/core/http/react-ui/public/locales/id/home.json @@ -139,5 +139,16 @@ }, "complete": "lengkapi" }, - "notice": { "appUnavailable": "Aplikasi tidak lagi tersedia." } + "notice": { "appUnavailable": "Aplikasi tidak lagi tersedia." }, + "picker": { + "aliasOf": "alias dari {{target}}", + "aliasBroken": "alias rusak", + "disabled": "nonaktif", + "pinned": "disematkan", + "useTyped": "Gunakan \"{{name}}\"", + "empty": "Tidak ada model", + "add": "Tambah model", + "remove": "Hapus {{name}}", + "clear": "Kosongkan" + } } diff --git a/core/http/react-ui/public/locales/it/home.json b/core/http/react-ui/public/locales/it/home.json index 41c5e1fa53..49cca23994 100644 --- a/core/http/react-ui/public/locales/it/home.json +++ b/core/http/react-ui/public/locales/it/home.json @@ -139,5 +139,16 @@ }, "complete": "completa" }, - "notice": { "appUnavailable": "L’app non è più disponibile." } + "notice": { "appUnavailable": "L’app non è più disponibile." }, + "picker": { + "aliasOf": "alias di {{target}}", + "aliasBroken": "alias non valido", + "disabled": "disattivato", + "pinned": "fissato", + "useTyped": "Usa \"{{name}}\"", + "empty": "Nessun modello disponibile", + "add": "Aggiungi un modello", + "remove": "Rimuovi {{name}}", + "clear": "Cancella" + } } diff --git a/core/http/react-ui/public/locales/ko/home.json b/core/http/react-ui/public/locales/ko/home.json index 1d256d61b4..13eb6d92fc 100644 --- a/core/http/react-ui/public/locales/ko/home.json +++ b/core/http/react-ui/public/locales/ko/home.json @@ -139,5 +139,16 @@ }, "complete": "완성" }, - "notice": { "appUnavailable": "앱을 더 이상 사용할 수 없습니다." } + "notice": { "appUnavailable": "앱을 더 이상 사용할 수 없습니다." }, + "picker": { + "aliasOf": "{{target}}의 별칭", + "aliasBroken": "깨진 별칭", + "disabled": "비활성", + "pinned": "고정됨", + "useTyped": "\"{{name}}\" 사용", + "empty": "사용 가능한 모델 없음", + "add": "모델 추가", + "remove": "{{name}} 제거", + "clear": "지우기" + } } diff --git a/core/http/react-ui/public/locales/pt-BR/home.json b/core/http/react-ui/public/locales/pt-BR/home.json index daf7e8ddfa..21a37efa6c 100644 --- a/core/http/react-ui/public/locales/pt-BR/home.json +++ b/core/http/react-ui/public/locales/pt-BR/home.json @@ -154,5 +154,16 @@ }, "complete": "completar" }, - "notice": { "appUnavailable": "O aplicativo não está mais disponível." } + "notice": { "appUnavailable": "O aplicativo não está mais disponível." }, + "picker": { + "aliasOf": "alias de {{target}}", + "aliasBroken": "alias quebrado", + "disabled": "desativado", + "pinned": "fixado", + "useTyped": "Usar \"{{name}}\"", + "empty": "Nenhum modelo disponível", + "add": "Adicionar um modelo", + "remove": "Remover {{name}}", + "clear": "Limpar" + } } diff --git a/core/http/react-ui/public/locales/zh-CN/home.json b/core/http/react-ui/public/locales/zh-CN/home.json index 6290fbf328..9111a54707 100644 --- a/core/http/react-ui/public/locales/zh-CN/home.json +++ b/core/http/react-ui/public/locales/zh-CN/home.json @@ -139,5 +139,16 @@ }, "complete": "补全" }, - "notice": { "appUnavailable": "应用已不可用。" } + "notice": { "appUnavailable": "应用已不可用。" }, + "picker": { + "aliasOf": "{{target}} 的别名", + "aliasBroken": "别名失效", + "disabled": "已停用", + "pinned": "已固定", + "useTyped": "使用 \"{{name}}\"", + "empty": "没有可用模型", + "add": "添加模型", + "remove": "移除 {{name}}", + "clear": "清除" + } } diff --git a/core/http/react-ui/src/addons/hostServices.js b/core/http/react-ui/src/addons/hostServices.js index e0d3fedf25..45cb7bb274 100644 --- a/core/http/react-ui/src/addons/hostServices.js +++ b/core/http/react-ui/src/addons/hostServices.js @@ -13,7 +13,7 @@ import { import LoadingSpinner from '../components/LoadingSpinner' import Modal from '../components/Modal' import Lightbox from '../components/Lightbox' -import HomeModelPicker from '../components/home/HomeModelPicker' +import ModelPicker from '../components/ModelPicker' import { useMediaCapture, audioBufferToWavBlob } from '../hooks/useMediaCapture' import { useModels } from '../hooks/useModels' import { useResources } from '../hooks/useResources' @@ -71,7 +71,7 @@ export const host = Object.freeze({ Section, Field, Select, TextInput, Switch, Choice, Callout, StatusChip, List, ListRow, Code, Actions, Table, ExternalLink, PageHeader, ConfirmDialog, ErrorWithTraceLink, ModelSelector, MediaInput, WaveformPlayer, DistributionBars, EmbeddingInspector, TabSwitch, - LoadingSpinner, Modal, Lightbox, HomeModelPicker, + LoadingSpinner, Modal, Lightbox, ModelPicker, HomeModelPicker: ModelPicker, }, hooks: { useMediaCapture, useObjectUrl, useDelayedAction, audioBufferToWavBlob, diff --git a/core/http/react-ui/src/components/ModelPicker.jsx b/core/http/react-ui/src/components/ModelPicker.jsx new file mode 100644 index 0000000000..24f47959d9 --- /dev/null +++ b/core/http/react-ui/src/components/ModelPicker.jsx @@ -0,0 +1,326 @@ +// eslint-disable-next-line no-unused-vars +import { Fragment, forwardRef, useCallback, useEffect, useImperativeHandle, useId, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useModels } from '../hooks/useModels' +import { readLastModel, writeLastModel, readPreferredModel } from '../utils/lastModel' +import Icon from './Icon' + +// How many models before the list gets a filter box, when `searchable` is left +// to decide. +const FILTER_FROM = 8 + +// The one model picker: a trigger and a listbox. A model is "warm" when the +// server reports it loaded, "cold" otherwise. Every entry the server lists +// shows what it is: an alias says which model it stands for, a disabled model +// cannot be chosen, a pinned one is marked, and an alias that points nowhere is +// listed as broken and cannot be chosen. +// +// value, onChange a name; with `multiple`, an array of names +// capability only models that have it (also read to remember a choice) +// multiple pick several; the list stays open and the picks show as chips +// allowCustom a typed name that is not in the list can be used as it is +// clearable a button empties the value +// searchable true: always a filter box; false: never; omitted: from 8 models +// variant "chip" (default, the compact Home chip) or "field" (a form control) +// autoSelect choose the remembered or the first model when the value is not listed +// models, loading the list is already held by the caller, so no second fetch +// grouped, loadedIds, showWarm, describe, onOpen, footer, labels +// defaultChoice, defaultSelected, onChooseDefault a first row for "use the default" +const ModelPicker = forwardRef(function ModelPicker( + { + value, onChange, capability, loadedIds, disabled = false, placeholder, + models: givenModels, loading: givenLoading, grouped = false, describe, onOpen, footer, + showWarm = true, labels, autoSelect = true, rememberSelection = true, + defaultChoice, defaultSelected = false, onChooseDefault, id, describedBy, + multiple = false, allowCustom = false, clearable = false, searchable, + variant = 'chip', className = '', + }, + ref, +) { + const { t } = useTranslation('home') + const own = useModels(capability, { enabled: !givenModels }) + const models = givenModels || own.models + const loading = givenModels ? !!givenLoading : own.loading + const byId = useMemo(() => new Map(models.map(m => [m.id, m])), [models]) + const names = useMemo(() => models.map(m => m.id), [models]) + const picked = useMemo(() => (multiple ? (Array.isArray(value) ? value : []) : []), [multiple, value]) + const instanceId = useId() + const menuId = `${instanceId}-menu` + const listId = `${instanceId}-list` + // A null row is a non-model action, never an installed model ID. + const [open, setOpen] = useState(false) + const [query, setQuery] = useState('') + const [active, setActive] = useState(0) + const rootRef = useRef(null) + const chipRef = useRef(null) + const menuRef = useRef(null) + const filterRef = useRef(null) + + // A chooseable model is listed, enabled and, when it is an alias, resolved. + const usable = useCallback((name) => { + const m = byId.get(name) + return !!m && !m.disabled && !m.alias_error + }, [byId]) + const usableNames = useMemo(() => names.filter(usable), [names, usable]) + + // The remembered model if it is still there, otherwise the first one. + // Auto-select is not a user choice, so it is not written back. + useEffect(() => { + if (multiple || allowCustom || !autoSelect || defaultSelected || usableNames.length === 0) return + if (value && usableNames.includes(value)) return + const preferred = readPreferredModel(capability) + const remembered = preferred && usableNames.includes(preferred) ? preferred : readLastModel(capability) + onChange(remembered && usableNames.includes(remembered) ? remembered : usableNames[0]) + }, [usableNames, value, onChange, capability, autoSelect, defaultSelected, multiple, allowCustom]) + + const showFilter = searchable ?? names.length >= FILTER_FROM + + // Loaded models first when the list is grouped; each group keeps its order. + // Rows are { name } for a model and { custom } for the typed name. + const shown = useMemo(() => { + const q = query.trim().toLowerCase() + const matches = (n) => { + if (!q) return true + const m = byId.get(n) + return n.toLowerCase().includes(q) || (m?.alias_of || '').toLowerCase().includes(q) + } + const found = names.filter(matches) + const ordered = grouped ? [...found.filter(n => loadedIds?.has(n)), ...found.filter(n => !loadedIds?.has(n))] : found + const rows = ordered.map(name => ({ name })) + const typed = query.trim() + if (allowCustom && typed && !names.includes(typed)) rows.push({ custom: typed }) + return defaultChoice && (!q || defaultChoice.toLowerCase().includes(q)) ? [{ name: null }, ...rows] : rows + }, [names, byId, query, grouped, loadedIds, defaultChoice, allowCustom]) + + const close = useCallback((restoreFocus = false) => { + setOpen(false) + setQuery('') + if (restoreFocus) chipRef.current?.focus() + }, []) + + const openMenu = useCallback(() => { + if (disabled || (names.length === 0 && !defaultChoice && !allowCustom)) return + setActive(defaultSelected || multiple ? 0 : Math.max(0, names.indexOf(value) + (defaultChoice ? 1 : 0))) + setQuery('') + setOpen(true) + onOpen?.() + }, [disabled, names, value, onOpen, defaultChoice, defaultSelected, multiple, allowCustom]) + + useImperativeHandle(ref, () => ({ open: openMenu, focus: () => chipRef.current?.focus() }), [openMenu]) + + useEffect(() => { + if (!open) return undefined + const onDown = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) close() } + document.addEventListener('mousedown', onDown) + return () => document.removeEventListener('mousedown', onDown) + }, [open, close]) + + // Focus moves into the menu so the arrow keys work, and back to the chip when + // it closes by keyboard. + useEffect(() => { + if (!open) return + const el = filterRef.current || menuRef.current + el?.focus() + }, [open]) + + useEffect(() => { + if (!open) return + menuRef.current?.querySelector('[data-active="true"]')?.scrollIntoView?.({ block: 'nearest' }) + }, [open, active, shown]) + + const pick = useCallback((row) => { + if (!row) return + if (row.name === null) { onChooseDefault?.(); close(true); return } + const name = row.custom ?? row.name + if (row.custom === undefined && !usable(name)) return + if (rememberSelection && !multiple) writeLastModel(capability, name) + if (multiple) { + onChange(picked.includes(name) ? picked.filter(n => n !== name) : [...picked, name]) + setQuery('') + return + } + onChange(name) + close(true) + }, [capability, onChange, close, rememberSelection, onChooseDefault, usable, multiple, picked]) + + const onMenuKey = (e) => { + const n = shown.length + if (e.key === 'ArrowDown') { e.preventDefault(); if (n) setActive(a => (a + 1) % n) } + else if (e.key === 'ArrowUp') { e.preventDefault(); if (n) setActive(a => (a + n - 1) % n) } + else if (e.key === 'Home' && !query) { e.preventDefault(); setActive(0) } + else if (e.key === 'End' && !query) { e.preventDefault(); setActive(Math.max(0, n - 1)) } + else if (e.key === 'Enter') { e.preventDefault(); if (active < n) pick(shown[active]) } + else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(true) } + else if (e.key === 'Tab') close() + } + + const isWarm = (name) => loadedIds?.has(name) + const current = multiple ? '' : (defaultSelected ? defaultChoice : value) + const emptyLabel = multiple ? (placeholder || t('picker.add')) : (placeholder || (loading ? t('picker.loading') : (labels?.none || t('picker.none')))) + const label = current || emptyLabel + const activeId = active < shown.length ? `${instanceId}-opt-${active}` : undefined + const trigger = variant === 'field' ? 'home-chip home-chip--field' : 'home-chip' + + const badges = (name) => { + const m = byId.get(name) + if (!m) return null + return ( + <> + {m.alias_of && !m.alias_error && {t('picker.aliasOf', { target: m.alias_of })}} + {m.alias_error && {t('picker.aliasBroken')}} + {m.disabled && {t('picker.disabled')}} + {m.pinned && {t('picker.pinned')}} + + ) + } + + return ( +
+ {multiple && picked.length > 0 && ( + + )} +
+ + {clearable && !multiple && value && ( + + )} +
+ {open && ( +
+ {(showFilter || allowCustom) && ( + { setQuery(e.target.value); setActive(0) }} + role="combobox" + aria-expanded="true" + aria-controls={listId} + aria-activedescendant={activeId} + /> + )} + + {footer} +
+ )} +
+ ) +}) + +export default ModelPicker diff --git a/core/http/react-ui/src/components/chat/ChatComposer.jsx b/core/http/react-ui/src/components/chat/ChatComposer.jsx index f7d2a9cc9b..62ed70dc37 100644 --- a/core/http/react-ui/src/components/chat/ChatComposer.jsx +++ b/core/http/react-ui/src/components/chat/ChatComposer.jsx @@ -12,7 +12,7 @@ import { useModelFit } from '../../hooks/useModelFit' import { useCommandsNotice } from '../../hooks/useCommandsNotice' import { fillStyle, hostMemory, memoryFigure } from '../home/memory' import HomeComposer from '../home/HomeComposer' -import HomeModelPicker from '../home/HomeModelPicker' +import ModelPicker from '../ModelPicker' import AddonSlot from '../../addons/AddonSlot' import AgentPicker from './AgentPicker' import SkillsMenu from './SkillsMenu' @@ -146,7 +146,7 @@ export default function ChatComposer({ const memory = hostMemory(modelFit.resources) const memoryNumbers = memory ? memoryFigure(memory.used, memory.total) : null const picker = ( - models.map(m => m.id), [models]) - const instanceId = useId() - const menuId = `${instanceId}-menu` - const listId = `${instanceId}-list` - // A null row is a non-model action, never an installed model ID. - const [open, setOpen] = useState(false) - const [query, setQuery] = useState('') - const [active, setActive] = useState(0) - const rootRef = useRef(null) - const chipRef = useRef(null) - const menuRef = useRef(null) - const filterRef = useRef(null) - - // Same defaulting as ModelSelector: the remembered model if it is still - // there, otherwise the first one. Auto-select is not a user choice, so it is - // not written back. - useEffect(() => { - if (autoSelect && !defaultSelected && names.length > 0 && (!value || !names.includes(value))) { - const preferred = readPreferredModel(capability) - const remembered = preferred && names.includes(preferred) ? preferred : readLastModel(capability) - onChange(remembered && names.includes(remembered) ? remembered : names[0]) - } - }, [names, value, onChange, capability, autoSelect, defaultSelected]) - - // Loaded models first when the list is grouped; each group keeps its order. - const shown = useMemo(() => { - const q = query.trim().toLowerCase() - const found = q ? names.filter(n => n.toLowerCase().includes(q)) : names - const ordered = grouped ? [...found.filter(n => loadedIds?.has(n)), ...found.filter(n => !loadedIds?.has(n))] : found - return defaultChoice && (!q || defaultChoice.toLowerCase().includes(q)) ? [null, ...ordered] : ordered - }, [names, query, grouped, loadedIds, defaultChoice]) - - const close = useCallback((restoreFocus = false) => { - setOpen(false) - setQuery('') - if (restoreFocus) chipRef.current?.focus() - }, []) - - const openMenu = useCallback(() => { - if (disabled || (names.length === 0 && !defaultChoice)) return - setActive(defaultSelected ? 0 : Math.max(0, names.indexOf(value) + (defaultChoice ? 1 : 0))) - setQuery('') - setOpen(true) - onOpen?.() - }, [disabled, names, value, onOpen, defaultChoice, defaultSelected]) - - useImperativeHandle(ref, () => ({ open: openMenu, focus: () => chipRef.current?.focus() }), [openMenu]) - - useEffect(() => { - if (!open) return undefined - const onDown = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) close() } - document.addEventListener('mousedown', onDown) - return () => document.removeEventListener('mousedown', onDown) - }, [open, close]) - - // Focus moves into the menu so the arrow keys work, and back to the chip when - // it closes by keyboard. - useEffect(() => { - if (!open) return - const el = filterRef.current || menuRef.current - el?.focus() - }, [open]) - - useEffect(() => { - if (!open) return - menuRef.current?.querySelector('[data-active="true"]')?.scrollIntoView?.({ block: 'nearest' }) - }, [open, active, shown]) - - const pick = useCallback((name) => { - if (name === null) onChooseDefault?.() - else { - if (rememberSelection) writeLastModel(capability, name) - onChange(name) - } - close(true) - }, [capability, onChange, close, rememberSelection, onChooseDefault]) - - const onMenuKey = (e) => { - const n = shown.length - if (e.key === 'ArrowDown') { e.preventDefault(); if (n) setActive(a => (a + 1) % n) } - else if (e.key === 'ArrowUp') { e.preventDefault(); if (n) setActive(a => (a + n - 1) % n) } - else if (e.key === 'Home' && !query) { e.preventDefault(); setActive(0) } - else if (e.key === 'End' && !query) { e.preventDefault(); setActive(Math.max(0, n - 1)) } - else if (e.key === 'Enter') { e.preventDefault(); if (active < n) pick(shown[active]) } - else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(true) } - else if (e.key === 'Tab') close() - } - - const isWarm = (name) => loadedIds?.has(name) - const label = (defaultSelected ? defaultChoice : value) || placeholder || (loading ? t('picker.loading') : (labels?.none || t('picker.none'))) - const activeId = active < shown.length ? `${instanceId}-opt-${active}` : undefined - - return ( -
- - {open && ( -
- {names.length >= FILTER_FROM && ( - { setQuery(e.target.value); setActive(0) }} - role="combobox" - aria-expanded="true" - aria-controls={listId} - aria-activedescendant={activeId} - /> - )} -
    - {!grouped &&
  • {labels?.heading || t('picker.heading')}
  • } - {shown.length === 0 &&
  • {t('picker.noMatch')}
  • } - {shown.map((name, i) => { - const warm = isWarm(name) - const info = name !== null && describe ? describe(name, warm) : null - const selected = name === null ? defaultSelected : !defaultSelected && name === value - const first = grouped && (i === 0 || isWarm(shown[i - 1]) !== warm) - return ( - - {first && ( -
  • - {warm ? t('picker.loadedNow') : t('picker.installed')} -
  • - )} -
  • setActive(i)} - onClick={() => pick(name)} - > - {showWarm && name !== null &&
  • -
    - ) - })} -
- {footer} -
- )} -
- ) -}) - -export default HomeModelPicker diff --git a/core/http/react-ui/src/components/home/composer.css b/core/http/react-ui/src/components/home/composer.css index aa2de75473..4c10a4f213 100644 --- a/core/http/react-ui/src/components/home/composer.css +++ b/core/http/react-ui/src/components/home/composer.css @@ -265,3 +265,19 @@ .home-send-btn:active:not(:disabled), .home-send-btn:hover:not(:disabled) .home-send-btn__glyph { transform: none; } } + +/* Shared model picker: form-field variant, alias and state tags, multi chips */ +.home-pick--field { width: 100%; } +.home-pick--field .home-pick__row { display: flex; gap: var(--sp-1); } +.home-chip--field { flex: 1; width: 100%; justify-content: flex-start; height: var(--ctl-h); } +.home-chip--field .home-chip__caret { margin-left: auto; } +.home-chip__alias { color: var(--dk-muted); font-size: var(--fs-meta); white-space: nowrap; } +.home-pick__chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin: 0 0 var(--sp-1); padding: 0; list-style: none; } +.home-pick__tag { display: inline-flex; align-items: center; gap: 4px; padding: 2px 4px 2px 10px; border-radius: var(--r-md); background: var(--dk-hover); } +.home-pick__tag code { font-family: var(--font-mono); font-size: var(--fs-small); background: none; padding: 0; } +.home-pick__remove { display: inline-flex; align-items: center; justify-content: center; border: 0; background: none; color: var(--dk-muted); cursor: pointer; padding: 4px; border-radius: var(--r-sm); } +.home-pick__remove:hover:not(:disabled) { background: var(--dk-hover); color: var(--dk-text); } +.home-menu__tag { flex: none; padding: 0 6px; border-radius: var(--r-sm); background: var(--dk-hover); color: var(--dk-muted); font-size: var(--fs-meta); white-space: nowrap; } +.home-menu__tag[data-kind="alias"] { background: var(--dk-accent-wash); color: var(--dk-accent-text); } +.home-menu__tag[data-kind="broken"] { color: var(--color-error, #c0392b); } +.home-menu__item[data-blocked="true"] { opacity: .55; cursor: not-allowed; } diff --git a/core/http/react-ui/src/pages/AgentCreate.jsx b/core/http/react-ui/src/pages/AgentCreate.jsx index 78704644b1..cc2dcd0d19 100644 --- a/core/http/react-ui/src/pages/AgentCreate.jsx +++ b/core/http/react-ui/src/pages/AgentCreate.jsx @@ -14,7 +14,7 @@ import './agents.css' import './library.css' import '../components/home/composer.css' // eslint-disable-next-line no-unused-vars -import HomeModelPicker from '../components/home/HomeModelPicker' +import ModelPicker from '../components/ModelPicker' // eslint-disable-next-line no-unused-vars import NameList from '../components/ChatNameList' // eslint-disable-next-line no-unused-vars @@ -297,7 +297,7 @@ export default function AgentCreate() { return (
-