feat: enhance detail result image actions
This commit is contained in:
parent
fe18aa0000
commit
801d2c4644
@ -67,6 +67,7 @@ import { AgentPreviewImage } from "@/components/agents/AgentPreviewImage";
|
|||||||
import { useAgentStylePerformanceMonitor } from "@/components/agents/use-agent-style-performance-monitor";
|
import { useAgentStylePerformanceMonitor } from "@/components/agents/use-agent-style-performance-monitor";
|
||||||
import { ImageAnnotator } from "@/components/image-annotator";
|
import { ImageAnnotator } from "@/components/image-annotator";
|
||||||
import { useCanvasStore } from "@/store/canvas";
|
import { useCanvasStore } from "@/store/canvas";
|
||||||
|
import { imageGenerationService } from "@/services/canvas";
|
||||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||||
import { requestLongTaskResult } from "@/lib/client/long-task";
|
import { requestLongTaskResult } from "@/lib/client/long-task";
|
||||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||||
@ -218,6 +219,19 @@ type BatchDownloadDialogState = {
|
|||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DetailResultLocalEditTarget = {
|
||||||
|
cardId: string;
|
||||||
|
imageUrl: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DetailResultLocalEditRect = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_CHAT_TEXT_PART_LENGTH = 2000;
|
const MAX_CHAT_TEXT_PART_LENGTH = 2000;
|
||||||
const MAX_CHAT_HIDDEN_PROMPT_LENGTH = 4000;
|
const MAX_CHAT_HIDDEN_PROMPT_LENGTH = 4000;
|
||||||
const MAX_CHAT_ATTACHMENT_NAME_LENGTH = 100;
|
const MAX_CHAT_ATTACHMENT_NAME_LENGTH = 100;
|
||||||
@ -700,6 +714,177 @@ function sleep(ms: number) {
|
|||||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DetailResultLocalEditDialog({
|
||||||
|
submitting,
|
||||||
|
target,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
submitting: boolean;
|
||||||
|
target: DetailResultLocalEditTarget | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (params: {
|
||||||
|
imageUrl: string;
|
||||||
|
prompt: string;
|
||||||
|
rect: DetailResultLocalEditRect;
|
||||||
|
title: string;
|
||||||
|
}) => void;
|
||||||
|
}) {
|
||||||
|
const [prompt, setPrompt] = useState("");
|
||||||
|
const [rect, setRect] = useState<DetailResultLocalEditRect | null>(null);
|
||||||
|
const dragStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!target) {
|
||||||
|
setPrompt("");
|
||||||
|
setRect(null);
|
||||||
|
dragStartRef.current = null;
|
||||||
|
}
|
||||||
|
}, [target]);
|
||||||
|
|
||||||
|
const getPoint = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
const bounds = event.currentTarget.getBoundingClientRect();
|
||||||
|
const x = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0;
|
||||||
|
const y = bounds.height > 0 ? (event.clientY - bounds.top) / bounds.height : 0;
|
||||||
|
return {
|
||||||
|
x: Math.min(1, Math.max(0, x)),
|
||||||
|
y: Math.min(1, Math.max(0, y)),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateRect = useCallback(
|
||||||
|
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
const start = dragStartRef.current;
|
||||||
|
if (!start) return;
|
||||||
|
const point = getPoint(event);
|
||||||
|
const left = Math.min(start.x, point.x);
|
||||||
|
const top = Math.min(start.y, point.y);
|
||||||
|
setRect({
|
||||||
|
x: left,
|
||||||
|
y: top,
|
||||||
|
width: Math.max(0.01, Math.abs(point.x - start.x)),
|
||||||
|
height: Math.max(0.01, Math.abs(point.y - start.y)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[getPoint]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
if (!target || !rect) return;
|
||||||
|
const normalizedPrompt = prompt.trim();
|
||||||
|
if (!normalizedPrompt) {
|
||||||
|
toast.error("请填写局部编辑提示词");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rect.width < 0.025 || rect.height < 0.025) {
|
||||||
|
toast.error("请框选更明确的编辑区域");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSubmit({
|
||||||
|
imageUrl: target.imageUrl,
|
||||||
|
prompt: normalizedPrompt,
|
||||||
|
rect,
|
||||||
|
title: target.title,
|
||||||
|
});
|
||||||
|
}, [onSubmit, prompt, rect, target]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={Boolean(target)} onOpenChange={(open) => (!open ? onClose() : undefined)}>
|
||||||
|
<DialogContent className="max-w-4xl border-white/10 bg-[#101214] text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>局部编辑详情图</DialogTitle>
|
||||||
|
<DialogDescription className="text-white/50">
|
||||||
|
在图片上拖拽框选需要编辑的位置,并填写局部修改要求。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_280px]">
|
||||||
|
<div className="flex min-h-[420px] items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-black/35 p-3">
|
||||||
|
{target?.imageUrl ? (
|
||||||
|
<div
|
||||||
|
className="relative max-h-[62vh] max-w-full touch-none select-none overflow-hidden rounded-xl"
|
||||||
|
onPointerCancel={(event) => {
|
||||||
|
updateRect(event);
|
||||||
|
dragStartRef.current = null;
|
||||||
|
}}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
|
const point = getPoint(event);
|
||||||
|
dragStartRef.current = point;
|
||||||
|
setRect({ x: point.x, y: point.y, width: 0.01, height: 0.01 });
|
||||||
|
}}
|
||||||
|
onPointerMove={(event) => {
|
||||||
|
if (!dragStartRef.current) return;
|
||||||
|
event.preventDefault();
|
||||||
|
updateRect(event);
|
||||||
|
}}
|
||||||
|
onPointerUp={(event) => {
|
||||||
|
updateRect(event);
|
||||||
|
dragStartRef.current = null;
|
||||||
|
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={target.title}
|
||||||
|
className="block max-h-[62vh] max-w-full object-contain"
|
||||||
|
draggable={false}
|
||||||
|
src={buildImageProxyUrl(target.imageUrl, { width: 1200, quality: 88 })}
|
||||||
|
/>
|
||||||
|
{rect ? (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute border-2 border-sky-300 bg-sky-400/20 shadow-[0_0_0_9999px_rgba(0,0,0,0.28)]"
|
||||||
|
style={{
|
||||||
|
left: `${rect.x * 100}%`,
|
||||||
|
top: `${rect.y * 100}%`,
|
||||||
|
width: `${rect.width * 100}%`,
|
||||||
|
height: `${rect.height * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/[0.04] p-3 text-sm text-white/58">
|
||||||
|
框选区域后,输入你想改的内容。处理完成的新图会自动加入 AI详情结果区。
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
className="min-h-[160px] border-white/10 bg-black/30 text-white placeholder:text-white/30"
|
||||||
|
disabled={submitting}
|
||||||
|
maxLength={600}
|
||||||
|
onChange={(event) => setPrompt(event.target.value)}
|
||||||
|
placeholder="例如:把产品区域替换为金属质感,保持背景和文字不变"
|
||||||
|
value={prompt}
|
||||||
|
/>
|
||||||
|
<div className="mt-auto flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
className="border-white/10 bg-white/[0.04] text-white hover:bg-white/[0.08]"
|
||||||
|
disabled={submitting}
|
||||||
|
onClick={onClose}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-white text-black hover:bg-white/90"
|
||||||
|
disabled={submitting}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{submitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||||
|
生成局部编辑
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function runAgentBoundedParallel<T>(
|
async function runAgentBoundedParallel<T>(
|
||||||
items: T[],
|
items: T[],
|
||||||
workerCount: number,
|
workerCount: number,
|
||||||
@ -1412,6 +1597,13 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
useState<AgentGenerationResultModeType>("scene_generation");
|
useState<AgentGenerationResultModeType>("scene_generation");
|
||||||
const [fullscreenPreview, setFullscreenPreview] = useState<string | null>(null);
|
const [fullscreenPreview, setFullscreenPreview] = useState<string | null>(null);
|
||||||
const [sceneResizeEditorTargetUrl, setSceneResizeEditorTargetUrl] = useState<string | null>(null);
|
const [sceneResizeEditorTargetUrl, setSceneResizeEditorTargetUrl] = useState<string | null>(null);
|
||||||
|
const [detailLocalEditTarget, setDetailLocalEditTarget] =
|
||||||
|
useState<DetailResultLocalEditTarget | null>(null);
|
||||||
|
const [detailLocalEditSubmittingId, setDetailLocalEditSubmittingId] =
|
||||||
|
useState<string | null>(null);
|
||||||
|
const [detailEnhancingCardIds, setDetailEnhancingCardIds] = useState<Set<string>>(
|
||||||
|
() => new Set()
|
||||||
|
);
|
||||||
const [isPsdTemplateDialogOpen, setIsPsdTemplateDialogOpen] = useState(false);
|
const [isPsdTemplateDialogOpen, setIsPsdTemplateDialogOpen] = useState(false);
|
||||||
const [psdTemplateDialogSourceUrl, setPsdTemplateDialogSourceUrl] = useState<string | null>(null);
|
const [psdTemplateDialogSourceUrl, setPsdTemplateDialogSourceUrl] = useState<string | null>(null);
|
||||||
const [psdTemplateDialogMode, setPsdTemplateDialogMode] = useState<AgentPsdTemplateMode>("main");
|
const [psdTemplateDialogMode, setPsdTemplateDialogMode] = useState<AgentPsdTemplateMode>("main");
|
||||||
@ -4397,6 +4589,136 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
},
|
},
|
||||||
[activeResultModeType, mergedResultCollections, onResultPreviewUrlsChange]
|
[activeResultModeType, mergedResultCollections, onResultPreviewUrlsChange]
|
||||||
);
|
);
|
||||||
|
const handleEnhanceDetailGenerationResult = useCallback(
|
||||||
|
async (params: { cardId: string; imageUrl: string }) => {
|
||||||
|
const cardId = String(params.cardId || "").trim();
|
||||||
|
const imageUrl = String(params.imageUrl || "").trim();
|
||||||
|
if (!cardId || !/^https?:\/\//i.test(imageUrl)) {
|
||||||
|
toast.error("详情图地址无效,无法高清处理");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (detailEnhancingCardIds.has(cardId)) return;
|
||||||
|
|
||||||
|
setDetailEnhancingCardIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
next.add(cardId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
toast.info("正在高清处理详情图...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await imageGenerationService.enhanceImage(imageUrl);
|
||||||
|
if (!result.success || !result.data?.url) {
|
||||||
|
throw new Error(result.error || "高清处理失败");
|
||||||
|
}
|
||||||
|
const nextUrl = result.data.url;
|
||||||
|
if (!appendResultUrlToCollection("detail_generation", nextUrl)) {
|
||||||
|
throw new Error("当前项目未接入结果保存能力");
|
||||||
|
}
|
||||||
|
setActiveResultModeType("detail_generation");
|
||||||
|
setSelectedResultUrl(nextUrl);
|
||||||
|
toast.success("高清处理完成,已加入 AI详情结果区");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentGenerationWorkspaceDarkV2] 详情图高清失败:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "高清处理失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setDetailEnhancingCardIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
next.delete(cardId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[appendResultUrlToCollection, detailEnhancingCardIds]
|
||||||
|
);
|
||||||
|
const handleSubmitDetailLocalEdit = useCallback(
|
||||||
|
async (params: {
|
||||||
|
imageUrl: string;
|
||||||
|
prompt: string;
|
||||||
|
rect: DetailResultLocalEditRect;
|
||||||
|
title: string;
|
||||||
|
}) => {
|
||||||
|
const target = detailLocalEditTarget;
|
||||||
|
const cardId = String(target?.cardId || "").trim();
|
||||||
|
const imageUrl = String(params.imageUrl || "").trim();
|
||||||
|
if (!target || !cardId || !/^https?:\/\//i.test(imageUrl)) {
|
||||||
|
toast.error("详情图地址无效,无法局部编辑");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (detailLocalEditSubmittingId) return;
|
||||||
|
|
||||||
|
setDetailLocalEditSubmittingId(cardId);
|
||||||
|
toast.info("正在提交局部编辑...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const submitResult = await imageGenerationService.localEdit({
|
||||||
|
aspectRatio: "auto",
|
||||||
|
imageSize: "2K",
|
||||||
|
sourceImageUrl: imageUrl,
|
||||||
|
sourceNodeId: `agent-detail-${cardId}`,
|
||||||
|
regions: [
|
||||||
|
{
|
||||||
|
id: "detail-local-edit-1",
|
||||||
|
index: 1,
|
||||||
|
color: "#1677ff",
|
||||||
|
prompt: params.prompt,
|
||||||
|
rect: params.rect,
|
||||||
|
referenceImages: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!submitResult.success || !submitResult.data?.taskId) {
|
||||||
|
throw new Error(submitResult.error || "局部编辑提交失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
const deadline = Date.now() + AGENT_IMAGE_TASK_TIMEOUT_MS;
|
||||||
|
let lastError = "";
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
await sleep(AGENT_IMAGE_TASK_POLL_INTERVAL_MS);
|
||||||
|
const taskResult = await imageGenerationService.getImageTask(
|
||||||
|
submitResult.data.taskId
|
||||||
|
);
|
||||||
|
const task = taskResult.data?.task;
|
||||||
|
const status = String(task?.status || "").toLowerCase();
|
||||||
|
|
||||||
|
if (!taskResult.success || !task) {
|
||||||
|
lastError = taskResult.error || "查询局部编辑任务失败";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "succeeded" || status === "completed") {
|
||||||
|
const generatedImage = task.result?.images?.[0];
|
||||||
|
const nextUrl =
|
||||||
|
generatedImage?.originalUrl || generatedImage?.thumbnailUrl || "";
|
||||||
|
if (!/^https?:\/\//i.test(nextUrl)) {
|
||||||
|
throw new Error("局部编辑完成但结果地址无效");
|
||||||
|
}
|
||||||
|
if (!appendResultUrlToCollection("detail_generation", nextUrl)) {
|
||||||
|
throw new Error("当前项目未接入结果保存能力");
|
||||||
|
}
|
||||||
|
setActiveResultModeType("detail_generation");
|
||||||
|
setSelectedResultUrl(nextUrl);
|
||||||
|
setDetailLocalEditTarget(null);
|
||||||
|
toast.success("局部编辑完成,已加入 AI详情结果区");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["failed", "cancelled", "expired", "banned"].includes(status)) {
|
||||||
|
throw new Error(task.error || "局部编辑生成失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(lastError || "局部编辑任务等待超时,请稍后重试");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentGenerationWorkspaceDarkV2] 详情图局部编辑失败:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "局部编辑失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setDetailLocalEditSubmittingId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[appendResultUrlToCollection, detailLocalEditSubmittingId, detailLocalEditTarget]
|
||||||
|
);
|
||||||
const handlePsdDerivedImageCreated = useCallback(
|
const handlePsdDerivedImageCreated = useCallback(
|
||||||
(imageUrl: string) => {
|
(imageUrl: string) => {
|
||||||
appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl);
|
appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl);
|
||||||
@ -7821,6 +8143,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
{gridResultCards.map((card) => {
|
{gridResultCards.map((card) => {
|
||||||
const isActive = card.url && displayedResultUrl === card.url;
|
const isActive = card.url && displayedResultUrl === card.url;
|
||||||
const isSceneResultCard = activeResultModeType === "scene_generation";
|
const isSceneResultCard = activeResultModeType === "scene_generation";
|
||||||
|
const isDetailGenerationCard =
|
||||||
|
activeResultModeType === "detail_generation";
|
||||||
|
const isDetailEnhancing = detailEnhancingCardIds.has(card.id);
|
||||||
|
const isDetailLocalEditing =
|
||||||
|
detailLocalEditSubmittingId === card.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -7930,7 +8257,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
{activeResultModeType === "detail_generation" ? (
|
{isDetailGenerationCard ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -7942,6 +8269,52 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
>
|
>
|
||||||
下载
|
下载
|
||||||
</button>
|
</button>
|
||||||
|
<div className="absolute bottom-3 left-3 flex max-w-[calc(100%-6.5rem)] flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isDetailLocalEditing}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setDetailLocalEditTarget({
|
||||||
|
cardId: card.id,
|
||||||
|
imageUrl: card.url!,
|
||||||
|
title: `AI详情 ${card.index + 1}`,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
IMAGE_OVERLAY_ACTION_CLASS,
|
||||||
|
"inline-flex items-center gap-1.5 disabled:cursor-not-allowed disabled:opacity-45"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isDetailLocalEditing ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Wand2 className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
<span>局部编辑</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isDetailEnhancing}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
void handleEnhanceDetailGenerationResult({
|
||||||
|
cardId: card.id,
|
||||||
|
imageUrl: card.url!,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
IMAGE_OVERLAY_ACTION_CLASS,
|
||||||
|
"inline-flex items-center gap-1.5 disabled:cursor-not-allowed disabled:opacity-45"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isDetailEnhancing ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Sparkles className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
<span>高清</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
@ -7951,10 +8324,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
title: `AI详情 ${card.index + 1}`,
|
title: `AI详情 ${card.index + 1}`,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className={cn("absolute bottom-3 left-3", IMAGE_OVERLAY_ACTION_CLASS)}
|
className={IMAGE_OVERLAY_ACTION_CLASS}
|
||||||
>
|
>
|
||||||
送入画布改字
|
送入画布改字
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
@ -8309,6 +8683,19 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
</ClientErrorBoundary>
|
</ClientErrorBoundary>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<DetailResultLocalEditDialog
|
||||||
|
submitting={Boolean(detailLocalEditSubmittingId)}
|
||||||
|
target={detailLocalEditTarget}
|
||||||
|
onClose={() => {
|
||||||
|
if (detailLocalEditSubmittingId) {
|
||||||
|
toast.info("局部编辑正在处理中,请稍候");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDetailLocalEditTarget(null);
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmitDetailLocalEdit}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Fullscreen Preview */}
|
{/* Fullscreen Preview */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{fullscreenPreview && (
|
{fullscreenPreview && (
|
||||||
|
|||||||
@ -3,19 +3,23 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
name: "ai-chatbot",
|
name: "ai-chatbot",
|
||||||
script: "node_modules/next/dist/bin/next",
|
script: "node_modules/next/dist/bin/next",
|
||||||
args: "start",
|
args: "start -p 3125 -H 127.0.0.1",
|
||||||
cwd: "/var/www/ai-chatbot",
|
cwd: "/var/www/ai-chatbot",
|
||||||
instances: 1,
|
instances: 1,
|
||||||
exec_mode: "fork",
|
exec_mode: "fork",
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
PORT: 3001,
|
PORT: 3125,
|
||||||
},
|
},
|
||||||
error_file: "/var/log/pm2/ai-chatbot-error.log",
|
error_file: "/var/log/pm2/ai-chatbot-error.log",
|
||||||
out_file: "/var/log/pm2/ai-chatbot-out.log",
|
out_file: "/var/log/pm2/ai-chatbot-out.log",
|
||||||
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
|
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
|
||||||
merge_logs: true,
|
merge_logs: true,
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
|
min_uptime: "30s",
|
||||||
|
max_restarts: 10,
|
||||||
|
restart_delay: 3000,
|
||||||
|
kill_timeout: 10000,
|
||||||
max_memory_restart: "3G",
|
max_memory_restart: "3G",
|
||||||
watch: false,
|
watch: false,
|
||||||
ignore_watch: ["node_modules", ".next", ".git"],
|
ignore_watch: ["node_modules", ".next", ".git"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user