feat: add custom realtime mode icons
This commit is contained in:
parent
7164304451
commit
3775485bdf
@ -113,6 +113,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Check,
|
Check,
|
||||||
Paintbrush,
|
Paintbrush,
|
||||||
|
Wand2,
|
||||||
Database,
|
Database,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
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(
|
function reorderEnhanceProPresets(
|
||||||
presets: EnhanceProPreset[],
|
presets: EnhanceProPreset[],
|
||||||
fromIndex: number,
|
fromIndex: number,
|
||||||
@ -581,6 +589,13 @@ type StorageConfig = {
|
|||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RealtimeCanvasModeIconConfig = {
|
||||||
|
realtimeIconUrl: string;
|
||||||
|
enhanceIconUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RealtimeCanvasModeIconKey = keyof RealtimeCanvasModeIconConfig;
|
||||||
|
|
||||||
type SiteSettings = {
|
type SiteSettings = {
|
||||||
siteName: string;
|
siteName: string;
|
||||||
siteLogo: string | null;
|
siteLogo: string | null;
|
||||||
@ -621,6 +636,7 @@ type SiteSettings = {
|
|||||||
};
|
};
|
||||||
agentWorkflowUiConfig: AgentWorkflowUiConfig;
|
agentWorkflowUiConfig: AgentWorkflowUiConfig;
|
||||||
realtimeCanvasEnabled: boolean;
|
realtimeCanvasEnabled: boolean;
|
||||||
|
realtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig;
|
||||||
aiDetailGenerationEnabled: boolean;
|
aiDetailGenerationEnabled: boolean;
|
||||||
canvasDefaultModelId: string | null;
|
canvasDefaultModelId: string | null;
|
||||||
canvasAutoModeDefaultModelId: string | null;
|
canvasAutoModeDefaultModelId: string | null;
|
||||||
@ -695,6 +711,25 @@ const defaultContactConfig: ContactConfig = {
|
|||||||
wechatQrUrl: "",
|
wechatQrUrl: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultRealtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig = {
|
||||||
|
realtimeIconUrl: "",
|
||||||
|
enhanceIconUrl: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeRealtimeCanvasModeIconConfig(
|
||||||
|
value: unknown
|
||||||
|
): RealtimeCanvasModeIconConfig {
|
||||||
|
const raw =
|
||||||
|
value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
realtimeIconUrl: String(raw.realtimeIconUrl || "").trim(),
|
||||||
|
enhanceIconUrl: String(raw.enhanceIconUrl || "").trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const defaultStorageFailoverConfig: StorageFailoverConfig = {
|
const defaultStorageFailoverConfig: StorageFailoverConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
backupStorageConfigId: "",
|
backupStorageConfigId: "",
|
||||||
@ -1163,6 +1198,7 @@ export function AdminSettingsClient() {
|
|||||||
},
|
},
|
||||||
agentWorkflowUiConfig: defaultAgentWorkflowUiConfig,
|
agentWorkflowUiConfig: defaultAgentWorkflowUiConfig,
|
||||||
realtimeCanvasEnabled: true,
|
realtimeCanvasEnabled: true,
|
||||||
|
realtimeCanvasModeIconConfig: defaultRealtimeCanvasModeIconConfig,
|
||||||
aiDetailGenerationEnabled: false,
|
aiDetailGenerationEnabled: false,
|
||||||
canvasDefaultModelId: null,
|
canvasDefaultModelId: null,
|
||||||
canvasAutoModeDefaultModelId: null,
|
canvasAutoModeDefaultModelId: null,
|
||||||
@ -1304,6 +1340,9 @@ export function AdminSettingsClient() {
|
|||||||
),
|
),
|
||||||
realtimeCanvasEnabled:
|
realtimeCanvasEnabled:
|
||||||
(siteSettingsData as any).realtimeCanvasEnabled ?? true,
|
(siteSettingsData as any).realtimeCanvasEnabled ?? true,
|
||||||
|
realtimeCanvasModeIconConfig: normalizeRealtimeCanvasModeIconConfig(
|
||||||
|
(siteSettingsData as any).realtimeCanvasModeIconConfig
|
||||||
|
),
|
||||||
aiDetailGenerationEnabled:
|
aiDetailGenerationEnabled:
|
||||||
siteSettingsData.aiDetailGenerationEnabled ?? false,
|
siteSettingsData.aiDetailGenerationEnabled ?? false,
|
||||||
canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null,
|
canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null,
|
||||||
@ -1748,6 +1787,26 @@ export function AdminSettingsClient() {
|
|||||||
}).apiKeys.length,
|
}).apiKeys.length,
|
||||||
[enhanceProApiKeysDraft]
|
[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(() => {
|
useEffect(() => {
|
||||||
if (enhanceProConfig.presets.length === 0) {
|
if (enhanceProConfig.presets.length === 0) {
|
||||||
@ -2282,6 +2341,8 @@ export function AdminSettingsClient() {
|
|||||||
const [canvasSkillTagUploadingId, setCanvasSkillTagUploadingId] = useState<
|
const [canvasSkillTagUploadingId, setCanvasSkillTagUploadingId] = useState<
|
||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const [realtimeModeIconUploading, setRealtimeModeIconUploading] =
|
||||||
|
useState<RealtimeCanvasModeIconKey | null>(null);
|
||||||
const { data: canvasSkillTagsData, mutate: mutateCanvasSkillTags } = useSWR<{
|
const { data: canvasSkillTagsData, mutate: mutateCanvasSkillTags } = useSWR<{
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
title?: string;
|
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 = () => {
|
const handleAddCanvasSkillTag = () => {
|
||||||
setCanvasSkillTags((prev) => [...prev, createEmptyCanvasSkillTag()]);
|
setCanvasSkillTags((prev) => [...prev, createEmptyCanvasSkillTag()]);
|
||||||
};
|
};
|
||||||
@ -9812,6 +9917,151 @@ export function AdminSettingsClient() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{siteSettings.realtimeCanvasEnabled && (
|
||||||
|
<div className="rounded-lg border p-4 space-y-4">
|
||||||
|
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
实时画布模式图标
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
上传 SVG 后会替换前端“实时画布模式”和“图像增强模式”的按钮图标;前端会按选中状态自动切换黑白色。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
key={item.field}
|
||||||
|
className="rounded-xl border bg-muted/10 p-3 space-y-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{item.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8"
|
||||||
|
disabled={isUploading}
|
||||||
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById(item.uploadId)
|
||||||
|
?.click()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||||
|
上传中
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"上传 SVG"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{iconUrl ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2 text-muted-foreground"
|
||||||
|
onClick={() =>
|
||||||
|
setSiteSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
realtimeCanvasModeIconConfig: {
|
||||||
|
...prev.realtimeCanvasModeIconConfig,
|
||||||
|
[item.field]: "",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
清除
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id={item.uploadId}
|
||||||
|
type="file"
|
||||||
|
accept=".svg,image/svg+xml"
|
||||||
|
className="hidden"
|
||||||
|
onChange={async (event) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
await handleRealtimeModeIconUpload(
|
||||||
|
item.field,
|
||||||
|
file
|
||||||
|
);
|
||||||
|
}
|
||||||
|
event.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="inline-flex h-10 items-center gap-2 rounded-[18px] bg-[#111111] px-3 text-xs font-medium text-white">
|
||||||
|
{iconUrl ? (
|
||||||
|
<img
|
||||||
|
alt={`${item.title}选中预览`}
|
||||||
|
className="h-4 w-4 object-contain brightness-0 invert"
|
||||||
|
src={iconUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FallbackIcon className="h-4 w-4 text-white" />
|
||||||
|
)}
|
||||||
|
选中态
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex h-10 items-center gap-2 rounded-[18px] border bg-white px-3 text-xs font-medium text-[#666666]">
|
||||||
|
{iconUrl ? (
|
||||||
|
<img
|
||||||
|
alt={`${item.title}未选中预览`}
|
||||||
|
className="h-4 w-4 object-contain brightness-0 opacity-70"
|
||||||
|
src={iconUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FallbackIcon className="h-4 w-4 text-[#666666]" />
|
||||||
|
)}
|
||||||
|
未选中态
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="videoCanvasEnabled">
|
<Label htmlFor="videoCanvasEnabled">
|
||||||
@ -9833,6 +10083,24 @@ export function AdminSettingsClient() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
视频画布功能配置
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
独立维护复刻画布里的功能预设提示词、默认模型、单功能模型和扣点配置。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<Link href="/settings/video-canvas">
|
||||||
|
前往视频画布功能配置
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{siteSettings.videoCanvasEnabled && (
|
{siteSettings.videoCanvasEnabled && (
|
||||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
@ -15680,23 +15948,18 @@ export function AdminSettingsClient() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-4 rounded-xl border p-4">
|
||||||
<Label htmlFor="enhanceProApiKeys">API Key 池</Label>
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<Textarea
|
<div>
|
||||||
id="enhanceProApiKeys"
|
<div className="text-sm font-medium">Krea Key 池</div>
|
||||||
value={enhanceProApiKeysDraft}
|
<div className="text-xs text-muted-foreground">
|
||||||
onChange={(e) => setEnhanceProApiKeysDraft(e.target.value)}
|
当前共 {enhanceProConfig.apiKeys.length} 个 Key
|
||||||
placeholder="为安全起见不回显已保存 Key;每行一个新 Krea Bearer Token,保存后会替换当前 Key 池。留空保存则保持当前池不变。"
|
{enhanceProApiKeysDraft.trim()
|
||||||
rows={4}
|
? `,本次保存会替换为 ${enhanceProApiKeysDraftCount} 个 Key`
|
||||||
/>
|
: ",留空保存不会覆盖已保存池"}
|
||||||
<div className="flex flex-col gap-2 text-xs text-muted-foreground md:flex-row md:items-center md:justify-between">
|
。增强 Pro 任务会轮询使用可用 Key。
|
||||||
<span>
|
</div>
|
||||||
已保存 {enhanceProConfig.apiKeys.length} 个 Key
|
</div>
|
||||||
{enhanceProApiKeysDraft.trim()
|
|
||||||
? `,本次保存会替换为 ${enhanceProApiKeysDraftCount} 个 Key`
|
|
||||||
: ";留空不会覆盖已保存池"}
|
|
||||||
。生成任务会轮询使用,异步查询会绑定回创建任务时使用的 Key。
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@ -15724,6 +15987,93 @@ export function AdminSettingsClient() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="enhanceProApiKeys">批量导入 Key</Label>
|
||||||
|
<Textarea
|
||||||
|
id="enhanceProApiKeys"
|
||||||
|
value={enhanceProApiKeysDraft}
|
||||||
|
onChange={(e) => setEnhanceProApiKeysDraft(e.target.value)}
|
||||||
|
placeholder="为安全起见不回显已保存 Key;每行一个新 Krea Bearer Token,保存后会替换当前 Key 池。留空保存则保持当前池不变。"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
异步查询会绑定回创建任务时使用的 Key,避免轮询切换 Key 后查不到任务。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{draftEnhanceProKeyRows.length ? (
|
||||||
|
<div className="rounded-lg border border-emerald-200 bg-emerald-50/70 p-4">
|
||||||
|
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="text-sm font-medium text-emerald-900">
|
||||||
|
待保存 Key 预览
|
||||||
|
</div>
|
||||||
|
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 text-xs text-emerald-700">
|
||||||
|
{draftEnhanceProKeyRows.length} 个
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{draftEnhanceProKeyRows.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="flex flex-col gap-2 rounded-lg border bg-background p-3 lg:flex-row lg:items-center lg:justify-between"
|
||||||
|
>
|
||||||
|
<div className="min-w-0 flex-1 space-y-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="font-medium">{item.name}</div>
|
||||||
|
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 text-xs text-emerald-600">
|
||||||
|
待保存
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-xs text-muted-foreground">
|
||||||
|
Key: {item.maskedKey}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
API: {enhanceProConfig.baseUrl || ENHANCE_PRO_KREA_DEFAULT_BASE_URL}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{savedEnhanceProKeyRows.length ? (
|
||||||
|
savedEnhanceProKeyRows.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="flex flex-col gap-4 rounded-lg border p-4 lg:flex-row lg:items-center lg:justify-between"
|
||||||
|
>
|
||||||
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="font-medium">{item.name}</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-2 py-0.5 text-xs",
|
||||||
|
enhanceProConfig.enabled
|
||||||
|
? "bg-emerald-500/10 text-emerald-600"
|
||||||
|
: "bg-zinc-200 text-zinc-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{enhanceProConfig.enabled ? "已启用" : "已禁用"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-xs text-muted-foreground">
|
||||||
|
Key: {item.maskedKey}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
API: {enhanceProConfig.baseUrl || ENHANCE_PRO_KREA_DEFAULT_BASE_URL}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||||||
|
当前还没有导入任何 Krea Key。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { upsertMediaAsset } from "@/lib/db/queries";
|
import { upsertMediaAsset } from "@/lib/db/queries";
|
||||||
|
import { isSiteAdminType } from "@/lib/middleware/admin";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,9 +91,15 @@ const FileSchema = z.object({
|
|||||||
message: "File size should be less than 50MB",
|
message: "File size should be less than 50MB",
|
||||||
})
|
})
|
||||||
// Update the file type based on the kind of files you want to accept
|
// Update the file type based on the kind of files you want to accept
|
||||||
.refine((file) => ["image/jpeg", "image/png", "image/webp"].includes(file.type), {
|
.refine(
|
||||||
message: "File type should be JPEG, PNG or WebP",
|
(file) =>
|
||||||
}),
|
["image/jpeg", "image/png", "image/webp", "image/svg+xml"].includes(
|
||||||
|
file.type
|
||||||
|
),
|
||||||
|
{
|
||||||
|
message: "File type should be JPEG, PNG, WebP or SVG",
|
||||||
|
}
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
@ -121,6 +128,18 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
|
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadPurpose = String(formData.get("purpose") || "");
|
||||||
|
const isSvgUpload = file.type === "image/svg+xml";
|
||||||
|
if (
|
||||||
|
isSvgUpload &&
|
||||||
|
(uploadPurpose !== "admin-svg-icon" || !isSiteAdminType(session.user.type))
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "SVG uploads are only allowed for administrator icon settings" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("[Upload API] File info:", {
|
console.log("[Upload API] File info:", {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
@ -153,9 +172,9 @@ export async function POST(request: Request) {
|
|||||||
// WebP格式不优化(用于预览图,保持原样)
|
// WebP格式不优化(用于预览图,保持原样)
|
||||||
if (file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png') {
|
if (file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png') {
|
||||||
optimizedBuffer = await optimizeImage(optimizedBuffer, file.type);
|
optimizedBuffer = await optimizeImage(optimizedBuffer, file.type);
|
||||||
} else if (file.type === 'image/webp') {
|
} else if (file.type === 'image/webp' || file.type === 'image/svg+xml') {
|
||||||
// WebP格式不优化(用于预览图),直接使用
|
// WebP/SVG 不优化;SVG 是矢量文件,交给前端按状态反色渲染。
|
||||||
console.log(`[Upload] WebP格式(预览图),跳过优化,直接上传`);
|
console.log(`[Upload] ${file.type}格式,跳过优化,直接上传`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将Buffer转换为ArrayBuffer用于上传
|
// 将Buffer转换为ArrayBuffer用于上传
|
||||||
|
|||||||
@ -107,6 +107,7 @@ const MEMBERSHIP_CONFIG_KEY = "membership_config";
|
|||||||
const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
|
const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
|
||||||
const STORAGE_FAILOVER_CONFIG_KEY = "storage_failover_config";
|
const STORAGE_FAILOVER_CONFIG_KEY = "storage_failover_config";
|
||||||
const REALTIME_CANVAS_CONFIG_KEY = "realtime_canvas_config";
|
const REALTIME_CANVAS_CONFIG_KEY = "realtime_canvas_config";
|
||||||
|
const REALTIME_CANVAS_MODE_ICON_CONFIG_KEY = "realtime_canvas_mode_icon_config";
|
||||||
const LAYER_SPLIT_CONFIG_KEY = "layer_split_config";
|
const LAYER_SPLIT_CONFIG_KEY = "layer_split_config";
|
||||||
const AGENT_CATEGORY_TREE_CONFIG_KEY = "agent_category_tree_config";
|
const AGENT_CATEGORY_TREE_CONFIG_KEY = "agent_category_tree_config";
|
||||||
const CANVAS_AGENT_MODE_ENABLED_KEY = "canvas_agent_mode_enabled";
|
const CANVAS_AGENT_MODE_ENABLED_KEY = "canvas_agent_mode_enabled";
|
||||||
@ -134,6 +135,31 @@ const AGENT_WORKFLOW_SETTINGS_FIELD_KEY_MAP = {
|
|||||||
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
||||||
} as const;
|
} as const;
|
||||||
type AgentWorkflowSettingsField = keyof typeof AGENT_WORKFLOW_SETTINGS_FIELD_KEY_MAP;
|
type AgentWorkflowSettingsField = keyof typeof AGENT_WORKFLOW_SETTINGS_FIELD_KEY_MAP;
|
||||||
|
type RealtimeCanvasModeIconConfig = {
|
||||||
|
realtimeIconUrl: string;
|
||||||
|
enhanceIconUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultRealtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig = {
|
||||||
|
realtimeIconUrl: "",
|
||||||
|
enhanceIconUrl: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeRealtimeCanvasModeIconConfig(
|
||||||
|
value: unknown
|
||||||
|
): RealtimeCanvasModeIconConfig {
|
||||||
|
const raw =
|
||||||
|
value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
const normalizeUrl = (input: unknown) =>
|
||||||
|
String(input || "").trim().slice(0, 2000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
realtimeIconUrl: normalizeUrl(raw.realtimeIconUrl),
|
||||||
|
enhanceIconUrl: normalizeUrl(raw.enhanceIconUrl),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function stableStringifySettingsValue(value: unknown): string {
|
function stableStringifySettingsValue(value: unknown): string {
|
||||||
if (value === null || typeof value !== "object") {
|
if (value === null || typeof value !== "object") {
|
||||||
@ -406,6 +432,7 @@ export async function GET() {
|
|||||||
pointsPackagesSetting,
|
pointsPackagesSetting,
|
||||||
dulupayConfigSetting,
|
dulupayConfigSetting,
|
||||||
realtimeCanvasConfigSetting,
|
realtimeCanvasConfigSetting,
|
||||||
|
realtimeCanvasModeIconConfigSetting,
|
||||||
layerSplitConfigSetting,
|
layerSplitConfigSetting,
|
||||||
canvasAgentModeEnabledSetting,
|
canvasAgentModeEnabledSetting,
|
||||||
videoCanvasEnabledSetting,
|
videoCanvasEnabledSetting,
|
||||||
@ -510,6 +537,12 @@ export async function GET() {
|
|||||||
.where(eq(systemSettings.key, REALTIME_CANVAS_CONFIG_KEY))
|
.where(eq(systemSettings.key, REALTIME_CANVAS_CONFIG_KEY))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.then(rows => rows[0]),
|
.then(rows => rows[0]),
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(systemSettings)
|
||||||
|
.where(eq(systemSettings.key, REALTIME_CANVAS_MODE_ICON_CONFIG_KEY))
|
||||||
|
.limit(1)
|
||||||
|
.then(rows => rows[0]),
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(systemSettings)
|
.from(systemSettings)
|
||||||
@ -642,6 +675,9 @@ export async function GET() {
|
|||||||
: normalizeDulupayForPublic(dulupayConfigSetting?.value);
|
: normalizeDulupayForPublic(dulupayConfigSetting?.value);
|
||||||
const realtimeCanvasServiceEnabled =
|
const realtimeCanvasServiceEnabled =
|
||||||
(realtimeCanvasConfigSetting?.value as { enabled?: boolean } | undefined)?.enabled !== false;
|
(realtimeCanvasConfigSetting?.value as { enabled?: boolean } | undefined)?.enabled !== false;
|
||||||
|
const realtimeCanvasModeIconConfig = normalizeRealtimeCanvasModeIconConfig(
|
||||||
|
realtimeCanvasModeIconConfigSetting?.value
|
||||||
|
);
|
||||||
const layerSplitEnabled = normalizeLayerSplitEnabled(
|
const layerSplitEnabled = normalizeLayerSplitEnabled(
|
||||||
layerSplitConfigSetting?.value
|
layerSplitConfigSetting?.value
|
||||||
);
|
);
|
||||||
@ -749,6 +785,7 @@ export async function GET() {
|
|||||||
agentPsdTemplateCategoryTagIndex:
|
agentPsdTemplateCategoryTagIndex:
|
||||||
defaultAgentPsdTemplateCategoryTagIndex,
|
defaultAgentPsdTemplateCategoryTagIndex,
|
||||||
realtimeCanvasEnabled: true,
|
realtimeCanvasEnabled: true,
|
||||||
|
realtimeCanvasModeIconConfig: defaultRealtimeCanvasModeIconConfig,
|
||||||
realtimeCanvasServiceEnabled: true,
|
realtimeCanvasServiceEnabled: true,
|
||||||
layerSplitEnabled,
|
layerSplitEnabled,
|
||||||
storageFailoverConfig,
|
storageFailoverConfig,
|
||||||
@ -809,6 +846,7 @@ export async function GET() {
|
|||||||
pointsPackages,
|
pointsPackages,
|
||||||
dulupayConfig,
|
dulupayConfig,
|
||||||
realtimeCanvasServiceEnabled,
|
realtimeCanvasServiceEnabled,
|
||||||
|
realtimeCanvasModeIconConfig,
|
||||||
layerSplitEnabled,
|
layerSplitEnabled,
|
||||||
canvasAgentModeEnabled,
|
canvasAgentModeEnabled,
|
||||||
videoCanvasEnabled,
|
videoCanvasEnabled,
|
||||||
@ -1149,6 +1187,12 @@ const updateSchema = z.object({
|
|||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
realtimeCanvasEnabled: z.boolean().optional(),
|
realtimeCanvasEnabled: z.boolean().optional(),
|
||||||
|
realtimeCanvasModeIconConfig: z
|
||||||
|
.object({
|
||||||
|
realtimeIconUrl: z.string().trim().max(2000).optional(),
|
||||||
|
enhanceIconUrl: z.string().trim().max(2000).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
aiDetailGenerationEnabled: z.boolean().optional(),
|
aiDetailGenerationEnabled: z.boolean().optional(),
|
||||||
canvasDefaultModelId: z.string().nullable().optional(),
|
canvasDefaultModelId: z.string().nullable().optional(),
|
||||||
canvasAutoModeDefaultModelId: z.string().nullable().optional(),
|
canvasAutoModeDefaultModelId: z.string().nullable().optional(),
|
||||||
@ -1275,6 +1319,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
dulupayConfig,
|
dulupayConfig,
|
||||||
storageFailoverConfig,
|
storageFailoverConfig,
|
||||||
ossImageMigrationConfig,
|
ossImageMigrationConfig,
|
||||||
|
realtimeCanvasModeIconConfig,
|
||||||
canvasAgentModeEnabled,
|
canvasAgentModeEnabled,
|
||||||
videoCanvasEnabled,
|
videoCanvasEnabled,
|
||||||
videoCanvasSidebarEnabled,
|
videoCanvasSidebarEnabled,
|
||||||
@ -1382,6 +1427,25 @@ export async function PUT(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const persistSystemSetting = async (key: string, value: unknown) => {
|
||||||
|
await db
|
||||||
|
.insert(systemSettings)
|
||||||
|
.values({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: systemSettings.key,
|
||||||
|
set: {
|
||||||
|
value,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const persistHomeAnimationConfig = async () => {
|
const persistHomeAnimationConfig = async () => {
|
||||||
if (homeAnimationConfig === undefined) {
|
if (homeAnimationConfig === undefined) {
|
||||||
return;
|
return;
|
||||||
@ -1918,6 +1982,13 @@ export async function PUT(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (realtimeCanvasModeIconConfig !== undefined) {
|
||||||
|
await persistSystemSetting(
|
||||||
|
REALTIME_CANVAS_MODE_ICON_CONFIG_KEY,
|
||||||
|
normalizeRealtimeCanvasModeIconConfig(realtimeCanvasModeIconConfig)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await persistHomeAnimationConfig();
|
await persistHomeAnimationConfig();
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@ -2018,6 +2089,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
ossImageMigrationConfig !== undefined
|
ossImageMigrationConfig !== undefined
|
||||||
? normalizeOssImageMigrationConfig(ossImageMigrationConfig)
|
? normalizeOssImageMigrationConfig(ossImageMigrationConfig)
|
||||||
: defaultOssImageMigrationConfig,
|
: defaultOssImageMigrationConfig,
|
||||||
|
realtimeCanvasModeIconConfig:
|
||||||
|
realtimeCanvasModeIconConfig !== undefined
|
||||||
|
? normalizeRealtimeCanvasModeIconConfig(realtimeCanvasModeIconConfig)
|
||||||
|
: defaultRealtimeCanvasModeIconConfig,
|
||||||
canvasAgentModeEnabled:
|
canvasAgentModeEnabled:
|
||||||
canvasAgentModeEnabled !== undefined
|
canvasAgentModeEnabled !== undefined
|
||||||
? normalizeCanvasAgentModeEnabled(canvasAgentModeEnabled)
|
? normalizeCanvasAgentModeEnabled(canvasAgentModeEnabled)
|
||||||
@ -2514,6 +2589,13 @@ export async function PUT(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (realtimeCanvasModeIconConfig !== undefined) {
|
||||||
|
await persistSystemSetting(
|
||||||
|
REALTIME_CANVAS_MODE_ICON_CONFIG_KEY,
|
||||||
|
normalizeRealtimeCanvasModeIconConfig(realtimeCanvasModeIconConfig)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (videoCanvasEnabled !== undefined) {
|
if (videoCanvasEnabled !== undefined) {
|
||||||
const normalized = normalizeVideoCanvasEnabled(videoCanvasEnabled);
|
const normalized = normalizeVideoCanvasEnabled(videoCanvasEnabled);
|
||||||
await db
|
await db
|
||||||
@ -2632,6 +2714,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
ossImageMigrationConfig !== undefined
|
ossImageMigrationConfig !== undefined
|
||||||
? normalizeOssImageMigrationConfig(ossImageMigrationConfig)
|
? normalizeOssImageMigrationConfig(ossImageMigrationConfig)
|
||||||
: defaultOssImageMigrationConfig,
|
: defaultOssImageMigrationConfig,
|
||||||
|
realtimeCanvasModeIconConfig:
|
||||||
|
realtimeCanvasModeIconConfig !== undefined
|
||||||
|
? normalizeRealtimeCanvasModeIconConfig(realtimeCanvasModeIconConfig)
|
||||||
|
: defaultRealtimeCanvasModeIconConfig,
|
||||||
canvasAgentModeEnabled:
|
canvasAgentModeEnabled:
|
||||||
canvasAgentModeEnabled !== undefined
|
canvasAgentModeEnabled !== undefined
|
||||||
? normalizeCanvasAgentModeEnabled(canvasAgentModeEnabled)
|
? normalizeCanvasAgentModeEnabled(canvasAgentModeEnabled)
|
||||||
|
|||||||
@ -82,6 +82,10 @@ type RealtimeConfig = {
|
|||||||
|
|
||||||
type RealtimeStatus = "idle" | "connecting" | "live" | "error";
|
type RealtimeStatus = "idle" | "connecting" | "live" | "error";
|
||||||
type RealtimeWorkspaceMode = "realtime" | "enhance-pro";
|
type RealtimeWorkspaceMode = "realtime" | "enhance-pro";
|
||||||
|
type RealtimeCanvasModeIconConfig = {
|
||||||
|
realtimeIconUrl: string;
|
||||||
|
enhanceIconUrl: string;
|
||||||
|
};
|
||||||
type RealtimeBrushMode =
|
type RealtimeBrushMode =
|
||||||
| "select"
|
| "select"
|
||||||
| "brush"
|
| "brush"
|
||||||
@ -849,6 +853,11 @@ export function RealtimeCanvasDialog({
|
|||||||
const [siteName, setSiteName] = useState("AI Chatbot");
|
const [siteName, setSiteName] = useState("AI Chatbot");
|
||||||
const [siteLogo, setSiteLogo] = useState<string | null>(null);
|
const [siteLogo, setSiteLogo] = useState<string | null>(null);
|
||||||
const [isLayerSplitEnabled, setIsLayerSplitEnabled] = useState(false);
|
const [isLayerSplitEnabled, setIsLayerSplitEnabled] = useState(false);
|
||||||
|
const [modeIconConfig, setModeIconConfig] =
|
||||||
|
useState<RealtimeCanvasModeIconConfig>({
|
||||||
|
realtimeIconUrl: "",
|
||||||
|
enhanceIconUrl: "",
|
||||||
|
});
|
||||||
|
|
||||||
const statusBadge = useMemo(() => {
|
const statusBadge = useMemo(() => {
|
||||||
if (status === "live")
|
if (status === "live")
|
||||||
@ -911,6 +920,21 @@ export function RealtimeCanvasDialog({
|
|||||||
setSiteLogo(data.siteLogoDark.trim());
|
setSiteLogo(data.siteLogoDark.trim());
|
||||||
}
|
}
|
||||||
setIsLayerSplitEnabled(data.layerSplitEnabled === true);
|
setIsLayerSplitEnabled(data.layerSplitEnabled === true);
|
||||||
|
const iconConfig =
|
||||||
|
data.realtimeCanvasModeIconConfig &&
|
||||||
|
typeof data.realtimeCanvasModeIconConfig === "object"
|
||||||
|
? data.realtimeCanvasModeIconConfig
|
||||||
|
: {};
|
||||||
|
setModeIconConfig({
|
||||||
|
realtimeIconUrl:
|
||||||
|
typeof iconConfig.realtimeIconUrl === "string"
|
||||||
|
? iconConfig.realtimeIconUrl.trim()
|
||||||
|
: "",
|
||||||
|
enhanceIconUrl:
|
||||||
|
typeof iconConfig.enhanceIconUrl === "string"
|
||||||
|
? iconConfig.enhanceIconUrl.trim()
|
||||||
|
: "",
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn("[RealtimeCanvas] Failed to load site settings:", error);
|
console.warn("[RealtimeCanvas] Failed to load site settings:", error);
|
||||||
@ -919,7 +943,7 @@ export function RealtimeCanvasDialog({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [open]);
|
||||||
|
|
||||||
const pushMetric = useCallback((event: any) => {
|
const pushMetric = useCallback((event: any) => {
|
||||||
metricsQueueRef.current.push(event);
|
metricsQueueRef.current.push(event);
|
||||||
@ -3160,7 +3184,7 @@ export function RealtimeCanvasDialog({
|
|||||||
<div className="inline-flex h-12 min-w-0 items-center gap-1 rounded-[22px] border border-[#ebebeb] bg-[#fafafa] p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.9)]">
|
<div className="inline-flex h-12 min-w-0 items-center gap-1 rounded-[22px] border border-[#ebebeb] bg-[#fafafa] p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.9)]">
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-full items-center gap-2 rounded-[18px] px-4 text-[14px] font-medium tracking-[-0.02em] transition",
|
"group inline-flex h-full items-center gap-2 rounded-[18px] px-4 text-[14px] font-medium tracking-[-0.02em] transition",
|
||||||
workspaceMode === "realtime"
|
workspaceMode === "realtime"
|
||||||
? softActionButtonClass
|
? softActionButtonClass
|
||||||
: "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]"
|
: "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]"
|
||||||
@ -3168,12 +3192,26 @@ export function RealtimeCanvasDialog({
|
|||||||
onClick={() => handleWorkspaceModeChange("realtime")}
|
onClick={() => handleWorkspaceModeChange("realtime")}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Paintbrush className="h-4 w-4" />
|
{modeIconConfig.realtimeIconUrl ? (
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 object-contain transition",
|
||||||
|
workspaceMode === "realtime"
|
||||||
|
? "brightness-0 invert"
|
||||||
|
: "brightness-0 opacity-70 group-hover:opacity-100"
|
||||||
|
)}
|
||||||
|
src={modeIconConfig.realtimeIconUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Paintbrush className="h-4 w-4" />
|
||||||
|
)}
|
||||||
实时画布模式
|
实时画布模式
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-full items-center gap-2 rounded-[18px] px-4 text-[14px] font-medium tracking-[-0.02em] transition",
|
"group inline-flex h-full items-center gap-2 rounded-[18px] px-4 text-[14px] font-medium tracking-[-0.02em] transition",
|
||||||
workspaceMode === "enhance-pro"
|
workspaceMode === "enhance-pro"
|
||||||
? softActionButtonClass
|
? softActionButtonClass
|
||||||
: "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]"
|
: "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]"
|
||||||
@ -3181,7 +3219,21 @@ export function RealtimeCanvasDialog({
|
|||||||
onClick={() => handleWorkspaceModeChange("enhance-pro")}
|
onClick={() => handleWorkspaceModeChange("enhance-pro")}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Wand2 className="h-4 w-4" />
|
{modeIconConfig.enhanceIconUrl ? (
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 object-contain transition",
|
||||||
|
workspaceMode === "enhance-pro"
|
||||||
|
? "brightness-0 invert"
|
||||||
|
: "brightness-0 opacity-70 group-hover:opacity-100"
|
||||||
|
)}
|
||||||
|
src={modeIconConfig.enhanceIconUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Wand2 className="h-4 w-4" />
|
||||||
|
)}
|
||||||
图像增强模式
|
图像增强模式
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user