├── .eslintrc.json
├── .gitignore
├── .npmrc
├── README.md
├── app
├── api
│ ├── approval
│ │ ├── [approvalId]
│ │ │ ├── resume
│ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── config
│ │ └── route.ts
│ ├── execute-agent
│ │ └── route.ts
│ ├── execute-extract
│ │ └── route.ts
│ ├── execute-firecrawl
│ │ └── route.ts
│ ├── execute-guardrails
│ │ └── route.ts
│ ├── execute-mcp
│ │ └── route.ts
│ ├── mcp
│ │ └── registry
│ │ │ └── route.ts
│ ├── templates
│ │ ├── seed
│ │ │ └── route.ts
│ │ └── update
│ │ │ └── route.ts
│ ├── test-mcp-connection
│ │ └── route.ts
│ ├── workflow
│ │ └── execute
│ │ │ └── route.ts
│ └── workflows
│ │ ├── [workflowId]
│ │ ├── execute-langgraph
│ │ │ └── route.ts
│ │ ├── execute-stream
│ │ │ └── route.ts
│ │ ├── execute
│ │ │ └── route.ts
│ │ ├── export-code
│ │ │ └── route.ts
│ │ ├── export-langgraph
│ │ │ └── route.ts
│ │ ├── resume
│ │ │ └── route.ts
│ │ └── route.ts
│ │ ├── cleanup
│ │ └── route.ts
│ │ ├── import-langgraph
│ │ └── route.ts
│ │ └── route.ts
├── layout.tsx
├── page.tsx
├── sign-in
│ └── [[...sign-in]]
│ │ └── page.tsx
├── sign-up
│ └── [[...sign-up]]
│ │ └── page.tsx
└── workflows
│ ├── [workflowId]
│ ├── page.tsx
│ └── run
│ │ └── page.tsx
│ └── page.tsx
├── atoms
└── sheets.ts
├── colors.json
├── components
├── FirecrawlIcon
│ └── FirecrawlIcon.tsx
├── app
│ └── (home)
│ │ └── sections
│ │ ├── endpoints
│ │ ├── EndpointsCrawl
│ │ │ └── EndpointsCrawl.tsx
│ │ ├── EndpointsExtract
│ │ │ └── EndpointsExtract.tsx
│ │ ├── EndpointsMap
│ │ │ └── EndpointsMap.tsx
│ │ ├── EndpointsScrape
│ │ │ └── EndpointsScrape.tsx
│ │ ├── EndpointsSearch
│ │ │ └── EndpointsSearch.tsx
│ │ ├── Extract
│ │ │ └── Extract.tsx
│ │ └── Mcp
│ │ │ └── Mcp.tsx
│ │ ├── hero-flame
│ │ ├── HeroFlame.tsx
│ │ └── data.json
│ │ ├── hero-input
│ │ ├── Button
│ │ │ └── Button.tsx
│ │ ├── HeroInput.tsx
│ │ ├── Tabs
│ │ │ ├── Mobile
│ │ │ │ └── Mobile.tsx
│ │ │ └── Tabs.tsx
│ │ └── _svg
│ │ │ ├── ArrowRight.tsx
│ │ │ └── Globe.tsx
│ │ ├── hero-scraping
│ │ ├── Code
│ │ │ ├── Code.tsx
│ │ │ └── Loading
│ │ │ │ ├── Loading.tsx
│ │ │ │ └── _svg
│ │ │ │ └── Check.tsx
│ │ ├── HeroScraping.css
│ │ ├── HeroScraping.tsx
│ │ ├── Tag
│ │ │ └── Tag.tsx
│ │ └── _svg
│ │ │ ├── BrowserMobile.tsx
│ │ │ └── BrowserTab.tsx
│ │ ├── hero
│ │ ├── Background
│ │ │ ├── Background.tsx
│ │ │ ├── BackgroundOuterPiece.tsx
│ │ │ └── _svg
│ │ │ │ └── CenterStar.tsx
│ │ ├── Badge
│ │ │ └── Badge.tsx
│ │ ├── Hero.tsx
│ │ ├── Pixi
│ │ │ ├── Pixi.tsx
│ │ │ └── tickers
│ │ │ │ ├── ascii.ts
│ │ │ │ └── features
│ │ │ │ ├── cell.ts
│ │ │ │ ├── cellReveal.ts
│ │ │ │ ├── components
│ │ │ │ ├── AnimatedRect.ts
│ │ │ │ ├── BlinkingContainer.ts
│ │ │ │ └── Dot.ts
│ │ │ │ ├── crawl.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mapping.ts
│ │ │ │ ├── scrape.ts
│ │ │ │ └── search.ts
│ │ └── Title
│ │ │ └── Title.tsx
│ │ ├── step2
│ │ └── Step2Placeholder.tsx
│ │ └── workflow-builder
│ │ ├── ConfirmDialog.tsx
│ │ ├── ConnectionMapperModal.tsx
│ │ ├── ConnectorsPanel.tsx
│ │ ├── CustomNodes.tsx
│ │ ├── DataNodePanel.tsx
│ │ ├── DataNodePanel.tsx.backup
│ │ ├── EdgeLabelModal.tsx
│ │ ├── ExecutionPanel.tsx
│ │ ├── ExportLangGraphButton.tsx
│ │ ├── ExtractNodePanel.tsx
│ │ ├── HTTPNodePanel.tsx
│ │ ├── LogicNodePanel.tsx
│ │ ├── MCPPanel.tsx
│ │ ├── NodeArgumentsPanel.tsx
│ │ ├── NodeIOBadges.tsx
│ │ ├── NodePanel.tsx
│ │ ├── NoteNodePanel.tsx
│ │ ├── OutputSchemaPanel.tsx
│ │ ├── PasteConfigModal.tsx
│ │ ├── PreviewPanel.tsx
│ │ ├── PublishModal.tsx
│ │ ├── SaveAsTemplateModal.tsx
│ │ ├── SettingsPanelSimple.tsx
│ │ ├── ShareWorkflowModal.tsx
│ │ ├── StartNodePanel.tsx
│ │ ├── TestEndpointPanel.tsx
│ │ ├── ToolsNodePanel.tsx
│ │ ├── UniversalOutputSelector.tsx
│ │ ├── VariableReferencePicker.tsx
│ │ ├── WorkflowBuilder.tsx
│ │ └── WorkflowNameEditor.tsx
├── hooks
│ └── use-dropdown-hover.ts
├── icons
│ └── FirecrawlLogo.tsx
├── providers
│ └── BigIntProvider.tsx
├── shared
│ ├── ErrorBoundary.tsx
│ ├── Playground
│ │ └── Context
│ │ │ └── types.ts
│ ├── animated-dot-icon.tsx
│ ├── ascii-background.tsx
│ ├── ascii-flame-background.tsx
│ ├── button
│ │ ├── Button.css
│ │ └── Button.tsx
│ ├── buttons
│ │ ├── capsule-button.tsx
│ │ ├── fire-action-link.tsx
│ │ ├── index.ts
│ │ └── slate-button.tsx
│ ├── color-styles
│ │ └── color-styles.tsx
│ ├── combobox
│ │ └── combobox.tsx
│ ├── effects
│ │ ├── flame
│ │ │ ├── Flame.tsx
│ │ │ ├── ascii-explosion.tsx
│ │ │ ├── auth-pulse
│ │ │ │ ├── auth-pulse.tsx
│ │ │ │ └── pulse-data.json
│ │ │ ├── core-flame.json
│ │ │ ├── core-flame.tsx
│ │ │ ├── explosion-data.json
│ │ │ ├── flame-background.tsx
│ │ │ ├── hero-flame-data.json
│ │ │ ├── hero-flame-right.tsx
│ │ │ ├── hero-flame.tsx
│ │ │ ├── index.ts
│ │ │ ├── slate-grid
│ │ │ │ ├── grid-data.json
│ │ │ │ └── slate-grid.tsx
│ │ │ ├── subtle-explosion.tsx
│ │ │ └── subtle-wave
│ │ │ │ ├── subtle-wave.tsx
│ │ │ │ └── wave-data.json
│ │ ├── index.ts
│ │ └── subtle-ascii-animation.tsx
│ ├── firecrawl-icon
│ │ ├── firecrawl-icon-static.tsx
│ │ └── firecrawl-icon.tsx
│ ├── flame-button.tsx
│ ├── header
│ │ ├── BrandKit
│ │ │ ├── BrandKit.tsx
│ │ │ └── _svg
│ │ │ │ ├── Download.tsx
│ │ │ │ ├── Guidelines.tsx
│ │ │ │ └── Icon.tsx
│ │ ├── Dropdown
│ │ │ ├── Content
│ │ │ │ ├── Content.tsx
│ │ │ │ └── NavItemRow.tsx
│ │ │ ├── Github
│ │ │ │ ├── Flame
│ │ │ │ │ ├── Flame.tsx
│ │ │ │ │ └── data.json
│ │ │ │ └── Github.tsx
│ │ │ ├── Mobile
│ │ │ │ ├── Item
│ │ │ │ │ └── Item.tsx
│ │ │ │ └── Mobile.tsx
│ │ │ ├── Stories
│ │ │ │ ├── Flame
│ │ │ │ │ └── Flame.tsx
│ │ │ │ ├── Stories.tsx
│ │ │ │ └── _svg
│ │ │ │ │ ├── ArrowUp.tsx
│ │ │ │ │ └── Replit.tsx
│ │ │ └── Wrapper
│ │ │ │ └── Wrapper.tsx
│ │ ├── Github
│ │ │ ├── GithubClient.tsx
│ │ │ └── _svg
│ │ │ │ └── GithubIcon.tsx
│ │ ├── HeaderContext.tsx
│ │ ├── Nav
│ │ │ ├── Item
│ │ │ │ ├── Item.tsx
│ │ │ │ └── _svg
│ │ │ │ │ └── ChevronDown.tsx
│ │ │ ├── Nav.tsx
│ │ │ ├── RenderEndpointIcon.tsx
│ │ │ └── _svg
│ │ │ │ ├── Affiliate.tsx
│ │ │ │ ├── Api.tsx
│ │ │ │ ├── ArrowRight.tsx
│ │ │ │ ├── Careers.tsx
│ │ │ │ ├── Changelog.tsx
│ │ │ │ ├── Chats.tsx
│ │ │ │ ├── Lead.tsx
│ │ │ │ ├── MCP.tsx
│ │ │ │ ├── Platforms.tsx
│ │ │ │ ├── Research.tsx
│ │ │ │ ├── Student.tsx
│ │ │ │ └── Templates.tsx
│ │ ├── Toggle
│ │ │ └── Toggle.tsx
│ │ ├── Wrapper
│ │ │ └── Wrapper.tsx
│ │ └── _svg
│ │ │ └── Logo.tsx
│ ├── hero-flame.tsx
│ ├── icons
│ │ ├── GitHub.tsx
│ │ ├── Logo.tsx
│ │ ├── animated-chevron.tsx
│ │ ├── animated-icons.tsx
│ │ ├── arrow-animated.tsx
│ │ ├── check.tsx
│ │ ├── chevron-slide.tsx
│ │ ├── copied.tsx
│ │ ├── copy.tsx
│ │ ├── curve.tsx
│ │ ├── fingerprint-icon.tsx
│ │ ├── openai.tsx
│ │ ├── source-icon.tsx
│ │ ├── symbol-colored.tsx
│ │ ├── symbol-white.tsx
│ │ ├── tremor-placeholder.tsx
│ │ ├── wordmark-colored.tsx
│ │ └── wordmark-white.tsx
│ ├── image
│ │ ├── Image.tsx
│ │ └── getImageSrc.ts
│ ├── layout
│ │ ├── animated-height.tsx
│ │ ├── animated-width.tsx
│ │ ├── curvy-rect-divider.tsx
│ │ └── curvy-rect.tsx
│ ├── loading
│ │ ├── Shimmer.tsx
│ │ └── usage-loading.tsx
│ ├── lockBody.tsx
│ ├── logo-cloud
│ │ ├── index.ts
│ │ ├── logo-cloud.tsx
│ │ └── logo-cloud2
│ │ │ ├── Logocloud.css
│ │ │ └── Logocloud.tsx
│ ├── macbook-scroll.tsx
│ ├── notifications
│ │ └── slack-notification.tsx
│ ├── pixi
│ │ ├── Pixi.tsx
│ │ ├── PixiAssetManager.ts
│ │ └── utils.ts
│ ├── play-tabs.tsx
│ ├── portal-to-body
│ │ └── PortalToBody.tsx
│ ├── preview
│ │ ├── json-error-highlighter.tsx
│ │ ├── live-preview-frame.tsx
│ │ ├── multiple-web-browsers.tsx
│ │ └── web-browser.tsx
│ ├── pylon.tsx
│ ├── search-params-provider
│ │ └── search-params-provider.tsx
│ ├── section-head
│ │ ├── SectionHead.css
│ │ └── SectionHead.tsx
│ ├── section-title
│ │ └── SectionTitle.tsx
│ ├── tabs
│ │ └── Tabs.tsx
│ ├── ui
│ │ ├── app-dialog.tsx
│ │ ├── ascii-dot-loader.tsx
│ │ ├── dot-grid-loader.tsx
│ │ ├── empty-state.tsx
│ │ ├── index.ts
│ │ ├── loading-state.tsx
│ │ ├── mobile-sheet.tsx
│ │ └── stat-card.tsx
│ └── utils
│ │ └── portal-to-body.tsx
├── ui
│ ├── LoadingDots
│ │ ├── LoadingDots.module.css
│ │ ├── LoadingDots.tsx
│ │ └── index.ts
│ ├── button-demo.tsx
│ ├── button.tsx
│ ├── code.tsx
│ ├── image.tsx
│ ├── index.ts
│ ├── loading-dashboard.tsx
│ ├── magic
│ │ ├── animated-shiny-text-lw.tsx
│ │ ├── animated-shiny-text.tsx
│ │ ├── dock.tsx
│ │ ├── dot-pattern.tsx
│ │ ├── gradual-spacing.tsx
│ │ └── ripple.tsx
│ ├── menu-header.tsx
│ ├── menu.tsx
│ ├── modal.tsx
│ ├── motion
│ │ ├── scramble-text.tsx
│ │ └── text-reveal.tsx
│ ├── scrollbar.tsx
│ ├── shadcn
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── badge.tsx
│ │ ├── button.css
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── combobox.tsx
│ │ ├── context-menu.tsx
│ │ ├── data-table.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── slider.tsx
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toggle.tsx
│ │ ├── tooltip-radix.tsx
│ │ └── tooltip.tsx
│ ├── spinner.tsx
│ ├── table.tsx
│ └── video.tsx
└── workflow
│ └── WorkflowExecutionRenderer.tsx
├── convex.json
├── convex
├── README.md
├── _generated
│ ├── api.d.ts
│ ├── api.js
│ ├── dataModel.d.ts
│ ├── server.d.ts
│ └── server.js
├── admin.ts
├── apiKeys.ts
├── approvals.ts
├── auth.config.ts
├── executions.ts
├── mcpServers.ts
├── schema.ts
├── templates.ts
├── tsconfig.json
├── tsconfig.tsbuildinfo
├── userLLMKeys.ts
├── userMCPs.ts
└── workflows.ts
├── hooks
├── useApprovalWatch.ts
├── useDebouncedEffect.ts
├── useWorkflow.ts
└── useWorkflowExecution.ts
├── lib
├── api
│ ├── auth.ts
│ ├── config.ts
│ ├── llm-keys.ts
│ └── models.ts
├── approval
│ └── approval-store.ts
├── arcade
│ ├── auth-store.ts
│ ├── openai-tools.ts
│ ├── tools.ts
│ └── workflow-helpers.ts
├── config
│ └── llm-config.ts
├── convex
│ └── client.ts
├── errors
│ ├── WorkflowError.ts
│ └── index.ts
├── mcp
│ ├── mcp-registry.ts
│ └── resolver.ts
└── workflow
│ ├── default-workflows.ts
│ ├── duplicate-detection.ts
│ ├── edge-cleanup.ts
│ ├── error-boundaries.ts
│ ├── executors
│ ├── agent.ts
│ ├── arcade.ts
│ ├── data.ts
│ ├── extract.ts
│ ├── http.ts
│ ├── logic.ts
│ ├── mcp.ts
│ └── tools.ts
│ ├── langgraph.ts
│ ├── mcp-registry.ts
│ ├── storage.ts
│ ├── templates.ts
│ ├── templates
│ └── examples
│ │ ├── 01-simple-agent.ts
│ │ ├── 02-agent-with-firecrawl.ts
│ │ ├── 03-scrape-summarize-docs.ts
│ │ ├── 04-advanced-workflow.ts
│ │ ├── README.md
│ │ └── index.ts
│ ├── types.ts
│ ├── validation.ts
│ └── variable-substitution.ts
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── proxy.ts
├── public
├── compressor.json
└── favicon.png
├── repomix.config.json
├── styles
├── additional-styles
│ ├── custom-fonts.css
│ ├── theme.css
│ └── utility-patterns.css
├── chrome-bug.css
├── colors.json
├── components
│ ├── button.css
│ ├── code.css
│ └── index.css
├── design-system
│ ├── animations.css
│ ├── base
│ │ ├── body.css
│ │ ├── layout.css
│ │ └── reset.css
│ ├── colors.css
│ ├── fonts.css
│ ├── typography.css
│ └── utilities.css
├── fire.css
├── main.css
└── workflow-execution.css
├── tailwind.config.ts
├── tsconfig.json
├── tsconfig.tsbuildinfo
└── utils
├── cn.ts
├── init-canvas.ts
├── on-visible.ts
├── set-timeout-on-visible.ts
└── sleep.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {}
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # editors
38 | .vscode
39 |
40 | # certificates
41 | certificates
42 |
43 |
44 | .env
45 | .env.*
46 | post_migrator.py
47 |
48 | # Server
49 | firecrawl-responses-api/.env
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps=true
2 |
--------------------------------------------------------------------------------
/app/api/approval/[approvalId]/resume/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { getApprovalRecord } from '@/lib/approval/approval-store';
3 |
4 | export const dynamic = 'force-dynamic';
5 |
6 | /**
7 | * GET /api/approval/[approvalId]/resume - Get workflow resume data
8 | * This endpoint returns the execution state needed to resume the workflow
9 | */
10 | export async function GET(
11 | request: NextRequest,
12 | { params }: { params: Promise<{ approvalId: string }> }
13 | ) {
14 | const { approvalId } = await params;
15 |
16 | if (!approvalId) {
17 | return NextResponse.json(
18 | { success: false, error: 'Approval ID is required' },
19 | { status: 400 }
20 | );
21 | }
22 |
23 | const record = await getApprovalRecord(approvalId);
24 | if (!record) {
25 | return NextResponse.json(
26 | { success: false, error: 'Approval record not found' },
27 | { status: 404 }
28 | );
29 | }
30 |
31 | if (record.status === 'pending') {
32 | return NextResponse.json(
33 | { success: false, error: 'Approval is still pending' },
34 | { status: 400 }
35 | );
36 | }
37 |
38 | return NextResponse.json({
39 | success: true,
40 | approved: record.status === 'approved',
41 | record,
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/app/api/approval/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { createOrUpdateApprovalRecord } from '@/lib/approval/approval-store';
3 |
4 | export const dynamic = 'force-dynamic';
5 |
6 | /**
7 | * POST /api/approval - Create a new approval request
8 | */
9 | export async function POST(request: NextRequest) {
10 | try {
11 | const body = await request.json();
12 | const { approvalId, executionId, workflowId, nodeId, message, userId } = body;
13 |
14 | if (!approvalId || !executionId || !workflowId || !nodeId) {
15 | return NextResponse.json(
16 | { success: false, error: 'Missing required fields' },
17 | { status: 400 }
18 | );
19 | }
20 |
21 | const record = await createOrUpdateApprovalRecord({
22 | approvalId,
23 | executionId,
24 | workflowId,
25 | nodeId,
26 | message: message || 'Approval required',
27 | userId,
28 | status: 'pending',
29 | });
30 |
31 | return NextResponse.json({ success: true, record });
32 | } catch (error) {
33 | console.error('Failed to create approval record:', error);
34 | return NextResponse.json(
35 | {
36 | success: false,
37 | error: error instanceof Error ? error.message : 'Failed to create approval',
38 | },
39 | { status: 500 }
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/api/config/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | /**
4 | * API route to securely provide environment variables
5 | * Only exposes API keys from .env.local, never from client
6 | */
7 | export async function GET() {
8 | try {
9 | const config = {
10 | anthropicConfigured: !!process.env.ANTHROPIC_API_KEY,
11 | groqConfigured: !!process.env.GROQ_API_KEY,
12 | openaiConfigured: !!process.env.OPENAI_API_KEY,
13 | firecrawlConfigured: !!process.env.FIRECRAWL_API_KEY,
14 | arcadeConfigured: !!process.env.ARCADE_API_KEY,
15 | hasKeys: !!(
16 | (process.env.ANTHROPIC_API_KEY || process.env.GROQ_API_KEY || process.env.OPENAI_API_KEY) &&
17 | process.env.FIRECRAWL_API_KEY
18 | ),
19 | };
20 |
21 | return NextResponse.json(config);
22 | } catch (error) {
23 | console.error('Config API error:', error);
24 | return NextResponse.json(
25 | { error: 'Failed to load configuration' },
26 | { status: 500 }
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/api/mcp/registry/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { officialMCPServers, getEnabledMCPServers, getMCPServerById } from '@/lib/mcp/mcp-registry';
3 |
4 | export const dynamic = 'force-dynamic';
5 |
6 | /**
7 | * GET /api/mcp/registry
8 | * List all MCP servers from registry
9 | */
10 | export async function GET() {
11 | try {
12 | // Get servers from code-defined configuration
13 | const servers = getEnabledMCPServers();
14 |
15 | return NextResponse.json({
16 | success: true,
17 | servers,
18 | source: 'config',
19 | });
20 | } catch (error) {
21 | console.error('Failed to get MCP registry:', error);
22 | return NextResponse.json(
23 | { success: false, error: 'Failed to load MCP registry' },
24 | { status: 500 }
25 | );
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/app/api/workflows/[workflowId]/export-code/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { workflowToLangGraphCode } from '@/lib/workflow/langgraph';
3 |
4 | export const dynamic = 'force-dynamic';
5 |
6 | /**
7 | * Export workflow as executable LangGraph TypeScript code
8 | * POST /api/workflows/:workflowId/export-code
9 | * Expects workflow data in request body
10 | */
11 | export async function POST(
12 | request: NextRequest,
13 | { params }: { params: Promise<{ workflowId: string }> }
14 | ) {
15 | try {
16 | const { workflowId } = await params;
17 | const workflow = await request.json();
18 |
19 | if (!workflow || !workflow.nodes) {
20 | return NextResponse.json(
21 | { error: 'Workflow data is required in request body' },
22 | { status: 400 }
23 | );
24 | }
25 |
26 | // Generate TypeScript code
27 | const code = workflowToLangGraphCode(workflow);
28 |
29 | // Return as JSON with code string
30 | return NextResponse.json({
31 | code,
32 | filename: `${workflow.name.replace(/\s+/g, '_')}.ts`,
33 | language: 'typescript',
34 | });
35 | } catch (error) {
36 | console.error('Code export error:', error);
37 | return NextResponse.json(
38 | {
39 | error: 'Code export failed',
40 | message: error instanceof Error ? error.message : 'Unknown error',
41 | },
42 | { status: 500 }
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/api/workflows/[workflowId]/export-langgraph/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { workflowToLangGraphJSON } from '@/lib/workflow/langgraph';
3 |
4 | export const dynamic = 'force-dynamic';
5 |
6 | /**
7 | * Export workflow as LangGraph JSON
8 | * POST /api/workflows/:workflowId/export-langgraph
9 | * Expects workflow data in request body
10 | */
11 | export async function POST(
12 | request: NextRequest,
13 | { params }: { params: Promise<{ workflowId: string }> }
14 | ) {
15 | try {
16 | const { workflowId } = await params;
17 | const workflow = await request.json();
18 |
19 | if (!workflow || !workflow.nodes) {
20 | return NextResponse.json(
21 | { error: 'Workflow data is required in request body' },
22 | { status: 400 }
23 | );
24 | }
25 |
26 | // Convert to LangGraph format
27 | const langGraphJSON = workflowToLangGraphJSON(workflow);
28 |
29 | // Return as downloadable JSON
30 | return new NextResponse(JSON.stringify(langGraphJSON, null, 2), {
31 | headers: {
32 | 'Content-Type': 'application/json',
33 | 'Content-Disposition': `attachment; filename="${workflow.name.replace(/\s+/g, '_')}_langgraph.json"`,
34 | },
35 | });
36 | } catch (error) {
37 | console.error('Export error:', error);
38 | return NextResponse.json(
39 | {
40 | error: 'Export failed',
41 | message: error instanceof Error ? error.message : 'Unknown error',
42 | },
43 | { status: 500 }
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/api/workflows/cleanup/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { getAuthenticatedConvexClient, api } from '@/lib/convex/client';
3 |
4 | /**
5 | * DELETE /api/workflows/cleanup
6 | * Clean up workflows without userId (development/admin only)
7 | */
8 | export async function DELETE() {
9 | try {
10 | const convex = await getAuthenticatedConvexClient();
11 | const result = await convex.mutation(api.workflows.deleteWorkflowsWithoutUserId, {});
12 |
13 | return NextResponse.json(result);
14 | } catch (error) {
15 | console.error('Error cleaning up workflows:', error);
16 | return NextResponse.json(
17 | {
18 | error: 'Failed to clean up workflows',
19 | message: error instanceof Error ? error.message : 'Unknown error',
20 | },
21 | { status: 500 }
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/api/workflows/import-langgraph/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { langGraphJSONToWorkflow } from '@/lib/workflow/langgraph';
3 | import { saveWorkflow } from '@/lib/workflow/storage';
4 |
5 | export const dynamic = 'force-dynamic';
6 |
7 | /**
8 | * Import workflow from LangGraph JSON
9 | * POST /api/workflows/import-langgraph
10 | */
11 | export async function POST(request: NextRequest) {
12 | try {
13 | const langGraphJSON = await request.json();
14 |
15 | // Validate input
16 | if (!langGraphJSON.nodes || !langGraphJSON.edges) {
17 | return NextResponse.json(
18 | { error: 'Invalid LangGraph JSON: missing nodes or edges' },
19 | { status: 400 }
20 | );
21 | }
22 |
23 | // Convert to workflow format
24 | const workflow = langGraphJSONToWorkflow(langGraphJSON);
25 |
26 | // Save workflow
27 | await saveWorkflow(workflow);
28 |
29 | return NextResponse.json({
30 | success: true,
31 | workflow: {
32 | id: workflow.id,
33 | name: workflow.name,
34 | description: workflow.description,
35 | nodeCount: workflow.nodes.length,
36 | edgeCount: workflow.edges.length,
37 | },
38 | });
39 | } catch (error) {
40 | console.error('Import error:', error);
41 | return NextResponse.json(
42 | {
43 | error: 'Import failed',
44 | message: error instanceof Error ? error.message : 'Unknown error',
45 | },
46 | { status: 500 }
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { GeistMono } from "geist/font/mono";
4 | import { Roboto_Mono } from "next/font/google";
5 | import { Toaster } from "sonner";
6 | import { ClerkProvider, useAuth } from '@clerk/nextjs';
7 | import { ConvexProviderWithClerk } from "convex/react-clerk";
8 | import { ConvexReactClient } from "convex/react";
9 | import ColorStyles from "@/components/shared/color-styles/color-styles";
10 | import Scrollbar from "@/components/ui/scrollbar";
11 | import { BigIntProvider } from "@/components/providers/BigIntProvider";
12 | import "styles/main.css";
13 |
14 | const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
15 |
16 | const robotoMono = Roboto_Mono({
17 | subsets: ["latin"],
18 | weight: ["400", "500"],
19 | variable: "--font-roboto-mono",
20 | });
21 |
22 | // Metadata must be in a separate server component
23 | // For now, set via document head
24 |
25 | export default function RootLayout({
26 | children,
27 | }: {
28 | children: React.ReactNode;
29 | }) {
30 | return (
31 |
32 |
33 |
34 |
35 | Open Agent Builder
36 |
37 |
38 |
39 |
40 |
43 |
44 | {children}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/app/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from '@clerk/nextjs'
2 |
3 | export default function SignInPage() {
4 | return (
5 |
6 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from '@clerk/nextjs'
2 |
3 | export default function SignUpPage() {
4 | return (
5 |
6 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/workflows/[workflowId]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import WorkflowBuilder from "@/components/app/(home)/sections/workflow-builder/WorkflowBuilder";
6 |
7 | export default function WorkflowPage({ params }: { params: Promise<{ workflowId: string }> }) {
8 | const [workflowId, setWorkflowId] = useState(null);
9 | const router = useRouter();
10 |
11 | useEffect(() => {
12 | params.then(({ workflowId }) => {
13 | setWorkflowId(workflowId);
14 | });
15 | }, [params]);
16 |
17 | const handleBack = () => {
18 | router.push('/');
19 | };
20 |
21 | if (!workflowId) {
22 | return Loading...
;
23 | }
24 |
25 | return (
26 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/app/workflows/[workflowId]/run/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import WorkflowBuilder from "@/components/app/(home)/sections/workflow-builder/WorkflowBuilder";
6 |
7 | export default function WorkflowRunPage({ params }: { params: Promise<{ workflowId: string }> }) {
8 | const [workflowId, setWorkflowId] = useState(null);
9 | const router = useRouter();
10 |
11 | useEffect(() => {
12 | params.then(({ workflowId }) => {
13 | setWorkflowId(workflowId);
14 | });
15 | }, [params]);
16 |
17 | const handleBack = () => {
18 | router.push('/');
19 | };
20 |
21 | if (!workflowId) {
22 | return Loading...
;
23 | }
24 |
25 | // This page auto-opens the execution panel
26 | return (
27 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/workflows/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 | import { useRouter } from "next/navigation";
5 |
6 | export default function WorkflowsPage() {
7 | const router = useRouter();
8 |
9 | useEffect(() => {
10 | // Redirect to home
11 | router.push('/');
12 | }, [router]);
13 |
14 | return null;
15 | }
16 |
--------------------------------------------------------------------------------
/atoms/sheets.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai';
2 |
3 | export const isMobileSheetOpenAtom = atom(false);
4 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/endpoints/EndpointsMap/EndpointsMap.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ComponentProps } from "react";
4 |
5 | import EndpointsScrape from "@/components/app/(home)/sections/endpoints/EndpointsScrape/EndpointsScrape";
6 |
7 | export default function EndpointsMap(
8 | props: ComponentProps,
9 | ) {
10 | return ;
11 | }
12 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero-flame/HeroFlame.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef } from "react";
4 |
5 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
6 |
7 | import data from "./data.json";
8 |
9 | export default function HeroFlame() {
10 | const ref = useRef(null);
11 | const ref2 = useRef(null);
12 | const wrapperRef = useRef(null);
13 |
14 | useEffect(() => {
15 | let index = 0;
16 |
17 | const interval = setIntervalOnVisible({
18 | element: wrapperRef.current,
19 | callback: () => {
20 | index++;
21 | if (index >= data.length) index = 0;
22 |
23 | ref.current!.innerHTML = data[index];
24 | ref2.current!.innerHTML = data[index];
25 | },
26 | interval: 85,
27 | });
28 |
29 | return () => interval?.();
30 | }, []);
31 |
32 | return (
33 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero-input/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatePresence, motion } from "motion/react";
2 |
3 | import AnimatedWidth from "@/components/shared/layout/animated-width";
4 | import ArrowRight from "@/components/app/(home)/sections/hero-input/_svg/ArrowRight";
5 | import Button from "@/components/shared/button/Button";
6 |
7 | export default function HeroInputSubmitButton({
8 | tab,
9 | dirty,
10 | }: {
11 | tab: string;
12 | dirty: boolean;
13 | }) {
14 | return (
15 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero-input/_svg/ArrowRight.tsx:
--------------------------------------------------------------------------------
1 | export default function ArrowRight() {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero-input/_svg/Globe.tsx:
--------------------------------------------------------------------------------
1 | export default function Globe() {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero-scraping/Code/Loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatePresence, motion } from "motion/react";
2 | import { useEffect, useState } from "react";
3 |
4 | import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
5 | import AnimatedWidth from "@/components/shared/layout/animated-width";
6 | import Spinner from "@/components/ui/spinner";
7 |
8 | export default function HeroScrapingCodeLoading({
9 | finished,
10 | }: {
11 | finished: boolean;
12 | }) {
13 | const [scrapingText, setScrapingText] = useState("Scraping...");
14 |
15 | useEffect(() => {
16 | if (finished) return;
17 |
18 | let timeout = 0;
19 | let tick = 0;
20 |
21 | const animate = () => {
22 | tick += 1;
23 |
24 | if (tick % 3 !== 0) {
25 | setScrapingText(
26 | encryptText("Scraping", 0, {
27 | randomizeChance: 0.6 + Math.random() * 0.3,
28 | }) + "...",
29 | );
30 | } else {
31 | setScrapingText("Scraping...");
32 | }
33 |
34 | const interval = 80;
35 | timeout = window.setTimeout(animate, interval);
36 | };
37 |
38 | animate();
39 |
40 | return () => {
41 | window.clearTimeout(timeout);
42 | };
43 | }, [finished]);
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
57 | {finished ? "Scrape Completed" : scrapingText}
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero-scraping/Code/Loading/_svg/Check.tsx:
--------------------------------------------------------------------------------
1 | export default function Check() {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero-scraping/HeroScraping.css:
--------------------------------------------------------------------------------
1 | .hero-scraping-highlight::before {
2 | animation: hero-scraping-highlight-before 1s linear infinite;
3 | transition: none !important;
4 | }
5 |
6 | @keyframes hero-scraping-highlight-before {
7 | 0% {
8 | border-color: var(--border-loud);
9 | opacity: 1;
10 | }
11 |
12 | 40% {
13 | opacity: 0.25;
14 | border-color: var(--heat-100);
15 | }
16 |
17 | 80% {
18 | border-color: var(--border-loud);
19 | opacity: 1;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero-scraping/Tag/Tag.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "motion/react";
2 | import { ComponentProps, useEffect, useState } from "react";
3 |
4 | import { encryptText } from "@/components/app/(home)/sections/hero/Title/Title";
5 | import { cn } from "@/utils/cn";
6 |
7 | export default function HeroScrapingTag({
8 | active,
9 | label,
10 | ...attrs
11 | }: ComponentProps & { active?: boolean; label: string }) {
12 | const [value, setValue] = useState(
13 | encryptText(label, 0, { randomizeChance: 0 }),
14 | );
15 |
16 | useEffect(() => {
17 | let progress = 0;
18 | let increaseProgress = -10;
19 |
20 | const animate = () => {
21 | increaseProgress = (increaseProgress + 1) % 5;
22 |
23 | if (increaseProgress === 4) {
24 | progress += 0.2;
25 | }
26 |
27 | if (progress > 1) {
28 | progress = 1;
29 | setValue(encryptText(label, progress, { randomizeChance: 0 }));
30 |
31 | return;
32 | }
33 |
34 | setValue(encryptText(label, progress, { randomizeChance: 0 }));
35 |
36 | const interval = 40 + progress * 20;
37 | setTimeout(animate, interval);
38 | };
39 |
40 | animate();
41 | }, []);
42 |
43 | return (
44 |
66 | {value}
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero-scraping/_svg/BrowserTab.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes } from "react";
2 |
3 | export default function BrowserTab(attrs: HTMLAttributes) {
4 | return (
5 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero/Background/BackgroundOuterPiece.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | import { Connector } from "@/components/shared/layout/curvy-rect";
6 | import {
7 | useHeaderContext,
8 | useHeaderHeight,
9 | } from "@/components/shared/header/HeaderContext";
10 | import { cn } from "@/utils/cn";
11 |
12 | export const BackgroundOuterPiece = () => {
13 | const [noRender, setNoRender] = useState(false);
14 | const { dropdownContent } = useHeaderContext();
15 | const { headerHeight } = useHeaderHeight();
16 |
17 | useEffect(() => {
18 | const heroContent = document.getElementById("hero-content");
19 | if (!heroContent) {
20 | // If hero-content doesn't exist, don't render the background piece
21 | setNoRender(true);
22 | return;
23 | }
24 |
25 | const heroContentHeight = heroContent.clientHeight;
26 |
27 | const onScroll = () => {
28 | setNoRender(window.scrollY > heroContentHeight - 120);
29 | };
30 |
31 | onScroll();
32 |
33 | window.addEventListener("scroll", onScroll);
34 |
35 | return () => {
36 | window.removeEventListener("scroll", onScroll);
37 | };
38 | }, []);
39 |
40 | return (
41 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero/Background/_svg/CenterStar.tsx:
--------------------------------------------------------------------------------
1 | export default function CenterStar({
2 | ...props
3 | }: React.SVGProps) {
4 | return (
5 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero/Badge/Badge.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function HomeHeroBadge() {
4 | return (
5 | e.preventDefault()}
9 | >
10 | OpenAI Inspired Agent Builder
11 |
12 |
13 |
14 |
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero/Pixi/Pixi.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Suspense, lazy, useState, useEffect } from "react";
4 |
5 | const Pixi = lazy(() => import("@/components/shared/pixi/Pixi"));
6 | import features from "./tickers/features";
7 |
8 | function PixiContent() {
9 | return (
10 |
19 | );
20 | }
21 |
22 | export default function HomeHeroPixi() {
23 | const [hasError, setHasError] = useState(false);
24 |
25 | useEffect(() => {
26 | const handleError = (e: ErrorEvent) => {
27 | if (e.message.includes('pixi') || e.message.includes('ChunkLoadError')) {
28 | setHasError(true);
29 | }
30 | };
31 |
32 | window.addEventListener('error', handleError);
33 | return () => window.removeEventListener('error', handleError);
34 | }, []);
35 |
36 | if (hasError) {
37 | // Return empty div as fallback if Pixi fails to load
38 | return ;
39 | }
40 |
41 | return (
42 | }>
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero/Pixi/tickers/features/cell.ts:
--------------------------------------------------------------------------------
1 | import { Ticker } from "@/components/shared/pixi/Pixi";
2 |
3 | import AnimatedRect from "./components/AnimatedRect";
4 | import BlinkingContainer from "./components/BlinkingContainer";
5 | import crawl from "./crawl";
6 | import mapping from "./mapping";
7 | import scrape from "./scrape";
8 | import search from "./search";
9 |
10 | type Props = Parameters[0] & {
11 | x: number;
12 | y: number;
13 | };
14 |
15 | export const CELL_SIZE = 80;
16 |
17 | export const MAIN_COLOR = 0xe6e6e6;
18 |
19 | const animations = [scrape, mapping, search, crawl];
20 |
21 | let lastActive = -1;
22 |
23 | export default function cell(props: Props) {
24 | const blinkingContainer = BlinkingContainer({
25 | x: props.x + 10,
26 | y: props.y + 10,
27 | app: props.app,
28 | });
29 |
30 | const anchorGraphic = AnimatedRect({
31 | app: props.app,
32 | x: CELL_SIZE / 2,
33 | y: CELL_SIZE / 2,
34 | width: 4,
35 | height: 4,
36 | radius: 10,
37 | color: MAIN_COLOR,
38 | });
39 |
40 | blinkingContainer.container.addChild(anchorGraphic.graphic);
41 |
42 | props.app.stage.addChild(blinkingContainer.container);
43 |
44 | let running = false;
45 |
46 | return {
47 | trigger: async () => {
48 | if (running) return;
49 |
50 | running = true;
51 |
52 | lastActive = (lastActive + 1) % animations.length;
53 |
54 | const fn = animations[lastActive];
55 |
56 | await fn({
57 | ...props,
58 | blinkingContainer,
59 | anchorGraphic,
60 | });
61 |
62 | running = false;
63 | },
64 | };
65 | }
66 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero/Pixi/tickers/features/cellReveal.ts:
--------------------------------------------------------------------------------
1 | import { Ticker } from "@/components/shared/pixi/Pixi";
2 | import AnimatedRect from "./components/AnimatedRect";
3 |
4 | type Props = Parameters[0] & {
5 | x: number;
6 | y: number;
7 | };
8 |
9 | export default function cellReveal(props: Props) {
10 | const graphic = AnimatedRect({
11 | app: props.app,
12 | x: props.x + 0.5,
13 | y: props.y + 0.5,
14 | width: 101,
15 | height: 101,
16 | radius: 0,
17 | alpha: 0,
18 | color: 0x000,
19 | centering: false,
20 | });
21 |
22 | props.app.stage.addChild(graphic.graphic);
23 |
24 | return {
25 | trigger: async () => {
26 | let cycleCount = 0;
27 |
28 | const cycle = async () => {
29 | await graphic.animate(
30 | {
31 | alpha: Math.random() * 0.04,
32 | },
33 | {
34 | ease: "linear",
35 | duration: 0.03,
36 | },
37 | );
38 |
39 | if (cycleCount < 5) {
40 | cycleCount += 1;
41 | cycle();
42 | } else {
43 | await graphic.animate({ alpha: 0 });
44 | graphic.graphic.destroy();
45 | }
46 | };
47 |
48 | cycle();
49 | },
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/hero/Pixi/tickers/features/components/Dot.ts:
--------------------------------------------------------------------------------
1 | import { MAIN_COLOR } from "@/components/app/(home)/sections/hero/Pixi/tickers/features/cell";
2 |
3 | import AnimatedRect from "./AnimatedRect";
4 |
5 | export default function Dot(
6 | props: Pick<
7 | Parameters[0],
8 | "x" | "y" | "app" | "animationConfig"
9 | >,
10 | ) {
11 | return AnimatedRect({
12 | ...props,
13 | width: 2,
14 | height: 2,
15 | radius: 10,
16 | color: MAIN_COLOR,
17 | type: "arc",
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/components/app/(home)/sections/workflow-builder/ExportLangGraphButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Download } from 'lucide-react';
4 | import { toast } from 'sonner';
5 |
6 | interface ExportLangGraphButtonProps {
7 | workflowId: string;
8 | workflowName: string;
9 | }
10 |
11 | export function ExportLangGraphButton({ workflowId, workflowName }: ExportLangGraphButtonProps) {
12 | const handleExport = async () => {
13 | try {
14 | const response = await fetch(`/api/workflows/${workflowId}/export-langgraph`);
15 |
16 | if (!response.ok) {
17 | throw new Error('Export failed');
18 | }
19 |
20 | // Download the JSON file
21 | const blob = await response.blob();
22 | const url = window.URL.createObjectURL(blob);
23 | const a = document.createElement('a');
24 | a.href = url;
25 | a.download = `${workflowName.replace(/\s+/g, '_')}_langgraph.json`;
26 | document.body.appendChild(a);
27 | a.click();
28 | window.URL.revokeObjectURL(url);
29 | document.body.removeChild(a);
30 |
31 | toast.success('Workflow exported as LangGraph JSON');
32 | } catch (error) {
33 | console.error('Export error:', error);
34 | toast.error('Failed to export workflow');
35 | }
36 | };
37 |
38 | return (
39 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/icons/FirecrawlLogo.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export default function FirecrawlLogo({ className }: { className?: string }) {
4 | return (
5 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/components/providers/BigIntProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 |
5 | /**
6 | * BigInt Serialization Provider
7 | *
8 | * Adds global JSON.stringify support for BigInt values.
9 | * Required for Next.js 16 beta + React 19 compatibility.
10 | */
11 | export function BigIntProvider({ children }: { children: React.ReactNode }) {
12 | useEffect(() => {
13 | // Add BigInt serialization support globally
14 | if (typeof BigInt !== 'undefined') {
15 | // @ts-ignore - Adding toJSON to BigInt prototype
16 | BigInt.prototype.toJSON = function() {
17 | return this.toString();
18 | };
19 | }
20 | }, []);
21 |
22 | return <>{children}>;
23 | }
24 |
--------------------------------------------------------------------------------
/components/shared/Playground/Context/types.ts:
--------------------------------------------------------------------------------
1 | export enum Endpoint {
2 | Scrape = "scrape",
3 | Crawl = "crawl",
4 | Search = "search",
5 | Map = "map",
6 | Extract = "extract",
7 | }
8 |
9 | export enum AgentModel {
10 | FIRE_1 = "FIRE-1",
11 | }
12 |
13 | export enum FormatType {
14 | Markdown = "markdown",
15 | Summary = "summary",
16 | Json = "json",
17 | RawHtml = "rawHtml",
18 | Html = "html",
19 | Screenshot = "screenshot",
20 | ScreenshotFullPage = "screenshot@fullPage",
21 | Links = "links",
22 | }
23 |
24 | export enum SearchFormatType {
25 | Web = "web",
26 | Images = "images",
27 | News = "news",
28 | }
29 |
30 | type Prev = [never, 0, 1, 2, 3, 4, 5];
31 |
32 | type Join = K extends string | number
33 | ? P extends string | number
34 | ? `${K}.${P}`
35 | : never
36 | : never;
37 |
38 | export type Paths = [D] extends [never]
39 | ? never
40 | : T extends object
41 | ? {
42 | [K in keyof T]-?: K extends string | number
43 | ? T[K] extends object
44 | ? K | Join>
45 | : K
46 | : never;
47 | }[keyof T]
48 | : "";
49 |
--------------------------------------------------------------------------------
/components/shared/ascii-flame-background.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useRef } from "react";
4 | import { cn } from "@/utils/cn";
5 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
6 | import data from "@/components/shared/effects/flame/explosion-data.json";
7 |
8 | interface AsciiFlameBackgroundProps {
9 | className?: string;
10 | colorClassName?: string;
11 | fontSizePx?: number;
12 | lineHeightPx?: number;
13 | }
14 |
15 | // Reusable ASCII flame background (same frames used by CoreReliableBarFlame)
16 | export default function AsciiFlameBackground({
17 | className,
18 | colorClassName = "text-heat-100/30",
19 | fontSizePx = 10,
20 | lineHeightPx = 12.5,
21 | }: AsciiFlameBackgroundProps) {
22 | const wrapperRef = useRef(null);
23 | const ref = useRef(null);
24 |
25 | useEffect(() => {
26 | let index = 0;
27 | const stop = setIntervalOnVisible({
28 | element: wrapperRef.current,
29 | callback: () => {
30 | index += 1;
31 | if (index >= (data as string[]).length) index = 0;
32 | if (ref.current) ref.current.innerHTML = (data as string[])[index];
33 | },
34 | interval: 80,
35 | });
36 |
37 | return () => stop?.();
38 | }, []);
39 |
40 | return (
41 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/components/shared/buttons/fire-action-link.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { cn } from "@/utils/cn";
3 |
4 | interface FireActionLinkProps {
5 | href?: string;
6 | label: string;
7 | className?: string;
8 | variant?: "link" | "button";
9 | onClick?: () => void;
10 | }
11 |
12 | export function FireActionLink({
13 | href,
14 | label,
15 | className,
16 | variant = "link",
17 | onClick,
18 | }: FireActionLinkProps) {
19 | const baseClasses =
20 | variant === "button"
21 | ? cn(
22 | "inline-block py-4 px-8 rounded-6",
23 | "text-label-small text-heat-100 bg-heat-4",
24 | "hover:bg-heat-8 transition-all",
25 | "active:scale-[0.98]",
26 | className,
27 | )
28 | : cn(
29 | "text-label-small text-secondary hover:text-heat-100 transition-all",
30 | "hover:underline underline-offset-4",
31 | "active:scale-[0.98]",
32 | className,
33 | );
34 |
35 | if (onClick) {
36 | return (
37 |
40 | );
41 | }
42 |
43 | return (
44 |
45 | {label}
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/components/shared/buttons/index.ts:
--------------------------------------------------------------------------------
1 | // Button Components
2 | export { SlateButton } from "./slate-button";
3 | // export { HeatButton } from "./heat-button";
4 | export { FireActionLink } from "./fire-action-link";
5 |
--------------------------------------------------------------------------------
/components/shared/color-styles/color-styles.tsx:
--------------------------------------------------------------------------------
1 | import colors from "@/styles/colors.json";
2 |
3 | const TYPED_COLORS = colors as unknown as Record<
4 | string,
5 | Record<"hex" | "p3", string>
6 | >;
7 |
8 | const hslValues = Object.entries(TYPED_COLORS).map(([key, value]) => {
9 | // Fix hex values - they need # prefix
10 | const hexValue = value.hex.startsWith("#") ? value.hex : `#${value.hex}`;
11 | return `--${key}: ${hexValue}`;
12 | });
13 |
14 | const p3Values = Object.entries(TYPED_COLORS)
15 | .filter(([, value]) => value.p3)
16 | .map(([key, value]) => `--${key}: color(display-p3 ${value.p3})`);
17 |
18 | const colorsStyle = `
19 | :root {
20 | ${hslValues.join(";\n ")}
21 | }
22 |
23 | @supports (color: color(display-p3 1 1 1)) {
24 | :root {
25 | ${p3Values.join(";\n ")}
26 | }
27 | }`;
28 |
29 | export default function ColorStyles() {
30 | return ;
31 | }
32 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/Flame.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { HTMLAttributes, useEffect, useRef } from "react";
4 |
5 | import { cn } from "@/utils/cn";
6 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
7 |
8 | import data from "./hero-flame-data.json";
9 |
10 | export default function CoreFlame(attrs: HTMLAttributes) {
11 | const ref = useRef(null);
12 | const wrapperRef = useRef(null);
13 |
14 | useEffect(() => {
15 | let index = 0;
16 |
17 | const interval = setIntervalOnVisible({
18 | element: wrapperRef.current,
19 | callback: () => {
20 | index++;
21 | if (index >= data.length) index = 0;
22 |
23 | const newStr = data[index];
24 |
25 | ref.current!.innerHTML = newStr;
26 | },
27 | interval: 80,
28 | });
29 |
30 | return () => interval?.();
31 | }, []);
32 |
33 | return (
34 | <>
35 |
55 | >
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/ascii-explosion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { HTMLAttributes, useEffect, useRef } from "react";
4 |
5 | import { cn } from "@/utils/cn";
6 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
7 |
8 | import data from "./explosion-data.json";
9 |
10 | export function AsciiExplosion(attrs: HTMLAttributes) {
11 | const ref = useRef(null);
12 | const wrapperRef = useRef(null);
13 |
14 | useEffect(() => {
15 | let index = -30;
16 |
17 | const interval = setIntervalOnVisible({
18 | element: wrapperRef.current,
19 | callback: () => {
20 | index++;
21 | if (index >= data.length) index = -40;
22 | if (index < 0) return;
23 |
24 | ref.current!.innerHTML = data[index];
25 | },
26 | interval: 40,
27 | });
28 |
29 | return () => interval?.();
30 | }, []);
31 |
32 | return (
33 |
52 | );
53 | }
54 |
55 | // Default export for backward compatibility
56 | export default AsciiExplosion;
57 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/auth-pulse/auth-pulse.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { HTMLAttributes, useEffect, useRef } from "react";
4 | import { cn } from "@/utils/cn";
5 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
6 | import data from "./pulse-data.json";
7 |
8 | interface AuthPulseProps extends HTMLAttributes {
9 | interval?: number;
10 | opacity?: number;
11 | }
12 |
13 | export function AuthPulse({
14 | interval = 100,
15 | opacity = 0.15,
16 | className,
17 | ...attrs
18 | }: AuthPulseProps) {
19 | const ref = useRef(null);
20 | const wrapperRef = useRef(null);
21 | const frameIndex = useRef(0);
22 |
23 | useEffect(() => {
24 | const animate = () => {
25 | if (ref.current) {
26 | ref.current.innerHTML = data[frameIndex.current];
27 | frameIndex.current = (frameIndex.current + 1) % data.length;
28 | }
29 | };
30 |
31 | // Initialize first frame
32 | animate();
33 |
34 | const cleanup = setIntervalOnVisible({
35 | element: wrapperRef.current,
36 | callback: animate,
37 | interval,
38 | });
39 |
40 | return () => cleanup?.();
41 | }, [interval]);
42 |
43 | return (
44 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/core-flame.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { HTMLAttributes, useEffect, useRef } from "react";
4 | import { cn } from "@/utils/cn";
5 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
6 | import data from "./core-flame.json";
7 |
8 | export function CoreFlame(attrs: HTMLAttributes) {
9 | const ref = useRef(null);
10 | const wrapperRef = useRef(null);
11 |
12 | useEffect(() => {
13 | let index = 0;
14 |
15 | const interval = setIntervalOnVisible({
16 | element: wrapperRef.current,
17 | callback: () => {
18 | index++;
19 | if (index >= data.length) index = 0;
20 |
21 | const newStr = data[index];
22 |
23 | ref.current!.innerHTML = newStr;
24 | },
25 | interval: 80,
26 | });
27 |
28 | return () => interval?.();
29 | }, []);
30 |
31 | return (
32 | <>
33 |
53 | >
54 | );
55 | }
56 |
57 | // Export default for backward compatibility
58 | export default CoreFlame;
59 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/flame-background.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { cn } from "@/utils/cn";
5 | import { CoreFlame } from "./core-flame";
6 |
7 | interface FlameBackgroundProps {
8 | intensity?: number; // 0-100, like CPU usage
9 | animate?: boolean;
10 | className?: string;
11 | children?: React.ReactNode;
12 | }
13 |
14 | export function FlameBackground({
15 | intensity = 0,
16 | animate = false,
17 | className,
18 | children,
19 | }: FlameBackgroundProps) {
20 | // Convert 0-100 to 0-0.3 opacity
21 | const opacity = Math.min((intensity / 100) * 0.3, 0.3);
22 |
23 | // Speed increases with intensity
24 | const speed = Math.max(80 - (intensity / 100) * 40, 40);
25 |
26 | // Color gets more orange with intensity
27 | const color =
28 | intensity > 80 ? "heat-100" : intensity > 50 ? "heat-40" : "black-alpha-20";
29 |
30 | return (
31 |
32 |
38 | {children &&
{children}
}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/hero-flame-right.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef } from "react";
4 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
5 | import data from "./hero-flame-data.json";
6 |
7 | interface HeroFlameRightProps {
8 | className?: string;
9 | style?: React.CSSProperties;
10 | hideOnMobile?: boolean;
11 | }
12 |
13 | export default function HeroFlameRight({
14 | className = "",
15 | style,
16 | hideOnMobile = true,
17 | }: HeroFlameRightProps) {
18 | const ref2 = useRef(null);
19 | const wrapperRef = useRef(null);
20 |
21 | useEffect(() => {
22 | let index = 0;
23 |
24 | const interval = setIntervalOnVisible({
25 | element: wrapperRef.current,
26 | callback: () => {
27 | index++;
28 | if (index >= data.length) index = 0;
29 |
30 | if (ref2.current) {
31 | ref2.current.innerHTML = data[index];
32 | }
33 | },
34 | interval: 85,
35 | });
36 |
37 | return () => interval?.();
38 | }, []);
39 |
40 | return (
41 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/hero-flame.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef } from "react";
4 |
5 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
6 | import data from "./hero-flame-data.json";
7 |
8 | export default function HeroFlame() {
9 | const ref = useRef(null);
10 | const ref2 = useRef(null);
11 | const wrapperRef = useRef(null);
12 |
13 | useEffect(() => {
14 | let index = 0;
15 |
16 | const interval = setIntervalOnVisible({
17 | element: wrapperRef.current,
18 | callback: () => {
19 | index++;
20 | if (index >= data.length) index = 0;
21 |
22 | if (ref.current) {
23 | ref.current.innerHTML = data[index];
24 | }
25 |
26 | if (ref2.current) {
27 | ref2.current.innerHTML = data[index];
28 | }
29 | },
30 | interval: 85,
31 | });
32 |
33 | return () => interval?.();
34 | }, []);
35 |
36 | return (
37 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/index.ts:
--------------------------------------------------------------------------------
1 | export { CoreFlame } from "./core-flame";
2 | export { AsciiExplosion } from "./ascii-explosion";
3 | export { default as HeroFlame } from "./hero-flame";
4 | export { SubtleExplosion } from "./subtle-explosion";
5 |
6 | // Convenience wrapper for dashboard usage
7 | export { FlameBackground } from "./flame-background";
8 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/slate-grid/slate-grid.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { HTMLAttributes, useEffect, useRef } from "react";
4 | import { cn } from "@/utils/cn";
5 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
6 | import data from "./grid-data.json";
7 |
8 | interface SlateGridProps extends HTMLAttributes {
9 | interval?: number;
10 | color?: string;
11 | }
12 |
13 | export function SlateGrid({
14 | interval = 200,
15 | color = "text-black-alpha-12",
16 | className,
17 | ...attrs
18 | }: SlateGridProps) {
19 | const ref = useRef(null);
20 | const wrapperRef = useRef(null);
21 | const frameIndex = useRef(0);
22 |
23 | useEffect(() => {
24 | const animate = () => {
25 | if (ref.current) {
26 | ref.current.innerHTML = data[frameIndex.current];
27 | frameIndex.current = (frameIndex.current + 1) % data.length;
28 | }
29 | };
30 |
31 | // Initialize first frame
32 | animate();
33 |
34 | const cleanup = setIntervalOnVisible({
35 | element: wrapperRef.current,
36 | callback: animate,
37 | interval,
38 | });
39 |
40 | return () => cleanup?.();
41 | }, [interval]);
42 |
43 | return (
44 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/subtle-explosion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { HTMLAttributes, useEffect, useRef } from "react";
4 | import { cn } from "@/utils/cn";
5 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
6 | import data from "./explosion-data.json";
7 |
8 | interface SubtleExplosionProps extends HTMLAttributes {
9 | interval?: number;
10 | delay?: number;
11 | opacity?: number;
12 | }
13 |
14 | export function SubtleExplosion({
15 | interval = 80,
16 | delay = 30,
17 | opacity = 0.08,
18 | className,
19 | ...attrs
20 | }: SubtleExplosionProps) {
21 | const ref = useRef(null);
22 | const wrapperRef = useRef(null);
23 |
24 | useEffect(() => {
25 | let index = -delay;
26 |
27 | const animate = () => {
28 | index++;
29 | if (index >= data.length) index = -delay;
30 | if (index < 0) return;
31 |
32 | if (ref.current) {
33 | ref.current.innerHTML = data[index];
34 | }
35 | };
36 |
37 | const cleanup = setIntervalOnVisible({
38 | element: wrapperRef.current,
39 | callback: animate,
40 | interval,
41 | });
42 |
43 | return () => cleanup?.();
44 | }, [interval, delay]);
45 |
46 | return (
47 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/components/shared/effects/flame/subtle-wave/subtle-wave.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useRef } from "react";
4 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
5 | import data from "./wave-data.json";
6 |
7 | export default function SubtleWave({ className = "" }: { className?: string }) {
8 | const containerRef = useRef(null);
9 | const frameIndex = useRef(0);
10 |
11 | useEffect(() => {
12 | const animateWave = () => {
13 | if (containerRef.current) {
14 | containerRef.current.innerHTML = data[frameIndex.current];
15 | frameIndex.current = (frameIndex.current + 1) % data.length;
16 | }
17 | };
18 |
19 | // Initialize first frame
20 | animateWave();
21 |
22 | // Start animation when visible
23 | const cleanup = setIntervalOnVisible({
24 | element: containerRef.current,
25 | callback: animateWave,
26 | interval: 150, // Slower for subtlety
27 | });
28 |
29 | return () => {
30 | cleanup?.();
31 | };
32 | }, []);
33 |
34 | return (
35 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/components/shared/effects/index.ts:
--------------------------------------------------------------------------------
1 | // Effect Components
2 | export { CoreFlame } from "./flame/core-flame";
3 | export { AsciiExplosion } from "./flame/ascii-explosion";
4 |
--------------------------------------------------------------------------------
/components/shared/effects/subtle-ascii-animation.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useRef } from "react";
4 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
5 |
6 | export default function SubtleAsciiAnimation({
7 | className = "",
8 | }: {
9 | className?: string;
10 | }) {
11 | const containerRef = useRef(null);
12 |
13 | // Simple ASCII pattern for subtle animation
14 | const asciiFrames = [
15 | "░░░░░░░░░░░░░░░░",
16 | "▒░░░░░░░░░░░░░░░",
17 | "▒▒░░░░░░░░░░░░░░",
18 | "░▒▒░░░░░░░░░░░░░",
19 | "░░▒▒░░░░░░░░░░░░",
20 | "░░░▒▒░░░░░░░░░░░",
21 | "░░░░▒▒░░░░░░░░░░",
22 | "░░░░░▒▒░░░░░░░░░",
23 | "░░░░░░▒▒░░░░░░░░",
24 | "░░░░░░░▒▒░░░░░░░",
25 | "░░░░░░░░▒▒░░░░░░",
26 | "░░░░░░░░░▒▒░░░░░",
27 | "░░░░░░░░░░▒▒░░░░",
28 | "░░░░░░░░░░░▒▒░░░",
29 | "░░░░░░░░░░░░▒▒░░",
30 | "░░░░░░░░░░░░░▒▒░",
31 | "░░░░░░░░░░░░░░▒▒",
32 | "░░░░░░░░░░░░░░░▒",
33 | ];
34 |
35 | useEffect(() => {
36 | let frameIndex = 0;
37 |
38 | const animateAscii = () => {
39 | if (containerRef.current) {
40 | containerRef.current.innerHTML = asciiFrames[frameIndex];
41 | frameIndex = (frameIndex + 1) % asciiFrames.length;
42 | }
43 | };
44 |
45 | // Initialize first frame
46 | animateAscii();
47 |
48 | // Start animation when visible
49 | const cleanup = setIntervalOnVisible({
50 | element: containerRef.current,
51 | callback: animateAscii,
52 | interval: 150, // Slightly slower for subtlety
53 | });
54 |
55 | return () => {
56 | cleanup?.();
57 | };
58 | }, []);
59 |
60 | return (
61 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/components/shared/firecrawl-icon/firecrawl-icon-static.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes } from "react";
2 |
3 | export default function FirecrawlIconStatic({
4 | fill = "var(--heat-100)",
5 | className = "",
6 | ...attrs
7 | }: HTMLAttributes & { fill?: string }) {
8 | return (
9 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/components/shared/header/BrandKit/_svg/Download.tsx:
--------------------------------------------------------------------------------
1 | export default function Download() {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/header/BrandKit/_svg/Guidelines.tsx:
--------------------------------------------------------------------------------
1 | export default function Guidelines() {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/header/BrandKit/_svg/Icon.tsx:
--------------------------------------------------------------------------------
1 | export default function Icon() {
2 | return (
3 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/components/shared/header/Dropdown/Github/Flame/Flame.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { HTMLAttributes, useEffect, useRef } from "react";
4 |
5 | import { cn } from "@/utils/cn";
6 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
7 |
8 | import data from "./data.json";
9 |
10 | export default function GithubFlame(attrs: HTMLAttributes) {
11 | const ref = useRef(null);
12 | const wrapperRef = useRef(null);
13 |
14 | useEffect(() => {
15 | let index = 0;
16 |
17 | const interval = setIntervalOnVisible({
18 | element: wrapperRef.current,
19 | callback: () => {
20 | index++;
21 | if (index >= data.length) index = 0;
22 |
23 | const newStr = data[index];
24 |
25 | if (!ref.current) return;
26 |
27 | ref.current!.innerHTML = newStr;
28 | },
29 | interval: 60,
30 | });
31 |
32 | return () => interval?.();
33 | }, []);
34 |
35 | return (
36 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/components/shared/header/Dropdown/Github/Github.tsx:
--------------------------------------------------------------------------------
1 | import Image from "@/components/shared/image/Image";
2 |
3 | import GithubFlame from "./Flame/Flame";
4 |
5 | export default function HeaderDropdownGithub() {
6 | return (
7 |
8 |
9 |
17 |
18 |
19 |
20 | Firecrawl is open source.
21 | Star us to show your support!
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/components/shared/header/Dropdown/Mobile/Mobile.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 |
3 | import Button from "@/components/ui/shadcn/button";
4 | import {
5 | ConnectorToBottom,
6 | ConnectorToLeft,
7 | ConnectorToRight,
8 | } from "@/components/shared/layout/curvy-rect";
9 | import HeaderGithubClient from "@/components/shared/header/Github/GithubClient";
10 | import { NAV_ITEMS } from "@/components/shared/header/Nav/Nav";
11 |
12 | import HeaderDropdownMobileItem from "./Item/Item";
13 | import Link from "next/link";
14 |
15 | export default function HeaderDropdownMobile({
16 | ctaHref = "/signin/signup",
17 | ctaLabel = "Sign up",
18 | }: {
19 | ctaHref?: string;
20 | ctaLabel?: string;
21 | }) {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 | {NAV_ITEMS.map((item) => (
30 |
31 |
32 |
33 | ))}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/components/shared/header/Dropdown/Stories/Flame/Flame.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { HTMLAttributes, useEffect, useRef } from "react";
4 |
5 | import data from "@/components/app/(home)/sections/hero-flame/data.json";
6 | import { cn } from "@/utils/cn";
7 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
8 |
9 | export default function StoriesFlame(attrs: HTMLAttributes) {
10 | const ref = useRef(null);
11 | const wrapperRef = useRef(null);
12 |
13 | useEffect(() => {
14 | let index = 0;
15 |
16 | const interval = setIntervalOnVisible({
17 | element: wrapperRef.current,
18 | callback: () => {
19 | index++;
20 | if (index >= data.length) index = 0;
21 |
22 | const newStr = data[index];
23 |
24 | if (!ref.current) return;
25 |
26 | ref.current!.innerHTML = newStr;
27 | },
28 | interval: 60,
29 | });
30 |
31 | return () => interval?.();
32 | }, []);
33 |
34 | return (
35 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/components/shared/header/Dropdown/Stories/Stories.tsx:
--------------------------------------------------------------------------------
1 | import ArrowUp from "./_svg/ArrowUp";
2 | import Replit from "./_svg/Replit";
3 | import StoriesFlame from "./Flame/Flame";
4 |
5 | export default function HeaderDropdownStories() {
6 | return (
7 |
11 |
12 |
13 | Customer story
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 | How Replit uses Firecrawl to
25 | power Replit Agent
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/components/shared/header/Dropdown/Stories/_svg/ArrowUp.tsx:
--------------------------------------------------------------------------------
1 | export default function ArrowUp() {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/header/Dropdown/Stories/_svg/Replit.tsx:
--------------------------------------------------------------------------------
1 | export default function Replit() {
2 | return (
3 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/shared/header/Github/GithubClient.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Button from "@/components/ui/shadcn/button";
4 | import GithubIcon from "./_svg/GithubIcon";
5 |
6 | export default function HeaderGithubClient() {
7 | return (
8 |
13 |
17 |
18 | );
19 | }
--------------------------------------------------------------------------------
/components/shared/header/Github/_svg/GithubIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function GithubIcon() {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/Item/Item.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { JSX } from "react";
4 |
5 | import { useHeaderContext } from "@/components/shared/header/HeaderContext";
6 | import { cn } from "@/utils/cn";
7 |
8 | import ChevronDown from "./_svg/ChevronDown";
9 |
10 | export default function HeaderNavItem({
11 | label,
12 | href,
13 | dropdown,
14 | }: {
15 | label: string;
16 | href: string;
17 | dropdown?: JSX.Element;
18 | }) {
19 | const { dropdownContent, setDropdownContent, clearDropdown } =
20 | useHeaderContext();
21 |
22 | const active = dropdownContent === dropdown;
23 |
24 | return (
25 | {
29 | if (dropdown) {
30 | setDropdownContent(dropdown);
31 | } else {
32 | clearDropdown(true);
33 | }
34 | }}
35 | onMouseLeave={() => {
36 | if (!dropdown) return;
37 |
38 | clearDropdown();
39 | }}
40 | >
41 |
47 |
48 | {label}
49 |
50 | {dropdown && }
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/Item/_svg/ChevronDown.tsx:
--------------------------------------------------------------------------------
1 | export default function ChevronDown() {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/RenderEndpointIcon.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import EndpointsScrape from "@/components/app/(home)/sections/endpoints/EndpointsScrape/EndpointsScrape";
4 | import { ComponentProps } from "react";
5 | import { useMediaQuery } from "usehooks-ts";
6 |
7 | export const RenderEndpointIcon = ({
8 | icon: Icon,
9 | ...props
10 | }: { icon: typeof EndpointsScrape } & ComponentProps<
11 | typeof EndpointsScrape
12 | >) => {
13 | const isMobile = useMediaQuery("(max-width: 996px)");
14 |
15 | return ;
16 | };
17 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Affiliate.tsx:
--------------------------------------------------------------------------------
1 | export default function Affiliate() {
2 | return (
3 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Api.tsx:
--------------------------------------------------------------------------------
1 | export default function Api() {
2 | return (
3 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/ArrowRight.tsx:
--------------------------------------------------------------------------------
1 | export default function ArrowRight() {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Careers.tsx:
--------------------------------------------------------------------------------
1 | export default function Careers() {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Changelog.tsx:
--------------------------------------------------------------------------------
1 | export default function Changelog() {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Chats.tsx:
--------------------------------------------------------------------------------
1 | export default function Ai() {
2 | return (
3 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Lead.tsx:
--------------------------------------------------------------------------------
1 | export default function Lead() {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/MCP.tsx:
--------------------------------------------------------------------------------
1 | export default function MCPIcon() {
2 | return (
3 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Platforms.tsx:
--------------------------------------------------------------------------------
1 | export default function Platforms() {
2 | return (
3 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Research.tsx:
--------------------------------------------------------------------------------
1 | export default function Research() {
2 | return (
3 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Student.tsx:
--------------------------------------------------------------------------------
1 | export default function Student() {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/header/Nav/_svg/Templates.tsx:
--------------------------------------------------------------------------------
1 | export default function Templates() {
2 | return (
3 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/shared/header/Toggle/Toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Button from "@/components/ui/shadcn/button";
4 | import { useHeaderContext } from "@/components/shared/header/HeaderContext";
5 | import { cn } from "@/utils/cn";
6 |
7 | export default function HeaderToggle({
8 | dropdownContent,
9 | }: {
10 | dropdownContent: React.ReactNode;
11 | }) {
12 | const {
13 | dropdownContent: headerDropdownContent,
14 | clearDropdown,
15 | setDropdownContent,
16 | } = useHeaderContext();
17 |
18 | return (
19 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/components/shared/header/Wrapper/Wrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname } from "next/navigation";
4 | import { useEffect, useState } from "react";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | export default function HeaderWrapper({
9 | children,
10 | }: {
11 | children: React.ReactNode;
12 | }) {
13 | const [shouldShrink, setShouldShrink] = useState(false);
14 | const pathname = usePathname();
15 |
16 | useEffect(() => {
17 | const heroContentHeight =
18 | document.getElementById("hero-content")?.clientHeight;
19 | const triggerTop = heroContentHeight ? heroContentHeight : 100;
20 |
21 | const onScroll = () => {
22 | setShouldShrink(window.scrollY > triggerTop);
23 | };
24 |
25 | onScroll();
26 |
27 | window.addEventListener("scroll", onScroll);
28 | }, [pathname]);
29 |
30 | return (
31 |
37 | {children}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/components/shared/icons/GitHub.tsx:
--------------------------------------------------------------------------------
1 | const GitHub = ({ ...props }) => {
2 | return (
3 |
18 | );
19 | };
20 |
21 | export default GitHub;
22 |
--------------------------------------------------------------------------------
/components/shared/icons/Logo.tsx:
--------------------------------------------------------------------------------
1 | const Logo = ({ ...props }) => (
2 |
18 | );
19 | export default Logo;
20 |
--------------------------------------------------------------------------------
/components/shared/icons/animated-chevron.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { ChevronDown, ChevronUp } from "lucide-react";
5 | import { motion, AnimatePresence } from "framer-motion";
6 | import { cn } from "@/utils/cn";
7 |
8 | interface AnimatedChevronProps {
9 | isOpen: boolean;
10 | className?: string;
11 | size?: "sm" | "md" | "lg";
12 | }
13 |
14 | export function AnimatedChevron({
15 | isOpen,
16 | className,
17 | size = "md",
18 | }: AnimatedChevronProps) {
19 | const sizeClasses = {
20 | sm: "h-12 w-12",
21 | md: "h-16 w-16",
22 | lg: "h-20 w-20",
23 | };
24 |
25 | return (
26 |
27 |
28 | {isOpen ? (
29 |
36 |
43 |
44 | ) : (
45 |
52 |
59 |
60 | )}
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/components/shared/icons/arrow-animated.tsx:
--------------------------------------------------------------------------------
1 | import { cx } from "class-variance-authority";
2 |
3 | export function ArrowAnimated({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/components/shared/icons/check.tsx:
--------------------------------------------------------------------------------
1 | export default function Check() {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/icons/chevron-slide.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { cn } from "@/utils/cn";
5 |
6 | type Direction = "left" | "right";
7 |
8 | interface ChevronSlideProps extends React.SVGAttributes {
9 | direction?: Direction;
10 | size?: number; // pixel size for width/height
11 | }
12 |
13 | export function ChevronSlide({
14 | direction = "right",
15 | size = 16,
16 | className,
17 | ...props
18 | }: ChevronSlideProps) {
19 | const translateClass =
20 | direction === "right"
21 | ? "group-hover:translate-x-8"
22 | : "group-hover:-translate-x-8";
23 | const orientationClass = direction === "right" ? "" : "rotate-180";
24 |
25 | return (
26 |
49 | );
50 | }
51 |
52 | export default ChevronSlide;
53 |
--------------------------------------------------------------------------------
/components/shared/icons/copied.tsx:
--------------------------------------------------------------------------------
1 | export default function CopiedIcon() {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/copy.tsx:
--------------------------------------------------------------------------------
1 | export default function CopyIcon() {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/curve.tsx:
--------------------------------------------------------------------------------
1 | interface CurveProps extends React.SVGAttributes {
2 | fill?: string;
3 | }
4 |
5 | export default function Curve({
6 | fill = "var(--border-faint)",
7 | ...props
8 | }: CurveProps) {
9 | return (
10 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/shared/icons/openai.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | function IconOpenai(props: React.SVGProps) {
4 | return (
5 |
15 | );
16 | }
17 |
18 | export default IconOpenai;
19 |
--------------------------------------------------------------------------------
/components/shared/icons/source-icon.tsx:
--------------------------------------------------------------------------------
1 | import { JSXElementConstructor } from "react";
2 | import Image from "next/image";
3 |
4 | export const SourceIcon = ({ id }: { id: string }) => {
5 | return (
6 |
7 |
8 | {id && (
9 |
16 | )}
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/components/shared/icons/symbol-colored.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SymbolColored = ({ ...props }) => {
4 | return (
5 |
19 | );
20 | };
21 |
22 | export default SymbolColored;
23 |
--------------------------------------------------------------------------------
/components/shared/icons/symbol-white.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SymbolWhite = ({ ...props }) => {
4 | return (
5 |
18 | );
19 | };
20 |
21 | export default SymbolWhite;
22 |
--------------------------------------------------------------------------------
/components/shared/image/Image.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import { ComponentProps } from "react";
3 |
4 | import compressorConfig from "@/public/compressor.json";
5 |
6 | interface Props extends ComponentProps<"img"> {
7 | src: string;
8 | alt: string;
9 | raw?: boolean;
10 | }
11 |
12 | const BASE_SRC = "/assets/";
13 | const RAW_SRC = "/assets-original/";
14 |
15 | export default function Image({ src, raw, ...attrs }: Props) {
16 | if (raw) {
17 | return (
18 |
25 | );
26 | }
27 |
28 | return (
29 |
30 | {compressorConfig.configs
31 | .sort((a, b) => {
32 | if (a.extension === "avif" && b.extension !== "avif") return -1;
33 | if (b.extension === "avif" && a.extension !== "avif") return 1;
34 |
35 | return a.scale - b.scale;
36 | })
37 | .map((c) => {
38 | return (
39 |
44 | );
45 | })}
46 |
47 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/components/shared/image/getImageSrc.ts:
--------------------------------------------------------------------------------
1 | import compressorConfig from "@/public/compressor.json";
2 |
3 | const avifConfig = compressorConfig.configs.find(
4 | (c) => c.extension === "avif",
5 | )!;
6 | const webpConfig = compressorConfig.configs.find(
7 | (c) => c.extension === "webp",
8 | )!;
9 |
10 | export async function getImageSrc(src: string) {
11 | const BASE_SRC = "/assets/";
12 |
13 | if (await supportsEncode()) {
14 | return `${BASE_SRC}${src}_q${avifConfig.quality}@${avifConfig.scale}x.avif`;
15 | }
16 |
17 | return `${BASE_SRC}${src}_q${webpConfig.quality}@${webpConfig.scale}x.webp`;
18 | }
19 |
20 | let promise: Promise | null = null;
21 |
22 | async function supportsEncode() {
23 | if (promise) return promise;
24 |
25 | const avifData =
26 | "data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUEAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAABYAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgSAAAAAAABNjb2xybmNseAACAAIABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB5tZGF0EgAKBzgADlAgIGkyCR/wAABAAACvcA==";
27 |
28 | promise = fetch(avifData)
29 | .then((r) => r.blob())
30 | .then((b) => createImageBitmap(b))
31 | .then(() => true)
32 | .catch(() => false);
33 |
34 | return promise;
35 | }
36 |
--------------------------------------------------------------------------------
/components/shared/layout/animated-height.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion, MotionProps, TargetAndTransition } from "motion/react";
4 | import { useEffect, useRef, useState } from "react";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | type AnimatedHeight = {
9 | children: React.ReactNode;
10 | animate?: TargetAndTransition;
11 | initial?: TargetAndTransition;
12 | exit?: TargetAndTransition;
13 | className?: string;
14 | transition?: MotionProps["transition"];
15 | };
16 |
17 | export default function AnimatedHeight({ children, ...attrs }: AnimatedHeight) {
18 | const containerRef = useRef(null);
19 |
20 | const [height, setHeight] = useState("auto");
21 |
22 | useEffect(() => {
23 | const child = containerRef.current?.children[0] as Element;
24 |
25 | const updateHeight = () => {
26 | if (!child) return;
27 |
28 | setHeight(child.clientHeight);
29 | };
30 |
31 | updateHeight();
32 |
33 | const resizeObserver = new ResizeObserver(updateHeight);
34 |
35 | resizeObserver.observe(child);
36 |
37 | return () => resizeObserver.disconnect();
38 | }, []);
39 |
40 | return (
41 |
54 | {children}
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/components/shared/layout/animated-width.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion, TargetAndTransition, Transition } from "motion/react";
4 | import { useEffect, useRef, useState } from "react";
5 |
6 | type AnimatedWidthProps = {
7 | children: React.ReactNode;
8 | animate?: TargetAndTransition;
9 | initial?: TargetAndTransition;
10 | transition?: Transition;
11 | };
12 |
13 | export default function AnimatedWidth({
14 | children,
15 | ...attrs
16 | }: AnimatedWidthProps) {
17 | const containerRef = useRef(null);
18 |
19 | const [width, setWidth] = useState("auto");
20 |
21 | useEffect(() => {
22 | const child = containerRef.current?.children[0] as Element;
23 |
24 | const updateWidth = () => {
25 | if (!child) return;
26 |
27 | setWidth(child.clientWidth);
28 | };
29 |
30 | updateWidth();
31 |
32 | const resizeObserver = new ResizeObserver(updateWidth);
33 |
34 | resizeObserver.observe(child);
35 |
36 | return () => resizeObserver.disconnect();
37 | }, []);
38 |
39 | return (
40 |
53 | {children}
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/components/shared/layout/curvy-rect-divider.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import CurvyRect from "./curvy-rect";
4 |
5 | export function CurvyRectDivider() {
6 | return (
7 |
8 |
9 |
13 |
17 |
21 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/components/shared/loading/usage-loading.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import ScrambleText from "@/components/ui/motion/scramble-text";
5 |
6 | export function UsageLoadingText({ text = "Loading..." }: { text?: string }) {
7 | const [isInView, setIsInView] = useState(false);
8 |
9 | useEffect(() => {
10 | setIsInView(true);
11 | }, []);
12 |
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/lockBody.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Utility to lock/unlock the document body based on a set of keys.
3 | * Each key can "lock" the body (e.g., prevent scrolling), and the lock is only released when all keys are removed.
4 | *
5 | * @param {string} key - Unique identifier for the lock (e.g., component name or id)
6 | * @param {'lock'|'unlock'} action - Whether to lock or unlock
7 | * @param {(locked: boolean) => void} [onLockChange] - Optional callback when lock state changes
8 | */
9 | const activeLocks = new Set();
10 |
11 | export function lockBody(
12 | key: string,
13 | action: boolean,
14 | onLockChange?: (locked: boolean) => void,
15 | ) {
16 | if (action) {
17 | activeLocks.add(key);
18 | } else {
19 | activeLocks.delete(key);
20 | }
21 |
22 | const shouldLock = activeLocks.size > 0;
23 | document.body.classList.toggle("overflow-hidden", shouldLock);
24 | if (onLockChange) onLockChange(shouldLock);
25 | }
26 |
--------------------------------------------------------------------------------
/components/shared/logo-cloud/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./logo-cloud";
2 |
--------------------------------------------------------------------------------
/components/shared/logo-cloud/logo-cloud.tsx:
--------------------------------------------------------------------------------
1 | export default function LogoCloud() {
2 | return (
3 |
4 |
5 | Brought to you by
6 |
7 |
8 | {/*
17 |
26 |
35 |
44 |
*/}
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/components/shared/logo-cloud/logo-cloud2/Logocloud.css:
--------------------------------------------------------------------------------
1 | .logocloud-items {
2 | animation: logocloud-items 100s infinite linear;
3 | will-change: transform;
4 | }
5 |
6 | @keyframes logocloud-items {
7 | 0% {
8 | transform: translateX(0);
9 | }
10 | 100% {
11 | transform: translateX(-50%);
12 | }
13 | }
14 |
15 | /* Allow this component to animate even when the user prefers reduced motion.
16 | This is a narrowly scoped opt-in to avoid disabling all animations globally. */
17 | @media (prefers-reduced-motion: reduce) {
18 | [data-allow-motion="true"] .logocloud-items {
19 | animation-duration: 100s !important;
20 | animation-iteration-count: infinite !important;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/components/shared/notifications/slack-notification.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | export default function SlackNotification({
6 | shouldNotify,
7 | onClick,
8 | }: {
9 | shouldNotify: boolean;
10 | onClick: () => void;
11 | }) {
12 | const [isOpen, setIsOpen] = useState(false);
13 |
14 | const handleClick = async () => {
15 | setIsOpen(false);
16 | if (onClick) {
17 | onClick();
18 | }
19 | };
20 |
21 | useEffect(() => {
22 | if (shouldNotify) {
23 | setIsOpen(true);
24 | }
25 | }, [shouldNotify]);
26 |
27 | return (
28 | <>
29 |
33 |
34 |
35 |

40 |
41 |
42 |
43 | New message in: #coach-gtm
44 |
45 |
46 | {`@CoachGTM: Your meeting prep for Pied Piper < > WindFlow Dynamics is ready! Meeting starts in 30 minutes`}
47 |
48 |
49 |
50 |
51 | >
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/components/shared/pixi/PixiAssetManager.ts:
--------------------------------------------------------------------------------
1 | import { Assets } from "pixi.js";
2 |
3 | class PixiAssetManager {
4 | /**
5 | * Loads assets from the given sources
6 | * @param sources The source URLs of the assets
7 | * @returns A promise that resolves with the loaded asset(s)
8 | */
9 | public static load(...sources: string[]): Promise {
10 | if (sources.length === 0) {
11 | return Promise.reject(new Error("No sources provided"));
12 | }
13 |
14 | if (sources.length === 1) {
15 | const src = sources[0];
16 |
17 | return Assets.load(src) as Promise;
18 | }
19 |
20 | // Handle multiple sources
21 | return Promise.all(
22 | sources.map((src) => this.load(src)),
23 | ) as unknown as Promise;
24 | }
25 | }
26 |
27 | export default PixiAssetManager;
28 |
--------------------------------------------------------------------------------
/components/shared/pixi/utils.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2 | // @ts-nocheck -- TODO: fix this
3 |
4 | import { Application, Assets, Sprite, Texture } from "pixi.js";
5 |
6 | export const isDestroyed = (app: Application) => {
7 | if (!app.ticker || !app.renderer || !app.stage || !app.renderer.gl)
8 | return true;
9 |
10 | return app.renderer.gl.isContextLost();
11 | };
12 |
13 | export const generateTexture = (app: Application, graphic: any) => {
14 | const renderer = app.renderer;
15 |
16 | if (!isDestroyed(app)) {
17 | return renderer.generateTexture(graphic);
18 | }
19 |
20 | return Texture.WHITE;
21 | };
22 |
23 | export const degreesToRadians = (degrees: number) => {
24 | return degrees * (Math.PI / 180);
25 | };
26 |
27 | export const imageToSprite = async (app: Application, path: string) => {
28 | let texture;
29 |
30 | if (Assets.cache.has(path)) {
31 | texture = Assets.cache.get(path);
32 | } else {
33 | texture = await Assets.load(path);
34 | }
35 |
36 | const sprite = Sprite.from(texture);
37 |
38 | return sprite;
39 | };
40 |
41 | export const createRenderWithFPS = (app: Application, fps: number) => {
42 | let lastUpdateTime = 0;
43 |
44 | return () => {
45 | const currentTime = performance.now();
46 | const timeSinceLastUpdate = currentTime - lastUpdateTime;
47 |
48 | if (timeSinceLastUpdate >= 1000 / fps) {
49 | app.ticker.update();
50 | app.render();
51 | lastUpdateTime = currentTime;
52 | }
53 | };
54 | };
55 |
56 | export const waitUntilPixiIsReady = (app: Application) => {
57 | return new Promise((resolve) => {
58 | app.canvas.addEventListener("pixi-initialized", resolve);
59 | });
60 | };
61 |
--------------------------------------------------------------------------------
/components/shared/play-tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/components/shared/portal-to-body/PortalToBody.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import { createPortal } from "react-dom";
3 |
4 | function PortalToBody({ children }: { children: React.ReactNode }) {
5 | return createPortal(children, document.body);
6 | }
7 |
8 | export default dynamic(() => Promise.resolve(PortalToBody), { ssr: false });
9 |
--------------------------------------------------------------------------------
/components/shared/search-params-provider/search-params-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { createContext, useContext, ReactNode } from "react";
4 |
5 | type SearchParamsContextType = { [key: string]: string | string[] | undefined };
6 |
7 | const SearchParamsContext = createContext(null);
8 |
9 | export const SearchParamsProvider = ({
10 | children,
11 | params,
12 | }: {
13 | children: ReactNode;
14 | params: SearchParamsContextType;
15 | }) => {
16 | return (
17 |
18 | {children}
19 |
20 | );
21 | };
22 |
23 | export const useSearchParamsContext = () => {
24 | const context = useContext(SearchParamsContext);
25 |
26 | if (!context) {
27 | throw new Error(
28 | "useSearchParamsContext must be used within a SearchParamsProvider",
29 | );
30 | }
31 |
32 | return context;
33 | };
34 |
--------------------------------------------------------------------------------
/components/shared/section-head/SectionHead.css:
--------------------------------------------------------------------------------
1 | .section-head-shadow {
2 | -webkit-text-stroke-color: var(--background-base);
3 | -webkit-text-stroke-width: 24px;
4 | }
5 |
--------------------------------------------------------------------------------
/components/shared/section-title/SectionTitle.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Connector,
3 | ConnectorToLeft,
4 | ConnectorToRight,
5 | } from "@/components/shared/layout/curvy-rect";
6 |
7 | export default function SectionTitle({
8 | index,
9 | max,
10 | title,
11 | }: {
12 | index: number;
13 | max: number;
14 | title: string;
15 | }) {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | [{" "}
41 |
42 | {index.toString().padStart(2, "0")}
43 | {" "}
44 | / {max.toString().padStart(2, "0")} ]
45 |
46 |
47 |
·
48 |
49 |
{title}
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/components/shared/ui/app-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { motion } from "framer-motion";
5 | import CurvyRect from "@/components/shared/layout/curvy-rect";
6 | import { cn } from "@/utils/cn";
7 | import {
8 | Dialog,
9 | DialogTrigger,
10 | DialogPortal,
11 | DialogOverlay,
12 | DialogClose,
13 | DialogHeader,
14 | DialogFooter,
15 | DialogTitle,
16 | DialogDescription,
17 | DialogContent as ShadDialogContent,
18 | } from "@/components/ui/shadcn/dialog";
19 |
20 | type AppDialogContentProps = React.ComponentPropsWithoutRef<
21 | typeof ShadDialogContent
22 | > & {
23 | withCurvyRect?: boolean;
24 | bodyClassName?: string;
25 | };
26 |
27 | export function AppDialogContent({
28 | className,
29 | children,
30 | withCurvyRect = true,
31 | bodyClassName,
32 | ...props
33 | }: AppDialogContentProps) {
34 | return (
35 |
42 | {withCurvyRect && (
43 |
44 | )}
45 |
51 | {children}
52 |
53 |
54 | );
55 | }
56 |
57 | export {
58 | Dialog,
59 | DialogTrigger,
60 | DialogPortal,
61 | DialogOverlay,
62 | DialogClose,
63 | DialogHeader,
64 | DialogFooter,
65 | DialogTitle,
66 | DialogDescription,
67 | };
68 |
--------------------------------------------------------------------------------
/components/shared/ui/ascii-dot-loader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { AnimatedDotIcon } from "@/components/shared/animated-dot-icon";
5 |
6 | interface AsciiDotLoaderProps {
7 | size?: number;
8 | animated?: boolean;
9 | className?: string;
10 | pattern?: Parameters[0]["pattern"];
11 | }
12 |
13 | // Thin wrapper to reuse the exact ASCII pixel effect used on the home hero
14 | export default function AsciiDotLoader({
15 | size = 20,
16 | animated = true,
17 | className,
18 | pattern = "logs",
19 | }: AsciiDotLoaderProps) {
20 | return (
21 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/components/shared/ui/dot-grid-loader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "motion/react";
4 | import React from "react";
5 |
6 | interface DotGridLoaderProps {
7 | size?: number; // pixel size of each dot
8 | cols?: number;
9 | rows?: number;
10 | className?: string;
11 | animated?: boolean;
12 | intensityMap?: number[]; // per-dot base opacity 0..1, length cols*rows
13 | }
14 |
15 | export function DotGridLoader({
16 | size = 10,
17 | cols = 3,
18 | rows = 3,
19 | className,
20 | animated = true,
21 | intensityMap,
22 | }: DotGridLoaderProps) {
23 | const total = cols * rows;
24 | const defaultMap = Array.from({ length: total }).map((_, i) => {
25 | // Row alphas tuned to hero prompt style: top=0.4, middle=1, bottom=0.12
26 | const row = Math.floor(i / cols);
27 | if (row === 0) return 0.4; // top
28 | if (row === 1) return 1.0; // middle
29 | return 0.12; // bottom
30 | });
31 | const bases =
32 | intensityMap && intensityMap.length === total ? intensityMap : defaultMap;
33 | return (
34 |
42 | {Array.from({ length: total }).map((_, i) => {
43 | const base = bases[i] ?? 0.8;
44 | return (
45 |
59 | );
60 | })}
61 |
62 | );
63 | }
64 |
65 | export default DotGridLoader;
66 |
--------------------------------------------------------------------------------
/components/shared/ui/empty-state.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { cn } from "@/utils/cn";
5 | import { AsciiExplosion } from "@/components/shared/effects/flame";
6 |
7 | interface EmptyStateProps {
8 | title?: string;
9 | description?: string;
10 | icon?: React.ReactNode;
11 | action?: React.ReactNode;
12 | showFlame?: boolean;
13 | className?: string;
14 | }
15 |
16 | export function EmptyState({
17 | title = "No data yet",
18 | description,
19 | icon,
20 | action,
21 | showFlame = true,
22 | className,
23 | }: EmptyStateProps) {
24 | return (
25 |
32 | {/* Subtle flame background */}
33 | {showFlame && (
34 |
37 | )}
38 |
39 |
40 | {icon && (
41 |
{icon}
42 | )}
43 |
44 |
45 |
{title}
46 | {description && (
47 |
48 | {description}
49 |
50 | )}
51 |
52 |
53 | {action &&
{action}
}
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/components/shared/ui/index.ts:
--------------------------------------------------------------------------------
1 | // UI Components
2 | export { StatCard } from "./stat-card";
3 | export { LoadingState } from "./loading-state";
4 | export { EmptyState } from "./empty-state";
5 | export { default as CurvyRect } from "@/components/shared/layout/curvy-rect";
6 |
--------------------------------------------------------------------------------
/components/shared/ui/loading-state.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { cn } from "@/utils/cn";
5 | import { CoreFlame } from "@/components/shared/effects/flame";
6 |
7 | interface LoadingStateProps {
8 | message?: string;
9 | showFlame?: boolean;
10 | size?: "sm" | "md" | "lg";
11 | className?: string;
12 | }
13 |
14 | export function LoadingState({
15 | message = "Loading...",
16 | showFlame = true,
17 | size = "md",
18 | className,
19 | }: LoadingStateProps) {
20 | const sizeClasses = {
21 | sm: "min-h-[200px]",
22 | md: "min-h-[300px]",
23 | lg: "min-h-[400px]",
24 | };
25 |
26 | const spinnerSizes = {
27 | sm: "w-6 h-6",
28 | md: "w-8 h-8",
29 | lg: "w-10 h-10",
30 | };
31 |
32 | return (
33 |
40 | {/* Subtle pulsing flame */}
41 | {showFlame && (
42 |
43 |
44 |
45 | )}
46 |
47 |
48 | {/* Spinner */}
49 |
55 |
56 | {/* Message */}
57 | {message && (
58 |
{message}
59 | )}
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/components/shared/utils/portal-to-body.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import { createPortal } from "react-dom";
3 |
4 | function PortalToBody({ children }: { children: React.ReactNode }) {
5 | return createPortal(children, document.body);
6 | }
7 |
8 | export default dynamic(() => Promise.resolve(PortalToBody), { ssr: false });
9 |
--------------------------------------------------------------------------------
/components/ui/LoadingDots/LoadingDots.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | @apply inline-flex text-center items-center leading-7;
3 | }
4 |
5 | .root span {
6 | @apply bg-zinc-400 rounded-full h-2 w-2;
7 | animation-name: blink;
8 | animation-duration: 1.4s;
9 | animation-iteration-count: infinite;
10 | animation-fill-mode: both;
11 | margin: 0 2px;
12 | }
13 |
14 | .root span:nth-of-type(2) {
15 | animation-delay: 0.2s;
16 | }
17 |
18 | .root span:nth-of-type(3) {
19 | animation-delay: 0.4s;
20 | }
21 |
22 | @keyframes blink {
23 | 0% {
24 | opacity: 0.2;
25 | }
26 | 20% {
27 | opacity: 1;
28 | }
29 | 100% {
30 | opacity: 0.2;
31 | }
32 | }
--------------------------------------------------------------------------------
/components/ui/LoadingDots/LoadingDots.tsx:
--------------------------------------------------------------------------------
1 | import s from "./LoadingDots.module.css";
2 | import cn from "classnames";
3 |
4 | const LoadingDots = ({ className }: { className?: string }) => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default LoadingDots;
15 |
--------------------------------------------------------------------------------
/components/ui/LoadingDots/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./LoadingDots";
2 |
--------------------------------------------------------------------------------
/components/ui/image.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import { ComponentProps } from "react";
3 |
4 | import compressorConfig from "@/public/compressor.json";
5 |
6 | interface Props extends ComponentProps<"img"> {
7 | src: string;
8 | alt: string;
9 | raw?: boolean;
10 | }
11 |
12 | const BASE_SRC = "/assets/";
13 | const RAW_SRC = "/assets-original/";
14 |
15 | export default function Image({ src, raw, ...attrs }: Props) {
16 | if (raw) {
17 | return (
18 |
25 | );
26 | }
27 |
28 | return (
29 |
30 | {compressorConfig.configs
31 | .sort((a, b) => {
32 | if (a.extension === "avif" && b.extension !== "avif") return -1;
33 | if (b.extension === "avif" && a.extension !== "avif") return 1;
34 |
35 | return a.scale - b.scale;
36 | })
37 | .map((c) => {
38 | return (
39 |
44 | );
45 | })}
46 |
47 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/components/ui/index.ts:
--------------------------------------------------------------------------------
1 | // UI Components
2 | export * from "./menu";
3 | export * from "./spinner";
4 | export * from "./shadcn/button";
5 | export * from "./shadcn/dropdown-menu";
6 | export * from "./shadcn/tooltip";
7 | export * from "./shadcn/dialog";
8 | export * from "./shadcn/sheet";
9 | export * from "./shadcn/badge";
10 | export * from "./shadcn/checkbox";
11 | export * from "./shadcn/input";
12 | export * from "./shadcn/label";
13 | export * from "./shadcn/select";
14 | export * from "./shadcn/switch";
15 | export * from "./shadcn/textarea";
16 | export * from "./shadcn/toast";
17 | export * from "./shadcn/card";
18 | export * from "./shadcn/popover";
19 | export * from "./shadcn/separator";
20 | export * from "./shadcn/alert-dialog";
21 | export * from "./shadcn/collapsible";
22 | export * from "./shadcn/context-menu";
23 | export * from "./shadcn/navigation-menu";
24 | export * from "./shadcn/progress";
25 | export * from "./shadcn/scroll-area";
26 | export * from "./shadcn/slider";
27 | export * from "./shadcn/toggle";
28 | // export * from "./tremor/line-chart"; // Not used
29 |
--------------------------------------------------------------------------------
/components/ui/loading-dashboard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef } from "react";
4 |
5 | import data from "@/components/shared/effects/flame/hero-flame-data.json";
6 | import { cn } from "@/utils/cn";
7 | import { setIntervalOnVisible } from "@/utils/set-timeout-on-visible";
8 |
9 | function LoadingDashboardFlame({ flameClassName }: { flameClassName: string }) {
10 | const ref = useRef(null);
11 | const wrapperRef = useRef(null);
12 |
13 | useEffect(() => {
14 | let index = 0;
15 |
16 | const interval = setIntervalOnVisible({
17 | element: wrapperRef.current,
18 | callback: () => {
19 | index++;
20 | if (index >= data.length) index = 0;
21 |
22 | if (ref.current) {
23 | ref.current.innerHTML = data[index];
24 | }
25 | },
26 | interval: 85,
27 | });
28 |
29 | return () => interval?.();
30 | }, []);
31 |
32 | return (
33 |
53 | );
54 | }
55 |
56 | export default function LoadingDashboard({
57 | flameClassName,
58 | }: {
59 | flameClassName: string;
60 | }) {
61 | return (
62 |
63 |
64 |
Loading...
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/components/ui/magic/animated-shiny-text-lw.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, FC, ReactNode } from "react";
2 |
3 | import { cn } from "@/utils/cn";
4 |
5 | interface AnimatedShinyTextProps {
6 | children: ReactNode;
7 | className?: string;
8 | shimmerWidth?: number;
9 | }
10 |
11 | const AnimatedShinyTextLw: FC = ({
12 | children,
13 | className,
14 | shimmerWidth = 100,
15 | }) => {
16 | return (
17 |
35 | {children}
36 |
37 | );
38 | };
39 |
40 | export default AnimatedShinyTextLw;
41 |
--------------------------------------------------------------------------------
/components/ui/magic/animated-shiny-text.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, FC, ReactNode } from "react";
2 |
3 | import { cn } from "@/utils/cn";
4 |
5 | interface AnimatedShinyTextProps {
6 | children: ReactNode;
7 | className?: string;
8 | shimmerWidth?: number;
9 | }
10 |
11 | const AnimatedShinyText: FC = ({
12 | children,
13 | className,
14 | shimmerWidth = 100,
15 | }) => {
16 | return (
17 |
35 | {children}
36 |
37 | );
38 | };
39 |
40 | export default AnimatedShinyText;
41 |
--------------------------------------------------------------------------------
/components/ui/magic/dot-pattern.tsx:
--------------------------------------------------------------------------------
1 | import { useId } from "react";
2 |
3 | import { cn } from "@/utils/cn";
4 |
5 | interface DotPatternProps {
6 | width?: any;
7 | height?: any;
8 | x?: any;
9 | y?: any;
10 | cx?: any;
11 | cy?: any;
12 | cr?: any;
13 | className?: string;
14 | [key: string]: any;
15 | }
16 | export function DotPattern({
17 | width = 16,
18 | height = 16,
19 | x = 0,
20 | y = 0,
21 | cx = 1,
22 | cy = 1,
23 | cr = 1,
24 | className,
25 | ...props
26 | }: DotPatternProps) {
27 | const id = useId();
28 |
29 | return (
30 |
53 | );
54 | }
55 |
56 | export default DotPattern;
57 |
--------------------------------------------------------------------------------
/components/ui/magic/gradual-spacing.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AnimatePresence, motion, Variants } from "framer-motion";
4 |
5 | import { cn } from "@/utils/cn";
6 |
7 | interface GradualSpacingProps {
8 | text: string;
9 | duration?: number;
10 | delayMultiple?: number;
11 | framerProps?: Variants;
12 | className?: string;
13 | }
14 |
15 | export default function GradualSpacing({
16 | text,
17 | duration = 0.5,
18 | delayMultiple = 0.04,
19 | framerProps = {
20 | hidden: { opacity: 0, x: -20 },
21 | visible: { opacity: 1, x: 0 },
22 | },
23 | className,
24 | }: GradualSpacingProps) {
25 | return (
26 |
27 |
28 | {text.split("").map((char, i) => (
29 |
38 | {char === " " ? : char}
39 |
40 | ))}
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/components/ui/magic/ripple.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from "react";
2 |
3 | interface RippleProps {
4 | mainCircleSize?: number;
5 | mainCircleOpacity?: number;
6 | numCircles?: number;
7 | }
8 |
9 | const Ripple = React.memo(function Ripple({
10 | mainCircleSize = 210,
11 | mainCircleOpacity = 0.24,
12 | numCircles = 8,
13 | }: RippleProps) {
14 | return (
15 |
16 | {Array.from({ length: numCircles }, (_, i) => {
17 | const size = mainCircleSize + i * 70;
18 | const opacity = mainCircleOpacity - i * 0.03;
19 | const animationDelay = `${i * 0.06}s`;
20 | const borderStyle = i === numCircles - 1 ? "dashed" : "solid";
21 | const borderOpacity = 5 + i * 5;
22 |
23 | return (
24 |
39 | );
40 | })}
41 |
42 | );
43 | });
44 |
45 | Ripple.displayName = "Ripple";
46 |
47 | export default Ripple;
48 |
--------------------------------------------------------------------------------
/components/ui/menu-header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { X } from "lucide-react";
5 | import { cn } from "@/utils/cn";
6 |
7 | interface MenuHeaderProps {
8 | title: string;
9 | onClose?: () => void;
10 | className?: string;
11 | }
12 |
13 | export function MenuHeader({ title, onClose, className }: MenuHeaderProps) {
14 | return (
15 |
21 |
{title}
22 | {onClose && (
23 |
34 | )}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/components/ui/modal.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | import { motion, AnimatePresence } from "motion/react";
4 |
5 | export interface ModalProps {
6 | children?: ReactNode;
7 | contentClassName?: string;
8 |
9 | isOpen: boolean;
10 | setIsOpen: (open: boolean) => void;
11 | }
12 |
13 | export default function Modal({ children, isOpen, setIsOpen }: ModalProps) {
14 | return (
15 |
16 | {isOpen && (
17 |
23 | setIsOpen(false)}
29 | />
30 |
31 |
42 | {children}
43 |
44 |
45 | )}
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/components/ui/motion/text-reveal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { motion, AnimatePresence } from "framer-motion";
5 | import { cn } from "@/utils/cn";
6 |
7 | interface TextRevealProps {
8 | text: string;
9 | isVisible: boolean;
10 | className?: string;
11 | delay?: number;
12 | duration?: number;
13 | }
14 |
15 | export function TextReveal({
16 | text,
17 | isVisible,
18 | className,
19 | delay = 0,
20 | duration = 0.3,
21 | }: TextRevealProps) {
22 | return (
23 |
24 | {isVisible && (
25 |
42 | {text}
43 |
44 | )}
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/ui/shadcn/badge.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-no-comment-textnodes */
2 | import { HTMLAttributes } from "react";
3 |
4 | import { cn } from "@/utils/cn";
5 |
6 | function Badge({ ...attrs }: HTMLAttributes) {
7 | return (
8 |
15 |
16 |
17 | //
18 |
19 |
{attrs.children}
20 |
21 | //
22 |
23 |
24 | );
25 | }
26 |
27 | export default Badge;
28 | export { Badge };
29 |
--------------------------------------------------------------------------------
/components/ui/shadcn/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatePresence, motion } from "motion/react";
2 |
3 | import { cn } from "@/utils/cn";
4 |
5 | export default function Checkbox({
6 | checked,
7 | onChange,
8 | }: {
9 | checked: boolean;
10 | onChange?: (checked: boolean) => void;
11 | }) {
12 | return (
13 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/components/ui/shadcn/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4 |
5 | const Collapsible = CollapsiblePrimitive.Root;
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent };
12 |
--------------------------------------------------------------------------------
/components/ui/shadcn/input.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from "react";
2 | import { cn } from "@/utils/cn";
3 |
4 | const Input = forwardRef<
5 | HTMLInputElement,
6 | React.ComponentPropsWithoutRef<"input">
7 | >(({ className, ...props }, ref) => {
8 | return (
9 |
24 | );
25 | });
26 |
27 | Input.displayName = "Input";
28 |
29 | export default Input;
30 |
--------------------------------------------------------------------------------
/components/ui/shadcn/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "@/utils/cn";
8 |
9 | const labelVariants = cva(
10 | "text-sm leading-tight peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/components/ui/shadcn/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/components/ui/shadcn/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ProgressPrimitive from "@radix-ui/react-progress";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ));
26 | Progress.displayName = ProgressPrimitive.Root.displayName;
27 |
28 | export { Progress };
29 |
--------------------------------------------------------------------------------
/components/ui/shadcn/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ));
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47 |
48 | export { ScrollArea, ScrollBar };
49 |
--------------------------------------------------------------------------------
/components/ui/shadcn/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref,
15 | ) => (
16 |
27 | ),
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/components/ui/shadcn/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SliderPrimitive from "@radix-ui/react-slider";
5 | import { cn } from "@/utils/cn";
6 |
7 | const Slider = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
20 |
21 |
22 |
23 |
24 | ));
25 |
26 | Slider.displayName = "Slider";
27 |
28 | export { Slider };
29 |
--------------------------------------------------------------------------------
/components/ui/shadcn/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SwitchPrimitives from "@radix-ui/react-switch";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "@/utils/cn";
8 |
9 | const switchVariants = cva(
10 | "peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-muted-foreground data-[state=unchecked]:bg-input",
11 | {
12 | variants: {
13 | size: {
14 | default: "h-5 w-9",
15 | sm: "h-4 w-7",
16 | },
17 | },
18 | defaultVariants: {
19 | size: "default",
20 | },
21 | },
22 | );
23 |
24 | const thumbVariants = cva(
25 | "pointer-events-none block rounded-full bg-background shadow-lg ring-0 transition-transform",
26 | {
27 | variants: {
28 | size: {
29 | default:
30 | "h-4 w-4 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
31 | sm: "h-3 w-3 data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
32 | },
33 | },
34 | defaultVariants: {
35 | size: "default",
36 | },
37 | },
38 | );
39 |
40 | interface SwitchProps
41 | extends React.ComponentPropsWithoutRef,
42 | VariantProps {}
43 |
44 | const Switch = React.forwardRef<
45 | React.ElementRef,
46 | SwitchProps
47 | >(({ className, size, ...props }, ref) => (
48 |
53 |
54 |
55 | ));
56 | Switch.displayName = SwitchPrimitives.Root.displayName;
57 |
58 | export { Switch };
59 |
--------------------------------------------------------------------------------
/components/ui/shadcn/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
49 | ));
50 | TabsContent.displayName = TabsPrimitive.Content.displayName;
51 |
52 | export { Tabs, TabsList, TabsTrigger, TabsContent };
53 |
--------------------------------------------------------------------------------
/components/ui/shadcn/textarea.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/cn";
2 |
3 | export default function Textarea(
4 | textareaProps: React.ComponentPropsWithoutRef<"textarea">,
5 | ) {
6 | return (
7 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/components/ui/shadcn/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Toaster as Sonner } from "sonner";
4 |
5 | type ToasterProps = React.ComponentProps;
6 |
7 | const Toaster = ({ theme = "light", ...props }: ToasterProps) => {
8 | const resolvedTheme = (theme ?? "light") as ToasterProps["theme"];
9 |
10 | return (
11 |
28 | );
29 | };
30 |
31 | export { Toaster };
32 |
--------------------------------------------------------------------------------
/components/ui/shadcn/tooltip-radix.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/convex.json:
--------------------------------------------------------------------------------
1 | {
2 | "node": {
3 | "externalPackages": [
4 | "@langchain/langgraph",
5 | "@langchain/core",
6 | "@anthropic-ai/sdk"
7 | ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/convex/_generated/api.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated `api` utility.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import type * as admin from "../admin.js";
12 | import type * as apiKeys from "../apiKeys.js";
13 | import type * as approvals from "../approvals.js";
14 | import type * as executions from "../executions.js";
15 | import type * as mcpServers from "../mcpServers.js";
16 | import type * as templates from "../templates.js";
17 | import type * as userLLMKeys from "../userLLMKeys.js";
18 | import type * as userMCPs from "../userMCPs.js";
19 | import type * as workflows from "../workflows.js";
20 |
21 | import type {
22 | ApiFromModules,
23 | FilterApi,
24 | FunctionReference,
25 | } from "convex/server";
26 |
27 | /**
28 | * A utility for referencing Convex functions in your app's API.
29 | *
30 | * Usage:
31 | * ```js
32 | * const myFunctionReference = api.myModule.myFunction;
33 | * ```
34 | */
35 | declare const fullApi: ApiFromModules<{
36 | admin: typeof admin;
37 | apiKeys: typeof apiKeys;
38 | approvals: typeof approvals;
39 | executions: typeof executions;
40 | mcpServers: typeof mcpServers;
41 | templates: typeof templates;
42 | userLLMKeys: typeof userLLMKeys;
43 | userMCPs: typeof userMCPs;
44 | workflows: typeof workflows;
45 | }>;
46 | declare const fullApiWithMounts: typeof fullApi;
47 |
48 | export declare const api: FilterApi<
49 | typeof fullApiWithMounts,
50 | FunctionReference
51 | >;
52 | export declare const internal: FilterApi<
53 | typeof fullApiWithMounts,
54 | FunctionReference
55 | >;
56 |
57 | export declare const components: {};
58 |
--------------------------------------------------------------------------------
/convex/_generated/api.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated `api` utility.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import { anyApi, componentsGeneric } from "convex/server";
12 |
13 | /**
14 | * A utility for referencing Convex functions in your app's API.
15 | *
16 | * Usage:
17 | * ```js
18 | * const myFunctionReference = api.myModule.myFunction;
19 | * ```
20 | */
21 | export const api = anyApi;
22 | export const internal = anyApi;
23 | export const components = componentsGeneric();
24 |
--------------------------------------------------------------------------------
/convex/_generated/dataModel.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated data model types.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import type {
12 | DataModelFromSchemaDefinition,
13 | DocumentByName,
14 | TableNamesInDataModel,
15 | SystemTableNames,
16 | } from "convex/server";
17 | import type { GenericId } from "convex/values";
18 | import schema from "../schema.js";
19 |
20 | /**
21 | * The names of all of your Convex tables.
22 | */
23 | export type TableNames = TableNamesInDataModel;
24 |
25 | /**
26 | * The type of a document stored in Convex.
27 | *
28 | * @typeParam TableName - A string literal type of the table name (like "users").
29 | */
30 | export type Doc = DocumentByName<
31 | DataModel,
32 | TableName
33 | >;
34 |
35 | /**
36 | * An identifier for a document in Convex.
37 | *
38 | * Convex documents are uniquely identified by their `Id`, which is accessible
39 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
40 | *
41 | * Documents can be loaded using `db.get(id)` in query and mutation functions.
42 | *
43 | * IDs are just strings at runtime, but this type can be used to distinguish them from other
44 | * strings when type checking.
45 | *
46 | * @typeParam TableName - A string literal type of the table name (like "users").
47 | */
48 | export type Id =
49 | GenericId;
50 |
51 | /**
52 | * A type describing your Convex data model.
53 | *
54 | * This type includes information about what tables you have, the type of
55 | * documents stored in those tables, and the indexes defined on them.
56 | *
57 | * This type is used to parameterize methods like `queryGeneric` and
58 | * `mutationGeneric` to make them type-safe.
59 | */
60 | export type DataModel = DataModelFromSchemaDefinition;
61 |
--------------------------------------------------------------------------------
/convex/admin.ts:
--------------------------------------------------------------------------------
1 | import { mutation } from "./_generated/server";
2 |
3 | /**
4 | * Admin functions for database management
5 | */
6 |
7 | // Clear all workflows (use with caution!)
8 | export const clearAllWorkflows = mutation({
9 | args: {},
10 | handler: async ({ db }) => {
11 | const workflows = await db.query("workflows").collect();
12 | let deleted = 0;
13 |
14 | for (const workflow of workflows) {
15 | await db.delete(workflow._id);
16 | deleted++;
17 | }
18 |
19 | return { deleted };
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/convex/auth.config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Convex Authentication Configuration
3 | *
4 | * Configures Clerk as the authentication provider for Convex
5 | * The CLERK_JWT_ISSUER_DOMAIN is set via: npx convex env set
6 | */
7 |
8 | export default {
9 | providers: [
10 | {
11 | // This reads from Convex environment variable (not process.env)
12 | // Set via: npx convex env set CLERK_JWT_ISSUER_DOMAIN "https://..."
13 | domain: "https://oriented-quetzal-4.clerk.accounts.dev",
14 | applicationID: "convex",
15 | },
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/convex/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "module": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "lib": ["ES2021"],
7 | "types": []
8 | },
9 | "include": ["."]
10 | }
11 |
--------------------------------------------------------------------------------
/hooks/useDebouncedEffect.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useEffect, useRef
3 | } from 'react';
4 |
5 | const DEFAULT_CONFIG = {
6 | timeout: 0,
7 | ignoreInitialCall: true
8 | };
9 |
10 | export function useDebouncedEffect(
11 | callback: () => (void | (() => void)),
12 | config: number | {
13 | timeout?: number;
14 | ignoreInitialCall?: boolean;
15 | },
16 | deps: any[] = []
17 | ): void {
18 | let currentConfig;
19 |
20 | if (typeof config === 'object') {
21 | currentConfig = {
22 | ...DEFAULT_CONFIG,
23 | ...config
24 | };
25 | } else {
26 | currentConfig = {
27 | ...DEFAULT_CONFIG,
28 | timeout: config
29 | };
30 | }
31 | const {
32 | timeout, ignoreInitialCall
33 | } = currentConfig;
34 |
35 | const data = useRef<{ firstTime: boolean }>({ firstTime: true });
36 |
37 | useEffect(() => {
38 | const { firstTime } = data.current;
39 |
40 | if (firstTime && ignoreInitialCall) {
41 | data.current.firstTime = false;
42 |
43 | return;
44 | }
45 |
46 | let clearFunc: (() => void) | undefined;
47 |
48 | const handler = setTimeout(() => {
49 | clearFunc = callback() ?? undefined;
50 | }, timeout);
51 |
52 | return () => {
53 | clearTimeout(handler);
54 |
55 | if (clearFunc && typeof clearFunc === 'function') {
56 | clearFunc();
57 | }
58 | };
59 | }, [
60 | callback,
61 | ignoreInitialCall,
62 | timeout,
63 | // eslint-disable-next-line react-hooks/exhaustive-deps
64 | ...deps
65 | ]);
66 | }
67 |
68 | export default useDebouncedEffect;
69 |
--------------------------------------------------------------------------------
/lib/api/config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Server-side API configuration utilities
3 | * Use this for getting API keys in API routes and server components
4 | */
5 |
6 | export interface APIKeys {
7 | anthropic?: string;
8 | groq?: string;
9 | openai?: string;
10 | firecrawl?: string;
11 | arcade?: string;
12 | e2b?: string;
13 | }
14 |
15 | /**
16 | * Get API keys from environment variables (server-side only)
17 | * Returns available keys even if some are missing
18 | */
19 | export function getServerAPIKeys(): APIKeys {
20 | const anthropic = process.env.ANTHROPIC_API_KEY;
21 | const groq = process.env.GROQ_API_KEY;
22 | const openai = process.env.OPENAI_API_KEY;
23 | const firecrawl = process.env.FIRECRAWL_API_KEY;
24 | const arcade = process.env.ARCADE_API_KEY;
25 | const e2b = process.env.E2B_API_KEY;
26 |
27 | return {
28 | anthropic,
29 | groq,
30 | openai,
31 | firecrawl,
32 | arcade,
33 | e2b,
34 | };
35 | }
36 |
37 | /**
38 | * Check if required API keys are configured
39 | */
40 | export function hasServerAPIKeys(): boolean {
41 | const hasLLMKey = !!(process.env.ANTHROPIC_API_KEY || process.env.GROQ_API_KEY || process.env.OPENAI_API_KEY);
42 | return hasLLMKey && !!process.env.FIRECRAWL_API_KEY;
43 | }
44 |
--------------------------------------------------------------------------------
/lib/errors/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | WorkflowError,
3 | NodeExecutionError,
4 | ValidationError,
5 | APIError,
6 | AuthorizationError,
7 | ErrorCodes,
8 | type ErrorCode,
9 | } from './WorkflowError';
10 |
--------------------------------------------------------------------------------
/lib/workflow/edge-cleanup.ts:
--------------------------------------------------------------------------------
1 | import type { WorkflowNode, WorkflowEdge } from './types';
2 |
3 | /**
4 | * Clean up invalid edges that point to non-existent nodes
5 | * This handles corrupted workflow data where edges reference deleted nodes
6 | */
7 | export function cleanupInvalidEdges(
8 | nodes: WorkflowNode[],
9 | edges: WorkflowEdge[]
10 | ): { nodes: WorkflowNode[]; edges: WorkflowEdge[]; removedCount: number } {
11 | const validNodeIds = new Set(nodes.map(n => n.id));
12 | const validEdges: WorkflowEdge[] = [];
13 | let removedCount = 0;
14 |
15 | for (const edge of edges) {
16 | // Check if both source and target nodes exist
17 | const sourceExists = validNodeIds.has(edge.source);
18 | const targetExists = validNodeIds.has(edge.target);
19 |
20 | if (!sourceExists || !targetExists) {
21 | console.warn(`🧹 Removing invalid edge ${edge.id}: source=${edge.source} (exists: ${sourceExists}), target=${edge.target} (exists: ${targetExists})`);
22 | removedCount++;
23 | continue;
24 | }
25 |
26 | validEdges.push(edge);
27 | }
28 |
29 | if (removedCount > 0) {
30 | console.log(`✅ Cleaned up ${removedCount} invalid edge(s) from workflow`);
31 | }
32 |
33 | return {
34 | nodes,
35 | edges: validEdges,
36 | removedCount,
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/lib/workflow/error-boundaries.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Workflow Error Boundaries
3 | * Provides better error handling and user-friendly messages
4 | */
5 |
6 | export class WorkflowExecutionError extends Error {
7 | constructor(
8 | message: string,
9 | public nodeId: string,
10 | public nodeType: string,
11 | public originalError?: Error
12 | ) {
13 | super(`[${nodeType}] ${nodeId}: ${message}`);
14 | this.name = 'WorkflowExecutionError';
15 | }
16 | }
17 |
18 | /**
19 | * Wrap node execution with error handling
20 | */
21 | export function wrapNodeExecution(
22 | nodeId: string,
23 | nodeType: string,
24 | fn: () => Promise
25 | ): Promise {
26 | return fn().catch(err => {
27 | throw new WorkflowExecutionError(
28 | err.message,
29 | nodeId,
30 | nodeType,
31 | err
32 | );
33 | });
34 | }
35 |
36 | /**
37 | * Get user-friendly error message
38 | */
39 | export function getUserFriendlyError(error: Error): string {
40 | const message = error.message;
41 |
42 | // API key errors
43 | if (message.includes('API key') || message.includes('api_key')) {
44 | return 'Missing API key. Please add your LLM provider key in Settings.';
45 | }
46 |
47 | // Rate limit errors
48 | if (message.includes('rate limit') || message.includes('429')) {
49 | return 'Rate limited. Please wait a moment and try again.';
50 | }
51 |
52 | // Network errors
53 | if (message.includes('fetch') || message.includes('network')) {
54 | return 'Network error. Please check your connection and try again.';
55 | }
56 |
57 | // Firecrawl-specific errors
58 | if (message.includes('FIRECRAWL')) {
59 | return 'Firecrawl API error. Please verify your FIRECRAWL_API_KEY in .env.local';
60 | }
61 |
62 | // Variable substitution errors
63 | if (message.includes('variable') || message.includes('{{')) {
64 | return `Variable error: ${message}. Check your variable references.`;
65 | }
66 |
67 | // Generic error
68 | return `Execution failed: ${message}`;
69 | }
70 |
--------------------------------------------------------------------------------
/lib/workflow/templates/examples/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Example Templates Index
3 | *
4 | * This file exports all example templates organized by complexity level.
5 | * Each example demonstrates specific features and use cases.
6 | */
7 |
8 | import { simpleAgent } from './01-simple-agent';
9 | import { agentWithFirecrawl } from './02-agent-with-firecrawl';
10 | import { scrapeSummarizeDocs } from './03-scrape-summarize-docs';
11 | import { advancedWorkflow } from './04-advanced-workflow';
12 |
13 | export const exampleTemplates = {
14 | 'example-01-simple-agent': simpleAgent,
15 | 'example-02-agent-with-firecrawl': agentWithFirecrawl,
16 | 'example-03-scrape-summarize-docs': scrapeSummarizeDocs,
17 | 'example-04-advanced-workflow': advancedWorkflow,
18 | };
19 |
20 | export const exampleTemplatesList = [
21 | {
22 | id: 'example-01-simple-agent',
23 | name: 'Example 1: Simple Agent',
24 | description: 'A basic workflow with one agent that answers questions',
25 | difficulty: 'beginner',
26 | estimatedTime: '1-2 minutes',
27 | },
28 | {
29 | id: 'example-02-agent-with-firecrawl',
30 | name: 'Example 2: Agent with Firecrawl',
31 | description: 'An agent that can search and scrape the web using Firecrawl',
32 | difficulty: 'beginner',
33 | estimatedTime: '2-3 minutes',
34 | },
35 | {
36 | id: 'example-03-scrape-summarize-docs',
37 | name: 'Example 3: Scrape, Summarize & Post to Docs',
38 | description: 'Scrape a website, summarize content, and create a Google Doc',
39 | difficulty: 'intermediate',
40 | estimatedTime: '3-5 minutes',
41 | },
42 | {
43 | id: 'example-04-advanced-workflow',
44 | name: 'Example 4: Advanced Competitive Analysis',
45 | description: 'Complete workflow using all node types: loops, conditions, approvals, and tools',
46 | difficulty: 'advanced',
47 | estimatedTime: '10-15 minutes',
48 | },
49 | ];
50 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | import "./.next/dev/types/routes.d.ts";
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: 'https',
7 | hostname: '*',
8 | pathname: '/**',
9 | },
10 | ],
11 | },
12 | // Mark server-only packages for Next.js 16+
13 | serverExternalPackages: [
14 | '@langchain/langgraph',
15 | '@langchain/langgraph-checkpoint-redis',
16 | 'redis',
17 | '@redis/client',
18 | '@e2b/code-interpreter',
19 | 'e2b',
20 | ],
21 | }
22 |
23 | module.exports = nextConfig
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | "postcss-import": {
4 | // Resolve @/ alias to make imports work during build
5 | resolve(id) {
6 | return id.startsWith("@/")
7 | ? id.replace("@/", "./")
8 | : id;
9 | },
10 | },
11 | "tailwindcss/nesting": {},
12 | tailwindcss: {},
13 | autoprefixer: {},
14 | 'postcss-nesting': {}
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/proxy.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
2 |
3 | // Define public routes that don't require authentication
4 | const isPublicRoute = createRouteMatcher([
5 | '/',
6 | '/sign-in(.*)',
7 | '/sign-up(.*)',
8 | '/api/public(.*)',
9 | '/api/config(.*)',
10 | '/api/templates(.*)',
11 | '/api/mcp(.*)',
12 | '/api/test-mcp-connection(.*)',
13 | ])
14 |
15 | // Define API routes that require API key authentication (bypass Clerk auth)
16 | const isApiKeyRoute = createRouteMatcher([
17 | '/api/workflows/:workflowId/execute',
18 | '/api/workflows/:workflowId/execute-stream',
19 | '/api/workflows/:workflowId/resume',
20 | ])
21 |
22 | export default clerkMiddleware(async (auth, request) => {
23 | // API key routes bypass Clerk auth (will be validated in the route handler)
24 | if (isApiKeyRoute(request)) {
25 | return
26 | }
27 |
28 | // Protect all routes except public ones
29 | if (!isPublicRoute(request)) {
30 | await auth.protect()
31 | }
32 | })
33 |
34 | export const config = {
35 | matcher: [
36 | // Skip Next.js internals and static files
37 | '/((?!_next|[^?]*\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
38 | // Always run for API routes
39 | '/(api|trpc)(.*)',
40 | ],
41 | }
42 |
--------------------------------------------------------------------------------
/public/compressor.json:
--------------------------------------------------------------------------------
1 | {
2 | "configs": [
3 | {
4 | "quality": 80,
5 | "scale": 1.0,
6 | "format": 0,
7 | "extension": "webp"
8 | },
9 | {
10 | "quality": 70,
11 | "scale": 1.0,
12 | "format": 1,
13 | "extension": "avif"
14 | }
15 | ],
16 | "version": "1.0",
17 | "last_updated": 1751380796
18 | }
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firecrawl/open-agent-builder/be856e57f8126e90915c898f473dc94fbaefc945/public/favicon.png
--------------------------------------------------------------------------------
/repomix.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "output": {
3 | "filePath": "repomix-output.txt",
4 | "style": "plain"
5 | },
6 | "ignore": {
7 | "customPatterns": [
8 | "**/.env*",
9 | ".env*",
10 | "**/*.md",
11 | "**/*.css",
12 | "**/*.scss",
13 | "test-*.js",
14 | "**/test-results/**",
15 | "**/playwright-report/**",
16 | "**/.playwright-mcp/**",
17 | "repomix-output.*",
18 | "**/*.tsbuildinfo",
19 | "convex/_generated/**",
20 | ".eslintrc.json",
21 | ".gitignore",
22 | ".npmrc",
23 | "colors.json",
24 | "convex.json",
25 | "next.config.js",
26 | "postcss.config.js",
27 | "tsconfig.json",
28 | "repomix.config.json",
29 | "next-env.d.ts",
30 | "scripts/**",
31 | "tests/**",
32 | "playwright.config.ts",
33 | "atoms/**",
34 | "**/*-data.json",
35 | "**/data.json",
36 | "**/core-flame.json",
37 | "**/explosion-data.json",
38 | "**/hero-flame-data.json",
39 | "**/disposableDomains.ts",
40 | "components/**",
41 | "styles/**",
42 | "public/**",
43 | "tailwind.config.ts",
44 | "postcss.config.mjs",
45 | "lib/workflow/templates.ts",
46 | ".cursor/**",
47 | "app/workflows/**",
48 | "app/page.tsx",
49 | "app/layout.tsx"
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/styles/additional-styles/custom-fonts.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firecrawl/open-agent-builder/be856e57f8126e90915c898f473dc94fbaefc945/styles/additional-styles/custom-fonts.css
--------------------------------------------------------------------------------
/styles/additional-styles/theme.css:
--------------------------------------------------------------------------------
1 | .form-input:focus,
2 | .form-textarea:focus,
3 | .form-multiselect:focus,
4 | .form-select:focus,
5 | .form-checkbox:focus,
6 | .form-radio:focus {
7 | @apply ring-0;
8 | }
9 |
10 | /* Hamburger button */
11 | .hamburger svg > *:nth-child(1),
12 | .hamburger svg > *:nth-child(2),
13 | .hamburger svg > *:nth-child(3) {
14 | transform-origin: center;
15 | transform: rotate(0deg);
16 | }
17 |
18 | .hamburger svg > *:nth-child(1) {
19 | transition:
20 | y 0.1s 0.25s ease-in,
21 | transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19),
22 | opacity 0.1s ease-in;
23 | }
24 |
25 | .hamburger svg > *:nth-child(2) {
26 | transition: transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19);
27 | }
28 |
29 | .hamburger svg > *:nth-child(3) {
30 | transition:
31 | y 0.1s 0.25s ease-in,
32 | transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19),
33 | width 0.1s 0.25s ease-in;
34 | }
35 |
36 | .hamburger.active svg > *:nth-child(1) {
37 | opacity: 0;
38 | y: 11;
39 | transform: rotate(225deg);
40 | transition:
41 | y 0.1s ease-out,
42 | transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1),
43 | opacity 0.1s 0.12s ease-out;
44 | }
45 |
46 | .hamburger.active svg > *:nth-child(2) {
47 | transform: rotate(225deg);
48 | transition: transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1);
49 | }
50 |
51 | .hamburger.active svg > *:nth-child(3) {
52 | y: 11;
53 | transform: rotate(135deg);
54 | transition:
55 | y 0.1s ease-out,
56 | transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1),
57 | width 0.1s ease-out;
58 | }
59 |
--------------------------------------------------------------------------------
/styles/chrome-bug.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Chrome has a bug with transitions on load since 2012!
3 | *
4 | * To prevent a "pop" of content, you have to disable all transitions until
5 | * the page is done loading.
6 | *
7 | * https://lab.laukstein.com/bug/input
8 | * https://twitter.com/timer150/status/1345217126680899584
9 | */
10 | body.loading * {
11 | transition: none !important;
12 | }
13 |
--------------------------------------------------------------------------------
/styles/components/code.css:
--------------------------------------------------------------------------------
1 | .string,
2 | .language-html .tag:not(.punctuation, .attr-name, .attr-value, .special-attr) {
3 | color: var(--heat-100) !important;
4 | }
5 |
6 | .punctuation,
7 | .operator {
8 | color: #c2c2c2 !important;
9 | }
10 |
11 | .language-html .attr-name {
12 | color: var(--black-alpha-64);
13 | }
14 |
15 | .comment {
16 | color: #999999 !important;
17 | }
18 |
19 | code:not(.language-html) .property:not(.literal-property),
20 | .class-name,
21 | code:not(.language-html) .function,
22 | .language-json .boolean {
23 | color: #9061ff;
24 | color: color(display-p3 0.566 0.38 1);
25 | }
26 |
27 | .language-json .property {
28 | color: inherit !important;
29 | }
30 |
31 | .prismjs {
32 | padding-top: 20px;
33 | @apply text-mono-medium font-mono;
34 | }
35 |
36 | .prismjs code {
37 | color: var(--accent-black) !important;
38 | }
39 |
40 | .linenumber {
41 | width: 48px;
42 | padding: 0;
43 | font-style: normal;
44 | @apply !text-black-alpha-12 !pl-20 !pr-0 !text-left;
45 | }
46 |
--------------------------------------------------------------------------------
/styles/components/index.css:
--------------------------------------------------------------------------------
1 | /* Component-specific styles from firecrawl-marketing */
2 |
3 | /* UI Components (for Dashboard v2) */
4 | @import "./button.css";
5 | @import "./code.css";
6 |
7 | /* Additional component CSS will be added as we migrate more components */
8 | /* @import "./modal.css"; */
9 | /* @import "./spinner.css"; */
10 | /* @import "./input.css"; */
--------------------------------------------------------------------------------
/styles/design-system/animations.css:
--------------------------------------------------------------------------------
1 | /* Animation Utilities */
2 |
3 | /* Cursor blink animation */
4 | .cursor {
5 | animation: cursor-blink 0.7s infinite;
6 | }
7 |
8 | @keyframes cursor-blink {
9 | 0%, 100% {
10 | opacity: 0;
11 | }
12 | 50% {
13 | opacity: 1;
14 | }
15 | }
16 |
17 | /* Reverse spin */
18 | .animate-spin-reverse {
19 | animation: spin-reverse 1s linear infinite;
20 | }
21 |
22 | @keyframes spin-reverse {
23 | from {
24 | transform: rotate(360deg);
25 | }
26 | }
27 |
28 | /* Fire-inspired animations */
29 | .animate-flicker {
30 | animation: flicker 2s ease-in-out infinite;
31 | }
32 |
33 | @keyframes flicker {
34 | 0%, 100% {
35 | opacity: 1;
36 | }
37 | 50% {
38 | opacity: 0.8;
39 | transform: scale(0.98);
40 | }
41 | }
42 |
43 | .animate-glow {
44 | animation: glow 2s ease-in-out infinite;
45 | }
46 |
47 | @keyframes glow {
48 | 0%, 100% {
49 | box-shadow: 0 0 20px rgba(250, 93, 25, 0.5);
50 | }
51 | 50% {
52 | box-shadow: 0 0 40px rgba(250, 93, 25, 0.8);
53 | }
54 | }
55 |
56 | /* Smooth transitions */
57 | .transition-all {
58 | transition: all 0.3s ease;
59 | }
60 |
61 | .transition-colors {
62 | transition: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
63 | }
64 |
65 | .transition-transform {
66 | transition: transform 0.3s ease;
67 | }
68 |
69 | .transition-opacity {
70 | transition: opacity 0.3s ease;
71 | }
72 |
73 | /* Animation delays */
74 | .delay-100 {
75 | animation-delay: 100ms;
76 | }
77 |
78 | .delay-200 {
79 | animation-delay: 200ms;
80 | }
81 |
82 | .delay-300 {
83 | animation-delay: 300ms;
84 | }
85 |
86 | .delay-400 {
87 | animation-delay: 400ms;
88 | }
89 |
90 | .delay-500 {
91 | animation-delay: 500ms;
92 | }
--------------------------------------------------------------------------------
/styles/design-system/fonts.css:
--------------------------------------------------------------------------------
1 | /* Custom Font Faces */
2 | @font-face {
3 | src: url("/fonts/SuisseIntl/400.woff2");
4 | font-weight: 400;
5 | font-display: swap;
6 | font-family: "SuisseIntl";
7 | }
8 |
9 | @font-face {
10 | src: url("/fonts/SuisseIntl/450.woff2");
11 | font-weight: 450;
12 | font-display: swap;
13 | font-family: "SuisseIntl";
14 | }
15 |
16 | @font-face {
17 | src: url("/fonts/SuisseIntl/500.woff2");
18 | font-weight: 500;
19 | font-display: swap;
20 | font-family: "SuisseIntl";
21 | }
22 |
23 | @font-face {
24 | src: url("/fonts/SuisseIntl/600.woff2");
25 | font-weight: 600;
26 | font-display: swap;
27 | font-family: "SuisseIntl";
28 | }
29 |
30 | @font-face {
31 | src: url("/fonts/SuisseIntl/700.woff2");
32 | font-weight: 700;
33 | font-display: swap;
34 | font-family: "SuisseIntl";
35 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "noImplicitAny": false,
13 | "forceConsistentCasingInFileNames": true,
14 | "noEmit": true,
15 | "esModuleInterop": true,
16 | "module": "esnext",
17 | "moduleResolution": "bundler",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "jsx": "react-jsx",
21 | "baseUrl": ".",
22 | "paths": {
23 | "@/*": [
24 | "./*"
25 | ]
26 | },
27 | "incremental": true,
28 | "plugins": [
29 | {
30 | "name": "next"
31 | }
32 | ]
33 | },
34 | "include": [
35 | "next-env.d.ts",
36 | "**/*.ts",
37 | "**/*.tsx",
38 | ".next/types/**/*.ts",
39 | ".next/dev/types/**/*.ts"
40 | ],
41 | "exclude": [
42 | "node_modules"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 |
3 | export function cn(...classes: classNames.ArgumentArray) {
4 | return classNames(...classes);
5 | }
6 |
--------------------------------------------------------------------------------
/utils/init-canvas.ts:
--------------------------------------------------------------------------------
1 | import { debounce } from "lodash-es";
2 |
3 | export default (canvas: HTMLCanvasElement) => {
4 | const { width, height } = canvas.getBoundingClientRect();
5 | const ctx = canvas.getContext("2d")!;
6 |
7 | canvas.style.width = `${width}px`;
8 | canvas.style.height = `${height}px`;
9 |
10 | const upscaleCanvas = () => {
11 | const scale = window.visualViewport?.scale || 1;
12 | const dpr = (window.devicePixelRatio || 1) * scale;
13 |
14 | canvas.width = width * dpr;
15 | canvas.height = height * dpr;
16 |
17 | ctx.scale(dpr, dpr);
18 |
19 | canvas.dispatchEvent(new Event("resize"));
20 | };
21 |
22 | upscaleCanvas();
23 |
24 | const handleResize = debounce(upscaleCanvas, 500);
25 |
26 | window.addEventListener("resize", handleResize);
27 | window.visualViewport?.addEventListener("resize", handleResize);
28 |
29 | return ctx;
30 | };
31 |
--------------------------------------------------------------------------------
/utils/on-visible.ts:
--------------------------------------------------------------------------------
1 | const onVisible = (
2 | element: HTMLElement,
3 | callback: () => void,
4 | threshold = 0.1,
5 | ) => {
6 | const observer = new IntersectionObserver(
7 | ([entry]) => {
8 | if (entry.isIntersecting) {
9 | callback();
10 | observer.disconnect();
11 | }
12 | },
13 | { threshold },
14 | );
15 |
16 | observer.observe(element);
17 |
18 | return () => {
19 | observer.disconnect();
20 | };
21 | };
22 |
23 | export default onVisible;
24 |
25 | export const waitUntilVisible = (element: HTMLElement, threshold = 0.1) => {
26 | return new Promise((resolve) => {
27 | onVisible(element, () => resolve(true), threshold);
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | export const sleep = (ms: number) =>
2 | new Promise((resolve) => setTimeout(resolve, ms));
3 |
--------------------------------------------------------------------------------