"use client"; import { useState, useEffect } from "react"; import { Table, message, Tooltip } from "antd"; import { DownloadOutlined, ExperimentOutlined, CheckOutlined, CloseOutlined, InfoCircleOutlined, UpOutlined, } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import Image from "next/image"; import { Button } from "@/components/ui/button"; import { motion, AnimatePresence } from "framer-motion"; import { useTranslation } from "react-i18next"; import { Progress } from "antd"; import { toast, Toaster } from "sonner"; import { EditableCell } from "@/components/editable-cell"; interface ModelResponse { id: string; name: string; base_model_id: string; system_prompt: string; imageUrl: string; input_price: number; output_price: number; per_msg_price: number; } interface Model { id: string; name: string; base_model_id: string; system_prompt: string; imageUrl: string; input_price: number; output_price: number; per_msg_price: number; testStatus?: "success" | "error" | "testing"; syncStatus?: "syncing" | "success" | "error"; } const TestStatusIndicator = ({ status }: { status: Model["testStatus"] }) => { if (!status) return null; const variants = { testing: { container: "bg-blue-100", icon: "w-3 h-3 rounded-full border-2 border-blue-500 border-t-transparent animate-spin", }, success: { container: "bg-green-100", icon: "text-[10px] text-green-500", }, error: { container: "bg-red-100", icon: "text-[10px] text-red-500", }, }; const variant = variants[status]; return ( {status === "testing" ? (
) : status === "success" ? ( ) : ( )} ); }; const TestProgressPanel = ({ isVisible, models, isComplete, t, }: { isVisible: boolean; models: Model[]; isComplete: boolean; t: (key: string) => string; }) => { const [isExpanded, setIsExpanded] = useState(true); const successCount = models.filter((m) => m.testStatus === "success").length; const errorCount = models.filter((m) => m.testStatus === "error").length; const testingCount = models.filter((m) => m.testStatus === "testing").length; const totalCount = models.length; const progress = Math.round(((successCount + errorCount) / totalCount) * 100); useEffect(() => { if (testingCount > 0) { setIsExpanded(true); } }, [testingCount]); return ( {isVisible && (
setIsExpanded(!isExpanded)} >

{isComplete ? t("models.testComplete") : t("models.testingModels")}

0 ? "testing" : isComplete ? "success" : "error" } />
{isExpanded && (
{successCount}
{t("models.testSuccess")}
{errorCount}
{t("models.testFailed")}
{testingCount}
{t("models.testing")}
{models.map((model) => ( {model.name}
{model.name}
))}
)}
)}
); }; const LoadingState = ({ t }: { t: (key: string) => string }) => (

{t("models.loading")}

); export default function ModelsPage() { const { t } = useTranslation("common"); const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingCell, setEditingCell] = useState<{ id: string; field: "input_price" | "output_price" | "per_msg_price"; } | null>(null); const [testing, setTesting] = useState(false); const [apiKey, setApiKey] = useState(null); const [isTestComplete, setIsTestComplete] = useState(false); const [syncing, setSyncing] = useState(false); useEffect(() => { const fetchModels = async () => { try { const token = localStorage.getItem("access_token"); const response = await fetch("/api/v1/models", { headers: { Authorization: `Bearer ${token}`, }, }); if (!response.ok) { throw new Error(t("error.model.failToFetchModels")); } const data = (await response.json()) as ModelResponse[]; setModels( data.map((model: ModelResponse) => ({ ...model, input_price: model.input_price ?? 60, output_price: model.output_price ?? 60, per_msg_price: model.per_msg_price ?? -1, })) ); } catch (err) { setError( err instanceof Error ? err.message : t("error.model.unknownError") ); } finally { setLoading(false); } }; fetchModels(); }, []); useEffect(() => { const fetchApiKey = async () => { try { const token = localStorage.getItem("access_token"); const response = await fetch("/api/v1/config/key", { headers: { Authorization: `Bearer ${token}`, }, }); if (!response.ok) { throw new Error( `${t("error.model.failToFetchApiKey")}: ${response.status}` ); } const data = await response.json(); if (!data.apiKey) { throw new Error(t("error.model.ApiKeyNotConfigured")); } setApiKey(data.apiKey); } catch (error) { console.error(t("error.model.failToFetchApiKey"), error); message.error( error instanceof Error ? error.message : t("error.model.failToFetchApiKey") ); } }; fetchApiKey(); }, []); const handlePriceUpdate = async ( id: string, field: "input_price" | "output_price" | "per_msg_price", value: number ): Promise => { try { const model = models.find((m) => m.id === id); if (!model) return; const validValue = Number(value); if ( field !== "per_msg_price" && (!isFinite(validValue) || validValue < 0) ) { throw new Error(t("error.model.nonePositiveNumber")); } if (field === "per_msg_price" && !isFinite(validValue)) { throw new Error(t("error.model.invalidNumber")); } const input_price = field === "input_price" ? validValue : model.input_price; const output_price = field === "output_price" ? validValue : model.output_price; const per_msg_price = field === "per_msg_price" ? validValue : model.per_msg_price; const token = localStorage.getItem("access_token"); const response = await fetch("/api/v1/models/price", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ updates: [ { id, input_price: Number(input_price), output_price: Number(output_price), per_msg_price: Number(per_msg_price), }, ], }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || t("error.model.priceUpdateFail")); if (data.results && data.results[0]?.success) { setModels((prevModels) => prevModels.map((model) => model.id === id ? { ...model, input_price: Number(data.results[0].data.input_price), output_price: Number(data.results[0].data.output_price), per_msg_price: Number(data.results[0].data.per_msg_price), } : model ) ); toast.success(t("error.model.priceUpdateSuccess")); } else { throw new Error( data.results[0]?.error || t("error.model.priceUpdateFail") ); } } catch (err) { toast.error( err instanceof Error ? err.message : t("error.model.priceUpdateFail") ); throw err; } }; const handleTestSingleModel = async (model: Model) => { try { setModels((prev) => prev.map((m) => m.id === model.id ? { ...m, testStatus: "testing" } : m ) ); const result = await testModel(model); setModels((prev) => prev.map((m) => m.id === model.id ? { ...m, testStatus: result.success ? "success" : "error" } : m ) ); } catch (error) { setModels((prev) => prev.map((m) => (m.id === model.id ? { ...m, testStatus: "error" } : m)) ); } }; const handleSyncAllDerivedModels = async () => { try { setSyncing(true); const token = localStorage.getItem("access_token"); const response = await fetch("/api/v1/models/sync-all-prices", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || t("models.syncFail")); } if (data.syncedModels && data.syncedModels.length > 0) { setModels((prev) => prev.map((model) => { const syncedModel = data.syncedModels.find( (m: any) => m.id === model.id && m.success ); if (syncedModel) { return { ...model, input_price: syncedModel.input_price, output_price: syncedModel.output_price, per_msg_price: syncedModel.per_msg_price, }; } return model; }) ); if (data.syncedModels.every((m: any) => m.success)) { toast.success(t("models.syncAllSuccess")); } else { toast.warning(t("models.syncAllFail")); } } else { toast.info(t("models.noDerivedModels")); } } catch (error) { console.error("Sync all derived models failed:", error); toast.error( error instanceof Error ? error.message : t("models.syncFail") ); } finally { setSyncing(false); } }; const columns: ColumnsType = [ { title: t("models.table.name"), key: "model", width: 200, render: (_, record) => (
handleTestSingleModel(record)} > {record.imageUrl && ( {record.name} )} {record.testStatus && (
{record.testStatus === "testing" && (
)} {record.testStatus === "success" && (
)} {record.testStatus === "error" && (
)}
)}
{record.name}
{record.id}
), }, { title: t("models.table.inputPrice"), key: "input_price", width: 150, dataIndex: "input_price", sorter: (a, b) => a.input_price - b.input_price, sortDirections: ["descend", "ascend", "descend"], render: (_, record) => renderPriceCell("input_price", record, true), }, { title: t("models.table.outputPrice"), key: "output_price", width: 150, dataIndex: "output_price", sorter: (a, b) => a.output_price - b.output_price, sortDirections: ["descend", "ascend", "descend"], render: (_, record) => renderPriceCell("output_price", record, true), }, { title: ( {t("models.table.perMsgPrice")}{" "} ), key: "per_msg_price", width: 150, dataIndex: "per_msg_price", sorter: (a, b) => a.per_msg_price - b.per_msg_price, sortDirections: ["descend", "ascend", "descend"], render: (_, record) => renderPriceCell("per_msg_price", record, true), }, ]; const handleExportPrices = () => { const priceData = models.map((model) => ({ id: model.id, input_price: model.input_price, output_price: model.output_price, per_msg_price: model.per_msg_price, })); const blob = new Blob([JSON.stringify(priceData, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `model_prices_${new Date().toISOString().split("T")[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleImportPrices = (file: File) => { const reader = new FileReader(); reader.onload = async (e) => { try { const importedData = JSON.parse(e.target?.result as string); if (!Array.isArray(importedData)) { throw new Error(t("error.model.invalidImportFormat")); } const validUpdates = importedData.filter((item) => models.some((model) => model.id === item.id) ); const response = await fetch("/api/v1/models/price", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ updates: validUpdates, }), }); if (!response.ok) { throw new Error(t("error.model.batchPriceUpdateFail")); } const data = await response.json(); console.log(t("error.model.serverResponse"), data); if (data.results) { setModels((prevModels) => prevModels.map((model) => { const update = data.results.find( (r: any) => r.id === model.id && r.success && r.data ); if (update) { return { ...model, input_price: Number(update.data.input_price), output_price: Number(update.data.output_price), per_msg_price: Number(update.data.per_msg_price), }; } return model; }) ); } message.success( `${t("error.model.updateSuccess")} ${ data.results.filter((r: any) => r.success).length } ${t("error.model.numberOfModelPrice")}` ); } catch (err) { console.error(t("error.model.failToImport"), err); message.error( err instanceof Error ? err.message : t("error.model.failToImport") ); } }; reader.readAsText(file); return false; }; const testModel = async ( model: Model ): Promise<{ id: string; success: boolean; error?: string; }> => { if (!apiKey) { return { id: model.id, success: false, error: t("error.model.ApiKeyNotFetched"), }; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); try { const token = localStorage.getItem("access_token"); const response = await fetch("/api/v1/models/test", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ modelId: model.id, }), signal: controller.signal, }); clearTimeout(timeoutId); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || t("error.model.failToTest")); } return { id: model.id, success: true, }; } catch (error) { clearTimeout(timeoutId); return { id: model.id, success: false, error: error instanceof Error ? error.message : t("error.model.unknownError"), }; } }; const handleTestModels = async () => { if (!apiKey) { message.error(t("error.model.failToTestWithoutApiKey")); return; } try { setModels((prev) => prev.map((m) => ({ ...m, testStatus: "testing" }))); setTesting(true); setIsTestComplete(false); const testPromises = models.map((model) => testModel(model).then((result) => { setModels((prev) => prev.map((m) => m.id === model.id ? { ...m, testStatus: result.success ? "success" : "error" } : m ) ); return result; }) ); await Promise.all(testPromises); setIsTestComplete(true); } catch (error) { console.error(t("error.model.failToTest"), error); message.error(t("error.model.failToTest")); } finally { setTesting(false); } }; const tableClassName = ` [&_.ant-table]:!border-b-0 [&_.ant-table-container]:!rounded-xl [&_.ant-table-container]:!border-hidden [&_.ant-table-cell]:!border-border/40 [&_.ant-table-thead_.ant-table-cell]:!bg-muted/30 [&_.ant-table-thead_.ant-table-cell]:!text-muted-foreground [&_.ant-table-thead_.ant-table-cell]:!font-medium [&_.ant-table-thead_.ant-table-cell]:!text-sm [&_.ant-table-thead]:!border-b [&_.ant-table-thead]:border-border/40 [&_.ant-table-row]:!transition-colors [&_.ant-table-row:hover>*]:!bg-muted/60 [&_.ant-table-tbody_.ant-table-row]:!cursor-pointer [&_.ant-table-tbody_.ant-table-cell]:!py-4 [&_.ant-table-row:last-child>td]:!border-b-0 [&_.ant-table-cell:first-child]:!pl-6 [&_.ant-table-cell:last-child]:!pr-6 `; const MobileCard = ({ record }: { record: Model }) => { const isPerMsgEnabled = record.per_msg_price >= 0; return (
handleTestSingleModel(record)} >
{record.imageUrl && ( {record.name} )}
{record.testStatus && (
)}

{record.name}

{record.id}

{[ { label: t("models.table.mobile.inputPrice"), field: "input_price" as const, disabled: isPerMsgEnabled, }, { label: t("models.table.mobile.outputPrice"), field: "output_price" as const, disabled: isPerMsgEnabled, }, { label: t("models.table.mobile.perMsgPrice"), field: "per_msg_price" as const, disabled: false, }, ].map(({ label, field, disabled }) => (
{label} {renderPriceCell(field, record, false)}
))}
); }; const renderPriceCell = ( field: "input_price" | "output_price" | "per_msg_price", record: Model, showTooltip: boolean = true ) => { const isEditing = editingCell?.id === record.id && editingCell?.field === field; const currentValue = Number(record[field]); const isDisabled = field !== "per_msg_price" && record.per_msg_price >= 0; return ( setEditingCell({ id: record.id, field })} onSubmit={async (value) => { try { await handlePriceUpdate(record.id, field, value); setEditingCell(null); } catch {} }} t={t} disabled={isDisabled} onCancel={() => setEditingCell(null)} tooltipText={ showTooltip && isDisabled ? t("models.table.priceOverriddenByPerMsg") : undefined } placeholder={t("models.table.enterPrice")} validateValue={(value) => ({ isValid: field === "per_msg_price" ? isFinite(value) : isFinite(value) && value >= 0, errorMessage: field === "per_msg_price" ? t("models.table.invalidNumber") : t("models.table.nonePositiveNumber"), })} isPerMsgPrice={field === "per_msg_price"} /> ); }; if (error) { return
错误: {error}
; } return (

{t("models.title")}

{t("models.description")}

{ const file = e.target.files?.[0]; if (file) { handleImportPrices(file); } e.target.value = ""; }} />
{loading ? ( ) : ( "group"} /> )}
{loading ? ( ) : (
{models.map((model) => ( ))}
)}
); }