diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx
index 0e03f11d..0e79af78 100644
--- a/src/i18n/index.tsx
+++ b/src/i18n/index.tsx
@@ -521,6 +521,9 @@ const en = {
"prompt.link": "Link",
"prompt.table": "Table",
"prompt.expandEditor": "Expand editor",
+ "prompt.uploadDocument": "Upload text document",
+ "prompt.replaceDocument": "Replace text document",
+ "prompt.unsupportedDocument": "Unsupported text document. Use TXT, MD, or DOCX.",
"prompt.addVariable": "Add variable",
"prompt.setVariableName": "Set Variable Name",
"prompt.variableHelp": "Use this prompt as a variable in PromptConstructor nodes",
@@ -1340,6 +1343,9 @@ const zhCN: Dictionary = {
"prompt.link": "链接",
"prompt.table": "表格",
"prompt.expandEditor": "展开编辑器",
+ "prompt.uploadDocument": "上传文本文档",
+ "prompt.replaceDocument": "替换文本文档",
+ "prompt.unsupportedDocument": "格式不支持。请使用 TXT、MD 或 DOCX。",
"prompt.addVariable": "添加变量",
"prompt.setVariableName": "设置变量名",
"prompt.variableHelp": "将此提示词作为 PromptConstructor 节点中的变量使用",
@@ -2009,6 +2015,9 @@ const zhTW: Dictionary = {
"prompt.link": "連結",
"prompt.table": "表格",
"prompt.expandEditor": "展開編輯器",
+ "prompt.uploadDocument": "上傳文字文件",
+ "prompt.replaceDocument": "替換文字文件",
+ "prompt.unsupportedDocument": "格式不支援。請使用 TXT、MD 或 DOCX。",
"prompt.addVariable": "新增變數",
"prompt.setVariableName": "設定變數名稱",
"prompt.variableHelp": "將此提示詞作為 PromptConstructor 節點中的變數使用",
@@ -2581,6 +2590,9 @@ const ja: Dictionary = {
"prompt.link": "リンク",
"prompt.table": "表",
"prompt.expandEditor": "エディタを展開",
+ "prompt.uploadDocument": "テキスト文書をアップロード",
+ "prompt.replaceDocument": "テキスト文書を置換",
+ "prompt.unsupportedDocument": "未対応の文書形式です。TXT、MD、DOCX を使用してください。",
"prompt.addVariable": "変数を追加",
"prompt.setVariableName": "変数名を設定",
"prompt.variableHelp": "このプロンプトを PromptConstructor ノードの変数として使用します",
diff --git a/src/types/nodes.ts b/src/types/nodes.ts
index 0d2473d5..d54d738c 100644
--- a/src/types/nodes.ts
+++ b/src/types/nodes.ts
@@ -94,8 +94,12 @@ export interface VideoInputNodeData extends BaseNodeData {
/**
* Prompt node - text input for AI generation
*/
+export type PromptDocumentFormat = "text" | "markdown" | "docx";
+
export interface PromptNodeData extends BaseNodeData {
prompt: string;
+ documentName?: string;
+ documentFormat?: PromptDocumentFormat;
variableName?: string; // Optional variable name for use in PromptConstructor templates
isOptional?: boolean;
}
diff --git a/src/utils/__tests__/textDocumentImport.test.ts b/src/utils/__tests__/textDocumentImport.test.ts
new file mode 100644
index 00000000..6df4fafa
--- /dev/null
+++ b/src/utils/__tests__/textDocumentImport.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from "vitest";
+import JSZip from "jszip";
+import { readTextDocumentFile } from "../textDocumentImport";
+
+describe("readTextDocumentFile", () => {
+ it("reads markdown files as text documents", async () => {
+ const file = new File(["# Title\n\nBody"], "brief.md", { type: "text/markdown" });
+
+ await expect(readTextDocumentFile(file)).resolves.toEqual({
+ text: "# Title\n\nBody",
+ filename: "brief.md",
+ format: "markdown",
+ });
+ });
+
+ it("extracts plain text from docx document.xml", async () => {
+ const zip = new JSZip();
+ zip.file(
+ "word/document.xml",
+ `
+
+
+ Hello
+ World
+
+ `
+ );
+ const buffer = await zip.generateAsync({ type: "arraybuffer" });
+ const file = new File([buffer], "brief.docx", {
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ });
+
+ await expect(readTextDocumentFile(file)).resolves.toEqual({
+ text: "Hello\n\nWorld",
+ filename: "brief.docx",
+ format: "docx",
+ });
+ });
+});
diff --git a/src/utils/textDocumentImport.ts b/src/utils/textDocumentImport.ts
new file mode 100644
index 00000000..851b3dc6
--- /dev/null
+++ b/src/utils/textDocumentImport.ts
@@ -0,0 +1,84 @@
+import JSZip from "jszip";
+import type { PromptDocumentFormat } from "@/types";
+
+export interface ImportedTextDocument {
+ text: string;
+ filename: string;
+ format: PromptDocumentFormat;
+}
+
+function getFormat(filename: string, type: string): PromptDocumentFormat | null {
+ const lower = filename.toLowerCase();
+ if (lower.endsWith(".md") || lower.endsWith(".markdown") || type === "text/markdown") return "markdown";
+ if (lower.endsWith(".txt") || type === "text/plain") return "text";
+ if (
+ lower.endsWith(".docx") ||
+ type === "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+ ) {
+ return "docx";
+ }
+ return null;
+}
+
+async function readBlobText(file: File): Promise
{
+ if (typeof file.text === "function") return file.text();
+
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(String(reader.result || ""));
+ reader.onerror = () => reject(reader.error || new Error("Failed to read text file"));
+ reader.readAsText(file);
+ });
+}
+
+async function readBlobArrayBuffer(file: File): Promise {
+ if (typeof file.arrayBuffer === "function") return file.arrayBuffer();
+
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result as ArrayBuffer);
+ reader.onerror = () => reject(reader.error || new Error("Failed to read binary file"));
+ reader.readAsArrayBuffer(file);
+ });
+}
+
+function textContentByTag(element: Element, tagName: string): string {
+ return Array.from(element.getElementsByTagName(tagName))
+ .map((node) => node.textContent || "")
+ .join("");
+}
+
+async function readDocxText(file: File): Promise {
+ const zip = await JSZip.loadAsync(await readBlobArrayBuffer(file));
+ const documentXml = await zip.file("word/document.xml")?.async("text");
+ if (!documentXml) throw new Error("DOCX document body not found");
+
+ const xml = new DOMParser().parseFromString(documentXml, "application/xml");
+ const paragraphs = [
+ ...Array.from(xml.getElementsByTagName("w:p")),
+ ...Array.from(xml.getElementsByTagName("p")),
+ ];
+
+ return paragraphs
+ .map((paragraph) => {
+ const text = textContentByTag(paragraph, "w:t") || textContentByTag(paragraph, "t");
+ return text.trim();
+ })
+ .filter(Boolean)
+ .join("\n\n");
+}
+
+export async function readTextDocumentFile(file: File): Promise {
+ const format = getFormat(file.name, file.type);
+ if (!format) throw new Error("Unsupported text document. Use TXT, MD, or DOCX.");
+
+ const text = format === "docx"
+ ? await readDocxText(file)
+ : await readBlobText(file);
+
+ return {
+ text,
+ filename: file.name,
+ format,
+ };
+}