mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
Refactor
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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" },
|
||||
};
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
// Shared Hooks - Export all
|
||||
export { useTheme } from "./useTheme";
|
||||
export { useModelCaps } from "./useModelCaps";
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user