add extraction block json validation (#3499)
Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
This commit is contained in:
@@ -12,11 +12,12 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|||||||
import { getClient } from "@/api/AxiosClient";
|
import { getClient } from "@/api/AxiosClient";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
import { helpTooltips } from "@/routes/workflows/editor/helpContent";
|
import { helpTooltips } from "@/routes/workflows/editor/helpContent";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { AutoResizingTextarea } from "../AutoResizingTextarea/AutoResizingTextarea";
|
import { AutoResizingTextarea } from "../AutoResizingTextarea/AutoResizingTextarea";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { toast } from "../ui/use-toast";
|
import { toast } from "../ui/use-toast";
|
||||||
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -35,6 +36,42 @@ function WorkflowDataSchemaInputGroup({
|
|||||||
const [generateWithAIActive, setGenerateWithAIActive] = useState(false);
|
const [generateWithAIActive, setGenerateWithAIActive] = useState(false);
|
||||||
const [generateWithAIPrompt, setGenerateWithAIPrompt] = useState("");
|
const [generateWithAIPrompt, setGenerateWithAIPrompt] = useState("");
|
||||||
|
|
||||||
|
function computeJsonError(
|
||||||
|
jsonText: string,
|
||||||
|
): { message: string; line?: number; column?: number } | null {
|
||||||
|
try {
|
||||||
|
JSON.parse(jsonText);
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
const message = e instanceof Error ? e.message : "Invalid JSON";
|
||||||
|
// Try to extract position and compute line/column for friendlier feedback
|
||||||
|
const match = message.match(/position\s+(\d+)/i);
|
||||||
|
if (!match) {
|
||||||
|
return { message };
|
||||||
|
}
|
||||||
|
const pos = Number(match[1]);
|
||||||
|
if (Number.isNaN(pos)) {
|
||||||
|
return { message };
|
||||||
|
}
|
||||||
|
let line = 1;
|
||||||
|
let col = 1;
|
||||||
|
for (let i = 0; i < Math.min(pos, jsonText.length); i++) {
|
||||||
|
if (jsonText[i] === "\n") {
|
||||||
|
line += 1;
|
||||||
|
col = 1;
|
||||||
|
} else {
|
||||||
|
col += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { message, line, column: col };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonError = useMemo(() => {
|
||||||
|
if (value === "null") return null;
|
||||||
|
return computeJsonError(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const getDataSchemaSuggestionMutation = useMutation({
|
const getDataSchemaSuggestionMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const client = await getClient(credentialGetter);
|
const client = await getClient(credentialGetter);
|
||||||
@@ -121,13 +158,27 @@ function WorkflowDataSchemaInputGroup({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<CodeEditor
|
<div
|
||||||
language="json"
|
className={cn(
|
||||||
value={value}
|
"rounded-md",
|
||||||
onChange={onChange}
|
jsonError ? "ring-1 ring-red-500" : undefined,
|
||||||
className="nopan"
|
)}
|
||||||
fontSize={8}
|
>
|
||||||
/>
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
className="nopan"
|
||||||
|
fontSize={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{jsonError && (
|
||||||
|
<div className="text-xs text-red-400">
|
||||||
|
{jsonError.line && jsonError.column
|
||||||
|
? `Invalid JSON (${jsonError.line}:${jsonError.column}) — ${jsonError.message}`
|
||||||
|
: `Invalid JSON — ${jsonError.message}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ import {
|
|||||||
getWorkflowSettings,
|
getWorkflowSettings,
|
||||||
layout,
|
layout,
|
||||||
} from "./workflowEditorUtils";
|
} from "./workflowEditorUtils";
|
||||||
|
import { getWorkflowErrors } from "./workflowEditorUtils";
|
||||||
|
import { toast } from "@/components/ui/use-toast";
|
||||||
import { useAutoPan } from "./useAutoPan";
|
import { useAutoPan } from "./useAutoPan";
|
||||||
|
|
||||||
const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
@@ -373,8 +375,25 @@ function FlowRenderer({
|
|||||||
setGetSaveDataRef.current(constructSaveData);
|
setGetSaveDataRef.current(constructSaveData);
|
||||||
}, [constructSaveData]);
|
}, [constructSaveData]);
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave(): Promise<boolean> {
|
||||||
return await saveWorkflow.mutateAsync();
|
// Validate before saving; block if any workflow errors exist
|
||||||
|
const errors = getWorkflowErrors(nodes);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
toast({
|
||||||
|
title: "Can not save workflow because of errors:",
|
||||||
|
description: (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{errors.map((error) => (
|
||||||
|
<p key={error}>{error}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await saveWorkflow.mutateAsync();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteNode(id: string) {
|
function deleteNode(id: string) {
|
||||||
@@ -605,8 +624,10 @@ function FlowRenderer({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSave().then(() => {
|
handleSave().then((ok) => {
|
||||||
blocker.proceed?.();
|
if (ok) {
|
||||||
|
blocker.proceed?.();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={workflowChangesStore.saveIsPending}
|
disabled={workflowChangesStore.saveIsPending}
|
||||||
|
|||||||
@@ -2186,6 +2186,14 @@ function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
|
|||||||
} catch {
|
} catch {
|
||||||
errors.push(`${node.data.label}: Error messages is not valid JSON.`);
|
errors.push(`${node.data.label}: Error messages is not valid JSON.`);
|
||||||
}
|
}
|
||||||
|
// Validate Task data schema JSON when enabled (value different from "null")
|
||||||
|
if (node.data.dataSchema && node.data.dataSchema !== "null") {
|
||||||
|
try {
|
||||||
|
JSON.parse(node.data.dataSchema);
|
||||||
|
} catch {
|
||||||
|
errors.push(`${node.data.label}: Data schema is not valid JSON.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const validationNodes = nodes.filter(isValidationNode);
|
const validationNodes = nodes.filter(isValidationNode);
|
||||||
@@ -2217,6 +2225,14 @@ function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
|
|||||||
if (node.data.dataExtractionGoal.length === 0) {
|
if (node.data.dataExtractionGoal.length === 0) {
|
||||||
errors.push(`${node.data.label}: Data extraction goal is required.`);
|
errors.push(`${node.data.label}: Data extraction goal is required.`);
|
||||||
}
|
}
|
||||||
|
// Validate Extraction data schema JSON when enabled (value different from "null")
|
||||||
|
if (node.data.dataSchema && node.data.dataSchema !== "null") {
|
||||||
|
try {
|
||||||
|
JSON.parse(node.data.dataSchema);
|
||||||
|
} catch {
|
||||||
|
errors.push(`${node.data.label}: Data schema is not valid JSON.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const textPromptNodes = nodes.filter(isTextPromptNode);
|
const textPromptNodes = nodes.filter(isTextPromptNode);
|
||||||
|
|||||||
Reference in New Issue
Block a user