From d8af0fa882ffb7ccc5c8b53bf140b5753fc7b777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 18 Sep 2026 13:17:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E7=95=99=E8=81=8A=E5=A4=A9=E8=8D=89?= =?UTF-8?q?=E7=A8=BF=E4=B8=AD=E7=9A=84=E9=99=84=E4=BB=B6=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 agc_attachment_reference 增加 Lexical 附件节点与可移除 chip。 恢复初始 canonical content 时不再静默丢弃附件,并让编辑器读回完整附件 part。 --- .../AttachmentReferenceChip.tsx | 40 +++++++ .../AttachmentReferenceNode.tsx | 107 ++++++++++++++++++ .../ResourceReferenceInput.tsx | 15 ++- 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceChip.tsx create mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceNode.tsx diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceChip.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceChip.tsx new file mode 100644 index 000000000..e533d6220 --- /dev/null +++ b/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceChip.tsx @@ -0,0 +1,40 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { $getNodeByKey, type NodeKey } from 'lexical'; +import { Paperclip, X } from 'lucide-react'; + +import type { DirectCodexUserAttachmentReferencePart } from './generated'; + +export function AttachmentReferenceChip({ + attachment, + nodeKey, +}: { + attachment: DirectCodexUserAttachmentReferencePart; + nodeKey: NodeKey; +}) { + const [editor] = useLexicalComposerContext(); + return ( + + + ); +} diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceNode.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceNode.tsx new file mode 100644 index 000000000..71b77dc85 --- /dev/null +++ b/apps/ai-game-creator-shell/src/features/project-workspace/AttachmentReferenceNode.tsx @@ -0,0 +1,107 @@ +import { + $applyNodeReplacement, + DecoratorNode, + type EditorConfig, + type LexicalNode, + type NodeKey, + type SerializedLexicalNode, + type Spread, +} from 'lexical'; +import type { ReactNode } from 'react'; + +import { AttachmentReferenceChip } from './AttachmentReferenceChip'; +import type { DirectCodexUserAttachmentReferencePart } from './generated'; + +export type SerializedAttachmentReferenceNode = Spread< + { + attachment: DirectCodexUserAttachmentReferencePart; + type: 'attachment-reference'; + version: 1; + }, + SerializedLexicalNode +>; + +/** 附件在纯文本里的唯一占位字符:一个附件 chip 就当一个字符。 */ +const OBJECT_REPLACEMENT = '\uFFFC'; + +/** + * Inline canonical attachment part rendered inside the Lexical composer. + * The node stores the complete part so read-back preserves the project path and status. + */ +export class AttachmentReferenceNode extends DecoratorNode { + __attachment: DirectCodexUserAttachmentReferencePart; + + static getType() { + return 'attachment-reference'; + } + + static clone(node: AttachmentReferenceNode) { + return new AttachmentReferenceNode(node.__attachment, node.__key); + } + + static importJSON(serializedNode: SerializedAttachmentReferenceNode) { + return $createAttachmentReferenceNode(serializedNode.attachment); + } + + constructor( + attachment: DirectCodexUserAttachmentReferencePart, + key?: NodeKey, + ) { + super(key); + this.__attachment = attachment; + } + + exportJSON(): SerializedAttachmentReferenceNode { + return { + ...super.exportJSON(), + attachment: this.__attachment, + type: 'attachment-reference', + version: 1, + }; + } + + createDOM(_config: EditorConfig) { + return document.createElement('span'); + } + + updateDOM() { + return false; + } + + getTextContent() { + return OBJECT_REPLACEMENT; + } + + getTextContentSize() { + return OBJECT_REPLACEMENT.length; + } + + isInline() { + return true; + } + + isKeyboardSelectable() { + return true; + } + + decorate() { + return ( + + ); + } +} + +export function $createAttachmentReferenceNode( + attachment: DirectCodexUserAttachmentReferencePart, +) { + return $applyNodeReplacement(new AttachmentReferenceNode(attachment)); +} + +export function $isAttachmentReferenceNode( + node: LexicalNode | null | undefined, +): node is AttachmentReferenceNode { + return node instanceof AttachmentReferenceNode; +} diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx index 033931eb8..7a8759cb9 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx @@ -61,6 +61,11 @@ import { createProjectResourcePreviewRequestId, createProjectResourcePreviewScopeId, } from '../../services/projectResourcePreviewTransport'; +import { + $createAttachmentReferenceNode, + $isAttachmentReferenceNode, + AttachmentReferenceNode, +} from './AttachmentReferenceNode'; import { chatPromptDraftKey, readChatPromptPolishReminderDisabled, @@ -199,6 +204,10 @@ function collectDraftParts( content.push({ type: 'input_text', text: '\n' }); return; } + if ($isAttachmentReferenceNode(node)) { + content.push({ type: 'agc_attachment_reference', ...node.__attachment }); + return; + } if ($isResourceReferenceNode(node)) { references.push(node.__reference); content.push(chatReferenceToContentPart(node.__reference)); @@ -422,6 +431,10 @@ function applyContentToRoot( }); return; } + if (part.type === 'agc_attachment_reference') { + paragraph.append($createAttachmentReferenceNode(part)); + return; + } const reference = referenceFromContentPart(part, assetsById); if (reference) { paragraph.append($createResourceReferenceNode(reference)); @@ -1442,7 +1455,7 @@ export const ResourceReferenceInput = forwardRef< return (