mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
@@ -12,6 +12,10 @@
|
|||||||
"start:bun": "NODE_ENV=production bun ./.next/standalone/server.js"
|
"start:bun": "NODE_ENV=production bun ./.next/standalone/server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@xyflow/react": "^12.10.1",
|
"@xyflow/react": "^12.10.1",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
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 { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal, Toggle, ConfirmModal } from "@/shared/components";
|
||||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
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({ id, index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown, onRemove }) {
|
||||||
function ModelItem({ 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 [editing, setEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(model);
|
const [draft, setDraft] = useState(model);
|
||||||
|
|
||||||
const commit = () => {
|
const commit = () => {
|
||||||
const trimmed = draft.trim();
|
const trimmed = draft.trim();
|
||||||
if (trimmed && trimmed !== model) onEdit(trimmed);
|
if (trimmed && trimmed !== model) onEdit(trimmed);
|
||||||
else setDraft(model); // revert if empty or unchanged
|
else setDraft(model);
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -301,7 +310,26 @@ function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Index badge */}
|
||||||
<span className="text-[10px] font-medium text-text-muted w-3 text-center shrink-0">{index + 1}</span>
|
<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 [nameError, setNameError] = useState("");
|
||||||
const [modelAliases, setModelAliases] = 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 () => {
|
const fetchModalData = async () => {
|
||||||
try {
|
try {
|
||||||
const aliasesRes = await fetch("/api/models/alias");
|
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>
|
<p className="text-xs text-text-muted">No models added yet</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex max-h-[55vh] min-w-0 flex-col gap-1 overflow-y-auto sm:max-h-[350px]">
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} modifiers={[restrictToVerticalAxis, restrictToParentElement]}>
|
||||||
{models.map((model, index) => (
|
<SortableContext items={modelItems.map((m) => m.uid)} strategy={verticalListSortingStrategy}>
|
||||||
<ModelItem
|
<div className="flex max-h-[55vh] min-w-0 flex-col gap-1 overflow-y-auto sm:max-h-[350px]">
|
||||||
key={index}
|
{modelItems.map(({ uid, model }, index) => (
|
||||||
index={index}
|
<ModelItem
|
||||||
model={model}
|
key={uid}
|
||||||
isFirst={index === 0}
|
id={uid}
|
||||||
isLast={index === models.length - 1}
|
index={index}
|
||||||
onEdit={(newVal) => {
|
model={model}
|
||||||
const updated = [...models];
|
isFirst={index === 0}
|
||||||
updated[index] = newVal;
|
isLast={index === modelItems.length - 1}
|
||||||
setModels(updated);
|
onEdit={(newVal) => {
|
||||||
}}
|
const updated = [...models];
|
||||||
onMoveUp={() => handleMoveUp(index)}
|
updated[index] = newVal;
|
||||||
onMoveDown={() => handleMoveDown(index)}
|
setModels(updated);
|
||||||
onRemove={() => handleRemoveModel(index)}
|
}}
|
||||||
/>
|
onMoveUp={() => handleMoveUp(index)}
|
||||||
))}
|
onMoveDown={() => handleMoveDown(index)}
|
||||||
</div>
|
onRemove={() => handleRemoveModel(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add Model button */}
|
{/* Add Model button */}
|
||||||
|
|||||||
Reference in New Issue
Block a user