-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add Apify integration for running Actors #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
62803a4
1cb415d
6f5074d
b598228
5aaeb7b
93aee74
ed8b30c
9e1ae7f
61207bc
850a060
9e1f48a
dacaa24
dc0ea28
9ec9d35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useRef, useState } from "react"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { TemplateBadgeTextarea } from "./template-badge-textarea"; | ||
|
|
||
| export interface TemplateBadgeJsonProps { | ||
| value?: string; | ||
| onChange?: (value: string) => void; | ||
| placeholder?: string; | ||
| disabled?: boolean; | ||
| className?: string; | ||
| id?: string; | ||
| rows?: number; | ||
| } | ||
|
|
||
| /** | ||
| * A textarea component that validates JSON input in real-time | ||
| * Wraps TemplateBadgeTextarea and adds JSON validation and formatting | ||
| */ | ||
| export function TemplateBadgeJson({ | ||
| value = "", | ||
| onChange, | ||
| placeholder, | ||
| disabled, | ||
| className, | ||
| id, | ||
| rows = 3, | ||
| }: TemplateBadgeJsonProps) { | ||
| const [jsonError, setJsonError] = useState<string | null>(null); | ||
| const [isFocused, setIsFocused] = useState(false); | ||
| const formatTimeoutRef = useRef<NodeJS.Timeout | null>(null); | ||
| const lastFormattedValueRef = useRef<string>(""); | ||
|
|
||
| // Validate JSON on value change | ||
| useEffect(() => { | ||
| if (!value || typeof value !== "string") { | ||
| setJsonError(null); | ||
| return; | ||
| } | ||
|
Comment on lines
+37
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // If empty or only whitespace, no error | ||
| if (!value.trim()) { | ||
| setJsonError(null); | ||
| return; | ||
| } | ||
|
|
||
| // Parse JSON directly - template variables will be treated as normal strings | ||
| try { | ||
| JSON.parse(value); | ||
| setJsonError(null); | ||
| } catch (error) { | ||
| setJsonError( | ||
| error instanceof Error ? error.message : "Invalid JSON format" | ||
| ); | ||
| } | ||
| }, [value]); | ||
|
|
||
| // Format JSON when it becomes valid (debounced to avoid formatting while typing) | ||
| useEffect(() => { | ||
| // Clear any pending format timeout | ||
| if (formatTimeoutRef.current) { | ||
| clearTimeout(formatTimeoutRef.current); | ||
| } | ||
|
|
||
| // Don't format if there's an error, field is focused, or value is empty | ||
| if (jsonError || isFocused || !value || typeof value !== "string") { | ||
| return; | ||
| } | ||
|
|
||
| if (!value.trim()) { | ||
| return; | ||
| } | ||
|
|
||
| // Debounce formatting - wait 500ms after user stops typing | ||
| formatTimeoutRef.current = setTimeout(() => { | ||
| try { | ||
| // Parse JSON directly - template variables are treated as normal strings | ||
| const parsed = JSON.parse(value); | ||
| const formatted = JSON.stringify(parsed, null, 2); | ||
|
|
||
| // Only format if different from current value and we haven't already formatted this value | ||
| if (formatted !== value && formatted !== lastFormattedValueRef.current) { | ||
| lastFormattedValueRef.current = formatted; | ||
| onChange?.(formatted); | ||
| } | ||
| } catch { | ||
| // If parsing fails, don't format | ||
| } | ||
| }, 500); | ||
|
|
||
| return () => { | ||
| if (formatTimeoutRef.current) { | ||
| clearTimeout(formatTimeoutRef.current); | ||
| } | ||
| }; | ||
| }, [value, isFocused, jsonError, onChange]); | ||
|
|
||
| // Track focus state by listening to focus/blur events on the wrapper | ||
| const handleWrapperFocus = () => { | ||
| setIsFocused(true); | ||
| }; | ||
|
|
||
| const handleWrapperBlur = () => { | ||
| setIsFocused(false); | ||
| // Format immediately on blur if JSON is valid | ||
| if (!jsonError && value && typeof value === "string" && value.trim()) { | ||
| try { | ||
| // Parse JSON directly - template variables are treated as normal strings | ||
| const parsed = JSON.parse(value); | ||
| const formatted = JSON.stringify(parsed, null, 2); | ||
|
|
||
| if (formatted !== value) { | ||
| onChange?.(formatted); | ||
| } | ||
| } catch { | ||
| // If parsing fails, don't format | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| className="space-y-1" | ||
| onBlur={handleWrapperBlur} | ||
| onFocus={handleWrapperFocus} | ||
| > | ||
| <TemplateBadgeTextarea | ||
| className={cn( | ||
| jsonError && "border-destructive focus-within:ring-destructive", | ||
| className | ||
| )} | ||
| disabled={disabled} | ||
| id={id} | ||
| onChange={onChange} | ||
| placeholder={placeholder} | ||
| rows={rows} | ||
| value={value} | ||
| /> | ||
| {jsonError && ( | ||
| <p className="ml-1 text-destructive text-xs">{jsonError}</p> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ | |
| * This registry enables dynamic step imports that are statically analyzable | ||
| * by the bundler. Each action type maps to its step importer function. | ||
| * | ||
| * Generated entries: 10 | ||
| * Generated entries: 12 | ||
| */ | ||
|
|
||
| import "server-only"; | ||
|
|
@@ -41,6 +41,14 @@ export const PLUGIN_STEP_IMPORTERS: Record<string, StepImporter> = { | |
| importer: () => import("@/plugins/ai-gateway/steps/generate-image"), | ||
| stepFunction: "generateImageStep", | ||
| }, | ||
| "apify/run-actor": { | ||
| importer: () => import("@/plugins/apify/steps/run-actor/step"), | ||
| stepFunction: "apifyRunActorStep", | ||
| }, | ||
| "apify/scrape-single-url": { | ||
| importer: () => import("@/plugins/apify/steps/scrape-single-url/step"), | ||
| stepFunction: "scrapeSingleUrlStep", | ||
| }, | ||
| "firecrawl/scrape": { | ||
| importer: () => import("@/plugins/firecrawl/steps/scrape"), | ||
| stepFunction: "firecrawlScrapeStep", | ||
|
|
@@ -114,6 +122,8 @@ export const PLUGIN_STEP_IMPORTERS: Record<string, StepImporter> = { | |
| export const ACTION_LABELS: Record<string, string> = { | ||
| "ai-gateway/generate-text": "Generate Text", | ||
| "ai-gateway/generate-image": "Generate Image", | ||
| "apify/run-actor": "Run Apify Actor", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's call it just "Run Actor" (Apify is already mentioned on the card) |
||
| "apify/scrape-single-url": "Scrape Single URL", | ||
| "firecrawl/scrape": "Scrape URL", | ||
| "firecrawl/search": "Search Web", | ||
| "linear/create-ticket": "Create Ticket", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /** | ||
| * Code generation template for Run Apify Actor action | ||
| * This template is used when exporting workflows to standalone Next.js projects | ||
| * It uses environment variables instead of integrationId | ||
| */ | ||
| export const runActorCodegenTemplate = `import { ApifyClient } from "apify-client"; | ||
| export async function apifyRunActorStep(input: { | ||
| actorId: string; | ||
| actorInput?: string; | ||
| }) { | ||
| "use step"; | ||
| const apiKey = process.env.APIFY_API_KEY; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe use our standard |
||
| if (!apiKey) { | ||
| throw new Error("Apify API Token is not configured. Set APIFY_API_KEY environment variable."); | ||
| } | ||
| let parsedActorInput: Record<string, unknown> = {}; | ||
| if (input.actorInput) { | ||
| try { | ||
| parsedActorInput = JSON.parse(input.actorInput); | ||
| } catch (err) { | ||
| throw new Error(\`Cannot parse Actor input: \${err instanceof Error ? err.message : String(err)}\`); | ||
| } | ||
| } | ||
| try { | ||
| const client = new ApifyClient({ token: apiKey }); | ||
| const actorClient = client.actor(input.actorId); | ||
| // Run synchronously and wait for completion | ||
| const runData = await actorClient.call(parsedActorInput); | ||
| // Get dataset items | ||
| let datasetItems: unknown[] = []; | ||
| if (runData.defaultDatasetId) { | ||
| const dataset = await client | ||
| .dataset(runData.defaultDatasetId) | ||
| .listItems(); | ||
| datasetItems = dataset.items; | ||
| } | ||
| return { | ||
| runId: runData.id || "unknown", | ||
| status: runData.status || "SUCCEEDED", | ||
| datasetId: runData.defaultDatasetId, | ||
| datasetItems, | ||
| }; | ||
| } catch (error) { | ||
| throw new Error(\`Failed to run Actor: \${error instanceof Error ? error.message : String(error)}\`); | ||
| } | ||
| }`; | ||

Uh oh!
There was an error while loading. Please reload this page.