feat(canvas): clone generator placeholders and place batch results as siblings
When an image-generator target requests multiple images, duplicate the target placeholder into N generating clones and lay them out beside the target. Incremental results after the first are appended as siblings rather than merged into the target, and placeholder cleanup now clears the target's generating clones on failure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,100 @@ func TestMergeGeneratedNodesIntoCompletedImageGeneratorTargetDoesNotReappend(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeGeneratedNodesIntoImageGeneratorTargetPlacesExtraImagesBesideTarget(t *testing.T) {
|
||||
nodes := []design.Node{
|
||||
{
|
||||
ID: "generator-1",
|
||||
Type: design.NodeTypeImage,
|
||||
Title: "图像生成器",
|
||||
X: 100,
|
||||
Y: 80,
|
||||
Width: 360,
|
||||
Height: 240,
|
||||
Status: "generating",
|
||||
LayerRole: "image-generator",
|
||||
},
|
||||
}
|
||||
generated := []design.Node{
|
||||
{
|
||||
ID: "generator-1",
|
||||
Type: design.NodeTypeImage,
|
||||
Title: "商品主图 1",
|
||||
Content: "https://example.com/generated-1.png",
|
||||
Tone: "visual",
|
||||
Status: "success",
|
||||
Width: 360,
|
||||
Height: 240,
|
||||
},
|
||||
{
|
||||
ID: "generated-2",
|
||||
Type: design.NodeTypeImage,
|
||||
Title: "商品主图 2",
|
||||
Content: "https://example.com/generated-2.png",
|
||||
Tone: "visual",
|
||||
Status: "success",
|
||||
Width: 360,
|
||||
Height: 240,
|
||||
},
|
||||
}
|
||||
|
||||
merged := mergeGeneratedNodesIntoTarget(nodes, generated, "generator-1")
|
||||
if len(merged) != 2 {
|
||||
t.Fatalf("expected target plus sibling image, got %#v", merged)
|
||||
}
|
||||
if merged[0].Content != "https://example.com/generated-1.png" || merged[0].LayerRole != "" {
|
||||
t.Fatalf("expected first image to replace target, got %#v", merged[0])
|
||||
}
|
||||
if merged[1].Content != "https://example.com/generated-2.png" || merged[1].X <= merged[0].X+merged[0].Width || merged[1].Y != merged[0].Y {
|
||||
t.Fatalf("expected second image beside target, got %#v after %#v", merged[1], merged[0])
|
||||
}
|
||||
|
||||
mergedAgain := mergeGeneratedNodesIntoTarget(merged, generated, "generator-1")
|
||||
if len(mergedAgain) != 2 {
|
||||
t.Fatalf("expected repeated merge to update sibling without duplicating, got %#v", mergedAgain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeGeneratedNodeIntoCompletedImageGeneratorTargetAppendsSibling(t *testing.T) {
|
||||
nodes := []design.Node{
|
||||
{
|
||||
ID: "generator-1",
|
||||
Type: design.NodeTypeImage,
|
||||
Title: "商品主图 1",
|
||||
Content: "https://example.com/generated-1.png",
|
||||
Tone: "visual",
|
||||
Status: "success",
|
||||
X: 100,
|
||||
Y: 80,
|
||||
Width: 360,
|
||||
Height: 240,
|
||||
},
|
||||
}
|
||||
generated := []design.Node{
|
||||
{
|
||||
ID: "generated-2",
|
||||
Type: design.NodeTypeImage,
|
||||
Title: "商品主图 2",
|
||||
Content: "https://example.com/generated-2.png",
|
||||
Tone: "visual",
|
||||
Status: "success",
|
||||
Width: 360,
|
||||
Height: 240,
|
||||
},
|
||||
}
|
||||
|
||||
merged := mergeGeneratedNodesIntoTarget(nodes, generated, "generator-1")
|
||||
if len(merged) != 2 {
|
||||
t.Fatalf("expected completed target plus sibling image, got %#v", merged)
|
||||
}
|
||||
if merged[0].Content != "https://example.com/generated-1.png" {
|
||||
t.Fatalf("expected completed target to stay intact, got %#v", merged[0])
|
||||
}
|
||||
if merged[1].Content != "https://example.com/generated-2.png" || merged[1].X <= merged[0].X+merged[0].Width {
|
||||
t.Fatalf("expected sibling appended beside completed target, got %#v", merged[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageGeneratorTargetNodeIDParsesDirectiveCaseInsensitively(t *testing.T) {
|
||||
targetID := imageGeneratorTargetNodeID([]design.AgentChatMessage{
|
||||
{
|
||||
@@ -102,6 +196,59 @@ func TestImageGeneratorTargetNodeIDParsesDirectiveCaseInsensitively(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkImageGeneratorTargetGeneratingCopiesPlaceholdersForRequestedCount(t *testing.T) {
|
||||
nodes := []design.Node{
|
||||
{
|
||||
ID: "generator-1",
|
||||
Type: design.NodeTypeImage,
|
||||
Title: "图像生成器",
|
||||
X: 100,
|
||||
Y: 80,
|
||||
Width: 360,
|
||||
Height: 240,
|
||||
Status: "draft",
|
||||
LayerRole: "image-generator",
|
||||
},
|
||||
{
|
||||
ID: "regular-image",
|
||||
Type: design.NodeTypeImage,
|
||||
Content: "https://example.com/keep.png",
|
||||
Status: "success",
|
||||
},
|
||||
}
|
||||
|
||||
marked := markImageGeneratorTargetGenerating(nodes, "generator-1", "生成五张商品主图", "1024x1024", 5)
|
||||
if len(marked) != 6 {
|
||||
t.Fatalf("expected target, four copied placeholders, and existing image, got %#v", marked)
|
||||
}
|
||||
if marked[0].ID != "generator-1" || marked[0].Status != "generating" || marked[0].Content != "生成五张商品主图" {
|
||||
t.Fatalf("expected original generator to become first placeholder, got %#v", marked[0])
|
||||
}
|
||||
for index := 1; index <= 4; index++ {
|
||||
node := marked[index]
|
||||
if node.ID == "" || node.ID == "generator-1" || node.ParentID != "generator-1" || node.LayerRole != "image-generator" || node.Status != "generating" {
|
||||
t.Fatalf("expected copied image generator placeholder at index %d, got %#v", index, node)
|
||||
}
|
||||
if node.X <= marked[index-1].X+marked[index-1].Width || node.Y != marked[0].Y {
|
||||
t.Fatalf("expected copied placeholder %d beside previous placeholder, got %#v", index, node)
|
||||
}
|
||||
}
|
||||
if marked[5].ID != "regular-image" {
|
||||
t.Fatalf("expected unrelated image to stay after placeholders, got %#v", marked[5])
|
||||
}
|
||||
|
||||
remarked := markImageGeneratorTargetGenerating(marked, "generator-1", "生成两张商品主图", "1024x1024", 2)
|
||||
if len(remarked) != 3 {
|
||||
t.Fatalf("expected stale copied placeholders to be replaced for new count, got %#v", remarked)
|
||||
}
|
||||
if remarked[1].ParentID != "generator-1" || remarked[1].Status != "generating" {
|
||||
t.Fatalf("expected one fresh copied placeholder, got %#v", remarked[1])
|
||||
}
|
||||
if remarked[2].ID != "regular-image" {
|
||||
t.Fatalf("expected unrelated image to remain, got %#v", remarked[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreFinishedGeneratedImageNodesConvertsCompletedImageGeneratorNode(t *testing.T) {
|
||||
now := time.Now()
|
||||
project := design.Project{
|
||||
|
||||
Reference in New Issue
Block a user