├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── analysis │ └── page.tsx ├── api │ ├── assistant │ │ ├── route.ts │ │ └── thoughts │ │ │ └── [session_id] │ │ │ └── route.ts │ ├── mcp-servers │ │ ├── mcp-servers.json │ │ ├── route.ts │ │ └── test │ │ │ └── route.ts │ └── router │ │ ├── route.ts │ │ └── validate-session │ │ └── [session_id] │ │ └── route.ts ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx └── page.tsx ├── assets ├── architecture.svg └── screenshots │ ├── fashion.gif │ ├── finance.gif │ ├── mcp.png │ ├── multi-session.png │ └── shopping.gif ├── components.json ├── components ├── BrowserControl.tsx ├── ChartRenderer.tsx ├── Chat.tsx ├── FilePreview.tsx ├── MCPServerSettings.tsx ├── MessageComponent.tsx ├── ThoughtProcess.tsx ├── TopNavBar.tsx ├── theme-provider.tsx └── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── chart.tsx │ ├── checkbox.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── toast.tsx │ └── toaster.tsx ├── config.ts ├── constants.ts ├── hooks ├── use-toast.ts ├── useAgentControl.ts ├── useAutoScroll.ts ├── useBrowserControl.ts ├── useChat.ts ├── useMCPServers.ts ├── useScrollHandling.ts └── useThoughtProcess.ts ├── lib └── utils.ts ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── amazon-dark.png ├── amazon.webp ├── ant-logo.svg ├── bedrock-logo.png ├── wordmark-dark.svg └── wordmark.svg ├── py-backend ├── .env.example ├── app │ ├── __init__.py │ ├── act_agent │ │ ├── client │ │ │ ├── __init__.py │ │ │ ├── agent_executor.py │ │ │ └── browser_manager.py │ │ ├── server │ │ │ ├── __init__.py │ │ │ └── nova-act-server │ │ │ │ ├── __init__.py │ │ │ │ ├── browser_controller.py │ │ │ │ ├── nova_act_config.py │ │ │ │ ├── nova_act_server.py │ │ │ │ └── schemas.py │ │ └── test-run.py │ ├── api_routes │ │ ├── __init__.py │ │ ├── agent_control.py │ │ ├── browser_control.py │ │ ├── mcp_servers.py │ │ ├── router.py │ │ └── thought_stream.py │ ├── app.py │ └── libs │ │ ├── __init__.py │ │ ├── config │ │ ├── __init__.py │ │ ├── config.py │ │ └── prompts.py │ │ ├── core │ │ ├── __init__.py │ │ ├── agent_manager.py │ │ ├── browser_state_manager.py │ │ ├── browser_utils.py │ │ ├── task_classifier.py │ │ ├── task_executors.py │ │ └── task_supervisor.py │ │ ├── data │ │ ├── __init__.py │ │ ├── conversation_manager.py │ │ ├── conversation_store.py │ │ ├── message.py │ │ ├── session_manager.py │ │ ├── session_models.py │ │ └── session_store.py │ │ └── utils │ │ ├── __init__.py │ │ ├── decorators.py │ │ ├── error_handler.py │ │ ├── error_responses.py │ │ ├── profile_manager.py │ │ ├── shutdown_manager.py │ │ ├── thought_stream.py │ │ └── utils.py ├── mcp_server_config.json ├── requirements.txt ├── test_concurrent_browsers.py └── test_nova_act.py ├── services └── eventService.ts ├── tailwind.config.ts ├── test.py ├── tsconfig.json ├── types ├── chart.ts └── chat.ts └── utils ├── api.ts ├── apiClient.ts ├── errorHandler.ts ├── fileHandling.ts ├── logger.ts ├── messageUtils.ts ├── streamUtils.ts ├── thoughtProcessingUtils.ts └── timerUtils.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | out/ 3 | 4 | # Python 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | .env 26 | env/ 27 | .venv/ 28 | venv/ 29 | ENV/ 30 | .python-version 31 | .pytest_cache/ 32 | .coverage 33 | htmlcov/ 34 | py-backend/data/ 35 | .claude 36 | 37 | # React/Node 38 | node_modules/ 39 | /build 40 | .venv 41 | .DS_Store 42 | .env.local 43 | .env.development.local 44 | .env.test.local 45 | .env.production.local 46 | npm-debug.log* 47 | yarn-debug.log* 48 | yarn-error.log* 49 | .pnp 50 | .pnp.js 51 | coverage/ 52 | 53 | # IDE 54 | .idea/ 55 | .vscode/ 56 | *.swp 57 | *.swo 58 | .project 59 | .classpath 60 | .settings/ 61 | *.sublime-workspace 62 | *.sublime-project 63 | 64 | # Misc 65 | .DS_Store 66 | Thumbs.db 67 | *.log 68 | logs/ 69 | tmp/ 70 | temp/ 71 | 72 | 73 | sample.db 74 | .env 75 | sample-data/ 76 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browser Automation with Amazon Nova Act 2 | 3 | Automate web tasks using natural language with Amazon Nova Act and Bedrock. Transform routine browser interactions into simple conversational commands that free up your time for more meaningful work. 4 | 5 | ## What is Nova Act? 6 | 7 | Nova Act is Amazon's specialized AI model designed specifically for reliable web browser automation. Unlike general-purpose language models, Nova Act excels at translating natural language instructions into precise browser actions—clicking, typing, scrolling, and navigating just like a human would. 8 | 9 | ## Key Features 10 | 11 | ### 🎯 **Natural Language Browser Control** 12 | Control any website using simple, conversational commands: 13 | ``` 14 | "Search for wireless headphones on Amazon" 15 | "Find the best-rated product under $100" 16 | "Add it to my cart and proceed to checkout" 17 | ``` 18 | 19 | ### 🧠 **Intelligent Agent Layer** 20 | Bridges the gap between human intent and browser actions: 21 | - **Purpose-Driven Navigation**: Knows which websites to visit and what elements matter 22 | - **Contextual Continuity**: Maintains context across complex multi-step tasks 23 | - **Smart Task Breakdown**: Converts high-level goals into step-by-step browser actions 24 | 25 | ### 🚀 Multi-Session Browsing 26 | Multi-Session 27 | 28 | Enable multiple sessions (or users) to automate browser tasks simultaneously: 29 | - Session-Based Isolation: Each user gets a dedicated browser instance with unique session ID 30 | - Independent Browser Profiles: Separate cookies, authentication, and browsing data per session 31 | - Parallel Task Execution: Multiple browser automation tasks run concurrently without interference 32 | - Scalable Architecture: Handles dozens of concurrent users with isolated browser contexts 33 | 34 | ### 👥 **Human-in-the-Loop** 35 | Seamlessly handles scenarios that require human judgment: 36 | - Authentication challenges and CAPTCHAs 37 | - Ambiguous UI elements 38 | - Unexpected interface changes 39 | - Intelligent handoff between automated and manual control 40 | 41 | ### 🔌 **Model Context Protocol (MCP) Integration** 42 | MCP 43 | 44 | Advanced tool integration through standardized protocol: 45 | - Standardized Tool Communication enables seamless integration of browser automation with external services 46 | - Streamable HTTP Transport enables real-time bidirectional communication between agents and tools with optimizerd resource usage 47 | 48 | ## Demo 49 | 50 | ### Real-World Use Cases 51 | This system enables automation across various domains: 52 | - **Fashion Research**: Trend analysis and product comparison 53 | - **Financial Analysis**: Market research and data gathering 54 | - **E-commerce**: Shopping, price comparison, and inventory management 55 | - **News Aggregation**: Technology trends and industry insights 56 | - **Travel Planning**: Flight searches, hotel bookings, and itinerary planning 57 | 58 | ### E-commerce Shopping (from Search to Cart) 59 | - `Go to Amazon and search for 'laptop stand'. Filter by brand 'AmazonBasics', check customer ratings above 4 stars, and add the adjustable one to your cart.` 60 | 61 | Retail Demo 62 | 63 | ### Financial Product (ETF) Comparison 64 | - `Go to https://investor.vanguard.com/investment-products/index-fudds` 65 | - `Filter for Stock-Sector funds only, then identify which sector ETF has the best YTD performance. Also note its expense ratio and 5-year return.` 66 | 67 | Finance Demo 68 | 69 | ### Fashion Trend Analysis 70 | - `Analyze current fashion trends on Pinterest for “summer 2025 fashion women".` 71 | 72 | Fashion Demo 73 | 74 | ## Quick Start 75 | 76 | ### Prerequisites 77 | - **Operating System**: MacOS (recommended) 78 | - **Python**: 3.10 or higher 79 | - **Node.js**: 18 or higher 80 | - **Package Manager**: npm or yarn 81 | 82 | ### Installation 83 | 84 | ```bash 85 | # Clone the repository 86 | git clone https://github.com/aws-samples/browser-control-with-nova-act.git 87 | cd browser-control-with-nova-act 88 | 89 | # Backend setup 90 | python -m venv venv 91 | source venv/bin/activate # On Windows: venv\Scripts\activate 92 | cd py-backend 93 | pip install -r requirements.txt 94 | 95 | # Frontend setup 96 | cd .. 97 | npm install 98 | ``` 99 | 100 | ### Configuration 101 | 102 | **1. Set up Environment Variables** 103 | ```bash 104 | # Copy the example environment file 105 | cd py-backend 106 | cp .env.example .env 107 | 108 | # Edit .env file and add your Nova Act API Key 109 | # NOVA_ACT_API_KEY=your_api_key_here 110 | ``` 111 | 112 | **Alternative: Use system environment variables** 113 | ```bash 114 | export NOVA_ACT_API_KEY="your_api_key_here" 115 | ``` 116 | 117 | **2. Configure Browser Settings (Optional)** 118 | All browser settings can be configured in the `.env` file or by editing `py-backend/app/libs/config/config.py`: 119 | ```python 120 | # Core browser settings 121 | BROWSER_HEADLESS = True # Set to False for debugging 122 | BROWSER_START_URL = "https://www.google.com" 123 | BROWSER_MAX_STEPS = 2 # Keep small for reliability 124 | 125 | # Browser profile (for persistent sessions) 126 | BROWSER_USER_DATA_DIR = '/path/to/chrome/profile' 127 | ``` 128 | 129 | **3. AI Model Configuration** 130 | ```python 131 | # Multimodal models required for screenshot interpretation 132 | DEFAULT_MODEL_ID = "us.amazon.nova-premier-v1:0" 133 | # Tested models: Nova Premier, Claude 3.7 Sonnet, Claude 3.5 Sonnet 134 | ``` 135 | 136 | ### Running the Application 137 | 138 | ```bash 139 | npm run dev 140 | ``` 141 | 142 | Visit **http://localhost:3000** to start automating! 143 | 144 | ## Usage Examples 145 | 146 | ### Basic Commands 147 | ``` 148 | # Simple navigation 149 | "Go to amazon.com" 150 | "Search for wireless headphones" 151 | 152 | # Interactive actions 153 | "Click the search bar and type 'gaming laptop'" 154 | "Scroll down to see more products" 155 | "Select the third result" 156 | 157 | # Complex research tasks 158 | "Find gaming laptops under $1000 and compare their specs" 159 | "Research the latest AI news and summarize key trends" 160 | "Book a flight from Seattle to New York for next Friday" 161 | ``` 162 | 163 | ## Architecture Overview 164 | 165 | Architecture 166 | 167 | The system uses a three-tier architecture: 168 | - **Supervisor Layer**: Breaks down complex tasks and coordinates workflow 169 | - **Agent Layer**: Executes browser missions and interprets results 170 | - **Nova Act Layer**: Performs direct browser interactions 171 | 172 | ## Learn More 173 | 174 | A detailed blog post covering technical implementation, architectural decisions, and advanced usage patterns will be published soon. It will include: 175 | - Deep dive into the agent architecture 176 | - Advanced prompting strategies 177 | - Performance optimization techniques 178 | - Troubleshooting common issues 179 | - Real-world deployment scenarios 180 | 181 | ## Contributing 182 | 183 | We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. 184 | 185 | ## License 186 | 187 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 188 | 189 | --- 190 | 191 | **Ready to automate your web workflows?** Start with `npm run dev` and experience the future of browser automation! 🚀 -------------------------------------------------------------------------------- /app/analysis/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import Chat from '@/components/Chat'; 4 | import ThoughtProcess from '@/components/ThoughtProcess'; 5 | import BrowserControl from '@/components/BrowserControl'; 6 | import { useChat } from "@/hooks/useChat"; 7 | import { models, regions } from '@/constants'; 8 | import TopNavBar from "@/components/TopNavBar"; 9 | 10 | export default function AIChat() { 11 | const [selectedRegion, setSelectedRegion] = useState("us-west-2"); 12 | const [selectedModel, setSelectedModel] = useState( 13 | "us.anthropic.claude-3-7-sonnet-20250219-v1:0" 14 | ); 15 | const [isUserControlInProgress, setIsUserControlInProgress] = useState(false); 16 | const { 17 | messages, 18 | input, 19 | isLoading, 20 | isThinking, 21 | isStopping, 22 | currentUpload, 23 | queryDetails, 24 | sessionId, 25 | actions, 26 | fileInputRef 27 | } = useChat(selectedModel, selectedRegion); 28 | 29 | const navFeatures = { 30 | showDomainSelector: false, 31 | showViewModeSelector: false, 32 | showPromptCaching: false 33 | }; 34 | 35 | return ( 36 |
37 | 41 | 42 |
43 |
44 | 66 | 67 | 74 |
75 |
76 | 77 | 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /app/api/assistant/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { redirect } from "next/navigation"; 3 | 4 | // This route is deprecated in favor of /api/router 5 | export async function POST(req: NextRequest) { 6 | // Redirect to the /api/router endpoint 7 | const { protocol, host } = new URL(req.url); 8 | const routerUrl = `${protocol}//${host}/api/router`; 9 | 10 | console.log(`Assistant API is deprecated. Redirecting request to ${routerUrl}`); 11 | 12 | return NextResponse.redirect(routerUrl, { status: 308 }); // 308 is Permanent Redirect 13 | } -------------------------------------------------------------------------------- /app/api/assistant/thoughts/[session_id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | export const dynamic = 'force-dynamic'; 3 | export const fetchCache = 'force-no-store'; 4 | 5 | async function connectWithRetry(backendUrl: string, maxAttempts = 5) { 6 | for (let attempt = 0; attempt < maxAttempts; attempt++) { 7 | try { 8 | const controller = new AbortController(); 9 | const timeoutId = setTimeout(() => controller.abort(), 120000); 10 | 11 | const response = await fetch(backendUrl, { 12 | cache: "no-store", 13 | headers: { "Accept": "text/event-stream" }, 14 | signal: controller.signal 15 | }); 16 | 17 | clearTimeout(timeoutId); 18 | 19 | if (response.ok) return response; 20 | 21 | console.log(`Connection attempt ${attempt + 1}/${maxAttempts} failed with status: ${response.status} ${response.statusText}`); 22 | 23 | } catch (error) { 24 | console.log(`Connection attempt ${attempt + 1}/${maxAttempts} failed with error:`, error); 25 | } 26 | 27 | if (attempt < maxAttempts - 1) { 28 | await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt))); 29 | } 30 | } 31 | throw new Error('Failed to connect after maximum attempts'); 32 | } 33 | 34 | export async function GET( 35 | req: NextRequest, 36 | { params }: { params: { session_id: string } } 37 | ) { 38 | const param = await params; 39 | const session_id = await param.session_id; 40 | 41 | if (!session_id) { 42 | return new Response(JSON.stringify({ error: "Session ID is required" }), { 43 | status: 400, 44 | headers: { 45 | 'Content-Type': 'application/json', 46 | }, 47 | }); 48 | } 49 | 50 | console.log(`Setting up SSE thought stream for session: ${session_id}`); 51 | 52 | try { 53 | const headers = new Headers({ 54 | "Content-Type": "text/event-stream", 55 | "Cache-Control": "no-cache, no-store, must-revalidate", 56 | "Connection": "keep-alive", 57 | "Transfer-Encoding": "chunked", 58 | "X-Accel-Buffering": "no", 59 | "Access-Control-Allow-Origin": "*", 60 | "Content-Encoding": "identity" 61 | }); 62 | 63 | const encoder = new TextEncoder(); 64 | let isStreamActive = true; 65 | 66 | const stream = new ReadableStream({ 67 | async start(controller) { 68 | try { 69 | controller.enqueue(encoder.encode(`data: ${JSON.stringify({ 70 | type: "connected", 71 | message: "SSE stream initialized" 72 | })}\n\n`)); 73 | } catch (err) { 74 | console.log(`Failed to send initial message: ${err.message}`); 75 | isStreamActive = false; 76 | return; 77 | } 78 | 79 | try { 80 | const apiBaseUrl = process.env.API_BASE_URL || 'http://127.0.0.1:8000'; 81 | const backendUrl = `${apiBaseUrl}/api/assistant/thoughts/${session_id}`; 82 | console.log(`Attempting backend connection: ${backendUrl}`); 83 | 84 | const response = await connectWithRetry(backendUrl); 85 | const reader = response.body?.getReader(); 86 | 87 | if (!reader) { 88 | throw new Error("No response stream available"); 89 | } 90 | 91 | let decoder = new TextDecoder(); 92 | let buffer = ""; 93 | 94 | while (isStreamActive) { 95 | const { done, value } = await reader.read(); 96 | 97 | if (done) { 98 | if (buffer.length > 0 && isStreamActive) { 99 | try { 100 | controller.enqueue(encoder.encode(buffer)); 101 | } catch (err) { 102 | if (err.code === 'ERR_INVALID_STATE') { 103 | console.log(`Stream already closed for session ${session_id}`); 104 | isStreamActive = false; 105 | } else { 106 | throw err; 107 | } 108 | } 109 | } 110 | break; 111 | } 112 | 113 | const chunk = decoder.decode(value, { stream: true }); 114 | buffer += chunk; 115 | 116 | const messages = buffer.split('\n\n'); 117 | buffer = messages.pop() || ""; 118 | 119 | if (messages.length > 0 && isStreamActive) { 120 | const completeMessages = messages.join('\n\n') + '\n\n'; 121 | try { 122 | controller.enqueue(encoder.encode(completeMessages)); 123 | } catch (err) { 124 | if (err.code === 'ERR_INVALID_STATE') { 125 | console.log(`Stream already closed for session ${session_id}`); 126 | isStreamActive = false; 127 | break; 128 | } else { 129 | throw err; 130 | } 131 | } 132 | } 133 | } 134 | } catch (error) { 135 | console.error(`Error in SSE stream for session ${session_id}:`, error); 136 | if (isStreamActive) { 137 | try { 138 | const errorMessage = error instanceof Error ? error.message : "Unknown error"; 139 | controller.enqueue(encoder.encode(`data: ${JSON.stringify({type: "error", message: errorMessage})}\n\n`)); 140 | } catch (err) { 141 | if (err.code !== 'ERR_INVALID_STATE') { 142 | console.error(`Failed to send error message: ${err.message}`); 143 | } 144 | } 145 | } 146 | } finally { 147 | 148 | isStreamActive = false; 149 | } 150 | }, 151 | cancel() { 152 | console.log(`SSE stream canceled for session: ${session_id}`); 153 | isStreamActive = false 154 | } 155 | }); 156 | 157 | return new Response(stream, { headers }); 158 | } catch (error) { 159 | console.error(`Error setting up SSE stream: ${error}`); 160 | return new Response( 161 | JSON.stringify({ error: "Failed to set up thought stream" }), 162 | { status: 500, headers: { "Content-Type": "application/json" } } 163 | ); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /app/api/mcp-servers/mcp-servers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "default-mcp-1", 4 | "name": "MCP Server (Local)", 5 | "hostname": "localhost:8000", 6 | "isActive": true, 7 | "isConnected": true 8 | } 9 | ] -------------------------------------------------------------------------------- /app/api/mcp-servers/route.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { promises as fs } from 'fs'; 3 | import { NextRequest } from 'next/server'; 4 | 5 | export const dynamic = 'force-dynamic'; 6 | 7 | // Path to our JSON file 8 | const dataFilePath = path.join(process.cwd(), 'app/api/mcp-servers/mcp-servers.json'); 9 | 10 | async function readServersData() { 11 | try { 12 | const data = await fs.readFile(dataFilePath, 'utf8'); 13 | return JSON.parse(data); 14 | } catch (error) { 15 | console.error('Error reading MCP servers file:', error); 16 | return []; 17 | } 18 | } 19 | 20 | async function writeServersData(data: any) { 21 | try { 22 | await fs.writeFile(dataFilePath, JSON.stringify(data, null, 2), 'utf8'); 23 | return true; 24 | } catch (error) { 25 | console.error('Error writing MCP servers file:', error); 26 | return false; 27 | } 28 | } 29 | 30 | // GET /api/mcp-servers 31 | export async function GET() { 32 | try { 33 | const data = await readServersData(); 34 | return new Response(JSON.stringify(data), { 35 | status: 200, 36 | headers: { 'Content-Type': 'application/json' }, 37 | }); 38 | } catch (error) { 39 | return new Response(JSON.stringify({ error: 'Failed to fetch MCP servers' }), { 40 | status: 500, 41 | headers: { 'Content-Type': 'application/json' }, 42 | }); 43 | } 44 | } 45 | 46 | // POST /api/mcp-servers 47 | export async function POST(request: NextRequest) { 48 | try { 49 | const data = await request.json(); 50 | const success = await writeServersData(data); 51 | 52 | if (success) { 53 | return new Response(JSON.stringify({ success: true }), { 54 | status: 200, 55 | headers: { 'Content-Type': 'application/json' }, 56 | }); 57 | } else { 58 | throw new Error('Failed to save servers'); 59 | } 60 | } catch (error) { 61 | return new Response(JSON.stringify({ error: 'Failed to save MCP servers' }), { 62 | status: 500, 63 | headers: { 'Content-Type': 'application/json' }, 64 | }); 65 | } 66 | } -------------------------------------------------------------------------------- /app/api/mcp-servers/test/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | 3 | export const dynamic = 'force-dynamic'; 4 | 5 | // POST /api/mcp-servers/test 6 | export async function POST(request: NextRequest) { 7 | try { 8 | const { hostname } = await request.json(); 9 | 10 | if (!hostname) { 11 | return new Response(JSON.stringify({ success: false, error: 'Hostname is required' }), { 12 | status: 400, 13 | headers: { 'Content-Type': 'application/json' }, 14 | }); 15 | } 16 | 17 | // Try to connect to the server with a timeout 18 | try { 19 | // We need to ensure the URL is properly formatted 20 | let url = hostname; 21 | if (!url.startsWith('http://') && !url.startsWith('https://')) { 22 | url = `http://${url}`; 23 | } 24 | 25 | // Add a health check endpoint path if needed 26 | if (!url.includes('/health') && !url.endsWith('/')) { 27 | url = `${url}/health`; 28 | } else if (url.endsWith('/')) { 29 | url = `${url}health`; 30 | } 31 | 32 | const controller = new AbortController(); 33 | const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout 34 | 35 | const response = await fetch(url, { 36 | method: 'GET', 37 | headers: { 'Accept': 'application/json' }, 38 | signal: controller.signal, 39 | }); 40 | 41 | clearTimeout(timeoutId); 42 | 43 | // Check if the response is ok (status in the range 200-299) 44 | const success = response.ok; 45 | 46 | return new Response(JSON.stringify({ success }), { 47 | status: 200, 48 | headers: { 'Content-Type': 'application/json' }, 49 | }); 50 | } catch (error) { 51 | console.error('Error testing server connection:', error); 52 | return new Response(JSON.stringify({ success: false, error: 'Failed to connect to server' }), { 53 | status: 200, // We return 200 but with success: false to indicate the test failed 54 | headers: { 'Content-Type': 'application/json' }, 55 | }); 56 | } 57 | } catch (error) { 58 | return new Response(JSON.stringify({ success: false, error: 'Invalid request' }), { 59 | status: 400, 60 | headers: { 'Content-Type': 'application/json' }, 61 | }); 62 | } 63 | } -------------------------------------------------------------------------------- /app/api/router/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { apiClient } from "@/utils/apiClient"; 3 | 4 | // Route path to the backend router API 5 | const ROUTER_API_PATH = "/api/router"; 6 | 7 | 8 | export async function POST(req: NextRequest) { 9 | try { 10 | console.log("Router API called directly"); 11 | const body = await req.json(); 12 | 13 | if (!body.messages || !Array.isArray(body.messages)) { 14 | return new Response( 15 | JSON.stringify({ error: "Messages array is required" }), 16 | { status: 400 } 17 | ); 18 | } 19 | 20 | if (!body.model || !body.region) { 21 | return new Response( 22 | JSON.stringify({ error: "Model and region are required" }), 23 | { status: 400 } 24 | ); 25 | } 26 | 27 | console.log("Router API forwarding request to backend:", body.messages.length, "messages"); 28 | 29 | // Use the API client for consistent error handling and retries 30 | const response = await apiClient.request(ROUTER_API_PATH, { 31 | method: "POST", 32 | body: JSON.stringify(body), 33 | }); 34 | 35 | if (!response.ok) { 36 | throw new Error(`Router backend API error! status: ${response.status}`); 37 | } 38 | 39 | const responseData = await response.json(); 40 | 41 | return new Response(JSON.stringify(responseData), { 42 | status: 200, 43 | headers: { 'Content-Type': 'application/json' } 44 | }); 45 | } catch (error) { 46 | console.error("Router API: Error in POST handler:", error); 47 | 48 | // Handle different error types 49 | const errorName = error.name; 50 | const errorMessage = error.message; 51 | 52 | // Check if it's an abort error (timeout) 53 | if (errorName === 'AbortError') { 54 | return new Response( 55 | JSON.stringify({ error: "Request to backend timed out. Please try again." }), 56 | { status: 504 } 57 | ); 58 | } 59 | 60 | // Handle ECONNREFUSED specifically 61 | if (error.code === 'ECONNREFUSED' || error.cause?.code === 'ECONNREFUSED') { 62 | return new Response( 63 | JSON.stringify({ 64 | error: "Could not connect to backend server. Make sure the backend service is running.", 65 | details: "Connection refused" 66 | }), 67 | { status: 502 } 68 | ); 69 | } 70 | 71 | return new Response( 72 | JSON.stringify({ 73 | error: "An error occurred while processing the request", 74 | details: errorMessage || "Unknown error" 75 | }), 76 | { status: 500 } 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/api/router/validate-session/[session_id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | // Configuration 4 | const BACKEND_URL = "http://localhost:8000/api/router/validate-session"; 5 | const MAX_RETRIES = 3; 6 | const RETRY_DELAY = 1000; // 1 second 7 | const REQUEST_TIMEOUT = 5000; // 5 seconds 8 | 9 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 10 | 11 | async function fetchWithRetry(url: string, options: RequestInit, retries = MAX_RETRIES) { 12 | try { 13 | const controller = new AbortController(); 14 | const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); 15 | 16 | const response = await fetch(url, { 17 | ...options, 18 | signal: controller.signal 19 | }); 20 | 21 | clearTimeout(timeoutId); 22 | return response; 23 | } catch (error) { 24 | if (retries > 0) { 25 | console.log(`Retrying validation (${retries} attempts left)...`); 26 | await sleep(RETRY_DELAY); 27 | return fetchWithRetry(url, options, retries - 1); 28 | } 29 | throw error; 30 | } 31 | } 32 | 33 | export async function GET(req: NextRequest, { params }: { params: { session_id: string } }) { 34 | try { 35 | const session_id = params.session_id; 36 | console.log(`Session validation requested for: ${session_id}`); 37 | 38 | if (!session_id) { 39 | return new Response( 40 | JSON.stringify({ valid: false, error: "Session ID is required" }), 41 | { status: 400, headers: { "Content-Type": "application/json" } } 42 | ); 43 | } 44 | 45 | // Validate session with backend 46 | const response = await fetchWithRetry(`${BACKEND_URL}/${session_id}`, { 47 | method: "GET", 48 | headers: { "Content-Type": "application/json" }, 49 | }); 50 | 51 | if (!response.ok) { 52 | return new Response( 53 | JSON.stringify({ valid: false, message: `Backend validation failed: ${response.status}` }), 54 | { status: 200, headers: { "Content-Type": "application/json" } } 55 | ); 56 | } 57 | 58 | const data = await response.json(); 59 | 60 | // Return the backend validation result 61 | return new Response( 62 | JSON.stringify(data), 63 | { status: 200, headers: { "Content-Type": "application/json" } } 64 | ); 65 | } catch (error) { 66 | console.error("Session validation error:", error); 67 | 68 | // Check if it's a timeout 69 | if (error.name === 'AbortError') { 70 | return new Response( 71 | JSON.stringify({ valid: false, error: "Validation request timed out" }), 72 | { status: 200, headers: { "Content-Type": "application/json" } } 73 | ); 74 | } 75 | 76 | return new Response( 77 | JSON.stringify({ valid: false, error: error.message || "Unknown error" }), 78 | { status: 200, headers: { "Content-Type": "application/json" } } 79 | ); 80 | } 81 | } -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/browser-control-with-nova-act/752ac679d1ba3fefaf3a11ce11cc50620145d610/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/browser-control-with-nova-act/752ac679d1ba3fefaf3a11ce11cc50620145d610/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/browser-control-with-nova-act/752ac679d1ba3fefaf3a11ce11cc50620145d610/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import localFont from "next/font/local"; 4 | import { ThemeProvider } from "@/components/theme-provider"; 5 | import { Toaster } from "@/components/ui/toaster"; 6 | import "./globals.css"; 7 | 8 | const inter = Inter({ 9 | subsets: ["latin"], 10 | variable: "--font-inter", 11 | display: "swap", 12 | weight: ["300", "400", "500", "600", "700"], 13 | }); 14 | 15 | const geistMono = localFont({ 16 | src: "./fonts/GeistMonoVF.woff", 17 | variable: "--font-geist-mono", 18 | weight: "100 900", 19 | }); 20 | 21 | export const metadata: Metadata = { 22 | title: "Browser Automation Agent", 23 | description: "Generated by create next app", 24 | }; 25 | 26 | export default function RootLayout({ 27 | children, 28 | }: Readonly<{ 29 | children: React.ReactNode; 30 | }>) { 31 | return ( 32 | 33 | 36 | 42 | {children} 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | // /app/page.tsx 2 | "use client"; 3 | 4 | import React from "react"; 5 | import AnalysisPage from "./analysis/page"; 6 | 7 | export default function Home() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /assets/screenshots/fashion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/browser-control-with-nova-act/752ac679d1ba3fefaf3a11ce11cc50620145d610/assets/screenshots/fashion.gif -------------------------------------------------------------------------------- /assets/screenshots/finance.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/browser-control-with-nova-act/752ac679d1ba3fefaf3a11ce11cc50620145d610/assets/screenshots/finance.gif -------------------------------------------------------------------------------- /assets/screenshots/mcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/browser-control-with-nova-act/752ac679d1ba3fefaf3a11ce11cc50620145d610/assets/screenshots/mcp.png -------------------------------------------------------------------------------- /assets/screenshots/multi-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/browser-control-with-nova-act/752ac679d1ba3fefaf3a11ce11cc50620145d610/assets/screenshots/multi-session.png -------------------------------------------------------------------------------- /assets/screenshots/shopping.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/browser-control-with-nova-act/752ac679d1ba3fefaf3a11ce11cc50620145d610/assets/screenshots/shopping.gif -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/FilePreview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { X, FileText } from "lucide-react"; 3 | import { Badge } from "@/components/ui/badge"; 4 | import Image from "next/image"; 5 | 6 | interface FilePreviewProps { 7 | file: { 8 | base64: string; 9 | fileName: string; 10 | mediaType: string; 11 | isText?: boolean; 12 | }; 13 | onRemove?: () => void; 14 | size?: "small" | "large"; 15 | } 16 | 17 | const FilePreview: React.FC = ({ 18 | file, 19 | onRemove, 20 | size = "large", 21 | }) => { 22 | const isImage = file.mediaType.startsWith("image/"); 23 | const fileExtension = file.fileName.split(".").pop()?.toLowerCase() || ""; 24 | 25 | const truncatedName = 26 | file.fileName.length > 7 27 | ? `${file.fileName.slice(0, 7)}...${file.fileName.slice( 28 | file.fileName.lastIndexOf("."), 29 | )}` 30 | : file.fileName; 31 | 32 | const imageUrl = isImage 33 | ? `data:${file.mediaType};base64,${file.base64}` 34 | : ""; 35 | 36 | if (size === "small") { 37 | return ( 38 | 39 | {isImage ? ( 40 |
41 | {file.fileName} 49 |
50 | ) : ( 51 | 52 | )} 53 | {truncatedName} 54 |
55 | ); 56 | } 57 | 58 | return ( 59 |
60 | {isImage ? ( 61 |
62 | {file.fileName} 70 |
71 | ) : ( 72 |
73 | 74 | {fileExtension} 75 |
76 | )} 77 | {onRemove && ( 78 | 87 | )} 88 |
89 | ); 90 | }; 91 | 92 | export default FilePreview; 93 | -------------------------------------------------------------------------------- /components/MCPServerSettings.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogFooter, 9 | DialogHeader, 10 | DialogTitle 11 | } from "@/components/ui/dialog"; 12 | import { Button } from "@/components/ui/button"; 13 | import { Input } from "@/components/ui/input"; 14 | import { Label } from "@/components/ui/label"; 15 | import { Checkbox } from "@/components/ui/checkbox"; 16 | import { useMCPServers, MCPServer } from "@/hooks/useMCPServers"; 17 | import { AlertCircle, CheckCircle, Loader2, RefreshCw, Trash2 } from "lucide-react"; 18 | 19 | interface MCPServerSettingsProps { 20 | isOpen: boolean; 21 | onClose: () => void; 22 | } 23 | 24 | export default function MCPServerSettings({ isOpen, onClose }: MCPServerSettingsProps) { 25 | const { 26 | servers, 27 | addServer, 28 | removeServer, 29 | toggleServerActive, 30 | retestServerConnection 31 | } = useMCPServers(); 32 | 33 | const [newServerName, setNewServerName] = useState(""); 34 | const [newServerHostname, setNewServerHostname] = useState(""); 35 | const [isSubmitting, setIsSubmitting] = useState(false); 36 | const [isTestingServer, setIsTestingServer] = useState(null); 37 | const [error, setError] = useState(null); 38 | 39 | const handleAddServer = async () => { 40 | if (!newServerName || !newServerHostname) { 41 | setError("Please enter both server name and hostname."); 42 | return; 43 | } 44 | 45 | setIsSubmitting(true); 46 | setError(null); 47 | 48 | try { 49 | const result = await addServer(newServerName, newServerHostname); 50 | if (result.success) { 51 | setNewServerName(""); 52 | setNewServerHostname(""); 53 | } else { 54 | setError("Failed to add server."); 55 | } 56 | } catch (err) { 57 | setError("An error occurred while adding server."); 58 | } finally { 59 | setIsSubmitting(false); 60 | } 61 | }; 62 | 63 | const handleServerTest = async (id: string) => { 64 | setIsTestingServer(id); 65 | try { 66 | await retestServerConnection(id); 67 | } finally { 68 | setIsTestingServer(null); 69 | } 70 | }; 71 | 72 | return ( 73 | !open && onClose()}> 74 | 75 | 76 | MCP Server Settings 77 | 78 | Manage MCP servers and test connections. 79 | 80 | 81 | 82 |
83 | {/* Adding MCP Server */} 84 |
85 |

Add New MCP Server

86 | 87 |
88 |
89 | 90 | setNewServerName(e.target.value)} 94 | placeholder="e.g. US West MCP Server" 95 | /> 96 |
97 | 98 |
99 | 100 | setNewServerHostname(e.target.value)} 104 | placeholder="e.g. mcp-west.example.com:8080" 105 | /> 106 |
107 | 108 | {error && ( 109 |
110 | 111 | {error} 112 |
113 | )} 114 | 115 | 125 |
126 |
127 | 128 | {/* Server List */} 129 |
130 |

Registered MCP Servers

131 | {servers.length === 0 ? ( 132 |

No registered MCP servers.

133 | ) : ( 134 |
135 | {servers.map((server) => ( 136 |
140 |
141 | toggleServerActive(server.id)} 144 | id={`server-${server.id}`} 145 | /> 146 |
147 | 164 |

{server.hostname}

165 |
166 |
167 | 168 |
169 | 182 | 183 | 192 |
193 |
194 | ))} 195 |
196 | )} 197 |
198 |
199 | 200 | 201 | 202 | 203 |
204 |
205 | ); 206 | } 207 | -------------------------------------------------------------------------------- /components/TopNavBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from 'next/link'; 3 | import React, { useState, useEffect } from "react"; 4 | import Image from "next/image"; 5 | import { useTheme } from "next-themes"; 6 | 7 | interface TopNavBarProps { 8 | features?: { 9 | showDomainSelector?: boolean; 10 | showViewModeSelector?: boolean; 11 | showPromptCaching?: boolean; 12 | }; 13 | onReset?: () => void; 14 | } 15 | 16 | const TopNavBar: React.FC = ({ features = {}, onReset }) => { 17 | const { theme, setTheme } = useTheme(); 18 | const [mounted, setMounted] = useState(false); 19 | const [isSettingsOpen, setIsSettingsOpen] = useState(false); 20 | 21 | useEffect(() => { 22 | setMounted(true); 23 | }, []); 24 | 25 | if (!mounted) { 26 | return null; 27 | } 28 | 29 | return ( 30 | <> 31 |
32 |
33 | Company Wordmark 40 |
41 | {/* ANTHROPIC logo removed as requested */} 42 |
43 | 44 | ); 45 | }; 46 | 47 | export default TopNavBar; -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | // components/theme-provider.tsx 2 | "use client"; 3 | 4 | import * as React from "react"; 5 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 6 | import { type ThemeProviderProps } from "next-themes/dist/types"; 7 | 8 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 9 | return {children}; 10 | } 11 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { Check } from "lucide-react"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Checkbox = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 23 | 24 | 25 | 26 | )); 27 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 28 | 29 | export { Checkbox }; 30 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { X } from "lucide-react"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Dialog = DialogPrimitive.Root; 9 | 10 | const DialogTrigger = DialogPrimitive.Trigger; 11 | 12 | const DialogPortal = DialogPrimitive.Portal; 13 | 14 | const DialogOverlay = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, ...props }, ref) => ( 18 | 26 | )); 27 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 28 | 29 | const DialogContent = React.forwardRef< 30 | React.ElementRef, 31 | React.ComponentPropsWithoutRef 32 | >(({ className, children, ...props }, ref) => ( 33 | 34 | 35 | 43 | {children} 44 | 45 | 46 | Close 47 | 48 | 49 | 50 | )); 51 | DialogContent.displayName = DialogPrimitive.Content.displayName; 52 | 53 | const DialogHeader = ({ 54 | className, 55 | ...props 56 | }: React.HTMLAttributes) => ( 57 |
64 | ); 65 | DialogHeader.displayName = "DialogHeader"; 66 | 67 | const DialogFooter = ({ 68 | className, 69 | ...props 70 | }: React.HTMLAttributes) => ( 71 |
78 | ); 79 | DialogFooter.displayName = "DialogFooter"; 80 | 81 | const DialogTitle = React.forwardRef< 82 | React.ElementRef, 83 | React.ComponentPropsWithoutRef 84 | >(({ className, ...props }, ref) => ( 85 | 93 | )); 94 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 95 | 96 | const DialogDescription = React.forwardRef< 97 | React.ElementRef, 98 | React.ComponentPropsWithoutRef 99 | >(({ className, ...props }, ref) => ( 100 | 105 | )); 106 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 107 | 108 | export { 109 | Dialog, 110 | DialogPortal, 111 | DialogOverlay, 112 | DialogTrigger, 113 | DialogContent, 114 | DialogHeader, 115 | DialogFooter, 116 | DialogTitle, 117 | DialogDescription, 118 | }; 119 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { 6 | CheckIcon, 7 | ChevronRightIcon, 8 | DotFilledIcon, 9 | } from "@radix-ui/react-icons" 10 | 11 | import { cn } from "@/lib/utils" 12 | 13 | const DropdownMenu = DropdownMenuPrimitive.Root 14 | 15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 16 | 17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 18 | 19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 20 | 21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 22 | 23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 24 | 25 | const DropdownMenuSubTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef & { 28 | inset?: boolean 29 | } 30 | >(({ className, inset, children, ...props }, ref) => ( 31 | 40 | {children} 41 | 42 | 43 | )) 44 | DropdownMenuSubTrigger.displayName = 45 | DropdownMenuPrimitive.SubTrigger.displayName 46 | 47 | const DropdownMenuSubContent = React.forwardRef< 48 | React.ElementRef, 49 | React.ComponentPropsWithoutRef 50 | >(({ className, ...props }, ref) => ( 51 | 59 | )) 60 | DropdownMenuSubContent.displayName = 61 | DropdownMenuPrimitive.SubContent.displayName 62 | 63 | const DropdownMenuContent = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, sideOffset = 4, ...props }, ref) => ( 67 | 68 | 78 | 79 | )) 80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 81 | 82 | const DropdownMenuItem = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef & { 85 | inset?: boolean 86 | } 87 | >(({ className, inset, ...props }, ref) => ( 88 | 97 | )) 98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 99 | 100 | const DropdownMenuCheckboxItem = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, children, checked, ...props }, ref) => ( 104 | 113 | 114 | 115 | 116 | 117 | 118 | {children} 119 | 120 | )) 121 | DropdownMenuCheckboxItem.displayName = 122 | DropdownMenuPrimitive.CheckboxItem.displayName 123 | 124 | const DropdownMenuRadioItem = React.forwardRef< 125 | React.ElementRef, 126 | React.ComponentPropsWithoutRef 127 | >(({ className, children, ...props }, ref) => ( 128 | 136 | 137 | 138 | 139 | 140 | 141 | {children} 142 | 143 | )) 144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 145 | 146 | const DropdownMenuLabel = React.forwardRef< 147 | React.ElementRef, 148 | React.ComponentPropsWithoutRef & { 149 | inset?: boolean 150 | } 151 | >(({ className, inset, ...props }, ref) => ( 152 | 161 | )) 162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 163 | 164 | const DropdownMenuSeparator = React.forwardRef< 165 | React.ElementRef, 166 | React.ComponentPropsWithoutRef 167 | >(({ className, ...props }, ref) => ( 168 | 173 | )) 174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 175 | 176 | const DropdownMenuShortcut = ({ 177 | className, 178 | ...props 179 | }: React.HTMLAttributes) => { 180 | return ( 181 | 185 | ) 186 | } 187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 188 | 189 | export { 190 | DropdownMenu, 191 | DropdownMenuTrigger, 192 | DropdownMenuContent, 193 | DropdownMenuItem, 194 | DropdownMenuCheckboxItem, 195 | DropdownMenuRadioItem, 196 | DropdownMenuLabel, 197 | DropdownMenuSeparator, 198 | DropdownMenuShortcut, 199 | DropdownMenuGroup, 200 | DropdownMenuPortal, 201 | DropdownMenuSub, 202 | DropdownMenuSubContent, 203 | DropdownMenuSubTrigger, 204 | DropdownMenuRadioGroup, 205 | } 206 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /components/ui/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 "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none 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/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |