diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index fe040c616..07d1edbc3 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -734,15 +734,23 @@ export function App({ ); } - const [chatInput, setChatInput] = useState(() => - supervisorChatOnly && initialProjectPath - ? readSupervisorChatDraft(initialProjectPath) - : '', - ); - const [chatReferences, setChatReferences] = useState([]); - const [chatContent, setChatContent] = useState( - [], - ); + const [chatDraft, setChatDraft] = useState(() => ({ + text: + supervisorChatOnly && initialProjectPath + ? readSupervisorChatDraft(initialProjectPath) + : '', + references: [], + content: [], + })); + const chatInput = chatDraft.text; + const chatReferences = chatDraft.references; + const chatContent = chatDraft.content ?? []; + const setChatInput = (text: string) => + setChatDraft((current) => ({ ...current, text })); + const setChatReferences = (references: ChatReference[]) => + setChatDraft((current) => ({ ...current, references })); + const setChatContent = (content: DirectCodexUserContentPart[]) => + setChatDraft((current) => ({ ...current, content })); const chatComposerRef = useRef(null); const [chatAgentBusy, setChatAgentBusy] = useState(false); const [directCodexProgress, setDirectCodexProgress] = useState(''); @@ -3937,9 +3945,7 @@ export function App({ } function handleChatComposerChange(draft: ChatComposerDraft) { - setChatInput(draft.text); - setChatReferences(draft.references); - setChatContent(draft.content ?? []); + setChatDraft(draft); } useEffect(() => { diff --git a/apps/ai-game-creator-shell/src/components/RichTextInput.tsx b/apps/ai-game-creator-shell/src/components/RichTextInput.tsx new file mode 100644 index 000000000..02c942e50 --- /dev/null +++ b/apps/ai-game-creator-shell/src/components/RichTextInput.tsx @@ -0,0 +1,102 @@ +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { + COMMAND_PRIORITY_HIGH, + type EditorState, + KEY_ENTER_COMMAND, + type Klass, + type LexicalNode, +} from 'lexical'; +import type { ReactElement, ReactNode, Ref } from 'react'; +import { useEffect } from 'react'; + +type RichTextInputProps = { + namespace: string; + nodes: Klass[]; + initialEditorState?: EditorState | null; + contentEditable?: ReactElement; + placeholder?: ReactElement; + containerClassName?: string; + containerRef?: Ref; + disabled?: boolean; + onChange?: (editorState: EditorState) => void; + onEnter?: () => void; + children?: ReactNode; +}; + +function SubmitOnEnter({ onEnter }: { onEnter?: () => void }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!onEnter) return undefined; + return editor.registerCommand( + KEY_ENTER_COMMAND, + (event) => { + if (!event || event.shiftKey || event.isComposing) return false; + event.preventDefault(); + onEnter(); + return true; + }, + COMMAND_PRIORITY_HIGH, + ); + }, [editor, onEnter]); + + return null; +} + +function SetEditorEditable({ disabled }: { disabled: boolean }) { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + editor.setEditable(!disabled); + }, [disabled, editor]); + + return null; +} + +export default function RichTextInput({ + namespace, + nodes, + initialEditorState, + contentEditable = , + placeholder, + containerClassName, + containerRef, + disabled = false, + onChange, + onEnter, + children, +}: RichTextInputProps) { + return ( + { + throw error; + }, + }} + > +
+ + {children} + + + {onChange ? : null} +
+
+ ); +} 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 473ba4f9c..815a20598 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 @@ -1,9 +1,6 @@ -import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; -import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; -import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { LexicalTypeaheadMenuPlugin, MenuOption, @@ -58,6 +55,7 @@ import { type GameIterationVersion, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import { resolveTauriInvoke } from '../../app/tauri'; +import RichTextInput from '../../components/RichTextInput'; import { cancelLocalProjectResourcePreviewScope, createProjectResourcePreviewRequestId, @@ -175,10 +173,16 @@ function referenceListKey(references: ChatReference[]) { return chatReferenceListKey(references); } -function sameDraft(left: ChatComposerDraft, right: ChatComposerDraft) { +function sameDraftValue(left: ChatComposerDraft, right: ChatComposerDraft) { return ( left.text === right.text && - referenceListKey(left.references) === referenceListKey(right.references) && + referenceListKey(left.references) === referenceListKey(right.references) + ); +} + +function sameCanonicalDraft(left: ChatComposerDraft, right: ChatComposerDraft) { + return ( + sameDraftValue(left, right) && JSON.stringify(left.content ?? []) === JSON.stringify(right.content ?? []) ); } @@ -453,16 +457,14 @@ function ResourceReferenceEditor({ activeVersionId = null, versions, disabled, - placeholder, - ariaLabel, multiline, - rows, showTriggerButton = true, showPolishAction = true, - inputRef, composerRef, + rootRef, }: ResourceReferenceInputProps & { composerRef?: Ref; + rootRef: RefObject; }) { const [editor] = useLexicalComposerContext(); const lastEmittedDraftRef = useRef({ @@ -491,8 +493,6 @@ function ResourceReferenceEditor({ bottom: number; width: number; } | null>(null); - const rootRef = useRef(null); - const assetsContentSignature = assetsSignature(assets); const versionsContentSignature = iterationsSignature(versions); const assetReferences = useMemo( @@ -610,7 +610,7 @@ function ResourceReferenceEditor({ text: value, references, }; - if (sameDraft(lastEmittedDraftRef.current, nextDraft)) { + if (sameDraftValue(lastEmittedDraftRef.current, nextDraft)) { return; } editor.update(() => { @@ -783,7 +783,7 @@ function ResourceReferenceEditor({ setReminderOpen(false); acknowledgedDraftKeyRef.current = chatPromptDraftKey(liveDraftRef.current); rootRef.current?.closest('form')?.requestSubmit(); - }, []); + }, [rootRef]); const useOriginalAndSubmit = useCallback(() => { clearPolishError(); @@ -831,7 +831,7 @@ function ResourceReferenceEditor({ }; form.addEventListener('submit', handleFormSubmit, true); return () => form.removeEventListener('submit', handleFormSubmit, true); - }, []); + }, [rootRef]); // 草稿发出去或被清空后重新开始一轮:清掉润色结果与「本轮已确认」标记。 useEffect(() => { @@ -906,7 +906,7 @@ function ResourceReferenceEditor({ document.body, ); }, - [], + [rootRef], ); const pickerReferences = useMemo(() => { @@ -937,7 +937,7 @@ function ResourceReferenceEditor({ bottom: Math.max(12, window.innerHeight - rect.top + 8), width, }); - }, []); + }, [rootRef]); useEffect(() => { if (!pickerOpen) { @@ -954,31 +954,7 @@ function ResourceReferenceEditor({ }, [pickerOpen, updatePickerPosition]); return ( -
- - } - placeholder={ - - {placeholder} - - } - ErrorBoundary={LexicalErrorBoundary} - /> + <>
{showTriggerButton ? (
+ ); } @@ -1285,17 +1261,33 @@ export const ResourceReferenceInput = forwardRef< ResourceReferenceInputHandle, ResourceReferenceInputProps >(function ResourceReferenceInput(props, ref) { + const rootRef = useRef(null); return ( - { - throw error; - }, - }} + + } + placeholder={ + + {props.placeholder} + + } > - - + + ); }); diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts b/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts index 3f8cada72..b6a2cc65d 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts @@ -115,26 +115,12 @@ export function chatReferenceToContentPart( } export function chatComposerDraftToDirectCodexUserItem( - draft: ChatComposerDraft, + draft: ChatComposerDraft & { content: DirectCodexUserContentPart[] }, id: string, ): DirectCodexUserItem { - let content: DirectCodexUserContentPart[]; - if (draft.content?.length) { - content = draft.content.filter( - (part) => part.type !== 'input_text' || part.text.trim().length > 0, - ); - } else if (draft.references.length > 0) { - content = [ - ...(draft.text - ? [{ type: 'input_text' as const, text: draft.text }] - : []), - ...draft.references.map(chatReferenceToContentPart), - ]; - } else { - content = draft.text - ? [{ type: 'input_text' as const, text: draft.text }] - : []; - } + const content = draft.content.filter( + (part) => part.type !== 'input_text' || part.text.trim().length > 0, + ); return { type: 'message', role: 'user', diff --git a/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/index.tsx b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/index.tsx index 1d627d7dc..76933a178 100644 --- a/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/index.tsx +++ b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/index.tsx @@ -1,9 +1,5 @@ -import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; -import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; -import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; -import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { readImage, readText } from '@tauri-apps/plugin-clipboard-manager'; import { $createParagraphNode, @@ -13,13 +9,13 @@ import { COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, createCommand, - KEY_ENTER_COMMAND, type LexicalCommand, PASTE_COMMAND, } from 'lexical'; import { Upload } from 'lucide-react'; import React, { useEffect, useRef } from 'react'; +import RichTextInput from '../../../../components/RichTextInput'; import type { Draft, HomeAttachmentDraft } from '../../useHomeDraftStore'; import { $createAttachmentNode, AttachmentNode } from './attachmentNode'; @@ -101,29 +97,9 @@ function selectEditableEndWhenNeeded() { root.selectEnd(); } -function EditorPlugins({ - onChange, - onEnter, -}: Pick) { +function EditorPlugins() { const [editor] = useLexicalComposerContext(); - useEffect( - () => - editor.registerCommand( - KEY_ENTER_COMMAND, - (event) => { - if (!event || event.shiftKey || event.isComposing) { - return false; - } - event.preventDefault(); - onEnter(); - return true; - }, - COMMAND_PRIORITY_HIGH, - ), - [editor, onEnter], - ); - useEffect( () => editor.registerCommand( @@ -188,13 +164,7 @@ function EditorPlugins({ [editor], ); - return ( - { - onChange(editorState); - }} - /> - ); + return null; } export function UploadButton() { @@ -229,34 +199,27 @@ export function UploadButton() { export default function RichInputArea(props: RichInputAreaProps) { return ( - { - throw error; - }, - }} - > -
- - } - placeholder={ - - {props.placeholder} - - } - ErrorBoundary={LexicalErrorBoundary} + - {props.children} -
- -
+ } + placeholder={ + + {props.placeholder} + + } + > + + {props.children} + ); } diff --git a/apps/ai-game-creator-shell/tests/resourceReferences.test.ts b/apps/ai-game-creator-shell/tests/resourceReferences.test.ts index ebcdf8794..b1dc12f27 100644 --- a/apps/ai-game-creator-shell/tests/resourceReferences.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceReferences.test.ts @@ -54,6 +54,10 @@ describe('DirectProject user Response item', () => { source: 'asset-picker', }, ], + content: [ + { type: 'input_text', text: '请使用素材' }, + { type: 'agc_resource_reference', resourceId: 'asset-hero' }, + ], }; expect(