diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index b58451c29..eab8aeacd 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useState, useEffect, useMemo, type ChangeEvent } from "react"; +import { useState, useEffect, useMemo, type ChangeEvent, type DragEvent } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { formatBeijingTime } from "@/lib/utils"; @@ -29,6 +29,16 @@ import { DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY, normalizeDetailAgentLocalMoonshotConfig, } from "@/lib/ai/detail-agent-model-config"; +import { + ENHANCE_PRO_KREA_DEFAULT_BASE_URL, + ENHANCE_PRO_KREA_DEFAULT_MODEL, + ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT, + ENHANCE_PRO_KREA_SETTING_KEY, + defaultEnhanceProKreaConfig, + defaultEnhanceProPresets, + normalizeEnhanceProKreaConfig, + type EnhanceProPreset, +} from "@/lib/enhance-pro/config"; import { defaultThirdPartySqlServerConfig, THIRD_PARTY_SQLSERVER_CONFIG_KEY, @@ -75,6 +85,8 @@ import { Clock, Sparkles, RefreshCw, + ArrowUp, + ArrowDown, Scissors, Expand, Move3D, @@ -156,6 +168,78 @@ function createEmptyCanvasSkillTag(): CanvasSkillTag { }; } +function createEmptyEnhanceProPreset(): EnhanceProPreset { + const timestamp = Date.now(); + return { + id: `enhance-pro-preset-${timestamp}-${Math.random().toString(36).slice(2, 6)}`, + name: "新预设", + description: "", + isDefault: false, + prompt: "", + batchSize: 1, + seed: 1337, + outputFormat: ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT, + subjectDetection: "All", + faceEnhancement: false, + faceEnhancementCreativity: 0.5, + faceEnhancementStrength: 0.5, + cropToFill: false, + model: ENHANCE_PRO_KREA_DEFAULT_MODEL, + upscalingActivated: false, + imageScalingFactor: 1, + creativity: 3, + texture: 3, + sharpen: 0.5, + denoise: 0.5, + detail: 0.5, + }; +} + +function reorderEnhanceProPresets( + presets: EnhanceProPreset[], + fromIndex: number, + toIndex: number, +) { + if ( + fromIndex === toIndex || + fromIndex < 0 || + toIndex < 0 || + fromIndex >= presets.length || + toIndex >= presets.length + ) { + return presets; + } + + const nextPresets = [...presets]; + const [movedPreset] = nextPresets.splice(fromIndex, 1); + if (!movedPreset) { + return presets; + } + nextPresets.splice(toIndex, 0, movedPreset); + return nextPresets; +} + +function getEnhanceProPresetSummary(preset: EnhanceProPreset) { + const parts = [ + preset.model, + preset.subjectDetection === "Foreground" + ? "前景" + : preset.subjectDetection === "Background" + ? "背景" + : "全部", + ]; + + if (preset.faceEnhancement) { + parts.push("人像增强"); + } + + if (preset.upscalingActivated) { + parts.push(`${preset.imageScalingFactor}x`); + } + + return parts.join(" · "); +} + // 类型定义 type User = { id: string; @@ -658,6 +742,11 @@ export function AdminSettingsClient() { `/api/admin/settings?key=${PRODUCT_PLANNING_AI_MODEL_CONFIG_KEY}`, fetcher, ); + const { data: enhanceProConfigData, mutate: mutateEnhanceProConfig } = + useSWR<{ setting?: { value?: unknown } }>( + `/api/admin/settings?key=${ENHANCE_PRO_KREA_SETTING_KEY}`, + fetcher, + ); const { data: homeAnimationSettingData, mutate: mutateHomeAnimationSetting } = useSWR<{ setting?: { value?: unknown } }>( `/api/admin/settings?key=${HOME_CHAT_ANIMATION_CONFIG_KEY}`, @@ -1007,6 +1096,12 @@ export function AdminSettingsClient() { backupScale: "4", // 备用API放大倍数 "2" | "4" | "8" backupModel: "RealESRGAN_x4plus", // 备用API模型 }); + const [enhanceProConfig, setEnhanceProConfig] = useState(defaultEnhanceProKreaConfig()); + const [enhanceProPresetEditorMode, setEnhanceProPresetEditorMode] = useState< + "simple" | "advanced" + >("simple"); + const [selectedEnhanceProPresetId, setSelectedEnhanceProPresetId] = useState(""); + const [draggingEnhanceProPresetId, setDraggingEnhanceProPresetId] = useState(""); // 图像拓展API配置状态 const [extendConfig, setExtendConfig] = useState({ @@ -1187,6 +1282,45 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } }, [enhanceConfigData]); + useEffect(() => { + if (!enhanceProConfigData) return; + setEnhanceProConfig(normalizeEnhanceProKreaConfig(enhanceProConfigData.setting?.value)); + }, [enhanceProConfigData]); + + useEffect(() => { + if (enhanceProConfig.presets.length === 0) { + if (selectedEnhanceProPresetId) { + setSelectedEnhanceProPresetId(""); + } + return; + } + + const selectedExists = enhanceProConfig.presets.some( + (preset) => preset.id === selectedEnhanceProPresetId, + ); + if (selectedExists) { + return; + } + + const fallbackPreset = + enhanceProConfig.presets.find((preset) => preset.isDefault) || + enhanceProConfig.presets[0]; + setSelectedEnhanceProPresetId(fallbackPreset?.id || ""); + }, [enhanceProConfig.presets, selectedEnhanceProPresetId]); + + const selectedEnhanceProPresetIndex = useMemo( + () => + enhanceProConfig.presets.findIndex( + (preset) => preset.id === selectedEnhanceProPresetId, + ), + [enhanceProConfig.presets, selectedEnhanceProPresetId], + ); + + const selectedEnhanceProPreset = + selectedEnhanceProPresetIndex >= 0 + ? enhanceProConfig.presets[selectedEnhanceProPresetIndex] + : null; + // 加载图像拓展API配置 const { data: extendConfigData } = useSWR("/api/admin/settings?key=extend_api_config", fetcher); useEffect(() => { @@ -2455,6 +2589,205 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } }; + const handleSaveEnhanceProConfig = async () => { + try { + if (enhanceProConfig.enabled) { + if (!enhanceProConfig.baseUrl.trim()) { + toast.error("请输入 Krea API 地址"); + return; + } + if (!enhanceProConfig.apiKey.trim()) { + toast.error("请输入 Krea API Key"); + return; + } + } + + const response = await fetch("/api/admin/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + key: ENHANCE_PRO_KREA_SETTING_KEY, + value: { + enabled: enhanceProConfig.enabled, + providerName: + enhanceProConfig.providerName.trim() || "Krea Topaz Generative", + baseUrl: + enhanceProConfig.baseUrl.trim() || ENHANCE_PRO_KREA_DEFAULT_BASE_URL, + apiKey: enhanceProConfig.apiKey.trim(), + defaultModel: enhanceProConfig.defaultModel, + defaultOutputFormat: enhanceProConfig.defaultOutputFormat, + allowBatchSize: enhanceProConfig.allowBatchSize, + maxBatchSize: enhanceProConfig.maxBatchSize, + maxOutputEdge: enhanceProConfig.maxOutputEdge, + enableAdvancedControls: enhanceProConfig.enableAdvancedControls, + presets: enhanceProConfig.presets, + }, + }), + }); + + const payload = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(String(payload?.message || payload?.error || "保存失败")); + } + + mutateEnhanceProConfig(); + toast.success("图像增强 Pro 配置保存成功"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "保存图像增强 Pro 配置失败"); + } + }; + + const handleEnhanceProPresetChange = ( + index: number, + key: K, + value: EnhanceProPreset[K], + ) => { + setEnhanceProConfig((prev) => { + const nextPresets = prev.presets.map((preset, presetIndex) => { + if (presetIndex !== index) { + if (key === "isDefault" && value === true) { + return { ...preset, isDefault: false }; + } + return preset; + } + + const nextPreset = { + ...preset, + [key]: value, + } as EnhanceProPreset; + + if (key === "name" && !nextPreset.id.startsWith("enhance-pro-preset-")) { + return nextPreset; + } + + if (key === "name") { + return { + ...nextPreset, + id: + String(value || "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") || nextPreset.id, + }; + } + + return nextPreset; + }); + + if (key === "isDefault" && !nextPresets.some((preset) => preset.isDefault)) { + return { + ...prev, + presets: nextPresets.map((preset, presetIndex) => ({ + ...preset, + isDefault: presetIndex === 0, + })), + }; + } + + return { + ...prev, + presets: nextPresets, + }; + }); + }; + + const handleAddEnhanceProPreset = () => { + const nextPreset = createEmptyEnhanceProPreset(); + setSelectedEnhanceProPresetId(nextPreset.id); + setEnhanceProConfig((prev) => ({ + ...prev, + presets: [ + ...prev.presets, + { + ...nextPreset, + isDefault: prev.presets.length === 0, + }, + ], + })); + }; + + const handleRemoveEnhanceProPreset = (index: number) => { + setEnhanceProConfig((prev) => { + const nextPresets = prev.presets.filter((_, presetIndex) => presetIndex !== index); + if (nextPresets.length === 0) { + return { + ...prev, + presets: [], + }; + } + + if (nextPresets.some((preset) => preset.isDefault)) { + return { + ...prev, + presets: nextPresets, + }; + } + + return { + ...prev, + presets: nextPresets.map((preset, presetIndex) => ({ + ...preset, + isDefault: presetIndex === 0, + })), + }; + }); + }; + + const handleResetEnhanceProPresets = () => { + setEnhanceProConfig((prev) => ({ + ...prev, + presets: defaultEnhanceProPresets(), + })); + }; + + const handleMoveEnhanceProPreset = (fromIndex: number, toIndex: number) => { + setEnhanceProConfig((prev) => ({ + ...prev, + presets: reorderEnhanceProPresets(prev.presets, fromIndex, toIndex), + })); + }; + + const handleEnhanceProPresetDragStart = ( + event: DragEvent, + presetId: string, + ) => { + setDraggingEnhanceProPresetId(presetId); + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", presetId); + }; + + const handleEnhanceProPresetDragEnd = () => { + setDraggingEnhanceProPresetId(""); + }; + + const handleEnhanceProPresetDrop = ( + event: DragEvent, + targetPresetId: string, + ) => { + event.preventDefault(); + const sourcePresetId = + draggingEnhanceProPresetId || event.dataTransfer.getData("text/plain"); + setDraggingEnhanceProPresetId(""); + + if (!sourcePresetId || sourcePresetId === targetPresetId) { + return; + } + + const fromIndex = enhanceProConfig.presets.findIndex( + (preset) => preset.id === sourcePresetId, + ); + const toIndex = enhanceProConfig.presets.findIndex( + (preset) => preset.id === targetPresetId, + ); + + if (fromIndex < 0 || toIndex < 0) { + return; + } + + handleMoveEnhanceProPreset(fromIndex, toIndex); + }; + // --- 图像拓展API配置逻辑 --- const handleSaveExtendConfig = async () => { try { @@ -3814,6 +4147,446 @@ const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ } }; + const renderEnhanceProPresetEditor = ( + preset: EnhanceProPreset, + index: number, + allowReorderControls = false, + ) => ( +
+
+
+
+ + + handleEnhanceProPresetChange(index, "name", e.target.value) + } + placeholder="例如:保真修复" + /> +
+
+ + + handleEnhanceProPresetChange(index, "description", e.target.value) + } + placeholder="告诉用户这个预设适合什么图片" + /> +
+
+ +
+ {allowReorderControls ? ( + <> + + + + ) : null} + +
+ + handleEnhanceProPresetChange(index, "isDefault", checked) + } + /> + 默认预设 +
+ + +
+
+ +
+ +