Add PDF Parser node in frontend (#1604)
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||||
|
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||||
|
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||||
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { helpTooltips } from "../../helpContent";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
import { NodeActionMenu } from "../NodeActionMenu";
|
||||||
|
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||||
|
import { type PDFParserNode } from "./types";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { dataSchemaExampleForFileExtraction } from "../types";
|
||||||
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
|
|
||||||
|
function PDFParserNode({ id, data }: NodeProps<PDFParserNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const deleteNodeCallback = useDeleteNodeCallback();
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
fileUrl: data.fileUrl,
|
||||||
|
dataSchema: data.jsonSchema,
|
||||||
|
});
|
||||||
|
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||||
|
id,
|
||||||
|
initialValue: data.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleChange(key: string, value: unknown) {
|
||||||
|
if (!data.editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputs({ ...inputs, [key]: value });
|
||||||
|
updateNodeData(id, { [key]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="a"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="b"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||||
|
<div className="flex h-[2.75rem] justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||||
|
<WorkflowBlockIcon
|
||||||
|
workflowBlockType={WorkflowBlockTypes.PDFParser}
|
||||||
|
className="size-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<EditableNodeTitle
|
||||||
|
value={label}
|
||||||
|
editable={data.editable}
|
||||||
|
onChange={setLabel}
|
||||||
|
titleClassName="text-base"
|
||||||
|
inputClassName="text-base"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-400">PDF Parser Block</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NodeActionMenu
|
||||||
|
onDelete={() => {
|
||||||
|
deleteNodeCallback(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">File URL</Label>
|
||||||
|
<HelpTooltip content={helpTooltips["fileParser"]["fileUrl"]} />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={inputs.fileUrl}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!data.editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputs({ ...inputs, fileUrl: event.target.value });
|
||||||
|
updateNodeData(id, { fileUrl: event.target.value });
|
||||||
|
}}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">Data Schema</Label>
|
||||||
|
<HelpTooltip content={helpTooltips["task"]["dataSchema"]} />
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
checked={inputs.dataSchema !== "null"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleChange(
|
||||||
|
"dataSchema",
|
||||||
|
checked
|
||||||
|
? JSON.stringify(
|
||||||
|
dataSchemaExampleForFileExtraction,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
: "null",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{inputs.dataSchema !== "null" && (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
value={inputs.dataSchema}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleChange("dataSchema", value);
|
||||||
|
}}
|
||||||
|
className="nowheel nopan"
|
||||||
|
fontSize={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PDFParserNode };
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Node } from "@xyflow/react";
|
||||||
|
import { NodeBaseData } from "../types";
|
||||||
|
|
||||||
|
export type PDFParserNodeData = NodeBaseData & {
|
||||||
|
fileUrl: string;
|
||||||
|
jsonSchema: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PDFParserNode = Node<PDFParserNodeData, "pdfParser">;
|
||||||
|
|
||||||
|
export const pdfParserNodeDefaultData: PDFParserNodeData = {
|
||||||
|
editable: true,
|
||||||
|
label: "",
|
||||||
|
fileUrl: "",
|
||||||
|
continueOnFailure: false,
|
||||||
|
jsonSchema: "null",
|
||||||
|
} as const;
|
||||||
@@ -67,6 +67,9 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) {
|
|||||||
case "wait": {
|
case "wait": {
|
||||||
return <StopwatchIcon className={className} />;
|
return <StopwatchIcon className={className} />;
|
||||||
}
|
}
|
||||||
|
case "pdf_parser": {
|
||||||
|
return <CursorTextIcon className={className} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import { WaitNode } from "./WaitNode/types";
|
|||||||
import { WaitNode as WaitNodeComponent } from "./WaitNode/WaitNode";
|
import { WaitNode as WaitNodeComponent } from "./WaitNode/WaitNode";
|
||||||
import { FileDownloadNode } from "./FileDownloadNode/types";
|
import { FileDownloadNode } from "./FileDownloadNode/types";
|
||||||
import { FileDownloadNode as FileDownloadNodeComponent } from "./FileDownloadNode/FileDownloadNode";
|
import { FileDownloadNode as FileDownloadNodeComponent } from "./FileDownloadNode/FileDownloadNode";
|
||||||
|
import { PDFParserNode } from "./PDFParserNode/types";
|
||||||
|
import { PDFParserNode as PDFParserNodeComponent } from "./PDFParserNode/PDFParserNode";
|
||||||
|
|
||||||
export type UtilityNode = StartNode | NodeAdderNode;
|
export type UtilityNode = StartNode | NodeAdderNode;
|
||||||
|
|
||||||
@@ -51,7 +53,8 @@ export type WorkflowBlockNode =
|
|||||||
| ExtractionNode
|
| ExtractionNode
|
||||||
| LoginNode
|
| LoginNode
|
||||||
| WaitNode
|
| WaitNode
|
||||||
| FileDownloadNode;
|
| FileDownloadNode
|
||||||
|
| PDFParserNode;
|
||||||
|
|
||||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||||
return node.type === "nodeAdder" || node.type === "start";
|
return node.type === "nodeAdder" || node.type === "start";
|
||||||
@@ -81,4 +84,5 @@ export const nodeTypes = {
|
|||||||
login: memo(LoginNodeComponent),
|
login: memo(LoginNodeComponent),
|
||||||
wait: memo(WaitNodeComponent),
|
wait: memo(WaitNodeComponent),
|
||||||
fileDownload: memo(FileDownloadNodeComponent),
|
fileDownload: memo(FileDownloadNodeComponent),
|
||||||
|
pdfParser: memo(PDFParserNodeComponent),
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ export const dataSchemaExampleValue = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const dataSchemaExampleForFileExtraction = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
extracted_information: {
|
||||||
|
type: "object",
|
||||||
|
description: "All of the information extracted from the file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const workflowBlockTitle: {
|
export const workflowBlockTitle: {
|
||||||
[blockType in WorkflowBlockType]: string;
|
[blockType in WorkflowBlockType]: string;
|
||||||
} = {
|
} = {
|
||||||
@@ -35,4 +45,5 @@ export const workflowBlockTitle: {
|
|||||||
upload_to_s3: "Upload",
|
upload_to_s3: "Upload",
|
||||||
validation: "Validation",
|
validation: "Validation",
|
||||||
wait: "Wait",
|
wait: "Wait",
|
||||||
|
pdf_parser: "PDF Parser",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -121,6 +121,17 @@ const nodeLibraryItems: Array<{
|
|||||||
title: "File Parser Block",
|
title: "File Parser Block",
|
||||||
description: "Downloads and parses a file",
|
description: "Downloads and parses a file",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
nodeType: "pdfParser",
|
||||||
|
icon: (
|
||||||
|
<WorkflowBlockIcon
|
||||||
|
workflowBlockType={WorkflowBlockTypes.PDFParser}
|
||||||
|
className="size-6"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "PDF Parser Block",
|
||||||
|
description: "Downloads and parses a PDF file with an optional data schema",
|
||||||
|
},
|
||||||
// disabled
|
// disabled
|
||||||
// {
|
// {
|
||||||
// nodeType: "download",
|
// nodeType: "download",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
LoginBlockYAML,
|
LoginBlockYAML,
|
||||||
WaitBlockYAML,
|
WaitBlockYAML,
|
||||||
FileDownloadBlockYAML,
|
FileDownloadBlockYAML,
|
||||||
|
PDFParserBlockYAML,
|
||||||
} from "../types/workflowYamlTypes";
|
} from "../types/workflowYamlTypes";
|
||||||
import {
|
import {
|
||||||
EMAIL_BLOCK_SENDER,
|
EMAIL_BLOCK_SENDER,
|
||||||
@@ -84,6 +85,7 @@ import { loginNodeDefaultData } from "./nodes/LoginNode/types";
|
|||||||
import { waitNodeDefaultData } from "./nodes/WaitNode/types";
|
import { waitNodeDefaultData } from "./nodes/WaitNode/types";
|
||||||
import { fileDownloadNodeDefaultData } from "./nodes/FileDownloadNode/types";
|
import { fileDownloadNodeDefaultData } from "./nodes/FileDownloadNode/types";
|
||||||
import { ProxyLocation } from "@/api/types";
|
import { ProxyLocation } from "@/api/types";
|
||||||
|
import { pdfParserNodeDefaultData } from "./nodes/PDFParserNode/types";
|
||||||
|
|
||||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||||
|
|
||||||
@@ -394,6 +396,19 @@ function convertToNode(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "pdf_parser": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "pdfParser",
|
||||||
|
data: {
|
||||||
|
...commonData,
|
||||||
|
fileUrl: block.file_url,
|
||||||
|
jsonSchema: JSON.stringify(block.json_schema, null, 2),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case "download_to_s3": {
|
case "download_to_s3": {
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
@@ -788,6 +803,17 @@ function createNode(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "pdfParser": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "pdfParser",
|
||||||
|
data: {
|
||||||
|
...pdfParserNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1020,6 +1046,14 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
|||||||
parameter_keys: node.data.parameterKeys,
|
parameter_keys: node.data.parameterKeys,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "pdfParser": {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
block_type: "pdf_parser",
|
||||||
|
file_url: node.data.fileUrl,
|
||||||
|
json_schema: JSONParseSafe(node.data.jsonSchema),
|
||||||
|
};
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error("Invalid node type for getWorkflowBlock");
|
throw new Error("Invalid node type for getWorkflowBlock");
|
||||||
}
|
}
|
||||||
@@ -1642,6 +1676,15 @@ function convertBlocksToBlockYAML(
|
|||||||
};
|
};
|
||||||
return blockYaml;
|
return blockYaml;
|
||||||
}
|
}
|
||||||
|
case "pdf_parser": {
|
||||||
|
const blockYaml: PDFParserBlockYAML = {
|
||||||
|
...base,
|
||||||
|
block_type: "pdf_parser",
|
||||||
|
file_url: block.file_url,
|
||||||
|
json_schema: block.json_schema,
|
||||||
|
};
|
||||||
|
return blockYaml;
|
||||||
|
}
|
||||||
case "send_email": {
|
case "send_email": {
|
||||||
const blockYaml: SendEmailBlockYAML = {
|
const blockYaml: SendEmailBlockYAML = {
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ export type WorkflowBlock =
|
|||||||
| ExtractionBlock
|
| ExtractionBlock
|
||||||
| LoginBlock
|
| LoginBlock
|
||||||
| WaitBlock
|
| WaitBlock
|
||||||
| FileDownloadBlock;
|
| FileDownloadBlock
|
||||||
|
| PDFParserBlock;
|
||||||
|
|
||||||
export const WorkflowBlockTypes = {
|
export const WorkflowBlockTypes = {
|
||||||
Task: "task",
|
Task: "task",
|
||||||
@@ -172,6 +173,7 @@ export const WorkflowBlockTypes = {
|
|||||||
Login: "login",
|
Login: "login",
|
||||||
Wait: "wait",
|
Wait: "wait",
|
||||||
FileDownload: "file_download",
|
FileDownload: "file_download",
|
||||||
|
PDFParser: "pdf_parser",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function isTaskVariantBlock(item: {
|
export function isTaskVariantBlock(item: {
|
||||||
@@ -369,6 +371,12 @@ export type FileDownloadBlock = WorkflowBlockBase & {
|
|||||||
cache_actions: boolean;
|
cache_actions: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PDFParserBlock = WorkflowBlockBase & {
|
||||||
|
block_type: "pdf_parser";
|
||||||
|
file_url: string;
|
||||||
|
json_schema: Record<string, unknown> | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowDefinition = {
|
export type WorkflowDefinition = {
|
||||||
parameters: Array<Parameter>;
|
parameters: Array<Parameter>;
|
||||||
blocks: Array<WorkflowBlock>;
|
blocks: Array<WorkflowBlock>;
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ export type BlockYAML =
|
|||||||
| ExtractionBlockYAML
|
| ExtractionBlockYAML
|
||||||
| LoginBlockYAML
|
| LoginBlockYAML
|
||||||
| WaitBlockYAML
|
| WaitBlockYAML
|
||||||
| FileDownloadBlockYAML;
|
| FileDownloadBlockYAML
|
||||||
|
| PDFParserBlockYAML;
|
||||||
|
|
||||||
export type BlockYAMLBase = {
|
export type BlockYAMLBase = {
|
||||||
block_type: WorkflowBlockType;
|
block_type: WorkflowBlockType;
|
||||||
@@ -265,3 +266,9 @@ export type ForLoopBlockYAML = BlockYAMLBase & {
|
|||||||
loop_blocks: Array<BlockYAML>;
|
loop_blocks: Array<BlockYAML>;
|
||||||
loop_variable_reference: string | null;
|
loop_variable_reference: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PDFParserBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "pdf_parser";
|
||||||
|
file_url: string;
|
||||||
|
json_schema: Record<string, unknown> | null;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user