From 3775485bdfdcbd4e8194695b0d11ff8291c7544d Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 20 Apr 2026 10:41:29 +0800 Subject: [PATCH] feat: add custom realtime mode icons --- .../settings/admin-settings-client.tsx | 384 +++++++++++++++++- app/(chat)/api/files/upload/route.ts | 31 +- app/api/site-settings/route.ts | 86 ++++ components/canvas/RealtimeCanvasDialog.tsx | 62 ++- 4 files changed, 535 insertions(+), 28 deletions(-) diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 39b0deca8..4b0692aac 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -113,6 +113,7 @@ import { Loader2, Check, Paintbrush, + Wand2, Database, } from "lucide-react"; import { @@ -284,6 +285,13 @@ function LocalAiSectionToggleButton({ ); } +function maskAdminSecret(value: string) { + const normalized = String(value || "").trim(); + if (!normalized) return "未填写"; + if (normalized.length <= 10) return "••••••"; + return `${normalized.slice(0, 6)}••••${normalized.slice(-4)}`; +} + function reorderEnhanceProPresets( presets: EnhanceProPreset[], fromIndex: number, @@ -581,6 +589,13 @@ type StorageConfig = { isEnabled: boolean; }; +type RealtimeCanvasModeIconConfig = { + realtimeIconUrl: string; + enhanceIconUrl: string; +}; + +type RealtimeCanvasModeIconKey = keyof RealtimeCanvasModeIconConfig; + type SiteSettings = { siteName: string; siteLogo: string | null; @@ -621,6 +636,7 @@ type SiteSettings = { }; agentWorkflowUiConfig: AgentWorkflowUiConfig; realtimeCanvasEnabled: boolean; + realtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig; aiDetailGenerationEnabled: boolean; canvasDefaultModelId: string | null; canvasAutoModeDefaultModelId: string | null; @@ -695,6 +711,25 @@ const defaultContactConfig: ContactConfig = { wechatQrUrl: "", }; +const defaultRealtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig = { + realtimeIconUrl: "", + enhanceIconUrl: "", +}; + +function normalizeRealtimeCanvasModeIconConfig( + value: unknown +): RealtimeCanvasModeIconConfig { + const raw = + value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; + + return { + realtimeIconUrl: String(raw.realtimeIconUrl || "").trim(), + enhanceIconUrl: String(raw.enhanceIconUrl || "").trim(), + }; +} + const defaultStorageFailoverConfig: StorageFailoverConfig = { enabled: false, backupStorageConfigId: "", @@ -1163,6 +1198,7 @@ export function AdminSettingsClient() { }, agentWorkflowUiConfig: defaultAgentWorkflowUiConfig, realtimeCanvasEnabled: true, + realtimeCanvasModeIconConfig: defaultRealtimeCanvasModeIconConfig, aiDetailGenerationEnabled: false, canvasDefaultModelId: null, canvasAutoModeDefaultModelId: null, @@ -1304,6 +1340,9 @@ export function AdminSettingsClient() { ), realtimeCanvasEnabled: (siteSettingsData as any).realtimeCanvasEnabled ?? true, + realtimeCanvasModeIconConfig: normalizeRealtimeCanvasModeIconConfig( + (siteSettingsData as any).realtimeCanvasModeIconConfig + ), aiDetailGenerationEnabled: siteSettingsData.aiDetailGenerationEnabled ?? false, canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null, @@ -1748,6 +1787,26 @@ export function AdminSettingsClient() { }).apiKeys.length, [enhanceProApiKeysDraft] ); + const savedEnhanceProKeyRows = useMemo( + () => + enhanceProConfig.apiKeys.map((apiKey, index) => ({ + id: `saved-enhance-pro-key-${index}-${apiKey.slice(-6)}`, + name: `Krea Key ${index + 1}`, + maskedKey: maskAdminSecret(apiKey), + })), + [enhanceProConfig.apiKeys] + ); + const draftEnhanceProKeyRows = useMemo( + () => + normalizeEnhanceProKreaConfig({ + apiKeys: enhanceProApiKeysDraft, + }).apiKeys.map((apiKey, index) => ({ + id: `draft-enhance-pro-key-${index}-${apiKey.slice(-6)}`, + name: `待保存 Key ${index + 1}`, + maskedKey: maskAdminSecret(apiKey), + })), + [enhanceProApiKeysDraft] + ); useEffect(() => { if (enhanceProConfig.presets.length === 0) { @@ -2282,6 +2341,8 @@ export function AdminSettingsClient() { const [canvasSkillTagUploadingId, setCanvasSkillTagUploadingId] = useState< string | null >(null); + const [realtimeModeIconUploading, setRealtimeModeIconUploading] = + useState(null); const { data: canvasSkillTagsData, mutate: mutateCanvasSkillTags } = useSWR<{ enabled?: boolean; title?: string; @@ -2418,6 +2479,50 @@ export function AdminSettingsClient() { } }; + const handleRealtimeModeIconUpload = async ( + field: RealtimeCanvasModeIconKey, + file: File + ) => { + const isSvg = + file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg"); + if (!isSvg) { + toast.error("请上传 SVG 图标文件"); + return; + } + + if (file.size > 512 * 1024) { + toast.error("SVG 图标不能超过 512KB"); + return; + } + + try { + setRealtimeModeIconUploading(field); + const formData = new FormData(); + formData.append("file", file); + formData.append("purpose", "admin-svg-icon"); + + const response = await fetchUploadWithRetry(formData); + const result = await response.json(); + + if (!response.ok || !result?.url) { + throw new Error(result?.error || "上传失败"); + } + + setSiteSettings((prev) => ({ + ...prev, + realtimeCanvasModeIconConfig: { + ...prev.realtimeCanvasModeIconConfig, + [field]: result.url, + }, + })); + toast.success("模式图标上传成功,请保存配置后生效"); + } catch (error: any) { + toast.error(error?.message || "模式图标上传失败"); + } finally { + setRealtimeModeIconUploading(null); + } + }; + const handleAddCanvasSkillTag = () => { setCanvasSkillTags((prev) => [...prev, createEmptyCanvasSkillTag()]); }; @@ -9812,6 +9917,151 @@ export function AdminSettingsClient() { /> + {siteSettings.realtimeCanvasEnabled && ( +
+
+
+
+ 实时画布模式图标 +
+
+ 上传 SVG 后会替换前端“实时画布模式”和“图像增强模式”的按钮图标;前端会按选中状态自动切换黑白色。 +
+
+
+
+ {( + [ + { + field: "realtimeIconUrl", + title: "实时画布模式", + description: "左侧模式切换按钮的画笔图标", + uploadId: "realtime-mode-icon-upload", + fallback: Paintbrush, + }, + { + field: "enhanceIconUrl", + title: "图像增强模式", + description: "右侧模式切换按钮的魔法棒图标", + uploadId: "enhance-mode-icon-upload", + fallback: Wand2, + }, + ] as const + ).map((item) => { + const iconUrl = + siteSettings.realtimeCanvasModeIconConfig[ + item.field + ]; + const FallbackIcon = item.fallback; + const isUploading = + realtimeModeIconUploading === item.field; + + return ( +
+
+
+
+ {item.title} +
+
+ {item.description} +
+
+
+ + {iconUrl ? ( + + ) : null} +
+ { + const file = event.target.files?.[0]; + if (file) { + await handleRealtimeModeIconUpload( + item.field, + file + ); + } + event.target.value = ""; + }} + /> +
+
+
+ {iconUrl ? ( + {`${item.title}选中预览`} + ) : ( + + )} + 选中态 +
+
+ {iconUrl ? ( + {`${item.title}未选中预览`} + ) : ( + + )} + 未选中态 +
+
+
+ ); + })} +
+
+ )} +
+
+
+
+
+ 视频画布功能配置 +
+
+ 独立维护复刻画布里的功能预设提示词、默认模型、单功能模型和扣点配置。 +
+
+ +
+
+ {siteSettings.videoCanvasEnabled && (
@@ -15680,23 +15948,18 @@ export function AdminSettingsClient() { />
-
- -