diff --git a/core/http/react-ui/src/App.css b/core/http/react-ui/src/App.css index 116574ecb..effea181e 100644 --- a/core/http/react-ui/src/App.css +++ b/core/http/react-ui/src/App.css @@ -1807,6 +1807,7 @@ select.input { flex-wrap: wrap; padding-left: var(--spacing-md); border-left: 1px solid var(--color-border-subtle); + margin-left: auto; } .filter-bar-group__toggle { display: flex; diff --git a/core/http/react-ui/src/hooks/useModels.js b/core/http/react-ui/src/hooks/useModels.js index 39a018a00..9d4b14e1a 100644 --- a/core/http/react-ui/src/hooks/useModels.js +++ b/core/http/react-ui/src/hooks/useModels.js @@ -6,9 +6,9 @@ export function useModels(capability) { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const fetchModels = useCallback(async () => { + const fetchModels = useCallback(async ({ silent = false } = {}) => { try { - setLoading(true) + if (!silent) setLoading(true) const data = await modelsApi.listCapabilities() let items = data?.data || [] if (capability) { @@ -30,15 +30,19 @@ export function useModels(capability) { setError(err.message) } } finally { - setLoading(false) + if (!silent) setLoading(false) } }, [capability]) + // Subsequent refetches stay silent so consumers don't blank their tables + // (e.g. the Manage page auto-refreshes every 10s in distributed mode). + const refetch = useCallback(() => fetchModels({ silent: true }), [fetchModels]) + useEffect(() => { fetchModels() }, [fetchModels]) - return { models, loading, error, refetch: fetchModels } + return { models, loading, error, refetch } } export function useGalleryModels(params = {}) { diff --git a/core/http/react-ui/src/pages/Manage.jsx b/core/http/react-ui/src/pages/Manage.jsx index fb61a8cc1..9188c80ef 100644 --- a/core/http/react-ui/src/pages/Manage.jsx +++ b/core/http/react-ui/src/pages/Manage.jsx @@ -7,12 +7,34 @@ import NodeDistributionChip from '../components/NodeDistributionChip' import FilterBar from '../components/FilterBar' import { useModels } from '../hooks/useModels' import { backendControlApi, modelsApi, backendsApi, systemApi, nodesApi } from '../utils/api' +import { + CAP_CHAT, CAP_COMPLETION, CAP_IMAGE, CAP_VIDEO, CAP_TTS, + CAP_TRANSCRIPT, CAP_SOUND_GENERATION, CAP_FACE_RECOGNITION, + CAP_SPEAKER_RECOGNITION, CAP_EMBEDDINGS, CAP_RERANK, +} from '../utils/capabilities' const TABS = [ { key: 'models', label: 'Models', icon: 'fa-brain' }, { key: 'backends', label: 'Backends', icon: 'fa-server' }, ] +// Capability → use-case badge. Entries with `route` become clickable links to +// the matching playground page; the rest render as informational badges. +// Order is the display order. CAP_CHAT covers CAP_COMPLETION too. +const USE_CASES = [ + { cap: CAP_CHAT, label: 'Chat', route: (id) => `/app/chat/${encodeURIComponent(id)}` }, + { cap: CAP_COMPLETION, label: 'Completion', route: (id) => `/app/chat/${encodeURIComponent(id)}`, hideIf: CAP_CHAT }, + { cap: CAP_IMAGE, label: 'Image', route: (id) => `/app/image/${encodeURIComponent(id)}` }, + { cap: CAP_VIDEO, label: 'Video', route: (id) => `/app/video/${encodeURIComponent(id)}` }, + { cap: CAP_TTS, label: 'TTS', route: (id) => `/app/tts/${encodeURIComponent(id)}` }, + { cap: CAP_TRANSCRIPT, label: 'Transcribe', route: () => '/app/talk' }, + { cap: CAP_SOUND_GENERATION, label: 'Sound', route: (id) => `/app/sound/${encodeURIComponent(id)}` }, + { cap: CAP_FACE_RECOGNITION, label: 'Face', route: (id) => `/app/face/${encodeURIComponent(id)}` }, + { cap: CAP_SPEAKER_RECOGNITION, label: 'Voice', route: (id) => `/app/voice/${encodeURIComponent(id)}` }, + { cap: CAP_EMBEDDINGS, label: 'Embeddings' }, + { cap: CAP_RERANK, label: 'Rerank' }, +] + // formatInstalledAt renders an installed_at timestamp as a short relative/abs // string suitable for dense tables. Returns the raw value if parsing fails so // we never display "Invalid Date". @@ -503,7 +525,24 @@ export default function Manage() { {/* Use Cases */}
- { e.preventDefault(); navigate(`/app/chat/${encodeURIComponent(model.id)}`) }} className="badge badge-info" style={{ textDecoration: 'none', cursor: 'pointer' }}>Chat + {(() => { + const caps = Array.isArray(model.capabilities) ? model.capabilities : [] + const matched = USE_CASES.filter(uc => caps.includes(uc.cap) && !(uc.hideIf && caps.includes(uc.hideIf))) + if (matched.length === 0) { + return + } + return matched.map(uc => uc.route ? ( + { e.preventDefault(); navigate(uc.route(model.id)) }} + className="badge badge-info" + style={{ textDecoration: 'none', cursor: 'pointer' }} + >{uc.label} + ) : ( + {uc.label} + )) + })()}
{/* Actions */}