"use client"; import { useState, useEffect } from "react"; import Modal from "./Modal"; import Input from "./Input"; import Button from "./Button"; import ModelSelectModal from "./ModelSelectModal"; const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/; // 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); setEditing(false); }; const handleKeyDown = (e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") { setDraft(model); setEditing(false); } }; return (
{index + 1} {editing ? ( setDraft(e.target.value)} onBlur={commit} onKeyDown={handleKeyDown} className="min-w-0 flex-1 rounded border border-primary/40 bg-white px-1.5 py-0.5 font-mono text-xs text-text-main outline-none dark:bg-black/20" /> ) : (
setEditing(true)} title="Click to edit">{model}
)}
); } // Reusable Combo create/edit modal. forcePrefix auto-prepends to name. export default function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindFilter = null, forcePrefix = "", title }) { // Strip prefix when editing existing combo so user only edits suffix const initialName = combo?.name ? (forcePrefix && combo.name.startsWith(forcePrefix) ? combo.name.slice(forcePrefix.length) : combo.name) : ""; const [name, setName] = useState(initialName); const [models, setModels] = useState(combo?.models || []); const [showModelSelect, setShowModelSelect] = useState(false); const [saving, setSaving] = useState(false); const [nameError, setNameError] = useState(""); const [modelAliases, setModelAliases] = useState({}); useEffect(() => { if (!isOpen) return; fetch("/api/models/alias").then((r) => r.ok ? r.json() : null).then((d) => d && setModelAliases(d.aliases || {})).catch(() => {}); }, [isOpen]); const validateName = (value) => { if (!value.trim()) { setNameError("Name is required"); return false; } const full = forcePrefix + value; if (!VALID_NAME_REGEX.test(full)) { setNameError("Only letters, numbers, -, _ and . allowed"); return false; } setNameError(""); return true; }; const handleNameChange = (e) => { let value = e.target.value; // If user types prefix manually, strip it (we always prepend) if (forcePrefix && value.startsWith(forcePrefix)) value = value.slice(forcePrefix.length); setName(value); if (value) validateName(value); else setNameError(""); }; const handleAddModel = (model) => { if (!models.includes(model.value)) setModels([...models, model.value]); }; const handleDeselectModel = (model) => { setModels(models.filter((m) => m !== model.value)); }; const handleRemoveModel = (i) => setModels(models.filter((_, idx) => idx !== i)); const handleMoveUp = (i) => { if (i === 0) return; const a = [...models]; [a[i - 1], a[i]] = [a[i], a[i - 1]]; setModels(a); }; const handleMoveDown = (i) => { if (i === models.length - 1) return; const a = [...models]; [a[i], a[i + 1]] = [a[i + 1], a[i]]; setModels(a); }; const handleSave = async () => { if (!validateName(name)) return; setSaving(true); await onSave({ name: forcePrefix + name.trim(), models }); setSaving(false); }; const isEdit = !!combo; return ( <>
{forcePrefix ? ( <>
{forcePrefix}
{nameError &&

{nameError}

} ) : ( )}

{forcePrefix ? `Auto-prefixed with "${forcePrefix}". ` : ""}Only letters, numbers, -, _ and . allowed

{models.length === 0 ? (
layers

No models added yet

) : (
{models.map((model, index) => ( { const a = [...models]; a[index] = v; setModels(a); }} onMoveUp={() => handleMoveUp(index)} onMoveDown={() => handleMoveDown(index)} onRemove={() => handleRemoveModel(index)} /> ))}
)}
setShowModelSelect(false)} onSelect={handleAddModel} onDeselect={handleDeselectModel} activeProviders={activeProviders} modelAliases={modelAliases} title="Add Model to Combo" kindFilter={kindFilter} addedModelValues={models} closeOnSelect={false} /> ); }