feat(plugin): AI 审美批改 - 增加结构化视觉分析与精准标注 (#387)
* feat(portrait-clearance): 肖像排查 - 增加画布插件与本机检测链路 * fix(web): 插件中心 - 修复应用插件筛选与自动定位 * feat(plugin): AI审美批改 - 完成分析报告与问题标注闭环
This commit is contained in:
1 parent
29f7a5c0e5
commit
a6504c0e00
29 files changed
+4909
-15
No files matched your search
@@ -95,6 +95,7 @@ import { CanvasProjectWorldLayers } from "./canvas-project-world-layers";
|
||||
import { CanvasNodeActionContext, type CanvasNodeActionContextValue } from "@/components/canvas/canvas-node-action-context";
|
||||
import { bringCanvasNodeToFront, type CanvasNodeStackOrder } from "@/lib/canvas/canvas-node-stack-order";
|
||||
import { PortraitClearanceModal } from "@/components/canvas/portrait-clearance/portrait-clearance-modal";
|
||||
import { AiArtCritiqueModal } from "@/components/canvas/art-critique/ai-art-critique-modal";
|
||||
import { CanvasNodeGraphContext, type CanvasNodeGraphContextValue } from "@/components/canvas/canvas-node-graph-context";
|
||||
import { CanvasRefreshShell } from "./canvas-refresh-shell";
|
||||
import { queryGenerationTask } from "@/services/api/task-center";
|
||||
@@ -147,6 +148,7 @@ import type { ReferenceImage } from "@/types/image";
|
||||
import type { PortraitClearanceNodeState } from "@/lib/portrait-clearance/contracts";
|
||||
import { createDefaultPortraitClearanceState, PORTRAIT_CLEARANCE_NODE_TYPE } from "@/lib/portrait-clearance/contracts";
|
||||
import { reconcilePortraitClearanceInputBindings } from "@/lib/portrait-clearance/input-bindings";
|
||||
import { ART_CRITIQUE_NODE_TYPE } from "@/lib/art-critique/contracts";
|
||||
|
||||
const CanvasDirectorWorkbench = lazy(() => import("@/components/canvas/director/canvas-director-workbench").then((module) => ({ default: module.CanvasDirectorWorkbench })));
|
||||
const CanvasDrawingEditorModal = lazy(() => import("@/components/canvas/canvas-drawing-editor-modal").then((module) => ({ default: module.CanvasDrawingEditorModal })));
|
||||
@@ -287,6 +289,7 @@ function InfiniteCanvasPage() {
|
||||
const [previewNodeId, setPreviewNodeId] = useState<string | null>(null);
|
||||
const [scriptEditorNodeId, setScriptEditorNodeId] = useState<string | null>(null);
|
||||
const [portraitClearanceNodeId, setPortraitClearanceNodeId] = useState<string | null>(null);
|
||||
const [artCritiqueNodeId, setArtCritiqueNodeId] = useState<string | null>(null);
|
||||
const [scriptScrollTopById, setScriptScrollTopById] = useState<Record<string, number>>({});
|
||||
const [directorNodeId, setDirectorNodeId] = useState<string | null>(null);
|
||||
const [versionCompareRootId, setVersionCompareRootId] = useState<string | null>(null);
|
||||
@@ -918,6 +921,7 @@ function InfiniteCanvasPage() {
|
||||
setRunningNodeId(clearDeletedId);
|
||||
setScriptEditorNodeId(clearDeletedId);
|
||||
setPortraitClearanceNodeId(clearDeletedId);
|
||||
setArtCritiqueNodeId(clearDeletedId);
|
||||
setDirectorNodeId(clearDeletedId);
|
||||
setVersionCompareRootId(clearDeletedId);
|
||||
setScriptScrollTopById((current) => Object.fromEntries(Object.entries(current).filter(([id]) => !removedIds.has(id))));
|
||||
@@ -928,7 +932,7 @@ function InfiniteCanvasPage() {
|
||||
}
|
||||
cleanupCanvasFiles({ projectId, nodes: nextNodes, chatSessions });
|
||||
},
|
||||
[chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setCropNodeId, setEmotionNodeId, setFrameDialogNodeId, setMaskEditNodeId, setSegmentDialogNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId],
|
||||
[chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setArtCritiqueNodeId, setCropNodeId, setEmotionNodeId, setFrameDialogNodeId, setMaskEditNodeId, setSegmentDialogNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -1030,6 +1034,9 @@ function InfiniteCanvasPage() {
|
||||
} else if (node.type === PORTRAIT_CLEARANCE_NODE_TYPE) {
|
||||
setDialogNodeId(null);
|
||||
setPortraitClearanceNodeId(node.id);
|
||||
} else if (node.type === ART_CRITIQUE_NODE_TYPE) {
|
||||
setDialogNodeId(null);
|
||||
setArtCritiqueNodeId(node.id);
|
||||
} else {
|
||||
// 选择参考媒体时保留当前工作流配置面板,避免点击图片后配置“返回/消失”。
|
||||
// 没有工作流配置面板时,媒体节点仍按原逻辑打开自己的面板。
|
||||
@@ -1047,7 +1054,7 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const handleNodeDragEnd = useCallback((nodeId: string) => {
|
||||
const node = nodesRef.current.find((item) => item.id === nodeId);
|
||||
if (!node || node.type === CanvasNodeType.Script || node.type === CanvasNodeType.Drawing || node.type === PORTRAIT_CLEARANCE_NODE_TYPE) {
|
||||
if (!node || node.type === CanvasNodeType.Script || node.type === CanvasNodeType.Drawing || node.type === PORTRAIT_CLEARANCE_NODE_TYPE || node.type === ART_CRITIQUE_NODE_TYPE) {
|
||||
setDialogNodeId(null);
|
||||
return;
|
||||
}
|
||||
@@ -1328,6 +1335,7 @@ function InfiniteCanvasPage() {
|
||||
const characterReferenceNode = characterReferenceNodeId ? nodeById.get(characterReferenceNodeId) || null : null;
|
||||
const drawingNode = drawingNodeId ? nodeById.get(drawingNodeId) || null : null;
|
||||
const portraitClearanceNode = portraitClearanceNodeId ? nodeById.get(portraitClearanceNodeId) || null : null;
|
||||
const artCritiqueNode = artCritiqueNodeId ? nodeById.get(artCritiqueNodeId) || null : null;
|
||||
const portraitClearanceInputs = portraitClearanceNode
|
||||
? connections
|
||||
.filter((connection) => connection.toNodeId === portraitClearanceNode.id)
|
||||
@@ -1335,6 +1343,13 @@ function InfiniteCanvasPage() {
|
||||
.map((connection) => nodeById.get(connection.fromNodeId))
|
||||
.filter((node): node is CanvasNodeData => Boolean(node))
|
||||
: [];
|
||||
const artCritiqueInputs = artCritiqueNode
|
||||
? connections
|
||||
.filter((connection) => connection.toNodeId === artCritiqueNode.id)
|
||||
.sort((left, right) => left.id.localeCompare(right.id))
|
||||
.map((connection) => nodeById.get(connection.fromNodeId))
|
||||
.filter((node): node is CanvasNodeData => Boolean(node))
|
||||
: [];
|
||||
const addPortraitCandidateToCanvas = useCallback(async (candidate: { id: string; title: string; imageArtifactId: string }, dataUrl: string) => {
|
||||
const target = portraitClearanceNodeId ? nodesRef.current.find((node) => node.id === portraitClearanceNodeId) : undefined;
|
||||
if (!target) return;
|
||||
@@ -1389,6 +1404,15 @@ function InfiniteCanvasPage() {
|
||||
setToolbarNodeId(null);
|
||||
setPortraitClearanceNodeId(node.id);
|
||||
}, []);
|
||||
const openArtCritique = useCallback((node: CanvasNodeData) => {
|
||||
if (node.type !== ART_CRITIQUE_NODE_TYPE) return;
|
||||
setSelectedNodeIds(new Set([node.id]));
|
||||
setSelectedConnectionId(null);
|
||||
setContextMenu(null);
|
||||
setDialogNodeId(null);
|
||||
setToolbarNodeId(null);
|
||||
setArtCritiqueNodeId(node.id);
|
||||
}, []);
|
||||
const duplicateNodeFromContent = useCallback((node: CanvasNodeData) => duplicateNode(node.id), [duplicateNode]);
|
||||
const deleteNodeFromContent = useCallback((node: CanvasNodeData) => deleteNodes(new Set([node.id])), [deleteNodes]);
|
||||
const updateNodeFromContent = useCallback((nodeId: string, update: (node: CanvasNodeData) => CanvasNodeData) => {
|
||||
@@ -1427,7 +1451,8 @@ function InfiniteCanvasPage() {
|
||||
updateNode: updateNodeFromContent,
|
||||
updateMediaNode: updateMediaNodeFromContent,
|
||||
openPortraitClearance,
|
||||
}), [deleteNodeFromContent, downloadNodeImage, duplicateNodeFromContent, openPortraitClearance, updateMediaNodeFromContent, updateNodeFromContent, updateNodeMetadataFromContent]);
|
||||
openArtCritique,
|
||||
}), [deleteNodeFromContent, downloadNodeImage, duplicateNodeFromContent, openArtCritique, openPortraitClearance, updateMediaNodeFromContent, updateNodeFromContent, updateNodeMetadataFromContent]);
|
||||
const { agentSnapshot, agentUndoCount, applyAgentOps, canUndoAgentOps, dismissLastAgentChange, lastAgentChange, undoAgentOps, viewLastAgentChange } = useCanvasAgentOperations({
|
||||
projectId,
|
||||
domainProjectId: currentProject?.projectId,
|
||||
@@ -1523,6 +1548,7 @@ function InfiniteCanvasPage() {
|
||||
setEmotionNodeId(null);
|
||||
setPreviewNodeId(null);
|
||||
setRunningNodeId(null);
|
||||
setArtCritiqueNodeId(null);
|
||||
deselectCanvas();
|
||||
setClearConfirmOpen(false);
|
||||
clearCanvasFiles();
|
||||
@@ -2757,12 +2783,20 @@ function InfiniteCanvasPage() {
|
||||
<PortraitClearanceModal
|
||||
projectId={projectId}
|
||||
node={portraitClearanceNode}
|
||||
upstreamNodes={portraitClearanceInputs}
|
||||
open={Boolean(portraitClearanceNode)}
|
||||
onClose={() => setPortraitClearanceNodeId(null)}
|
||||
onUpdateState={(nodeId, state: PortraitClearanceNodeState) => handleConfigNodeChange(nodeId, { portraitClearance: state })}
|
||||
onAddCandidate={addPortraitCandidateToCanvas}
|
||||
/>
|
||||
upstreamNodes={portraitClearanceInputs}
|
||||
open={Boolean(portraitClearanceNode)}
|
||||
onClose={() => setPortraitClearanceNodeId(null)}
|
||||
onUpdateState={(nodeId, state: PortraitClearanceNodeState) => handleConfigNodeChange(nodeId, { portraitClearance: state })}
|
||||
onAddCandidate={addPortraitCandidateToCanvas}
|
||||
/>
|
||||
|
||||
<AiArtCritiqueModal
|
||||
node={artCritiqueNode}
|
||||
upstreamNodes={artCritiqueInputs}
|
||||
open={Boolean(artCritiqueNode)}
|
||||
onClose={() => setArtCritiqueNodeId(null)}
|
||||
onUpdateState={(nodeId, state) => handleConfigNodeChange(nodeId, { artCritique: state })}
|
||||
/>
|
||||
|
||||
<CanvasScriptEditor
|
||||
node={activeScriptNode}
|
||||
|
||||
Reference in new issue
Block a user