This commit is contained in:
decolua
2026-06-15 18:18:04 +07:00
parent 8ab5af0052
commit b282f05549
66 changed files with 2328 additions and 213 deletions
+28
View File
@@ -0,0 +1,28 @@
"use client";
import { CAPACITY_META } from "@/shared/constants/models";
import Tooltip from "./Tooltip";
// Render small icon badges for a model's capabilities (only those set true).
// colorOverride: force a single color class for all badges (default: per-cap color).
// size: icon font-size in px (default 16).
export default function CapacityBadges({ caps, className = "", colorOverride, size = 16 }) {
if (!caps) return null;
const active = Object.keys(CAPACITY_META).filter((k) => caps[k]);
if (active.length === 0) return null;
return (
<span className={`inline-flex items-center gap-0.5 ${className}`}>
{active.map((k) => (
<Tooltip key={k} text={`${CAPACITY_META[k].label}${CAPACITY_META[k].desc}`}>
<span
className={`material-symbols-outlined leading-none cursor-help ${colorOverride || CAPACITY_META[k].color}`}
style={{ fontSize: `${size}px` }}
>
{CAPACITY_META[k].icon}
</span>
</Tooltip>
))}
</span>
);
}
+8 -1
View File
@@ -4,6 +4,8 @@ import { useState, useMemo, useEffect } from "react";
import PropTypes from "prop-types";
import Modal from "./Modal";
import ProviderIcon from "./ProviderIcon";
import CapacityBadges from "./CapacityBadges";
import { useModelCaps } from "@/shared/hooks/useModelCaps";
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, AI_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, getProviderAlias } from "@/shared/constants/providers";
@@ -40,6 +42,7 @@ export default function ModelSelectModal({
return kinds.includes(kindFilter);
});
}, [activeProviders, kindFilter]);
const { getCaps } = useModelCaps();
const [searchQuery, setSearchQuery] = useState("");
const [combos, setCombos] = useState([]);
const [providerNodes, setProviderNodes] = useState([]);
@@ -499,9 +502,13 @@ export default function ModelSelectModal({
<>
{model.name}
<span className="text-[9px] opacity-60 font-normal">custom</span>
<CapacityBadges caps={getCaps(model.value)} />
</>
) : (
model.name
<>
{model.name}
<CapacityBadges caps={getCaps(model.value)} />
</>
)}
</span>
</button>
+2 -2
View File
@@ -12,10 +12,10 @@ export default function Tooltip({ text, children, position = "top", color }) {
const bgClass = color ? "" : "bg-gray-900";
return (
<div className="relative inline-flex group">
<div className="relative inline-flex group/tt">
{children}
<div
className={`pointer-events-none absolute ${posClass} z-50 w-max max-w-56 rounded px-2 py-1 text-[11px] leading-snug ${bgClass} text-white opacity-0 group-hover:opacity-100 transition-opacity duration-150 whitespace-normal`}
className={`pointer-events-none absolute ${posClass} z-50 w-max max-w-56 rounded px-2 py-1 text-[11px] leading-snug ${bgClass} text-white opacity-0 group-hover/tt:opacity-100 transition-opacity duration-150 whitespace-normal`}
style={bgStyle}
>
{text}
+1
View File
@@ -36,6 +36,7 @@ export { default as NoAuthProxyCard } from "./NoAuthProxyCard";
export { default as SegmentedControl } from "./SegmentedControl";
export { default as Tooltip } from "./Tooltip";
export { default as ProviderInfoCard } from "./ProviderInfoCard";
export { default as CapacityBadges } from "./CapacityBadges";
// Layouts
export * from "./layouts";
+7
View File
@@ -38,3 +38,10 @@ export const AI_MODELS = Object.entries(MODELS).flatMap(([alias, models]) =>
);
export const getModelKind = (m, fallback = null) => m?.kind || m?.type || fallback;
// Capacity metadata for UI badges — icon + label + color per capability.
export const CAPACITY_META = {
vision: { icon: "visibility", label: "Vision", desc: "Supports image input", color: "text-blue-500" },
// search: temporarily hidden (feature not wired yet)
reasoning: { icon: "neurology", label: "Reasoning", desc: "Supports reasoning / thinking", color: "text-amber-500" },
};
+4 -1
View File
@@ -1,5 +1,6 @@
// Provider definitions
import REGISTRY from "open-sse/providers/registry/index.js";
import { RISK_NOTICE } from "@/shared/constants/providersDisplay";
const MEDIA_ENTRY_KEYS = [
"serviceKinds", "ttsConfig", "sttConfig", "embeddingConfig",
@@ -15,8 +16,10 @@ function buildProviderEntry(r) {
for (const k of MEDIA_ENTRY_KEYS) {
if (r[k] !== undefined) mediaFields[k] = r[k];
}
const display = { ...(r.display || {}) };
if (display.deprecationNotice === "RISK_NOTICE") display.deprecationNotice = RISK_NOTICE;
return {
...(r.display || {}),
...display,
id: r.id,
alias: r.uiAlias || r.alias,
...(r.hidden ? { hidden: true } : {}),
+1
View File
@@ -1,2 +1,3 @@
// Shared Hooks - Export all
export { useTheme } from "./useTheme";
export { useModelCaps } from "./useModelCaps";
+39
View File
@@ -0,0 +1,39 @@
"use client";
import { useState, useEffect } from "react";
// Fetch model capabilities once and expose a lookup by fullModel ("provider/model") or bare model id.
export function useModelCaps() {
const [byFull, setByFull] = useState({});
const [byId, setById] = useState({});
useEffect(() => {
let alive = true;
(async () => {
try {
const res = await fetch("/api/models");
if (!res.ok) return;
const data = await res.json();
const full = {};
const id = {};
for (const m of data.models || []) {
if (!m.caps) continue;
if (m.fullModel) full[m.fullModel] = m.caps;
if (m.model) id[m.model] = m.caps;
}
if (alive) { setByFull(full); setById(id); }
} catch { /* ignore */ }
})();
return () => { alive = false; };
}, []);
// Resolve caps from a "provider/model" string or a bare model id.
const getCaps = (key) => {
if (!key) return null;
if (byFull[key]) return byFull[key];
const bare = key.includes("/") ? key.slice(key.indexOf("/") + 1) : key;
return byId[bare] || null;
};
return { getCaps };
}