保留聊天草稿中的附件引用节点
为 agc_attachment_reference 增加 Lexical 附件节点与可移除 chip。 恢复初始 canonical content 时不再静默丢弃附件,并让编辑器读回完整附件 part。
This commit is contained in:
@@ -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 (
|
||||
<span
|
||||
className="resource-reference-chip resource-attachment-chip"
|
||||
data-attachment-reference="true"
|
||||
data-attachment-status={attachment.status}
|
||||
contentEditable={false}
|
||||
title={`${attachment.name} · ${attachment.mediaType}`}
|
||||
>
|
||||
<Paperclip size={12} aria-hidden="true" />
|
||||
<span className="resource-reference-chip-label">{attachment.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`移除附件 ${attachment.name}`}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
editor.update(() => {
|
||||
const node = $getNodeByKey(nodeKey);
|
||||
node?.remove();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X size={11} aria-hidden="true" />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
+107
@@ -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<ReactNode> {
|
||||
__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 (
|
||||
<AttachmentReferenceChip
|
||||
attachment={this.__attachment}
|
||||
nodeKey={this.__key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function $createAttachmentReferenceNode(
|
||||
attachment: DirectCodexUserAttachmentReferencePart,
|
||||
) {
|
||||
return $applyNodeReplacement(new AttachmentReferenceNode(attachment));
|
||||
}
|
||||
|
||||
export function $isAttachmentReferenceNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
): node is AttachmentReferenceNode {
|
||||
return node instanceof AttachmentReferenceNode;
|
||||
}
|
||||
+14
-1
@@ -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 (
|
||||
<RichTextInput
|
||||
namespace="agc-resource-reference-input"
|
||||
nodes={[ResourceReferenceNode]}
|
||||
nodes={[ResourceReferenceNode, AttachmentReferenceNode]}
|
||||
containerRef={rootRef}
|
||||
containerClassName={`resource-reference-input${props.multiline ? '' : ' is-single-line'}`}
|
||||
disabled={props.disabled}
|
||||
|
||||
Reference in New Issue
Block a user