From 6539d4173d4cdb336bd2d1e05118614d243136bc Mon Sep 17 00:00:00 2001 From: bobbravo2 Date: Wed, 5 Nov 2025 10:22:04 -0500 Subject: [PATCH 1/5] wip with cthao --- components/frontend/.env.production | 2 ++ .../scripts/local-dev/crc-restart-frontend.sh | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 components/frontend/.env.production create mode 100755 components/scripts/local-dev/crc-restart-frontend.sh diff --git a/components/frontend/.env.production b/components/frontend/.env.production new file mode 100644 index 000000000..38dbff7f8 --- /dev/null +++ b/components/frontend/.env.production @@ -0,0 +1,2 @@ +NEXT_PUBLIC_GITHUB_APP_SLUG=ambient-code-local +NEXT_PUBLIC_VTEAM_VERSION=v0.0.3 diff --git a/components/scripts/local-dev/crc-restart-frontend.sh b/components/scripts/local-dev/crc-restart-frontend.sh new file mode 100755 index 000000000..52fdec87f --- /dev/null +++ b/components/scripts/local-dev/crc-restart-frontend.sh @@ -0,0 +1,29 @@ +#!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +MANIFESTS_DIR="${SCRIPT_DIR}/manifests" +STATE_DIR="${SCRIPT_DIR}/state" +mkdir -p "${STATE_DIR}" + +# CRC Configuration +CRC_CPUS="${CRC_CPUS:-4}" +CRC_MEMORY="${CRC_MEMORY:-11264}" +CRC_DISK="${CRC_DISK:-50}" + +# Project Configuration +PROJECT_NAME="${PROJECT_NAME:-vteam-dev}" +DEV_MODE="${DEV_MODE:-false}" + +# Component directories +BACKEND_DIR="${REPO_ROOT}/components/backend" +FRONTEND_DIR="${REPO_ROOT}/components/frontend" +OPERATOR_DIR="${REPO_ROOT}/components/operator" +CRDS_DIR="${REPO_ROOT}/components/manifests/crds" + + +build_and_deploy() { + oc start-build vteam-frontend --from-dir="$FRONTEND_DIR" --wait -n "$PROJECT_NAME" + oc apply -f "${MANIFESTS_DIR}/frontend-deployment.yaml" -n "$PROJECT_NAME" +} + +build_and_deploy From 64de006d320c8a6055dba91dbb7411b19409cd04 Mon Sep 17 00:00:00 2001 From: bobbravo2 Date: Wed, 5 Nov 2025 10:23:12 -0500 Subject: [PATCH 2/5] wip with charles --- .../components/steps/GitHubStep.tsx | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 components/frontend/src/app/onboarding/components/steps/GitHubStep.tsx diff --git a/components/frontend/src/app/onboarding/components/steps/GitHubStep.tsx b/components/frontend/src/app/onboarding/components/steps/GitHubStep.tsx new file mode 100644 index 000000000..88ea1393c --- /dev/null +++ b/components/frontend/src/app/onboarding/components/steps/GitHubStep.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { CheckCircle2, Github, Loader2, AlertCircle } from 'lucide-react'; +import { useGitHubStatus } from '@/services/queries'; + +type GitHubStepProps = { + appSlug?: string; + onConnectionVerified: () => void; + isProcessing?: boolean; +}; + +export function GitHubStep({ appSlug, onConnectionVerified, isProcessing = false }: GitHubStepProps) { + const { data: status, isLoading, refetch } = useGitHubStatus(); + const [isConnecting, setIsConnecting] = useState(false); + + useEffect(() => { + if (status?.installed) { + onConnectionVerified(); + } + }, [status?.installed, onConnectionVerified]); + + // Poll for connection status when user returns from GitHub OAuth + useEffect(() => { + if (isConnecting) { + const interval = setInterval(() => { + refetch(); + }, 2000); + + return () => clearInterval(interval); + } + }, [isConnecting, refetch]); + + const handleConnect = () => { + if (!appSlug) return; + setIsConnecting(true); + const setupUrl = new URL('/onboarding', window.location.origin); + const redirectUri = encodeURIComponent(setupUrl.toString()); + const url = `https://github.com/apps/${appSlug}/installations/new?redirect_uri=${redirectUri}`; + window.location.href = url; + }; + + if (isLoading || isProcessing) { + return ( + + + Connect GitHub + + {isProcessing ? 'Verifying GitHub installation...' : 'Checking GitHub connection status...'} + + + + + + + ); + } + + return ( + + + Connect GitHub + + Enable your AI agents to work with GitHub repositories + + + + {status?.installed ? ( + <> + + + + GitHub is connected + {status.githubUserId && ` as ${status.githubUserId}`} + + + +
+

What you can do with GitHub:

+
    +
  • + + Browse and work with repository code +
  • +
  • + + Create forks and pull requests automatically +
  • +
  • + + Let AI agents make code changes and commit them +
  • +
+
+ + ) : ( + <> +
+

+ Connect your GitHub account to enable AI agents to interact with your repositories. + You'll be able to: +

+
    +
  • + + Browse repository files and branches +
  • +
  • + + Create and manage forks automatically +
  • +
  • + + Submit pull requests with AI-generated code +
  • +
+
+ + {!appSlug && ( + + + + GitHub App is not configured. Please contact your administrator. + + + )} + + + +

+ You'll be redirected to GitHub to authorize the Ambient Code app +

+ + )} +
+
+ ); +} + From 01f73ea3aa6d5d5f1f5a3d3c02ea7fd53464ad57 Mon Sep 17 00:00:00 2001 From: bobbravo2 Date: Wed, 5 Nov 2025 13:24:01 -0500 Subject: [PATCH 3/5] get stuff from stashes --- components/backend/handlers/github_auth.go | 15 + components/frontend/.env.example | 2 +- components/frontend/Dockerfile | 1 - .../app/integrations/IntegrationsClient.tsx | 1 + .../src/app/onboarding/OnboardingClient.tsx | 44 +++ .../components/OnboardingWizard.tsx | 269 ++++++++++++++++++ .../components/steps/CreateProjectStep.tsx | 176 ++++++++++++ .../components/steps/ReviewStep.tsx | 109 +++++++ .../components/steps/WelcomeStep.tsx | 64 +++++ .../frontend/src/app/onboarding/error.tsx | 51 ++++ .../frontend/src/app/onboarding/loading.tsx | 37 +++ .../frontend/src/app/onboarding/page.tsx | 10 + .../frontend/src/app/onboarding/types.ts | 15 + components/frontend/src/app/page.tsx | 18 +- .../manifests/rbac/backend-clusterrole.yaml | 15 +- .../scripts/local-dev/manifests/README.md | 130 +++++++++ .../local-dev/manifests/backend-rbac.yaml | 39 +++ .../local-dev/manifests/frontend-auth.yaml | 15 + .../manifests/frontend-deployment.yaml | 6 +- 19 files changed, 1006 insertions(+), 11 deletions(-) create mode 100644 components/frontend/src/app/onboarding/OnboardingClient.tsx create mode 100644 components/frontend/src/app/onboarding/components/OnboardingWizard.tsx create mode 100644 components/frontend/src/app/onboarding/components/steps/CreateProjectStep.tsx create mode 100644 components/frontend/src/app/onboarding/components/steps/ReviewStep.tsx create mode 100644 components/frontend/src/app/onboarding/components/steps/WelcomeStep.tsx create mode 100644 components/frontend/src/app/onboarding/error.tsx create mode 100644 components/frontend/src/app/onboarding/loading.tsx create mode 100644 components/frontend/src/app/onboarding/page.tsx create mode 100644 components/frontend/src/app/onboarding/types.ts create mode 100644 components/scripts/local-dev/manifests/README.md diff --git a/components/backend/handlers/github_auth.go b/components/backend/handlers/github_auth.go index 4ecd4553b..f4b79bbb1 100644 --- a/components/backend/handlers/github_auth.go +++ b/components/backend/handlers/github_auth.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "strconv" "strings" @@ -209,6 +210,20 @@ func HandleGitHubUserOAuthCallback(c *gin.Context) { if retURL == "" { retURL = "/integrations" } + + // Check if we need to preserve onboarding parameter + onboarding := c.Query("onboarding") + if onboarding != "" { + // Parse the URL to add query parameter properly + parsedURL, err := url.Parse(retURL) + if err == nil { + q := parsedURL.Query() + q.Set("onboarding", onboarding) + parsedURL.RawQuery = q.Encode() + retURL = parsedURL.String() + } + } + c.Redirect(http.StatusFound, retURL) } diff --git a/components/frontend/.env.example b/components/frontend/.env.example index 01a918caf..80d919f96 100644 --- a/components/frontend/.env.example +++ b/components/frontend/.env.example @@ -6,7 +6,7 @@ # GitHub App identifier used to initiate installation # This may be your GitHub App slug or Client ID, depending on your setup -GITHUB_APP_SLUG=ambient-code-vteam +NEXT_PUBLIC_GITHUB_APP_SLUG=ambient-code-local # Direct backend base URL (used by server-side code where applicable) # Default local backend URL diff --git a/components/frontend/Dockerfile b/components/frontend/Dockerfile index c656ac5ac..0e8933298 100644 --- a/components/frontend/Dockerfile +++ b/components/frontend/Dockerfile @@ -59,7 +59,6 @@ EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" - # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/next-config-js/output CMD ["node", "server.js"] diff --git a/components/frontend/src/app/integrations/IntegrationsClient.tsx b/components/frontend/src/app/integrations/IntegrationsClient.tsx index 657447dcd..7726f50aa 100644 --- a/components/frontend/src/app/integrations/IntegrationsClient.tsx +++ b/components/frontend/src/app/integrations/IntegrationsClient.tsx @@ -23,6 +23,7 @@ export default function IntegrationsClient({ appSlug }: Props) { const handleDisconnect = async () => { disconnectMutation.mutate(undefined, { onSuccess: () => { + // TODO actually invalidate the github application remotely successToast('GitHub disconnected successfully') refetch() }, diff --git a/components/frontend/src/app/onboarding/OnboardingClient.tsx b/components/frontend/src/app/onboarding/OnboardingClient.tsx new file mode 100644 index 000000000..d91740540 --- /dev/null +++ b/components/frontend/src/app/onboarding/OnboardingClient.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { useProjects } from '@/services/queries'; +import { OnboardingWizard } from './components/OnboardingWizard'; +import { Skeleton } from '@/components/ui/skeleton'; + +type OnboardingClientProps = { + appSlug?: string; +}; + +export default function OnboardingClient({ appSlug }: OnboardingClientProps) { + const { data: projects, isLoading } = useProjects(); + + // If user already has projects, allow them to skip onboarding + const canSkip = !isLoading && (projects?.length ?? 0) > 0; + + // Show loading state while checking projects + if (isLoading) { + return ( +
+
+
+
+ + +
+
+ +
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
+
+ +
+
+
+ ); + } + + return ; +} + diff --git a/components/frontend/src/app/onboarding/components/OnboardingWizard.tsx b/components/frontend/src/app/onboarding/components/OnboardingWizard.tsx new file mode 100644 index 000000000..d9f21421f --- /dev/null +++ b/components/frontend/src/app/onboarding/components/OnboardingWizard.tsx @@ -0,0 +1,269 @@ +'use client'; + +import { useState, useCallback, useEffect } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { ArrowLeft, ArrowRight, X } from 'lucide-react'; +import { WelcomeStep } from './steps/WelcomeStep'; +import { CreateProjectStep } from './steps/CreateProjectStep'; +import { GitHubStep } from './steps/GitHubStep'; +import { ReviewStep } from './steps/ReviewStep'; +import { useConnectGitHub } from '@/services/queries'; +import type { WizardData, WizardStep } from '../types'; + +const WIZARD_STEPS: WizardStep[] = [ + { + id: 'welcome', + title: 'Welcome', + description: 'Get started with Ambient Code', + }, + { + id: 'github', + title: 'Connect GitHub', + description: 'Enable repository access', + }, + { + id: 'project', + title: 'Create Project', + description: 'Set up your workspace', + }, + { + id: 'review', + title: 'Review', + description: 'Confirm and complete', + }, +]; + +type OnboardingWizardProps = { + appSlug?: string; + canSkip?: boolean; +}; + +export function OnboardingWizard({ appSlug, canSkip = false }: OnboardingWizardProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + const connectGitHubMutation = useConnectGitHub(); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [wizardData, setWizardData] = useState({ + projectName: '', + projectDisplayName: '', + projectDescription: '', + githubConnected: false, + }); + const [processingInstallation, setProcessingInstallation] = useState(false); + + const currentStep = WIZARD_STEPS[currentStepIndex]; + const isFirstStep = currentStepIndex === 0; + const isLastStep = currentStepIndex === WIZARD_STEPS.length - 1; + const progress = ((currentStepIndex + 1) / WIZARD_STEPS.length) * 100; + + const handleNext = useCallback(() => { + if (!isLastStep) { + setCurrentStepIndex((prev) => prev + 1); + } + }, [isLastStep]); + + const handleBack = useCallback(() => { + if (!isFirstStep) { + setCurrentStepIndex((prev) => prev - 1); + } + }, [isFirstStep]); + + const handleProjectCreated = useCallback( + (projectName: string) => { + setWizardData((prev) => ({ + ...prev, + createdProjectName: projectName, + })); + handleNext(); + }, + [handleNext] + ); + + const handleGitHubVerified = useCallback(() => { + setWizardData((prev) => ({ + ...prev, + githubConnected: true, + })); + }, []); + + const handleComplete = useCallback(() => { + const projectName = wizardData.createdProjectName || wizardData.projectName; + if (projectName) { + router.push(`/projects/${projectName}`); + } else { + router.push('/projects'); + } + }, [wizardData, router]); + + const handleSkip = useCallback(() => { + router.push('/projects'); + }, [router]); + + const canProceedToNext = useCallback(() => { + switch (currentStep.id) { + case 'welcome': + return true; + case 'github': + return wizardData.githubConnected; + case 'project': + return !!wizardData.createdProjectName; + case 'review': + return true; + default: + return false; + } + }, [currentStep.id, wizardData]); + + // Handle GitHub installation callback + useEffect(() => { + const installationId = searchParams.get('installation_id'); + const isOnGitHubStep = currentStep.id === 'github'; + + if (installationId && isOnGitHubStep && !processingInstallation && !wizardData.githubConnected) { + setProcessingInstallation(true); + + connectGitHubMutation.mutate( + { installationId: Number(installationId) }, + { + onSuccess: () => { + handleGitHubVerified(); + // Remove the installation_id from URL without navigation + const newUrl = new URL(window.location.href); + newUrl.searchParams.delete('installation_id'); + window.history.replaceState({}, '', newUrl); + }, + onError: (error) => { + console.error('Failed to connect GitHub:', error); + setProcessingInstallation(false); + }, + } + ); + } + }, [searchParams, currentStep.id, processingInstallation, wizardData.githubConnected, connectGitHubMutation, handleGitHubVerified]); + + return ( +
+
+ {/* Header */} +
+
+
+

Get Started

+

+ Set up your Ambient Code workspace +

+
+ {canSkip && ( + + )} +
+ + {/* Progress */} +
+
+ + Step {currentStepIndex + 1} of {WIZARD_STEPS.length} + + {currentStep.title} +
+ +
+ + {/* Step Indicators */} +
+ {WIZARD_STEPS.map((step, index) => ( +
+
+
+ {index + 1} +
+
+

+ {step.title} +

+
+
+
+ ))} +
+
+ + {/* Step Content */} +
+ {currentStep.id === 'welcome' && } + {currentStep.id === 'project' && ( + + )} + {currentStep.id === 'github' && ( + + )} + {currentStep.id === 'review' && } +
+ + {/* Navigation */} +
+ + + {isLastStep ? ( + + ) : ( + + )} +
+
+
+ ); +} + diff --git a/components/frontend/src/app/onboarding/components/steps/CreateProjectStep.tsx b/components/frontend/src/app/onboarding/components/steps/CreateProjectStep.tsx new file mode 100644 index 000000000..9eb74e837 --- /dev/null +++ b/components/frontend/src/app/onboarding/components/steps/CreateProjectStep.tsx @@ -0,0 +1,176 @@ +'use client'; + +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { AlertCircle, Loader2 } from 'lucide-react'; +import { useCreateProject } from '@/services/queries'; +import { useEffect } from 'react'; + +const projectSchema = z.object({ + name: z + .string() + .min(3, 'Project name must be at least 3 characters') + .max(63, 'Project name must be at most 63 characters') + .regex(/^[a-z0-9-]+$/, 'Project name must be lowercase alphanumeric with dashes') + .regex(/^[a-z]/, 'Project name must start with a letter') + .regex(/[a-z0-9]$/, 'Project name must end with a letter or number'), + displayName: z + .string() + .min(1, 'Display name is required') + .max(100, 'Display name must be at most 100 characters'), + description: z.string().max(500, 'Description must be at most 500 characters').optional(), +}); + +type ProjectFormData = z.infer; + +type CreateProjectStepProps = { + onProjectCreated: (projectName: string) => void; + initialData?: { + name: string; + displayName: string; + description: string; + }; +}; + +export function CreateProjectStep({ onProjectCreated, initialData }: CreateProjectStepProps) { + const createProjectMutation = useCreateProject(); + + const form = useForm({ + resolver: zodResolver(projectSchema), + defaultValues: { + name: initialData?.name || '', + displayName: initialData?.displayName || '', + description: initialData?.description || '', + }, + }); + + const onSubmit = (data: ProjectFormData) => { + createProjectMutation.mutate( + { + name: data.name, + displayName: data.displayName, + description: data.description || '', + }, + { + onSuccess: () => { + onProjectCreated(data.name); + }, + } + ); + }; + + // Auto-generate project name from display name + const watchDisplayName = form.watch('displayName'); + useEffect(() => { + if (watchDisplayName && !form.formState.dirtyFields.name) { + const kebabCase = watchDisplayName + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + form.setValue('name', kebabCase, { shouldValidate: true }); + } + }, [watchDisplayName, form]); + + return ( + + + Create Your First Project + + Projects provide isolated namespaces for your agentic sessions and configurations + + + +
+ + ( + + Display Name + + + + A human-readable name for your project + + + )} + /> + + ( + + Project Name + + + + + Unique identifier (lowercase, alphanumeric, dashes only) + + + + )} + /> + + ( + + Description (Optional) + +