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:
@@ -640,7 +640,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
|
||||
shouldGenerateImage := isolatedImageGenerator || taskPlan.Intent == "image"
|
||||
if shouldGenerateImage {
|
||||
if targetNodeID != "" {
|
||||
project.Nodes = markImageGeneratorTargetGenerating(project.Nodes, targetNodeID, prompt, req.ImageSize)
|
||||
project.Nodes = markImageGeneratorTargetGenerating(project.Nodes, targetNodeID, prompt, req.ImageSize, plannedImageCount(taskPlan))
|
||||
} else {
|
||||
project.Nodes = append(project.Nodes, newGeneratingImageNodesForPlan(prompt, len(project.Nodes), taskPlan, req.ImageSize)...)
|
||||
}
|
||||
@@ -3059,7 +3059,7 @@ func (s *DesignService) planWithIncrementalImages(ctx context.Context, projectID
|
||||
}
|
||||
node := nodes[0]
|
||||
persistedNodes = append(persistedNodes, node)
|
||||
return s.applyIncrementalGeneratedNode(ctx, projectID, threadID, prompt, targetNodeID, node, appendArtifacts)
|
||||
return s.applyIncrementalGeneratedNode(ctx, projectID, threadID, prompt, targetNodeID, node, result.Index, appendArtifacts)
|
||||
})
|
||||
if len(persistedNodes) > 0 {
|
||||
plan.GeneratedNodes = append([]design.Node(nil), persistedNodes...)
|
||||
@@ -3133,7 +3133,7 @@ func plannedGenerationResultCapacity(req design.AgentRequest) int {
|
||||
return count
|
||||
}
|
||||
|
||||
func (s *DesignService) applyIncrementalGeneratedNode(ctx context.Context, projectID string, threadID string, prompt string, targetNodeID string, node design.Node, appendArtifact bool) error {
|
||||
func (s *DesignService) applyIncrementalGeneratedNode(ctx context.Context, projectID string, threadID string, prompt string, targetNodeID string, node design.Node, resultIndex int, appendArtifact bool) error {
|
||||
project, err := s.repo.Get(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -3142,7 +3142,12 @@ func (s *DesignService) applyIncrementalGeneratedNode(ctx context.Context, proje
|
||||
return context.Canceled
|
||||
}
|
||||
if strings.TrimSpace(targetNodeID) != "" {
|
||||
project.Nodes = mergeGeneratedNodesIntoTarget(nonGeneratingNodesExceptTarget(project.Nodes, targetNodeID), []design.Node{node}, targetNodeID)
|
||||
baseNodes := nonGeneratingNodesExceptTarget(project.Nodes, targetNodeID)
|
||||
if resultIndex > 0 {
|
||||
project.Nodes = appendGeneratedNodeBesideTarget(baseNodes, targetNodeID, node)
|
||||
} else {
|
||||
project.Nodes = mergeGeneratedNodesIntoTarget(baseNodes, []design.Node{node}, targetNodeID)
|
||||
}
|
||||
} else {
|
||||
project.Nodes = upsertGeneratedNode(project.Nodes, node)
|
||||
}
|
||||
@@ -4429,23 +4434,89 @@ func failRemainingAgentGenerationPlaceholders(nodes []design.Node, targetNodeID
|
||||
|
||||
func shouldClearRemainingGenerationPlaceholder(node design.Node, targetNodeID string) bool {
|
||||
if targetNodeID != "" {
|
||||
return node.ID == targetNodeID
|
||||
return node.ID == targetNodeID || (node.ParentID == targetNodeID && node.LayerRole == "image-generator")
|
||||
}
|
||||
return node.LayerRole != "image-generator"
|
||||
}
|
||||
|
||||
func markImageGeneratorTargetGenerating(nodes []design.Node, targetNodeID string, prompt string, imageSize string) []design.Node {
|
||||
next := append([]design.Node(nil), nodes...)
|
||||
for index := range next {
|
||||
if next[index].ID == targetNodeID && next[index].Type == design.NodeTypeImage && next[index].LayerRole == "image-generator" {
|
||||
next[index].Title = generationPlaceholderTitle(prompt)
|
||||
next[index].Content = prompt
|
||||
next[index].Tone = "image-generator"
|
||||
next[index].Status = "generating"
|
||||
return next
|
||||
func markImageGeneratorTargetGenerating(nodes []design.Node, targetNodeID string, prompt string, imageSize string, count int) []design.Node {
|
||||
count = normalizeImageGeneratorTargetCount(count)
|
||||
targetNodeID = strings.TrimSpace(targetNodeID)
|
||||
next := make([]design.Node, 0, len(nodes)+count-1)
|
||||
targetIndex := -1
|
||||
var target design.Node
|
||||
for _, node := range nodes {
|
||||
if isImageGeneratorTargetClone(node, targetNodeID) {
|
||||
continue
|
||||
}
|
||||
if node.ID == targetNodeID && node.Type == design.NodeTypeImage && node.LayerRole == "image-generator" {
|
||||
node.Title = generationPlaceholderTitle(prompt)
|
||||
node.Content = prompt
|
||||
node.Tone = "image-generator"
|
||||
node.Status = "generating"
|
||||
targetIndex = len(next)
|
||||
target = node
|
||||
}
|
||||
next = append(next, node)
|
||||
}
|
||||
return append(next, newGeneratingImageNode(prompt, len(next), imageSize))
|
||||
if targetIndex < 0 {
|
||||
return append(next, newGeneratingImageNodes(prompt, len(next), count, imageSize)...)
|
||||
}
|
||||
clones := imageGeneratorTargetCloneNodes(target, prompt, imageSize, count-1)
|
||||
if len(clones) == 0 {
|
||||
return next
|
||||
}
|
||||
next = append(next, make([]design.Node, len(clones))...)
|
||||
copy(next[targetIndex+1+len(clones):], next[targetIndex+1:])
|
||||
copy(next[targetIndex+1:], clones)
|
||||
return next
|
||||
}
|
||||
|
||||
func normalizeImageGeneratorTargetCount(count int) int {
|
||||
if count < 1 {
|
||||
return 1
|
||||
}
|
||||
if count > 10 {
|
||||
return 10
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func isImageGeneratorTargetClone(node design.Node, targetNodeID string) bool {
|
||||
return targetNodeID != "" && node.ParentID == targetNodeID && node.Type == design.NodeTypeImage && node.LayerRole == "image-generator" && node.Status == "generating"
|
||||
}
|
||||
|
||||
func imageGeneratorTargetCloneNodes(target design.Node, prompt string, imageSize string, count int) []design.Node {
|
||||
if count <= 0 {
|
||||
return nil
|
||||
}
|
||||
width, height := target.Width, target.Height
|
||||
if width <= 0 || height <= 0 {
|
||||
width, height = design.GeneratedImageDimensions(imageSize)
|
||||
}
|
||||
if width <= 0 {
|
||||
width = design.DefaultGeneratedImageWidth
|
||||
}
|
||||
if height <= 0 {
|
||||
height = design.DefaultGeneratedImageHeight
|
||||
}
|
||||
clones := make([]design.Node, 0, count)
|
||||
for index := 0; index < count; index++ {
|
||||
clone := target
|
||||
clone.ID = newID()
|
||||
clone.ParentID = target.ID
|
||||
clone.Title = generationPlaceholderTitle(prompt)
|
||||
clone.Content = prompt
|
||||
clone.Tone = "image-generator"
|
||||
clone.Status = "generating"
|
||||
clone.LayerRole = "image-generator"
|
||||
clone.Width = width
|
||||
clone.Height = height
|
||||
clone.X = positionGeneratedSiblingNextToTarget(target, clone, index).X
|
||||
clone.Y = target.Y
|
||||
clones = append(clones, clone)
|
||||
}
|
||||
return clones
|
||||
}
|
||||
|
||||
func isImageGeneratorRequest(mode string, targetNodeID string) bool {
|
||||
@@ -4491,6 +4562,9 @@ func mergeGeneratedNodesIntoTarget(nodes []design.Node, generated []design.Node,
|
||||
primary = normalized
|
||||
}
|
||||
target := nodes[targetIndex]
|
||||
if len(generated) == 1 && primary.ID != targetNodeID && !isActiveImageGeneratorTarget(target) {
|
||||
return appendGeneratedSiblingsNextToTarget(nodes, target, []design.Node{primary})
|
||||
}
|
||||
centerX := target.X + target.Width/2
|
||||
centerY := target.Y + target.Height/2
|
||||
target.Title = primary.Title
|
||||
@@ -4511,11 +4585,52 @@ func mergeGeneratedNodesIntoTarget(nodes []design.Node, generated []design.Node,
|
||||
}
|
||||
nodes[targetIndex] = target
|
||||
if len(generated) > 1 {
|
||||
return append(nodes, completedImageGeneratorNodes(generated[1:])...)
|
||||
return appendGeneratedSiblingsNextToTarget(nodes, target, generated[1:])
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func isActiveImageGeneratorTarget(node design.Node) bool {
|
||||
if node.Type != design.NodeTypeImage {
|
||||
return false
|
||||
}
|
||||
return node.Status == "generating" || node.LayerRole == "image-generator" || strings.TrimSpace(node.Content) == ""
|
||||
}
|
||||
|
||||
func appendGeneratedSiblingsNextToTarget(nodes []design.Node, target design.Node, generated []design.Node) []design.Node {
|
||||
siblings := completedImageGeneratorNodes(generated)
|
||||
for index, sibling := range siblings {
|
||||
sibling = positionGeneratedSiblingNextToTarget(target, sibling, index)
|
||||
if existingIndex := findNodeIndex(nodes, sibling.ID); existingIndex >= 0 {
|
||||
nodes[existingIndex] = sibling
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, sibling)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func appendGeneratedNodeBesideTarget(nodes []design.Node, targetNodeID string, generated design.Node) []design.Node {
|
||||
targetIndex := findNodeIndex(nodes, strings.TrimSpace(targetNodeID))
|
||||
if targetIndex < 0 {
|
||||
return append(nodes, completedImageGeneratorNodes([]design.Node{generated})...)
|
||||
}
|
||||
return appendGeneratedSiblingsNextToTarget(nodes, nodes[targetIndex], []design.Node{generated})
|
||||
}
|
||||
|
||||
func positionGeneratedSiblingNextToTarget(target design.Node, sibling design.Node, index int) design.Node {
|
||||
if sibling.Width <= 0 {
|
||||
sibling.Width = target.Width
|
||||
}
|
||||
if sibling.Height <= 0 {
|
||||
sibling.Height = target.Height
|
||||
}
|
||||
gap := math.Max(48, math.Min(120, target.Width*0.08))
|
||||
sibling.X = target.X + target.Width + gap + float64(index)*(sibling.Width+gap)
|
||||
sibling.Y = target.Y
|
||||
return sibling
|
||||
}
|
||||
|
||||
func completedImageGeneratorNodes(nodes []design.Node) []design.Node {
|
||||
next := make([]design.Node, len(nodes))
|
||||
for index, node := range nodes {
|
||||
|
||||
Reference in New Issue
Block a user