From 26b15520d895651dc5c4ae7813b82d21329d2982 Mon Sep 17 00:00:00 2001 From: Luckyu_js <11670186+luckyu-js@user.noreply.gitee.com> Date: Thu, 16 Jul 2026 18:35:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=94=BE=E5=A4=A7=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/GenerationComposer.test.tsx | 65 ++-- .../composer/GenerationComposer.tsx | 368 ++++++++++-------- src/i18n/index.tsx | 8 +- 3 files changed, 236 insertions(+), 205 deletions(-) diff --git a/src/components/__tests__/GenerationComposer.test.tsx b/src/components/__tests__/GenerationComposer.test.tsx index 88f05107..54f52a38 100644 --- a/src/components/__tests__/GenerationComposer.test.tsx +++ b/src/components/__tests__/GenerationComposer.test.tsx @@ -70,8 +70,8 @@ vi.mock("@/i18n", () => { "composer.videoStitchReady": "Video stitch ready", "composer.videoStitchNeedTwo": "Connect at least 2 video clips to stitch.", "composer.describeContent": "Describe content", - "composer.expand": "Expand composer", - "composer.collapse": "Collapse composer", + "composer.enlarge": "Enlarge composer", + "composer.shrink": "Shrink composer", "composer.selectModel": "Select model", "composer.mainPlaceholder": "Describe what you want to generate, press / for commands, @ to reference assets", "composer.promptProvidedByUpstream": "Prompt provided by upstream", @@ -696,14 +696,6 @@ function getPromptEditor(expectedText?: string): HTMLDivElement { return editor; } -function queryPromptEditor(expectedText: string): HTMLDivElement | null { - return screen.queryAllByRole("textbox").find((element) => - element instanceof HTMLDivElement && - element.getAttribute("contenteditable") === "true" && - element.textContent === expectedText - ) as HTMLDivElement | undefined ?? null; -} - function setPromptEditorCaret(editor: HTMLDivElement, offset: number): void { const selection = window.getSelection(); if (!selection) return; @@ -738,12 +730,16 @@ function queryGenerateButton(): HTMLElement | null { return screen.queryByRole("button", { name: /Generate|生成/ }); } -function getCollapseButton(): HTMLElement { - return screen.getByRole("button", { name: /Collapse composer|收起底部栏/ }); +function getEnlargeButton(): HTMLElement { + return screen.getByRole("button", { name: /Enlarge composer|放大/ }); } -function getExpandButton(): HTMLElement { - return screen.getByRole("button", { name: /Expand composer|展开底部栏/ }); +function getShrinkButton(): HTMLElement { + const closeButton = document.querySelector(".ant-modal-close"); + if (!(closeButton instanceof HTMLElement)) { + throw new Error("Enlarged composer modal close button not found"); + } + return closeButton; } function getSelectModelButton(): HTMLElement { @@ -1262,40 +1258,52 @@ describe("GenerationComposer", () => { expect(screen.queryByTestId("node-inline-composer")).not.toBeInTheDocument(); }); - it("collapses and expands the bottom composer without losing the selected node context", () => { + it("enlarges and shrinks the composer without losing the selected node context", () => { useWorkflowStore.setState({ nodes: [imageNode("img-1", true, { inputPrompt: "visible prompt" })] }); render(); expectPromptEditorValue("visible prompt"); - fireEvent.click(getCollapseButton()); + fireEvent.click(getEnlargeButton()); - expect(queryPromptEditor("visible prompt")).not.toBeInTheDocument(); + // The node-anchored panel is replaced by the enlarged modal. + expect(screen.queryByTestId("node-inline-composer-panel")).not.toBeInTheDocument(); + // The same editor keeps its value inside the enlarged modal. + expectPromptEditorValue("visible prompt"); expect(screen.getByText("apiyi_nano_banana_2")).toBeInTheDocument(); - expect(screen.getByText("visible prompt")).toBeInTheDocument(); - fireEvent.click(getExpandButton()); + fireEvent.click(getShrinkButton()); + expect(screen.getByTestId("node-inline-composer-panel")).toBeInTheDocument(); expectPromptEditorValue("visible prompt"); }); - it("expands the collapsed composer when clicking the summary content", () => { + it("shrinks the enlarged composer when clicking outside the modal", () => { useWorkflowStore.setState({ nodes: [imageNode("img-1", true, { inputPrompt: "visible prompt" })] }); render(); - fireEvent.click(getCollapseButton()); - fireEvent.click(screen.getByText("visible prompt")); + fireEvent.click(getEnlargeButton()); + expect(screen.queryByTestId("node-inline-composer-panel")).not.toBeInTheDocument(); + const mask = document.querySelector(".ant-modal-wrap"); + if (!(mask instanceof HTMLElement)) { + throw new Error("Enlarged composer modal mask not found"); + } + // antd only closes on a full press+release that both land on the mask. + fireEvent.mouseDown(mask); + fireEvent.mouseUp(mask); + fireEvent.click(mask); + + expect(screen.getByTestId("node-inline-composer-panel")).toBeInTheDocument(); expectPromptEditorValue("visible prompt"); }); - it("opens model selection from the collapsed model label without expanding", () => { + it("opens model selection from inside the enlarged composer", () => { useWorkflowStore.setState({ nodes: [imageNode("img-1", true, { inputPrompt: "visible prompt" })] }); render(); - fireEvent.click(getCollapseButton()); + fireEvent.click(getEnlargeButton()); fireEvent.click(getSelectModelButton()); expect(screen.getByTestId("model-dialog")).toHaveAttribute("data-filter", "image"); - expect(queryPromptEditor("visible prompt")).not.toBeInTheDocument(); }); it("shows the concrete selected image model name in the composer", async () => { @@ -2148,15 +2156,12 @@ describe("GenerationComposer", () => { expect(fetchMock.mock.calls.some(([input]) => String(input).startsWith("/api/points/calculate-task-price"))).toBe(false); }); - it("expands from the collapsed summary without submitting", async () => { + it("enlarges the composer without submitting", async () => { useWorkflowStore.setState({ nodes: [imageNode("img-1", true, { inputPrompt: "visible prompt" })] }); render(); await act(async () => { - fireEvent.click(getCollapseButton()); - }); - await act(async () => { - fireEvent.click(screen.getByText("visible prompt")); + fireEvent.click(getEnlargeButton()); }); expect(useWorkflowStore.getState().regenerateNode).not.toHaveBeenCalled(); diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index 5ffff736..a68b8dea 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -419,7 +419,7 @@ function ComposerEditor({ const referenceMaterialsRef = useRef([]); const [config, setConfig] = useState(() => configFromDraft(initialDraft)); const [prompt, setPrompt] = useState(initialDraft.prompt); - const [isComposerCollapsed, setIsComposerCollapsed] = useState(false); + const [isEnlarged, setIsEnlarged] = useState(false); const [playingReferenceAudioKey, setPlayingReferenceAudioKey] = useState(null); const [priceState, setPriceState] = useState<{ amount: number | null; isLoading: boolean }>({ amount: null, @@ -841,6 +841,7 @@ function ComposerEditor({ onPersistConfig(previous.context, buildGenerationConfigForContext(previous.context)); setConfig(configFromDraft(initialDraft)); setPrompt(initialDraft.prompt); + setIsEnlarged(false); referenceMaterialsRef.current = []; previousContextRef.current = { key: contextKey, context }; // The current local draft belongs to the previous composer identity. @@ -851,6 +852,7 @@ function ComposerEditor({ if (isHidden) { persistCurrentComposerConfig(); stopReferenceAudioPreview(); + setIsEnlarged(false); onHiddenPersisted?.(); return; } @@ -993,189 +995,213 @@ function ComposerEditor({ onPersistConfig(context, generationConfig); const { prompt: _prompt, ...nextConfig } = generationConfig; void onRun(context, prompt, nextConfig); + if (isEnlarged) setIsEnlarged(false); }, [ buildGenerationConfigForContext, context, isCurrentNodeRunning, + isEnlarged, onCancelRunning, onPersistConfig, onRun, prompt, ]); - return ( + // 放大态输入框固定 17 行高度,超出后出现滚动条;普通态沿用 2~4 行。 + const promptMinHeightPx = isEnlarged ? PROMPT_LINE_HEIGHT_PX * 17 : PROMPT_MIN_HEIGHT_PX; + const promptMaxHeightPx = isEnlarged ? PROMPT_LINE_HEIGHT_PX * 17 : PROMPT_MAX_HEIGHT_PX; + + const composerBody = ( <> - -
event.stopPropagation()} - onMouseDown={(event) => event.stopPropagation()} - onClick={(event) => event.stopPropagation()} - > -
- { - if (!isComposerCollapsed) persistCurrentComposerConfig(); - setIsComposerCollapsed((value) => !value); - }} - > - - {isComposerCollapsed ? : } - - + onRemoveEdge(edgeId)} + onReferenceMaterialDragStart={(event, material) => { + event.dataTransfer.effectAllowed = "copyMove"; + event.dataTransfer.setData("application/x-popiart-reference-material", getReferenceMaterialKey(material)); + }} + onReferenceMaterialDrop={handleReferenceMaterialDrop} + onRemoveReferenceMaterial={(material) => material.edgeId && onRemoveEdge(material.edgeId)} + onToggleReferenceAudio={handleReferenceAudioPreviewToggle} + onStopReferenceAudio={stopReferenceAudioPreview} + onVideoSubTypeChange={(subType) => { + if (context.mode !== "node-edit" || !context.node) return; + const nextConfig = { + ...config, + subType, + parameters: filterTaskRoutingParameters(config.parameters), + }; + setConfig(nextConfig); + updateNodeData(context.node.id, { + subType, + parameters: nextConfig.parameters, + } as Partial); + }} + promptInputRef={promptInputRef} + promptValue={prompt} + promptReadOnly={false} + promptMinHeightPx={promptMinHeightPx} + promptMaxHeightPx={promptMaxHeightPx} + referenceItems={mentionReferenceItems} + onDraftPatch={(patch) => { + const nextPrompt = typeof patch.prompt === "string" ? patch.prompt : prompt; + if (typeof patch.prompt === "string") { + setPrompt(patch.prompt); + } + if (patch.parameters) { + const nextParameters = patch.parameters; + const nextConfig = { + ...config, + parameters: nextParameters, + }; + setConfig(nextConfig); + } + }} + onSubmitShortcut={runAction} + bodyContent={processBody} + /> + + {errorMessage && ( +
+ {errorMessage} +
+ )} + +
+ {isProcessNode ? ( +
+ + {context.node?.type === "easeCurve" + ? ((connectedInputs?.videos.length ?? 0) >= 1 ? t("composer.easeCurveReady") : t("composer.easeCurveNoVideo")) + : ((connectedInputs?.videos.length ?? 0) >= 2 ? t("composer.videoStitchReady") : t("composer.videoStitchNeedTwo"))} +
- - {isComposerCollapsed ? ( - - ) : ( - <> - onRemoveEdge(edgeId)} - onReferenceMaterialDragStart={(event, material) => { - event.dataTransfer.effectAllowed = "copyMove"; - event.dataTransfer.setData("application/x-popiart-reference-material", getReferenceMaterialKey(material)); - }} - onReferenceMaterialDrop={handleReferenceMaterialDrop} - onRemoveReferenceMaterial={(material) => material.edgeId && onRemoveEdge(material.edgeId)} - onToggleReferenceAudio={handleReferenceAudioPreviewToggle} - onStopReferenceAudio={stopReferenceAudioPreview} - onVideoSubTypeChange={(subType) => { - if (context.mode !== "node-edit" || !context.node) return; - const nextConfig = { - ...config, - subType, - parameters: filterTaskRoutingParameters(config.parameters), - }; - setConfig(nextConfig); - updateNodeData(context.node.id, { - subType, - parameters: nextConfig.parameters, - } as Partial); - }} - promptInputRef={promptInputRef} - promptValue={prompt} - promptReadOnly={false} - promptMinHeightPx={PROMPT_MIN_HEIGHT_PX} - promptMaxHeightPx={PROMPT_MAX_HEIGHT_PX} - referenceItems={mentionReferenceItems} - onDraftPatch={(patch) => { - const nextPrompt = typeof patch.prompt === "string" ? patch.prompt : prompt; - if (typeof patch.prompt === "string") { - setPrompt(patch.prompt); - } - if (patch.parameters) { - const nextParameters = patch.parameters; - const nextConfig = { - ...config, - parameters: nextParameters, - }; - setConfig(nextConfig); - } - }} - onSubmitShortcut={runAction} - bodyContent={processBody} - /> - - {errorMessage && ( -
- {errorMessage} -
- )} - -
- {isProcessNode ? ( -
- - {context.node?.type === "easeCurve" - ? ((connectedInputs?.videos.length ?? 0) >= 1 ? t("composer.easeCurveReady") : t("composer.easeCurveNoVideo")) - : ((connectedInputs?.videos.length ?? 0) >= 2 ? t("composer.videoStitchReady") : t("composer.videoStitchNeedTwo"))} - -
- ) : ( - { - if (context.mode === "node-edit" && context.node) onApplyNodeSize(context.node.id, value); - }} - getPopupContainer={getComposerModelPopupContainer} - selectModelPlaceholder={t("composer.selectModel")} - showLlmModelConfig={false} - llmModelConfigContent={null} - llmModelConfigSummary="" - /> - )} - -
- {priceLabel ? ( -
- - {priceLabel} -
- ) : null} - -
-
- - )} -
-
+ + {priceLabel} + + ) : null} + + + ); + + if (isEnlarged) { + return ( + { + persistCurrentComposerConfig(); + setIsEnlarged(false); + }} + footer={null} + width={880} + centered + mask={{ closable: true }} + destroyOnHidden + getContainer={() => document.body} + title={null} + closable + closeIcon={ + + + + + + } + styles={{ body: { padding: 0 } }} + className="composer-enlarged-modal" + > +
{composerBody}
+
+ ); + } + + return ( + +
event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onClick={(event) => event.stopPropagation()} + > +
+ { + persistCurrentComposerConfig(); + setIsEnlarged(true); + }} + > + + + + +
+ + {composerBody} +
+
+ ); } diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index 8c2d1bb5..1fdc952c 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -783,8 +783,8 @@ const en = { "composer.describeContent": "Describe what you want to generate", "composer.mainPlaceholder": "Describe what you want to generate, press / for commands, @ to reference assets", "composer.uploadReferenceImage": "Upload reference image", - "composer.expand": "Expand composer", - "composer.collapse": "Collapse composer", + "composer.enlarge": "Enlarge composer", + "composer.shrink": "Shrink composer", "composer.style": "Style", "composer.mark": "Mark", "composer.focus": "Focus", @@ -1854,8 +1854,8 @@ const zhCN: Dictionary = { "composer.describeContent": "描述你想生成的内容", "composer.mainPlaceholder": "描述你想生成的内容,按@ 引用素材", "composer.uploadReferenceImage": "上传参考图片", - "composer.expand": "展开底部栏", - "composer.collapse": "收起底部栏", + "composer.enlarge": "放大", + "composer.shrink": "缩小", "composer.style": "风格", "composer.mark": "标记", "composer.focus": "聚焦",