-
Notifications
You must be signed in to change notification settings - Fork 134
feat: Add Apify Scrape Single URL and Run Actor actions #136
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
Open
drobnikj
wants to merge
21
commits into
vercel-labs:main
Choose a base branch
from
apify:feat/apify
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
62803a4
feat: add Apify integration for running Actors
yfe404 1cb415d
fix: lint and formatting issues
yfe404 6f5074d
fix: sort CSS classes and format onChange handler
yfe404 b598228
fix: add apify to IntegrationType in api-client and integrations-manager
yfe404 5aaeb7b
feat: polish apify actions
drobnikj 93aee74
fix: adding apify-client package
drobnikj ed8b30c
fix: update action
drobnikj 9e1ae7f
Merge main into feat/apify - resolved all conflicts
drobnikj 61207bc
fix: after merge fixes
drobnikj 850a060
fix: sync codegen
drobnikj 9e1f48a
fix: fix lint
drobnikj dacaa24
fix: remove spaces to reduce diff
drobnikj dc0ea28
fix: remove debugging logs
drobnikj 9ec9d35
fix: clean up code, add JSON error parsing
drobnikj fce3519
fix: review comments fix
drobnikj 3885666
fix: update readme
drobnikj d391d21
fix: update apifyRunActorStep to accept object same as string
drobnikj 84b56de
Merge main into feat/apify - resolved conflicts
drobnikj 9966acc
Merge branch 'refs/heads/main' into feat/apify
drobnikj eb90102
fix: resolving merge conflicts
drobnikj 83a1f46
fix: fix readme imports
drobnikj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| "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; | ||
| } | ||
|
|
||
| // If empty or only whitespace, no error | ||
| if (!value.trim()) { | ||
| setJsonError(null); | ||
| return; | ||
| } | ||
|
|
||
| // Ensure that parsable values (not object) throws | ||
| if (!/^\s*\{[\s\S]*\}\s*$/.test(value)) { | ||
| setJsonError("Value must be a JSON object"); | ||
| 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> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /** | ||
| * Code generation template for Run 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 | Record<string, unknown> | null; | ||
| }) { | ||
| "use step"; | ||
|
|
||
| const apiKey = process.env.APIFY_API_TOKEN; | ||
|
|
||
| if (!apiKey) { | ||
| throw new Error("Apify API Token is not configured. Set APIFY_API_TOKEN environment variable."); | ||
| } | ||
|
|
||
| let parsedActorInput: Record<string, unknown> = {}; | ||
| if (input.actorInput) { | ||
| // If it's already an object, use it directly | ||
| if (typeof input.actorInput === "object" && !Array.isArray(input.actorInput)) { | ||
| parsedActorInput = input.actorInput; | ||
| } else if (typeof input.actorInput === "string") { | ||
| // If it's a string, parse it | ||
| 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, | ||
| data: datasetItems, | ||
| }; | ||
| } catch (error) { | ||
| throw new Error(\`Failed to run Actor: \${error instanceof Error ? error.message : String(error)}\`); | ||
| } | ||
| }`; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /** | ||
| * Code generation template for Scrape Single URL action | ||
| * This template is used when exporting workflows to standalone Next.js projects | ||
| * It uses environment variables instead of integrationId | ||
| */ | ||
| export const scrapeSingleUrlCodegenTemplate = `import { ApifyClient } from "apify-client"; | ||
|
|
||
| export async function scrapeSingleUrlStep(input: { | ||
| url: string; | ||
| crawlerType?: string; | ||
| }) { | ||
| "use step"; | ||
|
|
||
| const apiKey = process.env.APIFY_API_TOKEN; | ||
|
|
||
| if (!apiKey) { | ||
| throw new Error("Apify API Token is not configured. Set APIFY_API_TOKEN environment variable."); | ||
| } | ||
|
|
||
| if (!input.url) { | ||
| throw new Error("URL is required."); | ||
| } | ||
|
|
||
| try { | ||
| const client = new ApifyClient({ token: apiKey }); | ||
| const actorClient = client.actor("apify/website-content-crawler"); | ||
| const crawlerType = input.crawlerType || "playwright:adaptive"; | ||
|
|
||
| // Prepare actor input | ||
| const actorInput = { | ||
| startUrls: [{ url: input.url }], | ||
| crawlerType, | ||
| maxCrawlDepth: 0, | ||
| maxCrawlPages: 1, | ||
| maxResults: 1, | ||
| proxyConfiguration: { | ||
| useApifyProxy: true, | ||
| }, | ||
| removeCookieWarnings: true, | ||
| saveMarkdown: true, | ||
| }; | ||
|
|
||
| // Run synchronously and wait for completion | ||
| const runData = await actorClient.call(actorInput); | ||
|
|
||
| // Get dataset items | ||
| let markdown: string | undefined; | ||
| if (runData.defaultDatasetId) { | ||
| const datasetItems = await client | ||
| .dataset(runData.defaultDatasetId) | ||
| .listItems(); | ||
|
|
||
| // Extract markdown from the first item | ||
| if (datasetItems.items && datasetItems.items.length > 0) { | ||
| const firstItem = datasetItems.items[0] as Record<string, unknown>; | ||
| markdown = firstItem.markdown as string; | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| runId: runData.id || "unknown", | ||
| status: runData.status || "SUCCEEDED", | ||
| markdown, | ||
| }; | ||
| } catch (error) { | ||
| throw new Error(\`Failed to scrape URL: \${error instanceof Error ? error.message : String(error)}\`); | ||
| } | ||
| }`; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.