mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
fix: update the logic code for combos pages
This commit is contained in:
@@ -7,7 +7,6 @@ import { CSS } from "@dnd-kit/utilities";
|
||||
import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers";
|
||||
import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal, ConfirmModal, CapacityBadges, Select } from "@/shared/components";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
|
||||
// Validate combo name: only a-z, A-Z, 0-9, -, _
|
||||
const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/;
|
||||
@@ -17,7 +16,7 @@ export default function CombosPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editingCombo, setEditingCombo] = useState(null);
|
||||
const [activeProviders, setActiveProviders] = useState([]);
|
||||
const [connectedModels, setConnectedModels] = useState([]);
|
||||
const [comboStrategies, setComboStrategies] = useState({});
|
||||
const [modelCaps, setModelCaps] = useState({});
|
||||
const [confirmState, setConfirmState] = useState(null);
|
||||
@@ -29,23 +28,19 @@ export default function CombosPage() {
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [combosRes, providersRes, settingsRes, modelsRes] = await Promise.all([
|
||||
const [combosRes, settingsRes, modelsRes] = await Promise.all([
|
||||
fetch("/api/combos"),
|
||||
fetch("/api/providers"),
|
||||
fetch("/api/settings"),
|
||||
fetch("/api/models"),
|
||||
fetch("/api/models/connected", { cache: "no-store" }),
|
||||
]);
|
||||
const combosData = await combosRes.json();
|
||||
const providersData = await providersRes.json();
|
||||
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
|
||||
|
||||
// Only LLM combos here - webSearch/webFetch combos belong to media-providers/web
|
||||
if (combosRes.ok) setCombos((combosData.combos || []).filter(c => !c.kind || c.kind === "llm"));
|
||||
if (providersRes.ok) {
|
||||
setActiveProviders(providersData.connections || []);
|
||||
}
|
||||
if (modelsRes.ok) {
|
||||
const md = await modelsRes.json();
|
||||
setConnectedModels(md.models || []);
|
||||
// Build fullModel -> caps map for badge lookup
|
||||
const map = {};
|
||||
for (const m of md.models || []) if (m.caps) map[m.fullModel] = m.caps;
|
||||
@@ -59,6 +54,11 @@ export default function CombosPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// The Models page is the source of truth for eligible models. Administrators
|
||||
// can still see disabled rows there to manage them, but disabled models must
|
||||
// not be added to new or edited combos because they cannot serve requests.
|
||||
const selectableModels = connectedModels.filter((model) => !model.disabled);
|
||||
|
||||
const handleCreate = async (data) => {
|
||||
try {
|
||||
const res = await fetch("/api/combos", {
|
||||
@@ -190,7 +190,7 @@ export default function CombosPage() {
|
||||
key={combo.id}
|
||||
combo={combo}
|
||||
modelCaps={modelCaps}
|
||||
activeProviders={activeProviders}
|
||||
availableModels={selectableModels}
|
||||
copied={copied}
|
||||
onCopy={copy}
|
||||
onEdit={() => setEditingCombo(combo)}
|
||||
@@ -208,7 +208,7 @@ export default function CombosPage() {
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onSave={handleCreate}
|
||||
activeProviders={activeProviders}
|
||||
availableModels={selectableModels}
|
||||
/>
|
||||
|
||||
{/* Edit Modal - Use key to force remount and reset state */}
|
||||
@@ -218,7 +218,7 @@ export default function CombosPage() {
|
||||
combo={editingCombo}
|
||||
onClose={() => setEditingCombo(null)}
|
||||
onSave={(data) => handleUpdate(editingCombo.id, data)}
|
||||
activeProviders={activeProviders}
|
||||
availableModels={selectableModels}
|
||||
/>
|
||||
|
||||
{/* Confirm Delete Modal */}
|
||||
@@ -240,7 +240,7 @@ const STRATEGY_OPTIONS = [
|
||||
{ value: "fusion", label: "Fusion — panel + judge" },
|
||||
];
|
||||
|
||||
function ComboCard({ combo, modelCaps = {}, activeProviders = [], copied, onCopy, onEdit, onDelete, strategy = {}, onSetStrategy }) {
|
||||
function ComboCard({ combo, modelCaps = {}, availableModels = [], copied, onCopy, onEdit, onDelete, strategy = {}, onSetStrategy }) {
|
||||
const [showJudgeSelect, setShowJudgeSelect] = useState(false);
|
||||
const current = strategy.fallbackStrategy || "fallback";
|
||||
const judge = strategy.judgeModel || "";
|
||||
@@ -344,7 +344,7 @@ function ComboCard({ combo, modelCaps = {}, activeProviders = [], copied, onCopy
|
||||
isOpen={showJudgeSelect}
|
||||
onClose={() => setShowJudgeSelect(false)}
|
||||
onSelect={(m) => { onSetStrategy({ judgeModel: m?.value || "" }); setShowJudgeSelect(false); }}
|
||||
activeProviders={activeProviders}
|
||||
availableModels={availableModels}
|
||||
title="Select Judge Model"
|
||||
addedModelValues={judge ? [judge] : []}
|
||||
closeOnSelect={true}
|
||||
@@ -451,14 +451,13 @@ function ModelItem({ id, index, model, isFirst, isLast, onEdit, onMoveUp, onMove
|
||||
);
|
||||
}
|
||||
|
||||
function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindFilter = null }) {
|
||||
function ComboFormModal({ isOpen, combo, onClose, onSave, availableModels = [], kindFilter = null }) {
|
||||
// Initialize state with combo values - key prop on parent handles reset on remount
|
||||
const [name, setName] = useState(combo?.name || "");
|
||||
const [models, setModels] = useState(combo?.models || []);
|
||||
const [showModelSelect, setShowModelSelect] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [nameError, setNameError] = useState("");
|
||||
const [modelAliases, setModelAliases] = useState({});
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
@@ -479,21 +478,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModalData = async () => {
|
||||
try {
|
||||
const aliasesRes = await fetch("/api/models/alias");
|
||||
if (!aliasesRes.ok) return;
|
||||
const aliasesData = await aliasesRes.json();
|
||||
setModelAliases(aliasesData.aliases || {});
|
||||
} catch (error) {
|
||||
console.error("Error fetching modal data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) fetchModalData();
|
||||
}, [isOpen]);
|
||||
|
||||
const validateName = (value) => {
|
||||
if (!value.trim()) {
|
||||
setNameError("Name is required");
|
||||
@@ -642,8 +626,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
|
||||
onClose={() => setShowModelSelect(false)}
|
||||
onSelect={handleAddModel}
|
||||
onDeselect={handleDeselectModel}
|
||||
activeProviders={activeProviders}
|
||||
modelAliases={modelAliases}
|
||||
availableModels={availableModels}
|
||||
title="Add Model to Combo"
|
||||
kindFilter={kindFilter}
|
||||
addedModelValues={models}
|
||||
|
||||
@@ -110,6 +110,7 @@ export default function ProvidersPage() {
|
||||
const searchQuery = useHeaderSearchStore((s) => s.query);
|
||||
const registerSearch = useHeaderSearchStore((s) => s.register);
|
||||
const unregisterSearch = useHeaderSearchStore((s) => s.unregister);
|
||||
const isAdmin = user?.role === "admin";
|
||||
|
||||
useEffect(() => {
|
||||
registerSearch("Search providers...");
|
||||
@@ -335,8 +336,10 @@ export default function ProvidersPage() {
|
||||
freeEntries.length > 0 ||
|
||||
freeTierEntries.length > 0 ||
|
||||
apikeyEntries.length > 0 ||
|
||||
compatibleProviders.length > 0 ||
|
||||
anthropicCompatibleProviders.length > 0;
|
||||
(isAdmin && (
|
||||
compatibleProviders.length > 0 ||
|
||||
anthropicCompatibleProviders.length > 0
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-6 px-1 sm:px-0">
|
||||
@@ -349,7 +352,8 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Providers (OpenAI/Anthropic Compatible) — dynamic */}
|
||||
{/* Custom provider configuration is administered centrally. */}
|
||||
{isAdmin && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
|
||||
@@ -400,6 +404,7 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OAuth Providers */}
|
||||
{oauthEntries.length > 0 && (
|
||||
@@ -577,7 +582,7 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<AddCompatibleModal
|
||||
{isAdmin && <AddCompatibleModal
|
||||
variant="openai"
|
||||
isOpen={showAddCompatibleModal}
|
||||
onClose={() => setShowAddCompatibleModal(false)}
|
||||
@@ -585,8 +590,8 @@ export default function ProvidersPage() {
|
||||
setProviderNodes((prev) => [...prev, node]);
|
||||
setShowAddCompatibleModal(false);
|
||||
}}
|
||||
/>
|
||||
<AddCompatibleModal
|
||||
/>}
|
||||
{isAdmin && <AddCompatibleModal
|
||||
variant="anthropic"
|
||||
isOpen={showAddAnthropicCompatibleModal}
|
||||
onClose={() => setShowAddAnthropicCompatibleModal(false)}
|
||||
@@ -594,7 +599,7 @@ export default function ProvidersPage() {
|
||||
setProviderNodes((prev) => [...prev, node]);
|
||||
setShowAddAnthropicCompatibleModal(false);
|
||||
}}
|
||||
/>
|
||||
/>}
|
||||
|
||||
{/* Test Results Modal */}
|
||||
{testResults && (
|
||||
|
||||
Reference in New Issue
Block a user