"use client"; import { useState, useEffect, useCallback } from "react"; import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal } 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_-]+$/; export default function CombosPage() { const [combos, setCombos] = useState([]); const [loading, setLoading] = useState(true); const [showCreateModal, setShowCreateModal] = useState(false); const [editingCombo, setEditingCombo] = useState(null); const [activeProviders, setActiveProviders] = useState([]); const { copied, copy } = useCopyToClipboard(); useEffect(() => { fetchData(); }, []); // eslint-disable-line react-hooks/exhaustive-deps const fetchData = async () => { try { const [combosRes, providersRes] = await Promise.all([ fetch("/api/combos"), fetch("/api/providers"), ]); const combosData = await combosRes.json(); const providersData = await providersRes.json(); if (combosRes.ok) setCombos(combosData.combos || []); if (providersRes.ok) { setActiveProviders(providersData.connections || []); } } catch (error) { console.log("Error fetching data:", error); } finally { setLoading(false); } }; const handleCreate = async (data) => { try { const res = await fetch("/api/combos", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (res.ok) { await fetchData(); setShowCreateModal(false); } else { const err = await res.json(); alert(err.error || "Failed to create combo"); } } catch (error) { console.log("Error creating combo:", error); } }; const handleUpdate = async (id, data) => { try { const res = await fetch(`/api/combos/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (res.ok) { await fetchData(); setEditingCombo(null); } else { const err = await res.json(); alert(err.error || "Failed to update combo"); } } catch (error) { console.log("Error updating combo:", error); } }; const handleDelete = async (id) => { if (!confirm("Delete this combo?")) return; try { const res = await fetch(`/api/combos/${id}`, { method: "DELETE" }); if (res.ok) { setCombos(combos.filter(c => c.id !== id)); } } catch (error) { console.log("Error deleting combo:", error); } }; if (loading) { return (
); } return (
{/* Header */}

Combos

Create model combos with fallback support

{/* Combos List */} {combos.length === 0 ? (
layers

No combos yet

Create model combos with fallback support

) : (
{combos.map((combo) => ( setEditingCombo(combo)} onDelete={() => handleDelete(combo.id)} /> ))}
)} {/* Create Modal - Use key to force remount and reset state */} setShowCreateModal(false)} onSave={handleCreate} activeProviders={activeProviders} /> {/* Edit Modal - Use key to force remount and reset state */} setEditingCombo(null)} onSave={(data) => handleUpdate(editingCombo.id, data)} activeProviders={activeProviders} />
); } function ComboCard({ combo, copied, onCopy, onEdit, onDelete }) { return (
layers
{combo.name}
{combo.models.length === 0 ? ( No models ) : ( combo.models.slice(0, 3).map((model, index) => ( {model} )) )} {combo.models.length > 3 && ( +{combo.models.length - 3} more )}
{/* Actions */}
); } // Inline editable model item function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown, onRemove }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(model); const commit = () => { const trimmed = draft.trim(); if (trimmed && trimmed !== model) onEdit(trimmed); else setDraft(model); // revert if empty or unchanged setEditing(false); }; const handleKeyDown = (e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") { setDraft(model); setEditing(false); } }; return (
{/* Index badge */} {index + 1} {/* Inline editable model value */} {editing ? ( setDraft(e.target.value)} onBlur={commit} onKeyDown={handleKeyDown} className="flex-1 min-w-0 px-1.5 py-0.5 text-xs font-mono bg-white dark:bg-black/20 border border-primary/40 rounded outline-none text-text-main" /> ) : (
setEditing(true)} title="Click to edit" > {model}
)} {/* Priority arrows */}
{/* Remove */}
); } function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { // 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 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"); return false; } if (!VALID_NAME_REGEX.test(value)) { setNameError("Only letters, numbers, - and _ allowed"); return false; } setNameError(""); return true; }; const handleNameChange = (e) => { const value = e.target.value; setName(value); if (value) validateName(value); else setNameError(""); }; const handleAddModel = (model) => { if (!models.includes(model.value)) { setModels([...models, model.value]); } }; const handleRemoveModel = (index) => { setModels(models.filter((_, i) => i !== index)); }; const handleMoveUp = (index) => { if (index === 0) return; const newModels = [...models]; [newModels[index - 1], newModels[index]] = [newModels[index], newModels[index - 1]]; setModels(newModels); }; const handleMoveDown = (index) => { if (index === models.length - 1) return; const newModels = [...models]; [newModels[index], newModels[index + 1]] = [newModels[index + 1], newModels[index]]; setModels(newModels); }; const handleSave = async () => { if (!validateName(name)) return; setSaving(true); await onSave({ name: name.trim(), models }); setSaving(false); }; const isEdit = !!combo; return ( <>
{/* Name */}

Only letters, numbers, - and _ allowed

{/* Models */}
{models.length === 0 ? (
layers

No models added yet

) : (
{models.map((model, index) => ( { const updated = [...models]; updated[index] = newVal; setModels(updated); }} onMoveUp={() => handleMoveUp(index)} onMoveDown={() => handleMoveDown(index)} onRemove={() => handleRemoveModel(index)} /> ))}
)} {/* Add Model button */}
{/* Actions */}
{/* Model Select Modal */} setShowModelSelect(false)} onSelect={handleAddModel} activeProviders={activeProviders} modelAliases={modelAliases} title="Add Model to Combo" /> ); }