Repository: firecrawl/open-lovable Branch: main Commit: 69bd93bae7a9 Files: 326 Total size: 2.0 MB Directory structure: gitextract__mw591t9/ ├── .cursor/ │ └── mcp.json ├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── api/ │ │ ├── analyze-edit-intent/ │ │ │ └── route.ts │ │ ├── apply-ai-code/ │ │ │ └── route.ts │ │ ├── apply-ai-code-stream/ │ │ │ └── route.ts │ │ ├── check-vite-errors/ │ │ │ └── route.ts │ │ ├── clear-vite-errors-cache/ │ │ │ └── route.ts │ │ ├── conversation-state/ │ │ │ └── route.ts │ │ ├── create-ai-sandbox/ │ │ │ └── route.ts │ │ ├── create-ai-sandbox-v2/ │ │ │ └── route.ts │ │ ├── create-zip/ │ │ │ └── route.ts │ │ ├── detect-and-install-packages/ │ │ │ └── route.ts │ │ ├── extract-brand-styles/ │ │ │ └── route.ts │ │ ├── generate-ai-code-stream/ │ │ │ └── route.ts │ │ ├── get-sandbox-files/ │ │ │ └── route.ts │ │ ├── install-packages/ │ │ │ └── route.ts │ │ ├── install-packages-v2/ │ │ │ └── route.ts │ │ ├── kill-sandbox/ │ │ │ └── route.ts │ │ ├── monitor-vite-logs/ │ │ │ └── route.ts │ │ ├── report-vite-error/ │ │ │ └── route.ts │ │ ├── restart-vite/ │ │ │ └── route.ts │ │ ├── run-command/ │ │ │ └── route.ts │ │ ├── run-command-v2/ │ │ │ └── route.ts │ │ ├── sandbox-logs/ │ │ │ └── route.ts │ │ ├── sandbox-status/ │ │ │ └── route.ts │ │ ├── scrape-screenshot/ │ │ │ └── route.ts │ │ ├── scrape-url-enhanced/ │ │ │ └── route.ts │ │ ├── scrape-website/ │ │ │ └── route.ts │ │ └── search/ │ │ └── route.ts │ ├── builder/ │ │ └── page.tsx │ ├── generation/ │ │ └── page.tsx │ ├── globals.css │ ├── landing.tsx │ ├── layout.tsx │ └── page.tsx ├── atoms/ │ └── sheets.ts ├── colors.json ├── components/ │ ├── CodeApplicationProgress.tsx │ ├── FirecrawlIcon.tsx │ ├── FirecrawlLogo.tsx │ ├── HMRErrorDetector.tsx │ ├── HeroInput.tsx │ ├── SandboxPreview.tsx │ ├── app/ │ │ ├── (home)/ │ │ │ └── sections/ │ │ │ ├── ai-readiness/ │ │ │ │ ├── ControlPanel.tsx │ │ │ │ ├── InlineResults.tsx │ │ │ │ ├── MetricBars.tsx │ │ │ │ ├── RadarChart.tsx │ │ │ │ └── ScoreChart.tsx │ │ │ ├── endpoints/ │ │ │ │ ├── EndpointsCrawl/ │ │ │ │ │ └── EndpointsCrawl.tsx │ │ │ │ ├── EndpointsExtract/ │ │ │ │ │ └── EndpointsExtract.tsx │ │ │ │ ├── EndpointsMap/ │ │ │ │ │ └── EndpointsMap.tsx │ │ │ │ ├── EndpointsScrape/ │ │ │ │ │ └── EndpointsScrape.tsx │ │ │ │ ├── EndpointsSearch/ │ │ │ │ │ └── EndpointsSearch.tsx │ │ │ │ ├── Extract/ │ │ │ │ │ └── Extract.tsx │ │ │ │ └── Mcp/ │ │ │ │ └── Mcp.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 │ │ │ ├── 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 │ │ ├── .cursor/ │ │ │ └── rules/ │ │ │ └── home-page-components.md │ │ └── generation/ │ │ ├── SidebarInput.tsx │ │ └── SidebarQuickInput.tsx │ ├── shared/ │ │ ├── Playground/ │ │ │ └── Context/ │ │ │ └── types.ts │ │ ├── animated-dot-icon.tsx │ │ ├── animated-height.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/ │ │ │ ├── .cursor/ │ │ │ │ └── rules/ │ │ │ │ └── flame-effects.md │ │ │ ├── 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.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 │ │ ├── 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 │ │ ├── notifications/ │ │ │ └── slack-notification.tsx │ │ ├── pixi/ │ │ │ ├── Pixi.tsx │ │ │ ├── PixiAssetManager.ts │ │ │ └── utils.ts │ │ ├── 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/ │ ├── button.tsx │ ├── checkbox.tsx │ ├── code.tsx │ ├── input.tsx │ ├── label.tsx │ ├── motion/ │ │ └── scramble-text.tsx │ ├── select.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 │ └── textarea.tsx ├── config/ │ └── app.config.ts ├── eslint.config.mjs ├── hooks/ │ ├── useDebouncedCallback.ts │ ├── useDebouncedEffect.ts │ └── useSwitchingCode.ts ├── lib/ │ ├── ai/ │ │ └── provider-manager.ts │ ├── build-validator.ts │ ├── context-selector.ts │ ├── edit-examples.ts │ ├── edit-intent-analyzer.ts │ ├── file-parser.ts │ ├── file-search-executor.ts │ ├── icons.ts │ ├── morph-fast-apply.ts │ ├── sandbox/ │ │ ├── factory.ts │ │ ├── providers/ │ │ │ ├── e2b-provider.ts │ │ │ └── vercel-provider.ts │ │ ├── sandbox-manager.ts │ │ └── types.ts │ └── utils.ts ├── next.config.ts ├── package.json ├── packages/ │ └── create-open-lovable/ │ ├── index.js │ ├── lib/ │ │ ├── installer.js │ │ └── prompts.js │ ├── package.json │ └── templates/ │ ├── e2b/ │ │ ├── .env.example │ │ └── README.md │ └── vercel/ │ ├── .env.example │ └── README.md ├── postcss.config.mjs ├── public/ │ ├── compressor.json │ └── firecrawl-logo ├── styles/ │ ├── additional-styles/ │ │ ├── custom-fonts.css │ │ ├── theme.css │ │ └── utility-patterns.css │ ├── chrome-bug.css │ ├── colors.json │ ├── components/ │ │ ├── .cursor/ │ │ │ └── rules/ │ │ │ └── component-styles.md │ │ ├── button.css │ │ ├── code.css │ │ └── index.css │ ├── design-system/ │ │ ├── .cursor/ │ │ │ └── rules/ │ │ │ └── design-system.md │ │ ├── animations.css │ │ ├── base/ │ │ │ ├── body.css │ │ │ ├── layout.css │ │ │ └── reset.css │ │ ├── colors.css │ │ ├── fonts.css │ │ ├── typography.css │ │ └── utilities.css │ ├── fire.css │ ├── inside-border-fix.css │ └── main.css ├── tailwind.config.ts ├── tsconfig.json ├── types/ │ ├── conversation.ts │ ├── file-manifest.ts │ └── sandbox.ts └── utils/ ├── cn.ts ├── init-canvas.ts ├── set-timeout-on-visible.ts └── sleep.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cursor/mcp.json ================================================ { "mcpServers": { "dev3000": { "type": "http", "url": "http://localhost:3684/mcp" } } } ================================================ FILE: .env.example ================================================ # Required FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) # ================================================================================= # SANDBOX PROVIDER - Choose Option 1 OR 2 # ================================================================================= # Option 1: Vercel Sandbox (recommended - default) # Set SANDBOX_PROVIDER=vercel and choose authentication method below SANDBOX_PROVIDER=vercel # Vercel Authentication - Choose method a OR b # Method a: OIDC Token (recommended for development) # Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull # Method b: Personal Access Token (for production or when OIDC unavailable) # VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID # VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID # VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard # Get yours at https://console.groq.com GROQ_API_KEY=your_groq_api_key_here ======= # Option 2: E2B Sandbox # Set SANDBOX_PROVIDER=e2b and configure E2B_API_KEY below # SANDBOX_PROVIDER=e2b # E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev # ================================================================================= # AI PROVIDERS - Need at least one # ================================================================================= # Vercel AI Gateway (recommended - provides access to multiple models) AI_GATEWAY_API_KEY=your_ai_gateway_api_key # Get from https://vercel.com/dashboard/ai-gateway/api-keys # Individual provider keys (used when AI_GATEWAY_API_KEY is not set) ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended) # Optional Morph Fast Apply # Get yours at https://morphllm.com/ MORPH_API_KEY=your_fast_apply_key ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules **/node_modules/ /.pnp .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/versions # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) .env* .env.local !.env.example # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts # E2B template builds *.tar.gz e2b-template-* # IDE .vscode/ .idea/ # Temporary files *.tmp *.temp repomix-output.txt bun.lockb .env*.local ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Open Lovable Chat with AI to build React apps instantly. An example app made by the [Firecrawl](https://firecrawl.dev/?ref=open-lovable-github) team. For a complete cloud solution, check out [Lovable.dev](https://lovable.dev/) ❤️. Open Lovable Demo ## Setup 1. **Clone & Install** ```bash git clone https://github.com/firecrawl/open-lovable.git cd open-lovable pnpm install # or npm install / yarn install ``` 2. **Add `.env.local`** ```env # ================================================================= # REQUIRED # ================================================================= FIRECRAWL_API_KEY=your_firecrawl_api_key # https://firecrawl.dev # ================================================================= # AI PROVIDER - Choose your LLM # ================================================================= GEMINI_API_KEY=your_gemini_api_key # https://aistudio.google.com/app/apikey ANTHROPIC_API_KEY=your_anthropic_api_key # https://console.anthropic.com OPENAI_API_KEY=your_openai_api_key # https://platform.openai.com GROQ_API_KEY=your_groq_api_key # https://console.groq.com # ================================================================= # FAST APPLY (Optional - for faster edits) # ================================================================= MORPH_API_KEY=your_morphllm_api_key # https://morphllm.com/dashboard # ================================================================= # SANDBOX PROVIDER - Choose ONE: Vercel (default) or E2B # ================================================================= SANDBOX_PROVIDER=vercel # or 'e2b' # Option 1: Vercel Sandbox (default) # Choose one authentication method: # Method A: OIDC Token (recommended for development) # Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull # Method B: Personal Access Token (for production or when OIDC unavailable) # VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID # VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID # VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard # Option 2: E2B Sandbox # E2B_API_KEY=your_e2b_api_key # https://e2b.dev ``` 3. **Run** ```bash pnpm dev # or npm run dev / yarn dev ``` Open [http://localhost:3000](http://localhost:3000) ## License MIT ================================================ FILE: app/api/analyze-edit-intent/route.ts ================================================ import { NextRequest, NextResponse } from 'next/server'; import { createGroq } from '@ai-sdk/groq'; import { createAnthropic } from '@ai-sdk/anthropic'; import { createOpenAI } from '@ai-sdk/openai'; import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { generateObject } from 'ai'; import { z } from 'zod'; // import type { FileManifest } from '@/types/file-manifest'; // Type is used implicitly through manifest parameter // Check if we're using Vercel AI Gateway const isUsingAIGateway = !!process.env.AI_GATEWAY_API_KEY; const aiGatewayBaseURL = 'https://ai-gateway.vercel.sh/v1'; const groq = createGroq({ apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GROQ_API_KEY, baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined, }); const anthropic = createAnthropic({ apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.ANTHROPIC_API_KEY, baseURL: isUsingAIGateway ? aiGatewayBaseURL : (process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1'), }); const openai = createOpenAI({ apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.OPENAI_API_KEY, baseURL: isUsingAIGateway ? aiGatewayBaseURL : process.env.OPENAI_BASE_URL, }); const googleGenerativeAI = createGoogleGenerativeAI({ apiKey: process.env.AI_GATEWAY_API_KEY ?? process.env.GEMINI_API_KEY, baseURL: isUsingAIGateway ? aiGatewayBaseURL : undefined, }); // Schema for the AI's search plan - not file selection! const searchPlanSchema = z.object({ editType: z.enum([ 'UPDATE_COMPONENT', 'ADD_FEATURE', 'FIX_ISSUE', 'UPDATE_STYLE', 'REFACTOR', 'ADD_DEPENDENCY', 'REMOVE_ELEMENT' ]).describe('The type of edit being requested'), reasoning: z.string().describe('Explanation of the search strategy'), searchTerms: z.array(z.string()).describe('Specific text to search for (case-insensitive). Be VERY specific - exact button text, class names, etc.'), regexPatterns: z.array(z.string()).optional().describe('Regex patterns for finding code structures (e.g., "className=[\\"\\\'].*header.*[\\"\\\']")'), fileTypesToSearch: z.array(z.string()).default(['.jsx', '.tsx', '.js', '.ts']).describe('File extensions to search'), expectedMatches: z.number().min(1).max(10).default(1).describe('Expected number of matches (helps validate search worked)'), fallbackSearch: z.object({ terms: z.array(z.string()), patterns: z.array(z.string()).optional() }).optional().describe('Backup search if primary fails') }); export async function POST(request: NextRequest) { try { const { prompt, manifest, model = 'openai/gpt-oss-20b' } = await request.json(); console.log('[analyze-edit-intent] Request received'); console.log('[analyze-edit-intent] Prompt:', prompt); console.log('[analyze-edit-intent] Model:', model); console.log('[analyze-edit-intent] Manifest files count:', manifest?.files ? Object.keys(manifest.files).length : 0); if (!prompt || !manifest) { return NextResponse.json({ error: 'prompt and manifest are required' }, { status: 400 }); } // Create a summary of available files for the AI const validFiles = Object.entries(manifest.files as Record) .filter(([path]) => { // Filter out invalid paths return path.includes('.') && !path.match(/\/\d+$/); }); const fileSummary = validFiles .map(([path, info]: [string, any]) => { const componentName = info.componentInfo?.name || path.split('/').pop(); // const hasImports = info.imports?.length > 0; // Kept for future use const childComponents = info.componentInfo?.childComponents?.join(', ') || 'none'; return `- ${path} (${componentName}, renders: ${childComponents})`; }) .join('\n'); console.log('[analyze-edit-intent] Valid files found:', validFiles.length); if (validFiles.length === 0) { console.error('[analyze-edit-intent] No valid files found in manifest'); return NextResponse.json({ success: false, error: 'No valid files found in manifest' }, { status: 400 }); } console.log('[analyze-edit-intent] Analyzing prompt:', prompt); console.log('[analyze-edit-intent] File summary preview:', fileSummary.split('\n').slice(0, 5).join('\n')); // Select the appropriate AI model based on the request let aiModel; if (model.startsWith('anthropic/')) { aiModel = anthropic(model.replace('anthropic/', '')); } else if (model.startsWith('openai/')) { if (model.includes('gpt-oss')) { aiModel = groq(model); } else { aiModel = openai(model.replace('openai/', '')); } } else if (model.startsWith('google/')) { aiModel = googleGenerativeAI(model.replace('google/', '')); } else { // Default to groq if model format is unclear aiModel = groq(model); } console.log('[analyze-edit-intent] Using AI model:', model); // Use AI to create a search plan const result = await generateObject({ model: aiModel, schema: searchPlanSchema, messages: [ { role: 'system', content: `You are an expert at planning code searches. Your job is to create a search strategy to find the exact code that needs to be edited. DO NOT GUESS which files to edit. Instead, provide specific search terms that will locate the code. SEARCH STRATEGY RULES: 1. For text changes (e.g., "change 'Start Deploying' to 'Go Now'"): - Search for the EXACT text: "Start Deploying" 2. For style changes (e.g., "make header black"): - Search for component names: "Header", "; packages: string[]; commands: string[]; structure: string | null; } function parseAIResponse(response: string): ParsedResponse { const sections = { files: [] as Array<{ path: string; content: string }>, commands: [] as string[], packages: [] as string[], structure: null as string | null, explanation: '', template: '' }; // Parse file sections - handle duplicates and prefer complete versions const fileMap = new Map(); const fileRegex = /([\s\S]*?)(?:<\/file>|$)/g; let match; while ((match = fileRegex.exec(response)) !== null) { const filePath = match[1]; const content = match[2].trim(); const hasClosingTag = response.substring(match.index, match.index + match[0].length).includes(''); // Check if this file already exists in our map const existing = fileMap.get(filePath); // Decide whether to keep this version let shouldReplace = false; if (!existing) { shouldReplace = true; // First occurrence } else if (!existing.isComplete && hasClosingTag) { shouldReplace = true; // Replace incomplete with complete console.log(`[parseAIResponse] Replacing incomplete ${filePath} with complete version`); } else if (existing.isComplete && hasClosingTag && content.length > existing.content.length) { shouldReplace = true; // Replace with longer complete version console.log(`[parseAIResponse] Replacing ${filePath} with longer complete version`); } else if (!existing.isComplete && !hasClosingTag && content.length > existing.content.length) { shouldReplace = true; // Both incomplete, keep longer one } if (shouldReplace) { // Additional validation: reject obviously broken content if (content.includes('...') && !content.includes('...props') && !content.includes('...rest')) { console.warn(`[parseAIResponse] Warning: ${filePath} contains ellipsis, may be truncated`); // Still use it if it's the only version we have if (!existing) { fileMap.set(filePath, { content, isComplete: hasClosingTag }); } } else { fileMap.set(filePath, { content, isComplete: hasClosingTag }); } } } // Convert map to array for sections.files for (const [path, { content, isComplete }] of fileMap.entries()) { if (!isComplete) { console.log(`[parseAIResponse] Warning: File ${path} appears to be truncated (no closing tag)`); } sections.files.push({ path, content }); } // Parse commands const cmdRegex = /(.*?)<\/command>/g; while ((match = cmdRegex.exec(response)) !== null) { sections.commands.push(match[1].trim()); } // Parse packages - support both and tags const pkgRegex = /(.*?)<\/package>/g; while ((match = pkgRegex.exec(response)) !== null) { sections.packages.push(match[1].trim()); } // Also parse tag with multiple packages const packagesRegex = /([\s\S]*?)<\/packages>/; const packagesMatch = response.match(packagesRegex); if (packagesMatch) { const packagesContent = packagesMatch[1].trim(); // Split by newlines or commas const packagesList = packagesContent.split(/[\n,]+/) .map(pkg => pkg.trim()) .filter(pkg => pkg.length > 0); sections.packages.push(...packagesList); } // Parse structure const structureMatch = /([\s\S]*?)<\/structure>/; const structResult = response.match(structureMatch); if (structResult) { sections.structure = structResult[1].trim(); } // Parse explanation const explanationMatch = /([\s\S]*?)<\/explanation>/; const explResult = response.match(explanationMatch); if (explResult) { sections.explanation = explResult[1].trim(); } // Parse template const templateMatch = /