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:
Suchintan
2026-02-02 23:38:34 -08:00
committed by GitHub
parent fcbb3daddd
commit ebe43e12b1
9 changed files with 86 additions and 136 deletions

View File

@@ -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>
); );

View File

@@ -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>
); );
} }

View File

@@ -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",

View File

@@ -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;
}; };

View File

@@ -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;
}; };

View File

@@ -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]

View File

@@ -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.

View File

@@ -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,

View File

@@ -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):