Move Run Workflow button to top of page (#4611)
Co-authored-by: Suchintan Singh <suchintan@skyvern.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
@@ -504,6 +504,60 @@ function RunWorkflowForm({
|
|||||||
onSubmit={form.handleSubmit(onSubmit, handleInvalid)}
|
onSubmit={form.handleSubmit(onSubmit, handleInvalid)}
|
||||||
className="space-y-8"
|
className="space-y-8"
|
||||||
>
|
>
|
||||||
|
<header className="flex items-end justify-between gap-4">
|
||||||
|
<div className="space-y-5">
|
||||||
|
<h1 className="text-3xl">
|
||||||
|
Parameters{workflow?.title ? ` - ${workflow.title}` : ""}
|
||||||
|
</h1>
|
||||||
|
<h2 className="text-lg text-slate-400">
|
||||||
|
Fill the placeholder values that you have linked throughout your
|
||||||
|
workflow.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 gap-2">
|
||||||
|
<CopyApiCommandDropdown
|
||||||
|
getOptions={() => {
|
||||||
|
const values = form.getValues();
|
||||||
|
const body = getRunWorkflowRequestBody(
|
||||||
|
values,
|
||||||
|
workflowParameters,
|
||||||
|
);
|
||||||
|
const transformedBody = transformToWorkflowRunRequest(
|
||||||
|
body,
|
||||||
|
workflowPermanentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build headers - x-max-steps-override is optional and can be added manually if needed
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": apiCredential ?? "<your-api-key>",
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: "POST",
|
||||||
|
url: `${runsApiBaseUrl}/run/workflows`,
|
||||||
|
body: transformedBody,
|
||||||
|
headers,
|
||||||
|
} satisfies ApiCommandOptions;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={
|
||||||
|
runWorkflowMutation.isPending || hasLoginBlockValidationError
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{runWorkflowMutation.isPending && (
|
||||||
|
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
{!runWorkflowMutation.isPending && (
|
||||||
|
<PlayIcon className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Run workflow
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{hasLoginBlockValidationError && (
|
{hasLoginBlockValidationError && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<ExclamationTriangleIcon className="h-4 w-4" />
|
<ExclamationTriangleIcon className="h-4 w-4" />
|
||||||
@@ -1051,49 +1105,6 @@ function RunWorkflowForm({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<CopyApiCommandDropdown
|
|
||||||
getOptions={() => {
|
|
||||||
const values = form.getValues();
|
|
||||||
const body = getRunWorkflowRequestBody(
|
|
||||||
values,
|
|
||||||
workflowParameters,
|
|
||||||
);
|
|
||||||
const transformedBody = transformToWorkflowRunRequest(
|
|
||||||
body,
|
|
||||||
workflowPermanentId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Build headers - x-max-steps-override is optional and can be added manually if needed
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-api-key": apiCredential ?? "<your-api-key>",
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
method: "POST",
|
|
||||||
url: `${runsApiBaseUrl}/run/workflows`,
|
|
||||||
body: transformedBody,
|
|
||||||
headers,
|
|
||||||
} satisfies ApiCommandOptions;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={
|
|
||||||
runWorkflowMutation.isPending || hasLoginBlockValidationError
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{runWorkflowMutation.isPending && (
|
|
||||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
)}
|
|
||||||
{!runWorkflowMutation.isPending && (
|
|
||||||
<PlayIcon className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Run workflow
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,22 +44,16 @@ function WorkflowRunParameters() {
|
|||||||
|
|
||||||
const initialValues = getInitialValues(location, workflowParameters ?? []);
|
const initialValues = getInitialValues(location, workflowParameters ?? []);
|
||||||
|
|
||||||
const header = (
|
|
||||||
<header className="space-y-5">
|
|
||||||
<h1 className="text-3xl">
|
|
||||||
Parameters{workflow?.title ? ` - ${workflow.title}` : ""}
|
|
||||||
</h1>
|
|
||||||
<h2 className="text-lg text-slate-400">
|
|
||||||
Fill the placeholder values that you have linked throughout your
|
|
||||||
workflow.
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{header}
|
<header className="space-y-5">
|
||||||
|
<h1 className="text-3xl">Parameters</h1>
|
||||||
|
<h2 className="text-lg text-slate-400">
|
||||||
|
Fill the placeholder values that you have linked throughout your
|
||||||
|
workflow.
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
<Skeleton className="h-96 w-full" />
|
<Skeleton className="h-96 w-full" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -70,26 +64,21 @@ function WorkflowRunParameters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<RunWorkflowForm
|
||||||
{header}
|
initialValues={initialValues}
|
||||||
<RunWorkflowForm
|
workflowParameters={workflowParameters}
|
||||||
initialValues={initialValues}
|
initialSettings={{
|
||||||
workflowParameters={workflowParameters}
|
proxyLocation:
|
||||||
initialSettings={{
|
proxyLocation ?? workflow.proxy_location ?? ProxyLocation.Residential,
|
||||||
proxyLocation:
|
webhookCallbackUrl:
|
||||||
proxyLocation ??
|
webhookCallbackUrl ?? workflow.webhook_callback_url ?? "",
|
||||||
workflow.proxy_location ??
|
maxScreenshotScrolls:
|
||||||
ProxyLocation.Residential,
|
maxScreenshotScrolls ?? workflow.max_screenshot_scrolls ?? null,
|
||||||
webhookCallbackUrl:
|
extraHttpHeaders:
|
||||||
webhookCallbackUrl ?? workflow.webhook_callback_url ?? "",
|
extraHttpHeaders ?? workflow.extra_http_headers ?? null,
|
||||||
maxScreenshotScrolls:
|
cdpAddress: null,
|
||||||
maxScreenshotScrolls ?? workflow.max_screenshot_scrolls ?? null,
|
}}
|
||||||
extraHttpHeaders:
|
/>
|
||||||
extraHttpHeaders ?? workflow.extra_http_headers ?? null,
|
|
||||||
cdpAddress: null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ const nodeLibraryItems: Array<{
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
title: "File Parser Block",
|
title: "File Parser Block",
|
||||||
description: "Parse PDFs, CSVs, Excel files, and Images",
|
description: "Parse PDFs, CSVs, and Excel files",
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// nodeType: "pdfParser",
|
// nodeType: "pdfParser",
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ export type SendEmailBlock = WorkflowBlockBase & {
|
|||||||
export type FileURLParserBlock = WorkflowBlockBase & {
|
export type FileURLParserBlock = WorkflowBlockBase & {
|
||||||
block_type: "file_url_parser";
|
block_type: "file_url_parser";
|
||||||
file_url: string;
|
file_url: string;
|
||||||
file_type: "csv" | "excel" | "pdf" | "image";
|
file_type: "csv" | "excel" | "pdf";
|
||||||
json_schema: Record<string, unknown> | null;
|
json_schema: Record<string, unknown> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ export type SendEmailBlockYAML = BlockYAMLBase & {
|
|||||||
export type FileUrlParserBlockYAML = BlockYAMLBase & {
|
export type FileUrlParserBlockYAML = BlockYAMLBase & {
|
||||||
block_type: "file_url_parser";
|
block_type: "file_url_parser";
|
||||||
file_url: string;
|
file_url: string;
|
||||||
file_type: "csv" | "excel" | "pdf" | "image";
|
file_type: "csv" | "excel" | "pdf";
|
||||||
json_schema?: Record<string, unknown> | null;
|
json_schema?: Record<string, unknown> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
FileType = typing.Union[typing.Literal["csv", "excel", "pdf", "image"], typing.Any]
|
FileType = typing.Union[typing.Literal["csv", "excel", "pdf"], typing.Any]
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
Extract all visible text from this image.
|
|
||||||
|
|
||||||
MAKE SURE YOU OUTPUT VALID JSON. No text before or after JSON, no trailing commas, no comments, no unnecessary quotes.
|
|
||||||
|
|
||||||
Reply in JSON format with the following keys:
|
|
||||||
{
|
|
||||||
"extracted_text": str // All text extracted from the image
|
|
||||||
}
|
|
||||||
|
|
||||||
TEXT EXTRACTION GUIDELINES:
|
|
||||||
- Preserve reading order (top to bottom, left to right)
|
|
||||||
- For tables: format as rows separated by newlines, columns separated by " | "
|
|
||||||
- For multi-column layouts: extract each column separately, separated by blank lines
|
|
||||||
- For forms: format as "Label: Value" on each line
|
|
||||||
- Preserve line breaks where they appear meaningful (paragraphs, list items)
|
|
||||||
- Include all visible text: headers, body text, labels, captions, watermarks
|
|
||||||
- For handwritten text: do your best to transcribe, use [illegible] for unclear parts
|
|
||||||
|
|
||||||
If no text is visible in the image, return an empty string for extracted_text.
|
|
||||||
@@ -3063,8 +3063,6 @@ class FileParserBlock(Block):
|
|||||||
return FileType.PDF
|
return FileType.PDF
|
||||||
elif suffix == ".tsv":
|
elif suffix == ".tsv":
|
||||||
return FileType.CSV # TSV files are handled by the CSV parser
|
return FileType.CSV # TSV files are handled by the CSV parser
|
||||||
elif suffix in (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff", ".tif"):
|
|
||||||
return FileType.IMAGE
|
|
||||||
else:
|
else:
|
||||||
return FileType.CSV # Default to CSV for .csv and any other extensions
|
return FileType.CSV # Default to CSV for .csv and any other extensions
|
||||||
|
|
||||||
@@ -3114,12 +3112,6 @@ class FileParserBlock(Block):
|
|||||||
validate_pdf_file(file_path, file_identifier=file_url_used)
|
validate_pdf_file(file_path, file_identifier=file_url_used)
|
||||||
except PDFParsingError as e:
|
except PDFParsingError as e:
|
||||||
raise InvalidFileType(file_url=file_url_used, file_type=self.file_type, error=str(e))
|
raise InvalidFileType(file_url=file_url_used, file_type=self.file_type, error=str(e))
|
||||||
elif self.file_type == FileType.IMAGE:
|
|
||||||
kind = filetype.guess(file_path)
|
|
||||||
if kind is None or not kind.mime.startswith("image/"):
|
|
||||||
raise InvalidFileType(
|
|
||||||
file_url=file_url_used, file_type=self.file_type, error="File is not a valid image"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _parse_csv_file(self, file_path: str) -> list[dict[str, Any]]:
|
async def _parse_csv_file(self, file_path: str) -> list[dict[str, Any]]:
|
||||||
"""Parse CSV/TSV file and return list of dictionaries."""
|
"""Parse CSV/TSV file and return list of dictionaries."""
|
||||||
@@ -3192,27 +3184,6 @@ class FileParserBlock(Block):
|
|||||||
except PDFParsingError as e:
|
except PDFParsingError as e:
|
||||||
raise InvalidFileType(file_url=self.file_url, file_type=self.file_type, error=str(e))
|
raise InvalidFileType(file_url=self.file_url, file_type=self.file_type, error=str(e))
|
||||||
|
|
||||||
async def _parse_image_file(self, file_path: str) -> str:
|
|
||||||
"""Parse image file using vision LLM for OCR."""
|
|
||||||
try:
|
|
||||||
with open(file_path, "rb") as f:
|
|
||||||
image_bytes = f.read()
|
|
||||||
|
|
||||||
llm_prompt = prompt_engine.load_prompt("extract-text-from-image")
|
|
||||||
llm_api_handler = LLMAPIHandlerFactory.get_override_llm_api_handler(
|
|
||||||
self.override_llm_key, default=app.LLM_API_HANDLER
|
|
||||||
)
|
|
||||||
llm_response = await llm_api_handler(
|
|
||||||
prompt=llm_prompt,
|
|
||||||
prompt_name="extract-text-from-image",
|
|
||||||
screenshots=[image_bytes],
|
|
||||||
force_dict=True,
|
|
||||||
)
|
|
||||||
return llm_response.get("extracted_text", "")
|
|
||||||
except Exception:
|
|
||||||
LOG.exception("Failed to extract text from image via OCR", file_url=self.file_url)
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def _extract_with_ai(
|
async def _extract_with_ai(
|
||||||
self, content: str | list[dict[str, Any]], workflow_run_context: WorkflowRunContext
|
self, content: str | list[dict[str, Any]], workflow_run_context: WorkflowRunContext
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -3239,8 +3210,9 @@ class FileParserBlock(Block):
|
|||||||
"extract-information-from-file-text", extracted_text_content=content_str, json_schema=schema_to_use
|
"extract-information-from-file-text", extracted_text_content=content_str, json_schema=schema_to_use
|
||||||
)
|
)
|
||||||
|
|
||||||
llm_key = self.override_llm_key
|
llm_api_handler = LLMAPIHandlerFactory.get_override_llm_api_handler(
|
||||||
llm_api_handler = LLMAPIHandlerFactory.get_override_llm_api_handler(llm_key, default=app.LLM_API_HANDLER)
|
self.override_llm_key, default=app.LLM_API_HANDLER
|
||||||
|
)
|
||||||
|
|
||||||
llm_response = await llm_api_handler(
|
llm_response = await llm_api_handler(
|
||||||
prompt=llm_prompt, prompt_name="extract-information-from-file-text", force_dict=False
|
prompt=llm_prompt, prompt_name="extract-information-from-file-text", force_dict=False
|
||||||
@@ -3289,9 +3261,9 @@ class FileParserBlock(Block):
|
|||||||
else:
|
else:
|
||||||
file_path = await download_file(self.file_url)
|
file_path = await download_file(self.file_url)
|
||||||
|
|
||||||
# Auto-detect file type if not explicitly set (IMAGE/EXCEL/PDF are explicit choices)
|
# Auto-detect file type based on file extension
|
||||||
if self.file_type not in (FileType.IMAGE, FileType.EXCEL, FileType.PDF):
|
detected_file_type = self._detect_file_type_from_url(self.file_url)
|
||||||
self.file_type = self._detect_file_type_from_url(self.file_url)
|
self.file_type = detected_file_type
|
||||||
|
|
||||||
# Validate the file type
|
# Validate the file type
|
||||||
self.validate_file_type(self.file_url, file_path)
|
self.validate_file_type(self.file_url, file_path)
|
||||||
@@ -3311,8 +3283,6 @@ class FileParserBlock(Block):
|
|||||||
parsed_data = await self._parse_excel_file(file_path)
|
parsed_data = await self._parse_excel_file(file_path)
|
||||||
elif self.file_type == FileType.PDF:
|
elif self.file_type == FileType.PDF:
|
||||||
parsed_data = await self._parse_pdf_file(file_path)
|
parsed_data = await self._parse_pdf_file(file_path)
|
||||||
elif self.file_type == FileType.IMAGE:
|
|
||||||
parsed_data = await self._parse_image_file(file_path)
|
|
||||||
else:
|
else:
|
||||||
return await self.build_block_result(
|
return await self.build_block_result(
|
||||||
success=False,
|
success=False,
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ class FileType(StrEnum):
|
|||||||
CSV = "csv"
|
CSV = "csv"
|
||||||
EXCEL = "excel"
|
EXCEL = "excel"
|
||||||
PDF = "pdf"
|
PDF = "pdf"
|
||||||
IMAGE = "image"
|
|
||||||
|
|
||||||
|
|
||||||
class PDFFormat(StrEnum):
|
class PDFFormat(StrEnum):
|
||||||
|
|||||||
Reference in New Issue
Block a user