import type { IconifyIcon } from "@iconify/react/offline"; import { useEffect, useRef } from "react"; import Sortable from "sortablejs"; import type { Model } from "../../api"; import { Button } from "../ui/Button"; import { Card } from "../ui/Card"; import { Icon } from "../ui/Icon"; import { claudeIcon, dragIcon, flatColorOrganizationIcon, openAiIcon } from "../ui/icons"; import { CursorModelTestResult, type CursorModelTestState } from "./CursorModelTestResult"; import styles from "./CursorSettings.module.scss"; export type CursorModelGrouping = "flat" | "provider" | "type"; export type CursorModelGroup = { key: string; label: string; icon: IconifyIcon; models: Model[]; }; type CursorModelCardsProps = { models: Model[]; grouping: CursorModelGrouping; disabled: boolean; testingModelHashes: Set; testResults: Map; onTest: (model: Model) => void; onEdit: (model: Model) => void; onDuplicate: (model: Model) => void; onDelete: (model: Model) => void; onReorder: (modelHashes: string[]) => void; }; type ModelGridProps = Omit & { sortable: boolean; }; export function cursorModelGroups(models: Model[], grouping: Exclude): CursorModelGroup[] { const groups = new Map(); for (const model of models) { const descriptor = grouping === "provider" ? providerGroup(model) : typeGroup(model); const group = groups.get(descriptor.key); if (group) { group.models.push(model); } else { groups.set(descriptor.key, { ...descriptor, models: [model] }); } } return [...groups.values()]; } export function CursorModelCards(props: CursorModelCardsProps) { if (props.grouping === "flat") return
; return
{cursorModelGroups(props.models, props.grouping).map((group) =>
{group.label}
)}
; } function ModelGrid({ models, sortable: sortableEnabled, disabled, testingModelHashes, testResults, onTest, onEdit, onDuplicate, onDelete, onReorder, }: ModelGridProps) { const grid = useRef(null); const sortable = useRef(null); const currentModels = useRef(models); const reorder = useRef(onReorder); currentModels.current = models; reorder.current = onReorder; useEffect(() => { if (!sortableEnabled || !grid.current) return; sortable.current = Sortable.create(grid.current, { animation: 160, dataIdAttr: "data-model-hash", draggable: `.${styles.modelCard}`, handle: `.${styles.sortHandle}`, ghostClass: styles.sortGhost, chosenClass: styles.sortChosen, dragClass: styles.sortDragging, forceFallback: true, fallbackOnBody: true, fallbackTolerance: 3, onEnd: (event) => { const oldIndex = event.oldDraggableIndex ?? event.oldIndex; const newIndex = event.newDraggableIndex ?? event.newIndex; if (typeof oldIndex !== "number" || typeof newIndex !== "number" || oldIndex === newIndex) { sortable.current?.sort(currentModels.current.map((model) => model.model_hash), false); return; } const reordered = currentModels.current.slice(); const [moved] = reordered.splice(oldIndex, 1); if (!moved || newIndex < 0 || newIndex > reordered.length) { sortable.current?.sort(currentModels.current.map((model) => model.model_hash), false); return; } reordered.splice(newIndex, 0, moved); reorder.current(reordered.map((model) => model.model_hash)); }, }); return () => { sortable.current?.destroy(); sortable.current = null; }; }, [sortableEnabled]); useEffect(() => { sortable.current?.option("disabled", disabled); sortable.current?.sort(models.map((model) => model.model_hash), false); }, [disabled, models]); return
{models.map((model) => { const result = testResults.get(model.model_hash); const testing = testingModelHashes.has(model.model_hash); return {sortableEnabled && }
{model.display_name} {model.model_id}
{model.type === "anthropic" ? "Anthropic" : "OpenAI"}
; })}
; } function providerGroup(model: Model) { const label = providerDomain(model.base_url); return { key: label, label, icon: flatColorOrganizationIcon }; } function providerDomain(baseUrl: string) { const value = baseUrl.trim(); try { return new URL(value).hostname.toLowerCase() || value; } catch { try { return new URL(`https://${value}`).hostname.toLowerCase() || value; } catch { return value; } } } function typeGroup(model: Model) { if (model.type === "anthropic") return { key: "anthropic", label: "Anthropic", icon: claudeIcon }; if (model.openai_endpoint === "/v1/chat/completions") return { key: "openai-chat", label: "OpenAI Chat", icon: openAiIcon }; return { key: "openai-responses", label: "OpenAI Responses", icon: openAiIcon }; }