From a8c7fedadbfaeb907a59b48eda02ae3ff91a9102 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 3 Sep 2026 19:05:10 +0300 Subject: [PATCH] feat(chat): unify composer attachments - Render node references and images with one attachment-card layout\n- Generate live previews for referenced scene nodes\n- Make the current-selection control toggle its active selection\n\nCo-authored-by: Jason Kneen --- src/components/chat/ChatInput.vue | 49 +++++++++++++------------ src/components/chat/ChatNodePreview.vue | 49 +++++++++++++++++++++++++ tests/e2e/chat/panel.spec.ts | 8 ++++ 3 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 src/components/chat/ChatNodePreview.vue diff --git a/src/components/chat/ChatInput.vue b/src/components/chat/ChatInput.vue index d3bb88030..b7e5eb720 100644 --- a/src/components/chat/ChatInput.vue +++ b/src/components/chat/ChatInput.vue @@ -5,6 +5,7 @@ import { computed, onBeforeUnmount, ref } from 'vue' import { useI18n, useSelectionState } from '@open-pencil/vue' +import ChatNodePreview from '@/components/chat/ChatNodePreview.vue' import ChatProfileSelect from '@/components/chat/ChatProfileSelect.vue' import ProviderModelSelect from '@/components/chat/ProviderModelSelect.vue' import IconButton from '@/components/ui/IconButton.vue' @@ -13,8 +14,7 @@ import { useAIChat } from '@/app/ai/chat/use' import { appendReferencedNodeContext, MAX_REFERENCED_NODES, - resolveReferencedNodes, - type ReferencedNode + resolveReferencedNodes } from '@/app/ai/chat/context' import { designModelProfile, designModelProfiles } from '@/app/ai/models' import { @@ -23,7 +23,6 @@ import { validateImageAttachmentFile } from '@/app/ai/attachment/image/prepare' import { MAX_IMAGE_ATTACHMENTS, type ImageAttachmentDraft } from '@/app/ai/attachment/image/types' -import { nodeIcon } from '@/app/editor/icons' import { openSettingsDialog } from '@/app/settings/dialog' import { ACP_AGENTS } from '@open-pencil/core/constants' @@ -51,11 +50,18 @@ const referencedNodeIds = ref([]) const referencedNodes = computed(() => resolveReferencedNodes(editor.graph, referencedNodeIds.value) ) +const selectedReferencedNodeIds = computed(() => + [...selectedIds.value].filter((id) => referencedNodeIds.value.includes(id)) +) const canAddSelection = computed( () => selectedIds.value.size > 0 && - referencedNodes.value.length < MAX_REFERENCED_NODES && - [...selectedIds.value].some((id) => !referencedNodeIds.value.includes(id)) + (selectedReferencedNodeIds.value.length > 0 || + referencedNodes.value.length < MAX_REFERENCED_NODES) +) +const selectionContextActive = computed( + () => + selectedIds.value.size > 0 && selectedReferencedNodeIds.value.length === selectedIds.value.size ) const { open: openImageDialog, @@ -157,18 +163,18 @@ function removeReferencedNode(id: string): void { referencedNodeIds.value = referencedNodeIds.value.filter((candidate) => candidate !== id) } -function addCurrentSelection(): void { +function toggleCurrentSelection(): void { + if (selectedReferencedNodeIds.value.length > 0) { + const selected = new Set(selectedIds.value) + referencedNodeIds.value = referencedNodeIds.value.filter((id) => !selected.has(id)) + return + } referencedNodeIds.value = resolveReferencedNodes(editor.graph, [ ...referencedNodeIds.value, ...selectedIds.value ]).map((node) => node.id) } -function referencedNodeIcon(node: ReferencedNode) { - const liveNode = editor.graph.getNode(node.id) - return liveNode ? nodeIcon(liveNode) : undefined -} - function handleSubmit(e: Event) { e.preventDefault() const text = input.value.trim() @@ -190,29 +196,25 @@ function handleSubmit(e: Event) {