Browse Source

Preserve collapsed composer action targets

The collapsed composer should expand from the surrounding summary area, but the model label and generate button are explicit action targets. This keeps model selection and generation isolated while retaining one-click expansion from the remaining collapsed surface.

Constraint: Red-marked collapsed controls have dedicated product actions
Rejected: Treat the whole collapsed summary as one expand button | steals the model selector action
Confidence: high
Scope-risk: narrow
Directive: Do not bind collapsed-container expansion over buttons with dedicated actions
Tested: npm run test:run -- src/components/__tests__/GenerationComposer.test.tsx
Tested: npm run build
Not-tested: npm run lint | existing script calls unsupported next lint under Next 16
Not-tested: npx tsc --noEmit | existing unrelated test type errors remain
feature/canvas-chatbot-copilot
jiajia 2 months ago
parent
commit
550faa911f
  1. 24
      src/components/__tests__/GenerationComposer.test.tsx
  2. 39
      src/components/composer/GenerationComposer.tsx

24
src/components/__tests__/GenerationComposer.test.tsx

@ -332,6 +332,30 @@ describe("GenerationComposer", () => {
expect(screen.getByDisplayValue("visible prompt")).toBeInTheDocument();
});
it("opens model selection from the collapsed model label without expanding", () => {
useWorkflowStore.setState({ nodes: [imageNode("img-1", true, { inputPrompt: "visible prompt" })] });
render(<GenerationComposer />);
fireEvent.click(screen.getByLabelText("Collapse composer"));
fireEvent.click(screen.getByLabelText("Select model"));
expect(screen.getByTestId("model-dialog")).toHaveAttribute("data-filter", "image");
expect(screen.queryByDisplayValue("visible prompt")).not.toBeInTheDocument();
});
it("submits from the collapsed generate button without expanding", async () => {
useWorkflowStore.setState({ nodes: [imageNode("img-1", true, { inputPrompt: "visible prompt" })] });
render(<GenerationComposer />);
fireEvent.click(screen.getByLabelText("Collapse composer"));
fireEvent.click(screen.getByLabelText("Generate"));
await waitFor(() => {
expect(useWorkflowStore.getState().regenerateNode).toHaveBeenCalledWith("img-1");
});
expect(screen.queryByDisplayValue("visible prompt")).not.toBeInTheDocument();
});
it("uses the video model filter and reruns the selected video node", async () => {
useWorkflowStore.setState({ nodes: [videoNode("vid-1", true)] });
render(<GenerationComposer />);

39
src/components/composer/GenerationComposer.tsx

@ -985,31 +985,50 @@ export function GenerationComposer() {
/>
{isComposerCollapsed ? (
<div className="flex h-16 cursor-pointer items-center gap-3 px-4" onClick={handleCollapsedComposerClick}>
<div className="min-w-0 flex-1">
<span className="flex min-w-0 items-center gap-2">
<button
type="button"
onClick={expandComposer}
title={t("composer.expand")}
className="min-w-0 flex-1 rounded-lg text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50"
className="shrink-0 rounded text-left text-[11px] text-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50"
>
<span className="flex min-w-0 items-center gap-2">
<span className="shrink-0 text-[11px] text-neutral-500">{modeLabel}</span>
{modeLabel}
</button>
<span className="h-3 w-px shrink-0 bg-neutral-700" />
{isProcessNode ? (
<span className="truncate text-sm text-neutral-300">{collapsedStatus}</span>
<button
type="button"
onClick={expandComposer}
title={t("composer.expand")}
className="min-w-0 truncate rounded text-left text-sm text-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50"
>
{collapsedStatus}
</button>
) : (
<span
className={`min-w-0 truncate text-sm ${canChooseModel ? "text-neutral-100" : "text-neutral-500"}`}
<button
type="button"
onClick={() => canChooseModel && setModelDialogOpen(true)}
disabled={!canChooseModel}
aria-label={t("composer.selectModel")}
className="min-w-0 truncate rounded-md text-left text-sm text-neutral-100 transition-colors hover:text-cyan-200 disabled:text-neutral-500"
title={t("composer.selectModelTitle", { model: getComposerModelLabel(draft.selectedModel) })}
>
{getComposerModelLabel(draft.selectedModel)}
</span>
</button>
)}
</span>
{!isProcessNode && (
<span className="mt-1 block truncate text-xs text-neutral-500">
<button
type="button"
onClick={expandComposer}
title={t("composer.expand")}
className="mt-1 block max-w-full truncate rounded text-left text-xs text-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/50"
>
{collapsedStatus}
</span>
)}
</button>
)}
</div>
<button
type="button"

Loading…
Cancel
Save