From 7281d4f593f278dfcc437b439e06d498d98bd975 Mon Sep 17 00:00:00 2001 From: jiajia Date: Fri, 29 May 2026 15:30:08 +0800 Subject: [PATCH] Let text nodes carry imported writing briefs Prompt nodes now accept TXT, Markdown, and DOCX uploads while preserving the node contract as plain prompt text. The canvas preview shows imported Markdown as a lightweight document card, and DOCX extraction stays local to body text so generation inputs remain deterministic. Constraint: Text-class nodes must still output plain text into existing workflow edges. Rejected: Add a full document editor/parser layer | too broad for the requested node-level upload and preview behavior. Confidence: high Scope-risk: moderate Directive: Keep uploaded document data normalized to prompt text unless downstream nodes explicitly gain rich document inputs. Tested: npm run test:run -- src/components/__tests__/PromptNode.test.tsx src/components/modals/__tests__/MarkdownEditorModal.test.tsx src/utils/__tests__/textDocumentImport.test.ts --reporter=dot Tested: npm run build Tested: git diff --check Not-tested: npm run lint still fails because Next 16 treats the existing next lint script as a /lint project path. --- src/components/__tests__/PromptNode.test.tsx | 35 +++++++- src/components/nodes/PromptNode.tsx | 90 ++++++++++++++++++- src/i18n/index.tsx | 12 +++ src/types/nodes.ts | 4 + .../__tests__/textDocumentImport.test.ts | 39 ++++++++ src/utils/textDocumentImport.ts | 84 +++++++++++++++++ 6 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 src/utils/__tests__/textDocumentImport.test.ts create mode 100644 src/utils/textDocumentImport.ts diff --git a/src/components/__tests__/PromptNode.test.tsx b/src/components/__tests__/PromptNode.test.tsx index b556f7d0..f00b3771 100644 --- a/src/components/__tests__/PromptNode.test.tsx +++ b/src/components/__tests__/PromptNode.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { PromptNode } from "@/components/nodes/PromptNode"; import { ReactFlowProvider } from "@xyflow/react"; @@ -108,6 +108,19 @@ describe("PromptNode", () => { expect(text).toHaveClass("break-words"); }); + it("should keep the selected preview surface draggable while not editing", () => { + render( + + + + ); + + const preview = screen.getByRole("button", { name: "Double-click to edit" }); + expect(preview).toHaveClass("cursor-move"); + expect(preview).not.toHaveClass("nodrag"); + expect(preview).not.toHaveClass("nopan"); + }); + it("should call updateNodeData when typing in textarea and blurring", () => { render( @@ -177,6 +190,26 @@ describe("PromptNode", () => { }); }); + it("should import a markdown file into the prompt node", async () => { + render( + + + + ); + + const input = screen.getByLabelText("Upload text document") as HTMLInputElement; + const file = new File(["# Uploaded\n\n- item"], "brief.md", { type: "text/markdown" }); + fireEvent.change(input, { target: { files: [file] } }); + + await waitFor(() => { + expect(mockUpdateNodeData).toHaveBeenCalledWith("test-prompt-1", { + prompt: "# Uploaded\n\n- item", + documentName: "brief.md", + documentFormat: "markdown", + }); + }); + }); + it("should hide unavailable rich text actions while editing", () => { render( diff --git a/src/components/nodes/PromptNode.tsx b/src/components/nodes/PromptNode.tsx index fc5620e6..3bdb7cdf 100644 --- a/src/components/nodes/PromptNode.tsx +++ b/src/components/nodes/PromptNode.tsx @@ -8,10 +8,12 @@ import { useWorkflowStore } from "@/store/workflowStore"; import { PromptNodeData } from "@/types"; import { useI18n } from "@/i18n"; import { MarkdownEditorModal } from "@/components/modals/MarkdownEditorModal"; +import { readTextDocumentFile } from "@/utils/textDocumentImport"; type PromptNodeType = Node; function getPromptNodeTitle(id: string, data: PromptNodeData, textLabel: string) { + if (data.documentName) return data.documentName; if (data.customTitle) return data.customTitle; if (data.label) return data.label; const match = id.match(/(\d+)$/); @@ -54,6 +56,41 @@ function ExpandIcon() { ); } +function UploadIcon() { + return ( + + + + ); +} + +function renderDocumentPreview(text: string) { + return text.split("\n").map((line, index) => { + const trimmed = line.trim(); + if (!trimmed) return + + {selected && !isEditing && (
+