parent
a8100e9444
commit
0a05285973
2 changed files with 80 additions and 24 deletions
|
|
@ -12,6 +12,10 @@
|
|||
"start:bun": "NODE_ENV=production bun ./.next/standalone/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers";
|
||||
import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal, Toggle, ConfirmModal } from "@/shared/components";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
|
|
@ -283,15 +287,20 @@ function ComboCard({ combo, copied, onCopy, onEdit, onDelete, roundRobinEnabled,
|
|||
);
|
||||
}
|
||||
|
||||
// Inline editable model item
|
||||
function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown, onRemove }) {
|
||||
function ModelItem({ id, index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown, onRemove }) {
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useSortable({ id });
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
// no transition — prevents the CSS settle animation fighting React's re-render on drop
|
||||
opacity: isDragging ? 0.4 : 1,
|
||||
zIndex: isDragging ? 999 : undefined,
|
||||
};
|
||||
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
|
||||
else setDraft(model);
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
|
|
@ -301,7 +310,26 @@ function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="group flex min-w-0 items-center gap-1.5 rounded-md bg-black/[0.02] px-2 py-1 transition-colors hover:bg-black/[0.04] dark:bg-white/[0.02] dark:hover:bg-white/[0.04]">
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`group flex min-w-0 items-center gap-1.5 rounded-md px-2 py-1 bg-black/[0.02] hover:bg-black/[0.04] dark:bg-white/[0.02] dark:hover:bg-white/[0.04] transition-colors ${isDragging ? "shadow-md ring-1 ring-primary/30" : ""}`}
|
||||
>
|
||||
{/* Drag handle */}
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
type="button"
|
||||
className="cursor-grab touch-none p-0.5 rounded text-text-muted hover:text-primary active:cursor-grabbing shrink-0"
|
||||
title="Drag to reorder"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="9" cy="4" r="2"/><circle cx="15" cy="4" r="2"/>
|
||||
<circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/>
|
||||
<circle cx="9" cy="20" r="2"/><circle cx="15" cy="20" r="2"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Index badge */}
|
||||
<span className="text-[10px] font-medium text-text-muted w-3 text-center shrink-0">{index + 1}</span>
|
||||
|
||||
|
|
@ -366,6 +394,25 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
|
|||
const [nameError, setNameError] = useState("");
|
||||
const [modelAliases, setModelAliases] = useState({});
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
||||
);
|
||||
|
||||
// Use stable index-based IDs so duplicates and similar names are handled correctly
|
||||
const modelItems = models.map((model, i) => ({ uid: `item-${i}`, model }));
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event;
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = modelItems.findIndex((m) => m.uid === active.id);
|
||||
const newIndex = modelItems.findIndex((m) => m.uid === over.id);
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
setModels((prev) => arrayMove(prev, oldIndex, newIndex));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModalData = async () => {
|
||||
try {
|
||||
const aliasesRes = await fetch("/api/models/alias");
|
||||
|
|
@ -470,25 +517,30 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
|
|||
<p className="text-xs text-text-muted">No models added yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex max-h-[55vh] min-w-0 flex-col gap-1 overflow-y-auto sm:max-h-[350px]">
|
||||
{models.map((model, index) => (
|
||||
<ModelItem
|
||||
key={index}
|
||||
index={index}
|
||||
model={model}
|
||||
isFirst={index === 0}
|
||||
isLast={index === models.length - 1}
|
||||
onEdit={(newVal) => {
|
||||
const updated = [...models];
|
||||
updated[index] = newVal;
|
||||
setModels(updated);
|
||||
}}
|
||||
onMoveUp={() => handleMoveUp(index)}
|
||||
onMoveDown={() => handleMoveDown(index)}
|
||||
onRemove={() => handleRemoveModel(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} modifiers={[restrictToVerticalAxis, restrictToParentElement]}>
|
||||
<SortableContext items={modelItems.map((m) => m.uid)} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex max-h-[55vh] min-w-0 flex-col gap-1 overflow-y-auto sm:max-h-[350px]">
|
||||
{modelItems.map(({ uid, model }, index) => (
|
||||
<ModelItem
|
||||
key={uid}
|
||||
id={uid}
|
||||
index={index}
|
||||
model={model}
|
||||
isFirst={index === 0}
|
||||
isLast={index === modelItems.length - 1}
|
||||
onEdit={(newVal) => {
|
||||
const updated = [...models];
|
||||
updated[index] = newVal;
|
||||
setModels(updated);
|
||||
}}
|
||||
onMoveUp={() => handleMoveUp(index)}
|
||||
onMoveDown={() => handleMoveDown(index)}
|
||||
onRemove={() => handleRemoveModel(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
)}
|
||||
|
||||
{/* Add Model button */}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue