import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import { toast } from "sonner"; import { Check, Loader2, Search } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useUpdateModelPricing } from "@/lib/query/usage"; import { fetchModelsDevPricing, flattenModels, formatPrice, type ModelsDevEntry, } from "@/lib/modelsDevPricing"; import { isTextEditableTarget } from "@/utils/domUtils"; export { flattenModels, formatPrice, normalizeModelIdForPricing, } from "@/lib/modelsDevPricing"; // 全量约 5000 条:默认只展示最新发布的一批,搜索时才做全量匹配 const DEFAULT_VISIBLE_ROWS = 50; const MAX_VISIBLE_ROWS = 200; interface ModelsDevPickerDialogProps { open: boolean; onClose: () => void; /** 导入成功后调用(此时定价列表已刷新) */ onImported: () => void; } export function ModelsDevPickerDialog({ open, onClose, onImported, }: ModelsDevPickerDialogProps) { const { t } = useTranslation(); const updatePricing = useUpdateModelPricing(); const [search, setSearch] = useState(""); const [providerFilter, setProviderFilter] = useState("all"); const [selected, setSelected] = useState(null); // 每次打开时重置选择与过滤条件 useEffect(() => { if (open) { setSearch(""); setProviderFilter("all"); setSelected(null); } }, [open]); const { data, isLoading, error, refetch } = useQuery({ queryKey: ["models-dev-pricing"], queryFn: fetchModelsDevPricing, enabled: open, staleTime: 60 * 60 * 1000, retry: 1, }); const entries = useMemo(() => (data ? flattenModels(data) : []), [data]); const providers = useMemo(() => { const map = new Map(); for (const entry of entries) { if (!map.has(entry.providerId)) { map.set(entry.providerId, entry.providerName); } } return Array.from(map, ([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name), ); }, [entries]); const isFiltering = search.trim() !== "" || providerFilter !== "all"; const filtered = useMemo(() => { const query = search.trim().toLowerCase(); return entries.filter( (entry) => (providerFilter === "all" || entry.providerId === providerFilter) && (!query || entry.modelId.toLowerCase().includes(query) || entry.normalizedId.includes(query) || entry.modelName.toLowerCase().includes(query) || entry.providerName.toLowerCase().includes(query)), ); }, [entries, search, providerFilter]); // 默认只展示最新发布的一批,搜索/筛选时展示全量匹配(设上限防卡顿) const visible = useMemo( () => filtered.slice(0, isFiltering ? MAX_VISIBLE_ROWS : DEFAULT_VISIBLE_ROWS), [filtered, isFiltering], ); // 单选:点击未选中的行替换选择,点击已选中的行取消选择。 // 限制单选是为了避免批量导入时每条都触发一次全量零成本回填扫描(见 update_model_pricing)。 const toggleEntry = (entry: ModelsDevEntry) => { setSelected((prev) => (prev?.key === entry.key ? null : entry)); }; const handleImport = async () => { if (!selected) return; try { await updatePricing.mutateAsync({ modelId: selected.normalizedId, displayName: selected.modelName, inputCost: formatPrice(selected.input), outputCost: formatPrice(selected.output), cacheReadCost: formatPrice(selected.cacheRead), cacheCreationCost: formatPrice(selected.cacheWrite), }); toast.success( t("usage.modelsDevImported", { name: selected.modelName, defaultValue: "已导入 {{name}} 的定价", }), { closeButton: true }, ); onImported(); } catch (error) { toast.error(String(error)); } }; const priceColumns = (entry: ModelsDevEntry) => [ { label: t("usage.inputCost", "输入成本"), value: entry.input }, { label: t("usage.outputCost", "输出成本"), value: entry.output }, { label: t("usage.cacheReadCost", "缓存命中"), value: entry.cacheRead }, { label: t("usage.cacheWriteCost", "缓存创建"), value: entry.cacheWrite, }, ] as const; return ( { if (!nextOpen && !updatePricing.isPending) { onClose(); } }} > { // 在搜索框里按 ESC 不应关闭弹窗丢掉已选模型(与 FullScreenPanel 的约定一致) if (isTextEditableTarget(e.target)) { e.preventDefault(); } }} > {t("usage.modelsDevPickerTitle", "从 models.dev 导入定价")} {t( "usage.modelsDevPickerDesc", "选择要导入的模型(价格单位:USD / 百万 tokens),每次导入一个", )}
{isLoading ? (
) : error ? ( {t("usage.modelsDevLoadError", "加载 models.dev 数据失败")}:{" "} {error instanceof Error ? error.message : String(error)} ) : ( <>
setSearch(e.target.value)} placeholder={t( "usage.modelsDevSearchPlaceholder", "搜索模型或供应商(全量搜索)...", )} className="pl-8" />
{filtered.length === 0 ? (
{t("usage.modelsDevNoResults", "没有匹配的模型")}
) : (
{visible.map((entry) => (
toggleEntry(entry)} className={`flex cursor-pointer items-center gap-3 px-3 py-2 ${ selected?.key === entry.key ? "bg-accent/50" : "hover:bg-muted/40" }`} >
{entry.modelName} {entry.providerName} {entry.releaseDate && ( {entry.releaseDate} )}
{entry.normalizedId}
{priceColumns(entry).map((column) => (
{column.label}
${formatPrice(column.value)}
))}
))} {filtered.length > visible.length && (
{isFiltering ? t("usage.modelsDevTruncated", { shown: visible.length, total: filtered.length, defaultValue: "仅显示前 {{shown}} 条,共 {{total}} 条结果,请缩小搜索范围", }) : t("usage.modelsDevDefaultHint", { shown: visible.length, total: filtered.length, defaultValue: "默认展示最新发布的 {{shown}} 个模型(共 {{total}} 个),输入关键字可全量搜索", })}
)}
)}
)}
); }