"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} />
>
);
}