diff --git a/apps/web-roo-code/src/app/cli-preview/cli-preview-content.tsx b/apps/web-roo-code/src/app/cli-preview/cli-preview-content.tsx new file mode 100644 index 0000000000..18119f4eb5 --- /dev/null +++ b/apps/web-roo-code/src/app/cli-preview/cli-preview-content.tsx @@ -0,0 +1,286 @@ +"use client" + +import { useEffect, useState } from "react" +import { useSearchParams, usePathname } from "next/navigation" +import { usePostHog } from "posthog-js/react" +import Link from "next/link" + +import { HubSpotForm } from "@/components/forms/hubspot-form" +import { parseAttributionParams, attributionToHiddenFields } from "@/lib/attribution" +import { EXTERNAL_LINKS } from "@/lib/constants" + +export function CliPreviewContent() { + const searchParams = useSearchParams() + const pathname = usePathname() + const posthog = usePostHog() + const [isSubmitted, setIsSubmitted] = useState(false) + const [hiddenFields, setHiddenFields] = useState>({}) + + // Get HubSpot config from environment + const portalId = process.env.NEXT_PUBLIC_HUBSPOT_PORTAL_ID + const formId = process.env.NEXT_PUBLIC_HUBSPOT_FORM_ID_CLI_PREVIEW + + // Parse attribution parameters and set hidden fields + useEffect(() => { + const attributionParams = parseAttributionParams(searchParams, pathname) + const fields = attributionToHiddenFields(attributionParams) + setHiddenFields(fields) + + // Capture pageview event with attribution + posthog?.capture("cli_preview_viewed", { + ...attributionParams, + page_path: pathname, + }) + }, [searchParams, pathname, posthog]) + + // Capture form render events + useEffect(() => { + if (portalId && formId) { + posthog?.capture("cli_preview_form_rendered", { + ...hiddenFields, + }) + } else { + posthog?.capture("cli_preview_form_placeholder_shown", { + ...hiddenFields, + }) + } + }, [portalId, formId, hiddenFields, posthog]) + + const handleFormSubmit = () => { + setIsSubmitted(true) + posthog?.capture("cli_preview_form_submitted", { + ...hiddenFields, + }) + } + + const handleLoginClick = () => { + posthog?.capture("cli_preview_nextstep_login_clicked", { + ...hiddenFields, + }) + } + + const handleSignupClick = () => { + posthog?.capture("cli_preview_nextstep_signup_clicked", { + ...hiddenFields, + }) + } + + return ( +
+ {/* Hero Section */} +
+
+
+
+ +

+ Get an early preview of the Roo Code CLI +

+

+ Bring Roo Code's iterative agent capabilities to your terminal. Access rolls out in small + batches, so join the queue now to be considered for an invite. +

+

+ If selected, we'll email you a private invite link with next steps. +

+
+
+ + {/* Benefits Section */} +
+
+
+
+

+ Why the CLI +

+

+ High-leverage workflows, without the IDE overhead +

+
+
+ Early access queue +
+
+ +
    +
  • +
    + + + +
    +
    +

    + Run the agent in your terminal +

    +

    + Trigger the "close the loop" workflow to run edits, commands, and + iterations without opening VS Code. +

    +
    +
  • +
  • +
    + + + +
    +
    +

    Reuse your profiles

    +

    + Your existing BYOK keys, models, and custom instructions work out of the box. No new + configuration required. +

    +
    +
  • +
  • +
    + + + +
    +
    +

    Script the repetition

    +

    + Pipe context directly to the agent to automate batch refactors, migrations, or + test-fix loops. +

    +
    +
  • +
  • +
    + + + +
    +
    +

    Define the spec

    +

    + This is an early preview. Help us decide which flags, outputs, and safeguards belong + in v1. +

    +
    +
  • +
+
+
+ + {/* Form Section */} +
+
+ {!isSubmitted ? ( + <> +
+ Heads up: we're granting access in batches. + If the current batch is full, we'll keep you in the queue for the next one. +
+ +
+

+ Join the Early Preview +

+

+ We're rolling access out in batches. Tell us what you want to do with the CLI + and we'll follow up with next steps. +

+
+ + + ) : ( +
+
+
+ + + +
+
+

+ You're in the queue +

+

+ We've received your request. If you're selected for a batch, we'll reach + out with an invite and next steps. +

+
+

+ While you wait, jump into Roo Code Cloud to try Cloud Agents and stay close to new + releases. +

+
+ + Log in to Cloud + + + Sign up for Cloud + +
+
+
+ )} +
+
+
+ ) +} diff --git a/apps/web-roo-code/src/app/cli-preview/page.tsx b/apps/web-roo-code/src/app/cli-preview/page.tsx new file mode 100644 index 0000000000..aeca404a82 --- /dev/null +++ b/apps/web-roo-code/src/app/cli-preview/page.tsx @@ -0,0 +1,57 @@ +import type { Metadata } from "next" +import { Suspense } from "react" + +import { SEO } from "@/lib/seo" +import { ogImageUrl } from "@/lib/og" +import { CliPreviewContent } from "./cli-preview-content" + +const TITLE = "CLI Early Preview" +const DESCRIPTION = + "Bring Roo Code's iterative agent capabilities to your terminal. Pipe context directly to the agent, automate repetitive refactors across files, and run tasks without the IDE overhead." +const OG_DESCRIPTION = "Early preview of the Roo Code CLI" +const PATH = "/cli-preview" + +export const metadata: Metadata = { + title: TITLE, + description: DESCRIPTION, + robots: { + index: false, + follow: false, + }, + alternates: { + canonical: `${SEO.url}${PATH}`, + }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + url: `${SEO.url}${PATH}`, + siteName: SEO.name, + images: [ + { + url: ogImageUrl(TITLE, OG_DESCRIPTION), + width: 1200, + height: 630, + alt: TITLE, + }, + ], + locale: SEO.locale, + type: "website", + }, + twitter: { + card: SEO.twitterCard, + title: TITLE, + description: DESCRIPTION, + images: [ogImageUrl(TITLE, OG_DESCRIPTION)], + }, +} + +export default function CliPreviewPage() { + return ( + + }> + + + ) +} diff --git a/apps/web-roo-code/src/components/forms/hubspot-form.tsx b/apps/web-roo-code/src/components/forms/hubspot-form.tsx new file mode 100644 index 0000000000..3e0456b8c9 --- /dev/null +++ b/apps/web-roo-code/src/components/forms/hubspot-form.tsx @@ -0,0 +1,173 @@ +"use client" + +import { useEffect, useRef, useState } from "react" + +declare global { + interface Window { + hbspt?: { + forms: { + create: (options: { + region?: string + portalId: string + formId: string + target: string + onFormSubmitted?: () => void + }) => void + } + } + } +} + +export interface HubSpotFormProps { + portalId?: string + formId?: string + region?: string + className?: string + onSubmitted?: () => void + hiddenFields?: Record +} + +export function HubSpotForm({ + portalId, + formId, + region = "na1", + className = "", + onSubmitted, + hiddenFields = {}, +}: HubSpotFormProps) { + const formContainerRef = useRef(null) + const [isScriptLoaded, setIsScriptLoaded] = useState(false) + const [hasError, setHasError] = useState(false) + const formCreatedRef = useRef(false) + + // Check if HubSpot IDs are configured + const isConfigured = Boolean(portalId && formId) + + // Load HubSpot script + useEffect(() => { + if (!isConfigured) return + + // Check if script already exists + const existingScript = document.querySelector('script[src*="js.hsforms.net"]') + if (existingScript) { + setIsScriptLoaded(true) + return + } + + // Create and load script + const script = document.createElement("script") + script.src = "https://js.hsforms.net/forms/v2.js" + script.async = true + script.defer = true + + script.onload = () => { + setIsScriptLoaded(true) + } + + script.onerror = () => { + console.error("Failed to load HubSpot form script") + setHasError(true) + } + + document.body.appendChild(script) + + return () => { + // Don't remove the script on unmount as it might be used by other forms + } + }, [isConfigured]) + + // Create form once script is loaded + useEffect(() => { + if (!isConfigured || !isScriptLoaded || !window.hbspt || !formContainerRef.current || formCreatedRef.current) { + return + } + + try { + // Mark as created before calling create to prevent double-creation + formCreatedRef.current = true + + window.hbspt.forms.create({ + region, + portalId: portalId!, + formId: formId!, + target: `#${formContainerRef.current.id}`, + onFormSubmitted: () => { + if (onSubmitted) { + onSubmitted() + } + }, + }) + + // Set hidden fields after form is created + // HubSpot forms need a moment to render + setTimeout(() => { + if (Object.keys(hiddenFields).length > 0) { + Object.entries(hiddenFields).forEach(([name, value]) => { + const input = document.querySelector(`input[name="${name}"]`) + if (input) { + input.value = value + } + }) + } + }, 500) + } catch (error) { + console.error("Error creating HubSpot form:", error) + setHasError(true) + } + }, [isConfigured, isScriptLoaded, portalId, formId, region, onSubmitted, hiddenFields]) + + // Show placeholder if not configured + if (!isConfigured) { + return ( +
+
+
📝
+

Form Configuration Pending

+

+ The HubSpot form for CLI preview is not yet configured. Please set the following environment + variables: +

+
+
NEXT_PUBLIC_HUBSPOT_PORTAL_ID
+
NEXT_PUBLIC_HUBSPOT_FORM_ID_CLI_PREVIEW
+
+ {process.env.NODE_ENV === "development" && ( +

+ This message is only visible in development. In production, consider showing a contact email + or alternative CTA. +

+ )} +
+
+ ) + } + + // Show error state if script failed to load + if (hasError) { + return ( +
+
+
⚠️
+

Unable to Load Form

+

+ We're having trouble loading the form. This might be due to an ad blocker or connectivity + issue. +

+

+ Please reach out to us directly at{" "} + + support@roocode.com + +

+
+
+ ) + } + + // Render form container + return ( +
+
+
+ ) +} diff --git a/apps/web-roo-code/src/lib/attribution.ts b/apps/web-roo-code/src/lib/attribution.ts new file mode 100644 index 0000000000..c7f7c532d9 --- /dev/null +++ b/apps/web-roo-code/src/lib/attribution.ts @@ -0,0 +1,82 @@ +/** + * Parse attribution parameters from URL search params + */ +export interface AttributionParams { + utm_source?: string + utm_medium?: string + utm_campaign?: string + utm_content?: string + utm_term?: string + ref?: string + landing_path?: string + landing_url?: string +} + +/** + * Extract attribution parameters from URLSearchParams + */ +export function parseAttributionParams(searchParams: URLSearchParams, pathname?: string): AttributionParams { + const params: AttributionParams = {} + + // Extract UTM parameters + const utmSource = searchParams.get("utm_source") + const utmMedium = searchParams.get("utm_medium") + const utmCampaign = searchParams.get("utm_campaign") + const utmContent = searchParams.get("utm_content") + const utmTerm = searchParams.get("utm_term") + const ref = searchParams.get("ref") + + if (utmSource) params.utm_source = utmSource + if (utmMedium) params.utm_medium = utmMedium + if (utmCampaign) params.utm_campaign = utmCampaign + if (utmContent) params.utm_content = utmContent + if (utmTerm) params.utm_term = utmTerm + if (ref) params.ref = ref + + // Add landing path and URL if available + if (typeof window !== "undefined") { + if (pathname) { + params.landing_path = pathname + } + params.landing_url = window.location.href + } + + return params +} + +/** + * Get referrer from document or explicit ref parameter + */ +export function getReferrer(searchParams: URLSearchParams): string | undefined { + // First check for explicit ref parameter + const explicitRef = searchParams.get("ref") + if (explicitRef) { + return explicitRef + } + + // Fall back to document referrer + if (typeof document !== "undefined" && document.referrer) { + return document.referrer + } + + return undefined +} + +/** + * Convert attribution params to HubSpot hidden fields format + */ +export function attributionToHiddenFields(params: AttributionParams): Record { + const fields: Record = {} + + // Only include fields that have values + if (params.utm_source) fields.utm_source = params.utm_source + if (params.utm_medium) fields.utm_medium = params.utm_medium + if (params.utm_campaign) fields.utm_campaign = params.utm_campaign + if (params.utm_content) fields.utm_content = params.utm_content + if (params.utm_term) fields.utm_term = params.utm_term + if (params.ref) fields.ref = params.ref + if (params.landing_path) fields.landing_path = params.landing_path + if (params.landing_url) fields.landing_url = params.landing_url + + return fields +}