"use client"; import { useState, useEffect } from "react"; import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; // 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(); }, []); 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) { const active = (providersData.connections || []).filter( c => c.testStatus === "active" || c.testStatus === "success" ); setActiveProviders(active); } } 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 */}
); } 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({}); // Fetch model aliases when modal opens useEffect(() => { if (isOpen) { const fetchModelAliases = async () => { try { const res = await fetch("/api/models/alias"); const data = await res.json(); if (res.ok) setModelAliases(data.aliases || {}); } catch (error) { console.log("Error fetching model aliases:", error); } }; fetchModelAliases(); } }, [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 handleModelChange = (index, value) => { const newModels = [...models]; newModels[index] = value; setModels(newModels); }; 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) => (
{/* Index badge */} {index + 1} {/* Model Input */} handleModelChange(index, e.target.value)} placeholder="provider/model" className="flex-1 min-w-0 px-1.5 py-0.5 text-xs font-mono bg-transparent border-0 focus:outline-none text-text-main placeholder:text-text-muted/50" /> {/* Priority arrows - horizontal, always visible */}
{/* Remove - always visible */}
))}
)} {/* Add Model button - moved to bottom */}
{/* Actions */}
{/* Model Select Modal */} setShowModelSelect(false)} onSelect={handleAddModel} activeProviders={activeProviders} modelAliases={modelAliases} title="Add Model to Combo" /> ); }