diff --git a/.cursor/rules/implementing-tools/RULE.md b/.cursor/rules/implementing-tools/RULE.md index adf00058bc..92723b9e21 100644 --- a/.cursor/rules/implementing-tools/RULE.md +++ b/.cursor/rules/implementing-tools/RULE.md @@ -168,6 +168,63 @@ Utility Tools: **Note:** Tools are allowed by default unless explicitly disabled. If your tool needs special conditional logic, modify `is_tool_allowed()` method, but prefer keeping it simple and using conditional registration in `tool_manager.py` instead. +### 9. Code Style & Architecture Standards + +#### Architecture +- All code must follow a modular, scalable, and extensible design +- Enforce single responsibility principle +- Favor composition over inheritance +- Separate concerns clearly: + - Configuration + - Business logic + - Tool registration + - Execution/runtime logic + - Database calls (use a repo directory) +- No god-classes, no god-modules + +#### Documentation & Comments +**Docstrings Only:** +- All documentation must be written using docstrings +- Inline comments are strongly discouraged + + +**Rules:** +- Docstrings must explain why, not just what +- Public functions, classes, and modules must have docstrings +- Private helpers require docstrings if intent is non-obvious + +#### Naming Conventions +- Functions: `snake_case` +- Classes: `PascalCase` +- Constants: `UPPER_SNAKE_CASE` +- Names must be descriptive and intention-revealing +- Avoid abbreviations unless universally understood + +#### Code Quality +- No dead code +- No commented-out code +- No speculative "might need this later" logic +- Keep functions small and focused +- Fail fast with explicit, meaningful errors + +#### Scalability Expectations +**Assume:** +- More tools will be added +- More contributors will touch the code +- The code will be read more often than it is written + +**Therefore:** +- Optimize for readability and predictability +- Make extension straightforward and safe +- Avoid cleverness in favor of clarity + +#### Non-Negotiables +- No ugly inline comments +- No hidden side effects +- No tight coupling between tools +- No violation of the established structure + + ## Frontend Implementation ### 1. Tool View Component diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json index 54bf9b6f71..ba6cf379f1 100644 --- a/.cursor/worktrees.json +++ b/.cursor/worktrees.json @@ -1,6 +1,6 @@ { "setup-worktree": [ - "cd frontend && npm install", + "cd apps/frontend && npm install", "cd apps/mobile && npm install", "cd backend && uv venv && uv sync" ] diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 4c5ff96caf..357c34aaaf 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -5,8 +5,6 @@ on: branches: - main - PRODUCTION - - '**' # All branches for preview deployments - workflow_dispatch: repository_dispatch: types: [production-updated] @@ -16,75 +14,30 @@ permissions: id-token: write jobs: - # verify-build: - # name: Verify Build - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # with: - # ref: ${{ github.event_name == 'repository_dispatch' && 'PRODUCTION' || github.ref }} - - # - name: Set up Python - # uses: actions/setup-python@v5 - # with: - # python-version: '3.12' - - # - name: Install uv - # uses: astral-sh/setup-uv@v4 - # with: - # version: "latest" - - # - name: Install backend dependencies - # working-directory: ./backend - # run: | - # uv sync - - # - name: Verify build - # working-directory: ./backend - # run: | - # uv run python core/utils/scripts/verify_build.py - build-and-push: - # needs: verify-build # Disabled - verify build step removed runs-on: ubuntu-latest + outputs: + environment: ${{ steps.get_tag_name.outputs.environment }} + image_tag: ${{ steps.get_tag_name.outputs.branch }} steps: - uses: actions/checkout@v4 with: ref: ${{ github.event_name == 'repository_dispatch' && 'PRODUCTION' || github.ref }} - - name: Get tag name + - name: Determine environment + id: get_tag_name shell: bash run: | - echo "Event name: ${{ github.event_name }}" - echo "Current ref: ${{ github.ref }}" BRANCH_NAME="${GITHUB_REF#refs/heads/}" echo "Branch: $BRANCH_NAME" - # Sanitize branch name for Docker tag (replace / with -, remove invalid chars) - SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') - - if [[ "${{ github.event_name }}" == "repository_dispatch" ]]; then - echo "Triggered by repository dispatch - setting prod environment" + if [[ "${{ github.event_name }}" == "repository_dispatch" ]] || [[ "$BRANCH_NAME" == "PRODUCTION" ]]; then echo "branch=prod" >> $GITHUB_OUTPUT echo "environment=prod" >> $GITHUB_OUTPUT - echo "is_preview=false" >> $GITHUB_OUTPUT elif [[ "$BRANCH_NAME" == "main" ]]; then echo "branch=latest" >> $GITHUB_OUTPUT echo "environment=staging" >> $GITHUB_OUTPUT - echo "is_preview=false" >> $GITHUB_OUTPUT - elif [[ "$BRANCH_NAME" == "PRODUCTION" ]]; then - echo "branch=prod" >> $GITHUB_OUTPUT - echo "environment=prod" >> $GITHUB_OUTPUT - echo "is_preview=false" >> $GITHUB_OUTPUT - else - # Preview deployment for non-main/PRODUCTION branches - echo "branch=$SANITIZED_BRANCH" >> $GITHUB_OUTPUT - echo "environment=preview" >> $GITHUB_OUTPUT - echo "is_preview=true" >> $GITHUB_OUTPUT - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT fi - echo "aws_region=us-west-2" >> $GITHUB_OUTPUT - id: get_tag_name - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -107,108 +60,168 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + deploy-staging: + needs: build-and-push + if: needs.build-and-push.outputs.environment == 'staging' + runs-on: ubuntu-latest + steps: + - name: Validate staging secrets + run: | + if [ -z "${{ secrets.AWS_STAGING_HOST }}" ]; then + echo "❌ Error: AWS_STAGING_HOST secret is not set" + exit 1 + fi + if [ -z "${{ secrets.AWS_STAGING_USERNAME }}" ]; then + echo "❌ Error: AWS_STAGING_USERNAME secret is not set" + exit 1 + fi + if [ -z "${{ secrets.AWS_STAGING_KEY }}" ]; then + echo "❌ Error: AWS_STAGING_KEY secret is not set" + exit 1 + fi + echo "✅ All staging secrets are configured" + - name: Deploy to staging - if: steps.get_tag_name.outputs.environment == 'staging' uses: appleboy/ssh-action@v1 with: host: ${{ secrets.AWS_STAGING_HOST }} username: ${{ secrets.AWS_STAGING_USERNAME }} key: ${{ secrets.AWS_STAGING_KEY }} + port: 22 + timeout: 300s script: | cd /home/ubuntu/suna/backend git fetch origin main git reset --hard origin/main - set -a && source .env && set +a echo "=== Pre-deployment disk usage ===" df -h / | tail -1 - # Clean up old images to save space + # Clean up old images echo "=== Cleaning up old images ===" docker image prune -af --filter "until=24h" || true - # Build and deploy - Docker Compose handles orchestration - echo "=== Building and deploying services ===" - docker compose build + # Pull pre-built image from registry and deploy + echo "=== Pulling and deploying services ===" + docker compose pull docker compose up -d --remove-orphans - # Quick cleanup + # Post cleanup echo "=== Post-deployment cleanup ===" docker image prune -af --filter "until=1h" || true - docker builder prune -af --keep-storage=2GB || true echo "=== Deployment complete ===" docker compose ps df -h / | tail -1 - # Temporarily disabled preview deployments - # Temporarily disabled preview deployments - # Temporarily disabled: Preview deployments - # - name: Deploy to staging [legacy] - # if: steps.get_tag_name.outputs.environment == 'staging' - # uses: appleboy/ssh-action@v1 - # with: - # host: ${{ secrets.STAGING_HOST }} - # username: ${{ secrets.STAGING_USERNAME }} - # key: ${{ secrets.STAGING_KEY }} - # script: | - # cd /home/suna/backend - # git fetch origin main - # git reset --hard origin/main - # docker compose down - # docker compose build - # docker compose up -d + # Deploy to Lightsail (existing production instance) + deploy-production-lightsail: + needs: build-and-push + if: needs.build-and-push.outputs.environment == 'prod' + runs-on: ubuntu-latest + steps: - name: Configure AWS credentials - if: steps.get_tag_name.outputs.environment == 'prod' uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ secrets.AWS_DEPLOYMENT_ROLE }} - aws-region: ${{ steps.get_tag_name.outputs.aws_region }} + aws-region: us-west-2 - - name: Update ECS services - if: steps.get_tag_name.outputs.environment == 'prod' + - name: Validate production secrets run: | - set -e - REGION="${{ steps.get_tag_name.outputs.aws_region }}" - CLUSTER="suna-ecs" - - # Get all service ARNs - SERVICES=$(aws ecs list-services --cluster $CLUSTER --region $REGION --query 'serviceArns' --output text) - - # Update API service - API_SERVICE_ARN=$(echo $SERVICES | tr ' ' '\n' | grep 'suna-api-svc' | head -1) - if [ -n "$API_SERVICE_ARN" ]; then - API_SERVICE=$(echo $API_SERVICE_ARN | awk -F'/' '{print $NF}') - echo "Updating API service: $API_SERVICE" - aws ecs update-service \ - --cluster $CLUSTER \ - --service $API_SERVICE \ - --force-new-deployment \ - --region $REGION \ - --no-cli-pager + if [ -z "${{ secrets.AWS_PRODUCTION_HOST }}" ]; then + echo "❌ Error: AWS_PRODUCTION_HOST secret is not set" + exit 1 fi - - # Update Worker service - WORKER_SERVICE_ARN=$(echo $SERVICES | tr ' ' '\n' | grep 'suna-worker-svc' | head -1) - if [ -n "$WORKER_SERVICE_ARN" ]; then - WORKER_SERVICE=$(echo $WORKER_SERVICE_ARN | awk -F'/' '{print $NF}') - echo "Updating Worker service: $WORKER_SERVICE" - aws ecs update-service \ - --cluster $CLUSTER \ - --service $WORKER_SERVICE \ - --force-new-deployment \ - --region $REGION \ - --no-cli-pager + if [ -z "${{ secrets.AWS_PRODUCTION_USERNAME }}" ]; then + echo "❌ Error: AWS_PRODUCTION_USERNAME secret is not set" + exit 1 + fi + if [ -z "${{ secrets.AWS_PRODUCTION_KEY }}" ]; then + echo "❌ Error: AWS_PRODUCTION_KEY secret is not set" + exit 1 fi + echo "✅ All production secrets are configured" + + - name: Deploy to Lightsail + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.AWS_PRODUCTION_HOST }} + username: ${{ secrets.AWS_PRODUCTION_USERNAME }} + key: ${{ secrets.AWS_PRODUCTION_KEY }} + port: 22 + timeout: 300s + script: | + cd /home/ubuntu/suna/backend + git fetch origin PRODUCTION + git reset --hard origin/PRODUCTION + + echo "=== Pre-deployment disk usage ===" + df -h / | tail -1 + + # Clean up old images + echo "=== Cleaning up old images ===" + docker image prune -af --filter "until=24h" || true + + # Pull pre-built image from registry and deploy + echo "=== Pulling and deploying services ===" + docker compose pull + docker compose up -d --remove-orphans + + # Post cleanup + echo "=== Post-deployment cleanup ===" + docker image prune -af --filter "until=1h" || true + + echo "=== Deployment complete ===" + docker compose ps + df -h / | tail -1 + + # Deploy to ECS (new scalable production cluster) + deploy-production-ecs: + needs: build-and-push + if: needs.build-and-push.outputs.environment == 'prod' + runs-on: ubuntu-latest + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v5 + with: + role-to-assume: ${{ secrets.AWS_DEPLOYMENT_ROLE }} + aws-region: us-west-2 - # - name: Deploy to prod [legacy] - # if: steps.get_tag_name.outputs.environment == 'prod' - # uses: appleboy/ssh-action@v1 - # with: - # host: ${{ secrets.PROD_HOST }} - # username: ${{ secrets.PROD_USERNAME }} - # key: ${{ secrets.PROD_KEY }} - # script: | - # cd /mnt/gluster-shared/data/infra/suna - # set -a; source .env; set +a - # docker stack deploy -c docker-compose.yml suna + - name: Deploy to ECS + run: | + echo "🚀 Deploying to ECS cluster..." + + # Force new deployment to pull latest image + aws ecs update-service \ + --cluster suna-ecs \ + --service suna-api-svc-6a0ece6 \ + --force-new-deployment \ + --region us-west-2 + + echo "✅ ECS deployment triggered" + echo "" + echo "📊 Waiting for deployment to stabilize..." + + # Wait for service to become stable (up to 10 minutes) + aws ecs wait services-stable \ + --cluster suna-ecs \ + --services suna-api-svc-6a0ece6 \ + --region us-west-2 || { + echo "⚠️ Deployment still in progress after timeout, checking status..." + aws ecs describe-services \ + --cluster suna-ecs \ + --services suna-api-svc-6a0ece6 \ + --region us-west-2 \ + --query 'services[0].{Status:status,Desired:desiredCount,Running:runningCount,Pending:pendingCount}' + } + + echo "" + echo "🎉 ECS deployment complete!" + + # Show final status + aws ecs describe-services \ + --cluster suna-ecs \ + --services suna-api-svc-6a0ece6 \ + --region us-west-2 \ + --query 'services[0].{Status:status,Desired:desiredCount,Running:runningCount}' \ + --output table diff --git a/.github/workflows/e2e-api-tests.yml b/.github/workflows/e2e-api-tests.yml new file mode 100644 index 0000000000..4a8658f343 --- /dev/null +++ b/.github/workflows/e2e-api-tests.yml @@ -0,0 +1,199 @@ +name: E2E API Tests + +on: + pull_request: + paths: + - 'backend/**' + push: + branches: + - main + - develop + paths: + - 'backend/**' + workflow_dispatch: + inputs: + environment: + description: 'Target environment' + required: false + type: choice + options: + - staging + - production + default: 'staging' + test_filter: + description: 'pytest filter expression (optional)' + required: false + type: string + api_url: + description: 'API base URL (overrides environment selection)' + required: false + type: string + workflow_run: + workflows: ["Build, push and deploy"] + types: + - completed + branches: + - main + +jobs: + e2e-tests: + runs-on: ubuntu-latest + # Only run if: + # - workflow_run: deployment completed successfully + # - push to main: run directly (deployment might be in progress, we'll poll) + # - pull_request: always run + # - workflow_dispatch: manual trigger + if: | + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || + (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')) || + (github.event_name == 'pull_request') || + (github.event_name == 'workflow_dispatch') + + steps: + - name: Determine environment + id: env + run: | + # If api_url is explicitly provided, use it + if [ -n "${{ github.event.inputs.api_url }}" ]; then + echo "environment=custom" >> $GITHUB_OUTPUT + echo "api_url=${{ github.event.inputs.api_url }}" >> $GITHUB_OUTPUT + exit 0 + fi + + # Check if environment is explicitly set to production + if [ "${{ github.event.inputs.environment }}" = "production" ]; then + echo "environment=production" >> $GITHUB_OUTPUT + echo "api_url=https://api.kortix.com" >> $GITHUB_OUTPUT + exit 0 + fi + + # Main branch maps to staging + if [ "${{ github.ref }}" = "refs/heads/main" ] || [ "${{ github.base_ref }}" = "main" ]; then + echo "environment=staging" >> $GITHUB_OUTPUT + echo "api_url=https://staging-api.suna.so" >> $GITHUB_OUTPUT + exit 0 + fi + + # Default to staging + echo "environment=staging" >> $GITHUB_OUTPUT + echo "api_url=https://staging-api.suna.so" >> $GITHUB_OUTPUT + + - name: Trigger E2E tests via API + env: + API_URL: ${{ steps.env.outputs.api_url }} + ADMIN_API_KEY: ${{ steps.env.outputs.environment == 'production' && secrets.PRODUCTION_KORTIX_ADMIN_API_KEY || secrets.STAGING_KORTIX_ADMIN_API_KEY }} + ENVIRONMENT: ${{ steps.env.outputs.environment }} + run: | + set -e + + # Validate admin API key is configured + if [ -z "$ADMIN_API_KEY" ]; then + if [ "$ENVIRONMENT" = "production" ]; then + echo "❌ Error: PRODUCTION_KORTIX_ADMIN_API_KEY is not set" + else + echo "❌ Error: STAGING_KORTIX_ADMIN_API_KEY is not set" + fi + echo "Please add it at: https://github.com/${{ github.repository }}/settings/secrets/actions" + exit 1 + fi + + # Mask secrets + echo "::add-mask::$ADMIN_API_KEY" + + # Ensure API_URL has /v1 path + if [[ "$API_URL" != */v1 ]]; then + API_URL="${API_URL}/v1" + fi + + TEST_FILTER="${{ github.event.inputs.test_filter || '' }}" + + echo "🧪 Starting E2E tests" + echo "📍 Environment: ${{ steps.env.outputs.environment }}" + echo "📍 API URL: $API_URL" + if [ -n "$TEST_FILTER" ]; then + echo "🔍 Test filter: $TEST_FILTER" + fi + echo "" + + # Wait for deployment to complete (2 min for staging, 6 min for production) + if [ "${{ steps.env.outputs.environment }}" = "production" ]; then + echo "⏳ Waiting 6 minutes for production deployment to complete..." + sleep 360 + else + echo "⏳ Waiting 2 minutes for staging deployment to complete..." + sleep 120 + fi + echo "✅ Wait complete, proceeding with tests" + echo "" + + # Build request URL + if [ -n "$TEST_FILTER" ]; then + REQUEST_URL="$API_URL/admin/tests/e2e?test_filter=$TEST_FILTER" + else + REQUEST_URL="$API_URL/admin/tests/e2e" + fi + + # Make API request with detailed error handling + HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" --max-time 900 -X POST "$REQUEST_URL" \ + -H "X-Admin-Api-Key: $ADMIN_API_KEY" \ + -H "Content-Type: application/json" 2>&1) + + # Extract HTTP status code and body + HTTP_STATUS=$(echo "$HTTP_RESPONSE" | tail -n1) + RESPONSE_BODY=$(echo "$HTTP_RESPONSE" | sed '$d') + + if [ "$HTTP_STATUS" -ne 200 ]; then + echo "❌ Failed to trigger E2E tests (HTTP $HTTP_STATUS)" + echo "" + echo "Response body:" + echo "$RESPONSE_BODY" | jq '.' 2>/dev/null || echo "$RESPONSE_BODY" + echo "" + echo "🔍 Troubleshooting:" + echo " 1. Verify API URL is correct: $API_URL" + echo " 2. Check that STAGING_KORTIX_ADMIN_API_KEY or PRODUCTION_KORTIX_ADMIN_API_KEY is valid" + echo " 3. Ensure the API server is running and accessible" + echo " 4. Check API server logs for more details" + exit 1 + fi + + echo "✅ E2E tests completed" + echo "" + echo "API Response:" + echo "$RESPONSE_BODY" | jq '.' 2>/dev/null || echo "$RESPONSE_BODY" + echo "" + + # Extract test status from response + TEST_STATUS=$(echo "$RESPONSE_BODY" | jq -r '.status // "unknown"' 2>/dev/null || echo "unknown") + TEST_RETURNCODE=$(echo "$RESPONSE_BODY" | jq -r '.returncode // -1' 2>/dev/null || echo "-1") + + # Create summary + echo "## E2E Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Environment**: ${{ steps.env.outputs.environment }}" >> $GITHUB_STEP_SUMMARY + echo "- **Status**: $TEST_STATUS" >> $GITHUB_STEP_SUMMARY + echo "- **Return Code**: $TEST_RETURNCODE" >> $GITHUB_STEP_SUMMARY + if [ -n "$TEST_FILTER" ]; then + echo "- **Test Filter**: \`$TEST_FILTER\`" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Add test output + echo "### Test Output:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$RESPONSE_BODY" | jq -r '.stdout // ""' 2>/dev/null >> $GITHUB_STEP_SUMMARY || echo "$RESPONSE_BODY" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + # Exit with test return code + if [ "$TEST_STATUS" = "passed" ] && [ "$TEST_RETURNCODE" = "0" ]; then + echo "✅ All tests passed" + exit 0 + elif [ "$TEST_STATUS" = "timeout" ]; then + echo "❌ Tests timed out" + exit 1 + elif [ "$TEST_STATUS" = "failed" ]; then + echo "❌ Tests failed (return code: $TEST_RETURNCODE)" + exit 1 + else + echo "⚠️ Unknown test status: $TEST_STATUS" + exit 1 + fi diff --git a/.github/workflows/e2e-benchmark-emergency-stop.yml b/.github/workflows/e2e-benchmark-emergency-stop.yml index c1279f78e3..f8db180617 100644 --- a/.github/workflows/e2e-benchmark-emergency-stop.yml +++ b/.github/workflows/e2e-benchmark-emergency-stop.yml @@ -31,7 +31,7 @@ jobs: - name: Emergency Stop All Tests env: API_URL: ${{ inputs.environment == 'production' && 'https://api.kortix.com' || 'https://staging-api.suna.so' }} - ADMIN_API_KEY: ${{ secrets.KORTIX_ADMIN_API_KEY }} + ADMIN_API_KEY: ${{ inputs.environment == 'production' && secrets.PRODUCTION_KORTIX_ADMIN_API_KEY || secrets.STAGING_KORTIX_ADMIN_API_KEY }} run: | set -e diff --git a/.github/workflows/e2e-benchmark.yml b/.github/workflows/e2e-benchmark.yml index cbb34bd9ca..64c53dc389 100644 --- a/.github/workflows/e2e-benchmark.yml +++ b/.github/workflows/e2e-benchmark.yml @@ -40,13 +40,18 @@ jobs: - name: Run E2E Benchmark env: API_URL: ${{ inputs.environment == 'production' && 'https://api.kortix.com' || 'https://staging-api.suna.so' }} - ADMIN_API_KEY: ${{ secrets.KORTIX_ADMIN_API_KEY }} + ADMIN_API_KEY: ${{ inputs.environment == 'production' && secrets.PRODUCTION_KORTIX_ADMIN_API_KEY || secrets.STAGING_KORTIX_ADMIN_API_KEY }} + ENVIRONMENT: ${{ inputs.environment }} run: | set -e # Validate admin API key is configured if [ -z "$ADMIN_API_KEY" ]; then - echo "❌ Error: KORTIX_ADMIN_API_KEY is not set" + if [ "$ENVIRONMENT" = "production" ]; then + echo "❌ Error: PRODUCTION_KORTIX_ADMIN_API_KEY is not set" + else + echo "❌ Error: STAGING_KORTIX_ADMIN_API_KEY is not set" + fi echo "Please add it at: https://github.com/${{ github.repository }}/settings/secrets/actions" exit 1 fi @@ -128,7 +133,7 @@ jobs: echo "" echo "🔍 Troubleshooting:" echo " 1. Verify API URL is correct: $API_URL" - echo " 2. Check that KORTIX_ADMIN_API_KEY is valid" + echo " 2. Check that STAGING_KORTIX_ADMIN_API_KEY or PRODUCTION_KORTIX_ADMIN_API_KEY is valid" echo " 3. Ensure the API server is running and accessible" echo " 4. Check API server logs for more details" exit 1 diff --git a/.github/workflows/mobile-eas-update.yml b/.github/workflows/mobile-eas-update.yml index bc53894b84..8053ee5585 100644 --- a/.github/workflows/mobile-eas-update.yml +++ b/.github/workflows/mobile-eas-update.yml @@ -5,7 +5,7 @@ on: branches: - main # Staging environment → main EAS branch/channel - staging # Staging environment → main EAS branch/channel - - PRODUCTION # Production environment → production EAS branch/channel + # PRODUCTION requires manual trigger via workflow_dispatch paths: - 'apps/mobile/**' - '.github/workflows/mobile-eas-update.yml' @@ -53,8 +53,11 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - cache: 'npm' - cache-dependency-path: apps/mobile/package-lock.json + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 - name: Setup EAS CLI uses: expo/expo-github-action@v8 @@ -64,8 +67,7 @@ jobs: - name: Install dependencies working-directory: apps/mobile - run: | - npm install --package-lock-only || npm install + run: pnpm install - name: Get branch name id: branch diff --git a/.github/workflows/update-PROD.yml b/.github/workflows/update-PROD.yml index 5dbbb31dd3..b88aa34ad0 100644 --- a/.github/workflows/update-PROD.yml +++ b/.github/workflows/update-PROD.yml @@ -1,4 +1,5 @@ name: Update PRODUCTION Branch + on: workflow_dispatch: @@ -7,7 +8,7 @@ permissions: jobs: update-production: - name: Rebase PRODUCTION to main + name: Sync PRODUCTION with main runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -15,34 +16,19 @@ jobs: fetch-depth: 0 clean: true token: ${{ secrets.GITHUB_TOKEN }} + - name: Configure Git run: | git config user.name "GitHub Actions" git config user.email "actions@github.com" + - name: Sync PRODUCTION with main run: | git fetch origin git checkout PRODUCTION git reset --hard origin/main git push origin PRODUCTION --force - # - name: Set up Supabase CLI - # uses: supabase/setup-cli@v1 - # with: - # version: latest - # - name: Push Supabase migrations to production - # env: - # SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} - # SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }} - # SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_REF }} - # run: | - # cd backend - # echo "Linking to Supabase project: $SUPABASE_PROJECT_ID" - # # Link project non-interactively using access token and database password - # supabase link --project-ref "$SUPABASE_PROJECT_ID" --password "$SUPABASE_DB_PASSWORD" - # echo "Pushing database migrations..." - # # Push migrations non-interactively (yes handles any confirmation prompts) - # yes | supabase db push - # continue-on-error: false + - name: Trigger Docker Build uses: peter-evans/repository-dispatch@v3 with: diff --git a/.gitignore b/.gitignore index 12ed76d393..282cabfb9e 100644 --- a/.gitignore +++ b/.gitignore @@ -212,7 +212,14 @@ dump.rdb # CodeWebChat state and logs (auto-added) .cwc/ - +# Node.js dependencies +node_modules/ +**/node_modules/ +**/node_modules/** +.pnpm-store/** +# Ensure .bin directories in node_modules are ignored +**/node_modules/.bin/** +**/.bin/** # mobile specific apps/mobile/node_modules/ apps/mobile/.expo/ @@ -220,7 +227,7 @@ apps/mobile/dist/ apps/mobile/web-build/ apps/mobile/expo-env.d.ts apps/mobile/.metro-health-check* -apps/mobile/npm-debug.* +apps/mobile/npm-debug.*apps/mobile/npm-debug.* apps/mobile/yarn-debug.* apps/mobile/yarn-error.* apps/mobile/*.tsbuildinfo diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..3a842259a9 --- /dev/null +++ b/.npmrc @@ -0,0 +1,7 @@ +public-hoist-pattern[]=*@codemirror/* +public-hoist-pattern[]=*@floating-ui/* +public-hoist-pattern[]=*@formatjs/* +public-hoist-pattern[]=*@dnd-kit/* +public-hoist-pattern[]=*@cyntler/* +strict-peer-dependencies=false + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a898477f9..234d90a344 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,4 +27,4 @@ This will guide you through configuring all required services and dependencies. For detailed setup instructions, please refer to: - [Backend Development Setup](backend/README.md) - Backend-specific development -- [Frontend Development Setup](frontend/README.md) - Frontend-specific development +- [Frontend Development Setup](apps/frontend/README.md) - Frontend-specific development diff --git a/README.md b/README.md index 74e80f8c08..a537a28330 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Build, manage, and train sophisticated AI agents for any use case. Create powerful agents that act autonomously on your behalf. -[![Discord Follow](https://dcbadge.limes.pink/api/server/Py6pCBUUPw?style=flat)](https://discord.gg/RvFhXUdZ9H) -[![Twitter Follow](https://img.shields.io/twitter/follow/kortix)](https://x.com/korti) +[![Discord Follow](https://dcbadge.limes.pink/api/server/RvFhXUdZ9H?style=flat)](https://discord.com/invite/RvFhXUdZ9H) +[![Twitter Follow](https://img.shields.io/twitter/follow/kortix)](https://x.com/kortix) [![GitHub Repo stars](https://img.shields.io/github/stars/kortix-ai/suna)](https://github.com/kortix-ai/suna) [![Issues](https://img.shields.io/github/issues/kortix-ai/suna)](https://github.com/kortix-ai/suna/labels/bug) @@ -21,7 +21,7 @@ Build, manage, and train sophisticated AI agents for any use case. Create powerf [Русский](https://www.readme-i18n.com/kortix-ai/suna?lang=ru) | [中文](https://www.readme-i18n.com/kortix-ai/suna?lang=zh) -![Kortix Screenshot](frontend/public/banner.png) +![Kortix Screenshot](apps/frontend/public/banner.png) @@ -177,6 +177,6 @@ Just use "setup.py". Ty mate. **Ready to build your first AI agent?** -[Get Started](./docs/SELF-HOSTING.md) • [Join Discord](https://discord.gg/RvFhXUdZ9H) • [Follow on Twitter](https://x.com/kortix) +[Get Started](./docs/SELF-HOSTING.md) • [Join Discord](https://discord.com/invite/RvFhXUdZ9H) • [Follow on Twitter](https://x.com/kortix) diff --git a/apps/desktop/main.js b/apps/desktop/main.js index c5de4cd423..cecf597a2b 100644 --- a/apps/desktop/main.js +++ b/apps/desktop/main.js @@ -116,6 +116,67 @@ if (isLocal) { app.commandLine.appendSwitch('ignore-ssl-errors'); } +// Circular loading animation HTML +const loadingHTML = ` + + + + + Kortix + + + +
+
+
Loading...
+
+ + +`; + function createWindow() { // Use .icns for macOS (proper styling), PNG for other platforms const iconPath = process.platform === 'darwin' @@ -133,6 +194,7 @@ function createWindow() { titleBarStyle: 'default', frame: true, transparent: false, + show: false, // Don't show until ready webPreferences: { nodeIntegration: false, contextIsolation: true, @@ -142,6 +204,12 @@ function createWindow() { const { webContents } = mainWindow; + // Show loading animation immediately + mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(loadingHTML)}`); + mainWindow.once('ready-to-show', () => { + mainWindow.show(); + }); + // Set custom user agent to identify Electron app webContents.setUserAgent(webContents.getUserAgent() + ' Electron/Kortix-Desktop'); @@ -241,7 +309,11 @@ function createWindow() { const authUrl = normalizedUrl.endsWith('/') ? normalizedUrl + 'auth' : normalizedUrl + '/auth'; - mainWindow.loadURL(authUrl); + + // Load the actual URL after the loading screen is shown + setTimeout(() => { + mainWindow.loadURL(authUrl); + }, 100); // Intercept navigation to prevent going to homepage and handle OAuth webContents.on('will-navigate', (event, navigationUrl) => { @@ -260,7 +332,7 @@ function createWindow() { console.log('✅ Opening OAuth in popup instead:', navigationUrl); event.preventDefault(); - // Create OAuth popup window + // Create OAuth popup window with loading animation const oauthWindow = new BrowserWindow({ width: 600, height: 800, @@ -268,13 +340,20 @@ function createWindow() { modal: false, autoHideMenuBar: true, title: 'Sign In', + backgroundColor: '#000000', + show: false, webPreferences: { nodeIntegration: false, contextIsolation: true, }, }); - oauthWindow.loadURL(navigationUrl); + // Show loading animation first, then load OAuth URL + oauthWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(loadingHTML)}`); + oauthWindow.once('ready-to-show', () => { + oauthWindow.show(); + oauthWindow.loadURL(navigationUrl); + }); // Handle OAuth callback - close popup and load callback in main window oauthWindow.webContents.on('will-navigate', (e, callbackUrl) => { @@ -414,6 +493,7 @@ function createWindow() { modal: false, autoHideMenuBar: true, title: 'Sign In', + backgroundColor: '#000000', webPreferences: { nodeIntegration: false, contextIsolation: true, diff --git a/frontend/.dockerignore b/apps/frontend/.dockerignore similarity index 100% rename from frontend/.dockerignore rename to apps/frontend/.dockerignore diff --git a/frontend/.env.example b/apps/frontend/.env.example similarity index 100% rename from frontend/.env.example rename to apps/frontend/.env.example diff --git a/frontend/.gitignore b/apps/frontend/.gitignore similarity index 100% rename from frontend/.gitignore rename to apps/frontend/.gitignore diff --git a/frontend/.npmrc b/apps/frontend/.npmrc similarity index 100% rename from frontend/.npmrc rename to apps/frontend/.npmrc diff --git a/frontend/.prettierignore b/apps/frontend/.prettierignore similarity index 100% rename from frontend/.prettierignore rename to apps/frontend/.prettierignore diff --git a/frontend/.prettierrc b/apps/frontend/.prettierrc similarity index 100% rename from frontend/.prettierrc rename to apps/frontend/.prettierrc diff --git a/frontend/Dockerfile b/apps/frontend/Dockerfile similarity index 100% rename from frontend/Dockerfile rename to apps/frontend/Dockerfile diff --git a/frontend/README.md b/apps/frontend/README.md similarity index 100% rename from frontend/README.md rename to apps/frontend/README.md diff --git a/frontend/components.json b/apps/frontend/components.json similarity index 100% rename from frontend/components.json rename to apps/frontend/components.json diff --git a/frontend/eslint.config.mjs b/apps/frontend/eslint.config.mjs similarity index 100% rename from frontend/eslint.config.mjs rename to apps/frontend/eslint.config.mjs diff --git a/frontend/instrumentation-client.ts b/apps/frontend/instrumentation-client.ts similarity index 100% rename from frontend/instrumentation-client.ts rename to apps/frontend/instrumentation-client.ts diff --git a/frontend/next b/apps/frontend/next similarity index 100% rename from frontend/next rename to apps/frontend/next diff --git a/frontend/next.config.ts b/apps/frontend/next.config.ts similarity index 97% rename from frontend/next.config.ts rename to apps/frontend/next.config.ts index 55da7047e6..90d749084d 100644 --- a/frontend/next.config.ts +++ b/apps/frontend/next.config.ts @@ -35,6 +35,9 @@ const getBackendUrl = (): string => { const nextConfig = (): NextConfig => ({ output: (process.env.NEXT_OUTPUT as 'standalone') || undefined, + // Transpile shared package + transpilePackages: ['@agentpress/shared'], + // Set environment variables env: { NEXT_PUBLIC_BACKEND_URL: getBackendUrl(), diff --git a/frontend/package.json b/apps/frontend/package.json similarity index 90% rename from frontend/package.json rename to apps/frontend/package.json index c4dad7092a..c68cd670ef 100644 --- a/frontend/package.json +++ b/apps/frontend/package.json @@ -11,7 +11,11 @@ "format:check": "prettier --check ." }, "dependencies": { + "@agentpress/shared": "workspace:*", "@calcom/embed-react": "^1.5.2", + "@codemirror/commands": "^6.10.1", + "@codemirror/language": "^6.12.1", + "@codemirror/state": "^6.5.3", "@codemirror/view": "^6.38.1", "@cyntler/react-doc-viewer": "^1.17.1", "@dnd-kit/core": "^6.3.1", @@ -19,6 +23,10 @@ "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", + "@floating-ui/dom": "^1.7.4", + "@floating-ui/react-dom": "^2.1.6", + "@formatjs/fast-memoize": "^3.0.2", + "@formatjs/icu-messageformat-parser": "^3.2.1", "@hookform/resolvers": "^5.2.1", "@icons-pack/react-simple-icons": "^13.7.0", "@liquidglass/react": "^0.1.3", @@ -50,6 +58,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-use-controllable-state": "^1.2.2", + "@radix-ui/react-visually-hidden": "^1.2.4", "@react-pdf/renderer": "^4.3.0", "@shikijs/transformers": "^3.12.0", "@silevis/reactgrid": "^4.1.17", @@ -69,16 +78,22 @@ "@tanstack/react-query": "^5.75.2", "@tanstack/react-query-devtools": "^5.75.2", "@tanstack/react-table": "^8.21.3", + "@tiptap/core": "^3.14.0", + "@tiptap/extension-blockquote": "^3.14.0", "@tiptap/extension-bubble-menu": "^3.13.0", "@tiptap/extension-character-count": "^3.3.0", + "@tiptap/extension-code-block": "^3.14.0", "@tiptap/extension-code-block-lowlight": "^3.3.0", "@tiptap/extension-collaboration": "^3.3.0", "@tiptap/extension-color": "^3.3.0", "@tiptap/extension-details": "^3.3.0", + "@tiptap/extension-document": "^3.14.0", "@tiptap/extension-dropcursor": "^3.3.0", "@tiptap/extension-emoji": "^3.3.0", "@tiptap/extension-font-family": "^3.3.0", "@tiptap/extension-gapcursor": "^3.3.0", + "@tiptap/extension-hard-break": "^3.14.0", + "@tiptap/extension-heading": "^3.14.0", "@tiptap/extension-highlight": "^3.3.0", "@tiptap/extension-history": "^3.3.0", "@tiptap/extension-horizontal-rule": "^3.3.0", @@ -87,15 +102,19 @@ "@tiptap/extension-list": "^3.3.0", "@tiptap/extension-mathematics": "^3.3.0", "@tiptap/extension-mention": "^3.3.0", + "@tiptap/extension-paragraph": "^3.14.0", "@tiptap/extension-placeholder": "^3.3.0", + "@tiptap/extension-strike": "^3.14.0", "@tiptap/extension-subscript": "^3.3.0", "@tiptap/extension-superscript": "^3.3.0", "@tiptap/extension-table": "^3.3.0", "@tiptap/extension-task-item": "^3.3.0", "@tiptap/extension-task-list": "^3.3.0", + "@tiptap/extension-text": "^3.14.0", "@tiptap/extension-text-align": "^3.3.0", "@tiptap/extension-text-style": "^3.13.0", "@tiptap/extension-typography": "^3.3.0", + "@tiptap/extension-underline": "^3.14.0", "@tiptap/extension-youtube": "^3.3.0", "@tiptap/pm": "^3.3.0", "@tiptap/react": "^3.3.0", @@ -184,7 +203,7 @@ "remark-gfm": "^4.0.1", "server-only": "^0.0.1", "sonner": "^2.0.3", - "streamdown": "^1.6.10", + "streamdown": "^1.6.11", "tailwind-merge": "^3.0.2", "tailwind-scrollbar": "^4.0.2", "tailwind-scrollbar-hide": "^2.0.0", diff --git a/frontend/postcss.config.mjs b/apps/frontend/postcss.config.mjs similarity index 100% rename from frontend/postcss.config.mjs rename to apps/frontend/postcss.config.mjs diff --git a/apps/frontend/public/Advanced-Dark.svg b/apps/frontend/public/Advanced-Dark.svg new file mode 100644 index 0000000000..bfb159ff6e --- /dev/null +++ b/apps/frontend/public/Advanced-Dark.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/Advanced-Light.svg b/apps/frontend/public/Advanced-Light.svg new file mode 100644 index 0000000000..426eb18cdc --- /dev/null +++ b/apps/frontend/public/Advanced-Light.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/Basic-Dark.svg b/apps/frontend/public/Basic-Dark.svg new file mode 100644 index 0000000000..92538badf1 --- /dev/null +++ b/apps/frontend/public/Basic-Dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/frontend/public/Basic-Light.svg b/apps/frontend/public/Basic-Light.svg new file mode 100644 index 0000000000..ccd490aef6 --- /dev/null +++ b/apps/frontend/public/Basic-Light.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/Logomark-Black.png b/apps/frontend/public/Logomark-Black.png similarity index 100% rename from frontend/public/Logomark-Black.png rename to apps/frontend/public/Logomark-Black.png diff --git a/frontend/public/Logomark.svg b/apps/frontend/public/Logomark.svg similarity index 100% rename from frontend/public/Logomark.svg rename to apps/frontend/public/Logomark.svg diff --git a/frontend/public/banner.png b/apps/frontend/public/banner.png similarity index 100% rename from frontend/public/banner.png rename to apps/frontend/public/banner.png diff --git a/frontend/public/favicon-light.png b/apps/frontend/public/favicon-light.png similarity index 100% rename from frontend/public/favicon-light.png rename to apps/frontend/public/favicon-light.png diff --git a/frontend/public/favicon.png b/apps/frontend/public/favicon.png similarity index 100% rename from frontend/public/favicon.png rename to apps/frontend/public/favicon.png diff --git a/frontend/public/fonts/roobert/RoobertItalicsVF.woff2 b/apps/frontend/public/fonts/roobert/RoobertItalicsVF.woff2 similarity index 100% rename from frontend/public/fonts/roobert/RoobertItalicsVF.woff2 rename to apps/frontend/public/fonts/roobert/RoobertItalicsVF.woff2 diff --git a/frontend/public/fonts/roobert/RoobertMonoItalicsVF.woff2 b/apps/frontend/public/fonts/roobert/RoobertMonoItalicsVF.woff2 similarity index 100% rename from frontend/public/fonts/roobert/RoobertMonoItalicsVF.woff2 rename to apps/frontend/public/fonts/roobert/RoobertMonoItalicsVF.woff2 diff --git a/frontend/public/fonts/roobert/RoobertMonoUprightsVF.woff2 b/apps/frontend/public/fonts/roobert/RoobertMonoUprightsVF.woff2 similarity index 100% rename from frontend/public/fonts/roobert/RoobertMonoUprightsVF.woff2 rename to apps/frontend/public/fonts/roobert/RoobertMonoUprightsVF.woff2 diff --git a/frontend/public/fonts/roobert/RoobertUprightsVF.woff2 b/apps/frontend/public/fonts/roobert/RoobertUprightsVF.woff2 similarity index 100% rename from frontend/public/fonts/roobert/RoobertUprightsVF.woff2 rename to apps/frontend/public/fonts/roobert/RoobertUprightsVF.woff2 diff --git a/frontend/public/grain-texture.png b/apps/frontend/public/grain-texture.png similarity index 100% rename from frontend/public/grain-texture.png rename to apps/frontend/public/grain-texture.png diff --git a/frontend/public/images/canvas/create.png b/apps/frontend/public/images/canvas/create.png similarity index 100% rename from frontend/public/images/canvas/create.png rename to apps/frontend/public/images/canvas/create.png diff --git a/frontend/public/images/canvas/edit.png b/apps/frontend/public/images/canvas/edit.png similarity index 100% rename from frontend/public/images/canvas/edit.png rename to apps/frontend/public/images/canvas/edit.png diff --git a/frontend/public/images/canvas/remove-bg.png b/apps/frontend/public/images/canvas/remove-bg.png similarity index 100% rename from frontend/public/images/canvas/remove-bg.png rename to apps/frontend/public/images/canvas/remove-bg.png diff --git a/frontend/public/images/canvas/upscale.png b/apps/frontend/public/images/canvas/upscale.png similarity index 100% rename from frontend/public/images/canvas/upscale.png rename to apps/frontend/public/images/canvas/upscale.png diff --git a/frontend/public/images/image-styles/abstract_organic-min.png b/apps/frontend/public/images/image-styles/abstract_organic-min.png similarity index 100% rename from frontend/public/images/image-styles/abstract_organic-min.png rename to apps/frontend/public/images/image-styles/abstract_organic-min.png diff --git a/frontend/public/images/image-styles/anime_forest-min.png b/apps/frontend/public/images/image-styles/anime_forest-min.png similarity index 100% rename from frontend/public/images/image-styles/anime_forest-min.png rename to apps/frontend/public/images/image-styles/anime_forest-min.png diff --git a/frontend/public/images/image-styles/comic_book_robot-min.png b/apps/frontend/public/images/image-styles/comic_book_robot-min.png similarity index 100% rename from frontend/public/images/image-styles/comic_book_robot-min.png rename to apps/frontend/public/images/image-styles/comic_book_robot-min.png diff --git a/frontend/public/images/image-styles/digital_art_cyberpunk-min.png b/apps/frontend/public/images/image-styles/digital_art_cyberpunk-min.png similarity index 100% rename from frontend/public/images/image-styles/digital_art_cyberpunk-min.png rename to apps/frontend/public/images/image-styles/digital_art_cyberpunk-min.png diff --git a/frontend/public/images/image-styles/geometric_crystal-min.png b/apps/frontend/public/images/image-styles/geometric_crystal-min.png similarity index 100% rename from frontend/public/images/image-styles/geometric_crystal-min.png rename to apps/frontend/public/images/image-styles/geometric_crystal-min.png diff --git a/frontend/public/images/image-styles/impressionist_garden-min.png b/apps/frontend/public/images/image-styles/impressionist_garden-min.png similarity index 100% rename from frontend/public/images/image-styles/impressionist_garden-min.png rename to apps/frontend/public/images/image-styles/impressionist_garden-min.png diff --git a/frontend/public/images/image-styles/isometric_bedroom-min.png b/apps/frontend/public/images/image-styles/isometric_bedroom-min.png similarity index 100% rename from frontend/public/images/image-styles/isometric_bedroom-min.png rename to apps/frontend/public/images/image-styles/isometric_bedroom-min.png diff --git a/frontend/public/images/image-styles/minimalist_coffee-min.png b/apps/frontend/public/images/image-styles/minimalist_coffee-min.png similarity index 100% rename from frontend/public/images/image-styles/minimalist_coffee-min.png rename to apps/frontend/public/images/image-styles/minimalist_coffee-min.png diff --git a/frontend/public/images/image-styles/neon_jellyfish-min.png b/apps/frontend/public/images/image-styles/neon_jellyfish-min.png similarity index 100% rename from frontend/public/images/image-styles/neon_jellyfish-min.png rename to apps/frontend/public/images/image-styles/neon_jellyfish-min.png diff --git a/frontend/public/images/image-styles/oil_painting_villa-min.png b/apps/frontend/public/images/image-styles/oil_painting_villa-min.png similarity index 100% rename from frontend/public/images/image-styles/oil_painting_villa-min.png rename to apps/frontend/public/images/image-styles/oil_painting_villa-min.png diff --git a/frontend/public/images/image-styles/pastel_landscape-min.png b/apps/frontend/public/images/image-styles/pastel_landscape-min.png similarity index 100% rename from frontend/public/images/image-styles/pastel_landscape-min.png rename to apps/frontend/public/images/image-styles/pastel_landscape-min.png diff --git a/frontend/public/images/image-styles/photorealistic_eagle-min.png b/apps/frontend/public/images/image-styles/photorealistic_eagle-min.png similarity index 100% rename from frontend/public/images/image-styles/photorealistic_eagle-min.png rename to apps/frontend/public/images/image-styles/photorealistic_eagle-min.png diff --git a/frontend/public/images/image-styles/surreal_islands-min.png b/apps/frontend/public/images/image-styles/surreal_islands-min.png similarity index 100% rename from frontend/public/images/image-styles/surreal_islands-min.png rename to apps/frontend/public/images/image-styles/surreal_islands-min.png diff --git a/frontend/public/images/image-styles/vintage_diner-min.png b/apps/frontend/public/images/image-styles/vintage_diner-min.png similarity index 100% rename from frontend/public/images/image-styles/vintage_diner-min.png rename to apps/frontend/public/images/image-styles/vintage_diner-min.png diff --git a/frontend/public/images/image-styles/watercolor_garden-min.png b/apps/frontend/public/images/image-styles/watercolor_garden-min.png similarity index 100% rename from frontend/public/images/image-styles/watercolor_garden-min.png rename to apps/frontend/public/images/image-styles/watercolor_garden-min.png diff --git a/frontend/public/images/landing-showcase/data.png b/apps/frontend/public/images/landing-showcase/data.png similarity index 100% rename from frontend/public/images/landing-showcase/data.png rename to apps/frontend/public/images/landing-showcase/data.png diff --git a/frontend/public/images/landing-showcase/docs.png b/apps/frontend/public/images/landing-showcase/docs.png similarity index 100% rename from frontend/public/images/landing-showcase/docs.png rename to apps/frontend/public/images/landing-showcase/docs.png diff --git a/frontend/public/images/landing-showcase/images.png b/apps/frontend/public/images/landing-showcase/images.png similarity index 100% rename from frontend/public/images/landing-showcase/images.png rename to apps/frontend/public/images/landing-showcase/images.png diff --git a/frontend/public/images/landing-showcase/research.png b/apps/frontend/public/images/landing-showcase/research.png similarity index 100% rename from frontend/public/images/landing-showcase/research.png rename to apps/frontend/public/images/landing-showcase/research.png diff --git a/frontend/public/images/landing-showcase/slides.png b/apps/frontend/public/images/landing-showcase/slides.png similarity index 100% rename from frontend/public/images/landing-showcase/slides.png rename to apps/frontend/public/images/landing-showcase/slides.png diff --git a/frontend/public/images/models/Anthropic.svg b/apps/frontend/public/images/models/Anthropic.svg similarity index 100% rename from frontend/public/images/models/Anthropic.svg rename to apps/frontend/public/images/models/Anthropic.svg diff --git a/frontend/public/images/models/Gemini.svg b/apps/frontend/public/images/models/Gemini.svg similarity index 100% rename from frontend/public/images/models/Gemini.svg rename to apps/frontend/public/images/models/Gemini.svg diff --git a/frontend/public/images/models/Grok.svg b/apps/frontend/public/images/models/Grok.svg similarity index 100% rename from frontend/public/images/models/Grok.svg rename to apps/frontend/public/images/models/Grok.svg diff --git a/frontend/public/images/models/Moonshot.svg b/apps/frontend/public/images/models/Moonshot.svg similarity index 100% rename from frontend/public/images/models/Moonshot.svg rename to apps/frontend/public/images/models/Moonshot.svg diff --git a/frontend/public/images/models/OAI.svg b/apps/frontend/public/images/models/OAI.svg similarity index 100% rename from frontend/public/images/models/OAI.svg rename to apps/frontend/public/images/models/OAI.svg diff --git a/frontend/public/images/presentation-templates/architect-min.png b/apps/frontend/public/images/presentation-templates/architect-min.png similarity index 100% rename from frontend/public/images/presentation-templates/architect-min.png rename to apps/frontend/public/images/presentation-templates/architect-min.png diff --git a/frontend/public/images/presentation-templates/black_and_white_clean-min.png b/apps/frontend/public/images/presentation-templates/black_and_white_clean-min.png similarity index 100% rename from frontend/public/images/presentation-templates/black_and_white_clean-min.png rename to apps/frontend/public/images/presentation-templates/black_and_white_clean-min.png diff --git a/frontend/public/images/presentation-templates/colorful-min.png b/apps/frontend/public/images/presentation-templates/colorful-min.png similarity index 100% rename from frontend/public/images/presentation-templates/colorful-min.png rename to apps/frontend/public/images/presentation-templates/colorful-min.png diff --git a/frontend/public/images/presentation-templates/competitor_analysis_blue-min.png b/apps/frontend/public/images/presentation-templates/competitor_analysis_blue-min.png similarity index 100% rename from frontend/public/images/presentation-templates/competitor_analysis_blue-min.png rename to apps/frontend/public/images/presentation-templates/competitor_analysis_blue-min.png diff --git a/frontend/public/images/presentation-templates/elevator_pitch-min.png b/apps/frontend/public/images/presentation-templates/elevator_pitch-min.png similarity index 100% rename from frontend/public/images/presentation-templates/elevator_pitch-min.png rename to apps/frontend/public/images/presentation-templates/elevator_pitch-min.png diff --git a/frontend/public/images/presentation-templates/gamer_gray-min.png b/apps/frontend/public/images/presentation-templates/gamer_gray-min.png similarity index 100% rename from frontend/public/images/presentation-templates/gamer_gray-min.png rename to apps/frontend/public/images/presentation-templates/gamer_gray-min.png diff --git a/frontend/public/images/presentation-templates/green-min.png b/apps/frontend/public/images/presentation-templates/green-min.png similarity index 100% rename from frontend/public/images/presentation-templates/green-min.png rename to apps/frontend/public/images/presentation-templates/green-min.png diff --git a/frontend/public/images/presentation-templates/hipster-min.png b/apps/frontend/public/images/presentation-templates/hipster-min.png similarity index 100% rename from frontend/public/images/presentation-templates/hipster-min.png rename to apps/frontend/public/images/presentation-templates/hipster-min.png diff --git a/frontend/public/images/presentation-templates/minimalist-min.png b/apps/frontend/public/images/presentation-templates/minimalist-min.png similarity index 100% rename from frontend/public/images/presentation-templates/minimalist-min.png rename to apps/frontend/public/images/presentation-templates/minimalist-min.png diff --git a/frontend/public/images/presentation-templates/minimalist_2-min.png b/apps/frontend/public/images/presentation-templates/minimalist_2-min.png similarity index 100% rename from frontend/public/images/presentation-templates/minimalist_2-min.png rename to apps/frontend/public/images/presentation-templates/minimalist_2-min.png diff --git a/frontend/public/images/presentation-templates/numbers_clean-min.png b/apps/frontend/public/images/presentation-templates/numbers_clean-min.png similarity index 100% rename from frontend/public/images/presentation-templates/numbers_clean-min.png rename to apps/frontend/public/images/presentation-templates/numbers_clean-min.png diff --git a/frontend/public/images/presentation-templates/numbers_colorful-min.png b/apps/frontend/public/images/presentation-templates/numbers_colorful-min.png similarity index 100% rename from frontend/public/images/presentation-templates/numbers_colorful-min.png rename to apps/frontend/public/images/presentation-templates/numbers_colorful-min.png diff --git a/frontend/public/images/presentation-templates/portfolio-min.png b/apps/frontend/public/images/presentation-templates/portfolio-min.png similarity index 100% rename from frontend/public/images/presentation-templates/portfolio-min.png rename to apps/frontend/public/images/presentation-templates/portfolio-min.png diff --git a/frontend/public/images/presentation-templates/premium_black-min.png b/apps/frontend/public/images/presentation-templates/premium_black-min.png similarity index 100% rename from frontend/public/images/presentation-templates/premium_black-min.png rename to apps/frontend/public/images/presentation-templates/premium_black-min.png diff --git a/frontend/public/images/presentation-templates/premium_green-min.png b/apps/frontend/public/images/presentation-templates/premium_green-min.png similarity index 100% rename from frontend/public/images/presentation-templates/premium_green-min.png rename to apps/frontend/public/images/presentation-templates/premium_green-min.png diff --git a/frontend/public/images/presentation-templates/professor_gray-min.png b/apps/frontend/public/images/presentation-templates/professor_gray-min.png similarity index 100% rename from frontend/public/images/presentation-templates/professor_gray-min.png rename to apps/frontend/public/images/presentation-templates/professor_gray-min.png diff --git a/frontend/public/images/presentation-templates/startup-min.png b/apps/frontend/public/images/presentation-templates/startup-min.png similarity index 100% rename from frontend/public/images/presentation-templates/startup-min.png rename to apps/frontend/public/images/presentation-templates/startup-min.png diff --git a/frontend/public/images/presentation-templates/textbook-min.png b/apps/frontend/public/images/presentation-templates/textbook-min.png similarity index 100% rename from frontend/public/images/presentation-templates/textbook-min.png rename to apps/frontend/public/images/presentation-templates/textbook-min.png diff --git a/apps/frontend/public/images/stamps/bali.svg b/apps/frontend/public/images/stamps/bali.svg new file mode 100644 index 0000000000..71bd141169 --- /dev/null +++ b/apps/frontend/public/images/stamps/bali.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/frontend/public/images/stamps/bg.svg b/apps/frontend/public/images/stamps/bg.svg new file mode 100644 index 0000000000..e5e4e60bdb --- /dev/null +++ b/apps/frontend/public/images/stamps/bg.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/images/stamps/lisbon.svg b/apps/frontend/public/images/stamps/lisbon.svg new file mode 100644 index 0000000000..5b08840ab5 --- /dev/null +++ b/apps/frontend/public/images/stamps/lisbon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/images/stamps/london.svg b/apps/frontend/public/images/stamps/london.svg new file mode 100644 index 0000000000..ecd61f156d --- /dev/null +++ b/apps/frontend/public/images/stamps/london.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/frontend/public/images/stamps/malaga.svg b/apps/frontend/public/images/stamps/malaga.svg new file mode 100644 index 0000000000..263c05e475 --- /dev/null +++ b/apps/frontend/public/images/stamps/malaga.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/images/stamps/nyc.svg b/apps/frontend/public/images/stamps/nyc.svg new file mode 100644 index 0000000000..38168bc478 --- /dev/null +++ b/apps/frontend/public/images/stamps/nyc.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/frontend/public/images/stamps/san francisco.svg b/apps/frontend/public/images/stamps/san francisco.svg new file mode 100644 index 0000000000..bb25f29732 --- /dev/null +++ b/apps/frontend/public/images/stamps/san francisco.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/frontend/public/images/stamps/sf.svg b/apps/frontend/public/images/stamps/sf.svg new file mode 100644 index 0000000000..bb25f29732 --- /dev/null +++ b/apps/frontend/public/images/stamps/sf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/frontend/public/images/team.webp b/apps/frontend/public/images/team.webp new file mode 100644 index 0000000000..76c2055e0d Binary files /dev/null and b/apps/frontend/public/images/team.webp differ diff --git a/frontend/public/images/video-styles/abstract.png b/apps/frontend/public/images/video-styles/abstract.png similarity index 100% rename from frontend/public/images/video-styles/abstract.png rename to apps/frontend/public/images/video-styles/abstract.png diff --git a/frontend/public/images/video-styles/animation.png b/apps/frontend/public/images/video-styles/animation.png similarity index 100% rename from frontend/public/images/video-styles/animation.png rename to apps/frontend/public/images/video-styles/animation.png diff --git a/frontend/public/images/video-styles/cinematic.png b/apps/frontend/public/images/video-styles/cinematic.png similarity index 100% rename from frontend/public/images/video-styles/cinematic.png rename to apps/frontend/public/images/video-styles/cinematic.png diff --git a/frontend/public/images/video-styles/nature.png b/apps/frontend/public/images/video-styles/nature.png similarity index 100% rename from frontend/public/images/video-styles/nature.png rename to apps/frontend/public/images/video-styles/nature.png diff --git a/frontend/public/images/video-styles/person.png b/apps/frontend/public/images/video-styles/person.png similarity index 100% rename from frontend/public/images/video-styles/person.png rename to apps/frontend/public/images/video-styles/person.png diff --git a/frontend/public/images/video-styles/product.png b/apps/frontend/public/images/video-styles/product.png similarity index 100% rename from frontend/public/images/video-styles/product.png rename to apps/frontend/public/images/video-styles/product.png diff --git a/frontend/public/kortix-brandmark-effect-full.svg b/apps/frontend/public/kortix-brandmark-effect-full.svg similarity index 100% rename from frontend/public/kortix-brandmark-effect-full.svg rename to apps/frontend/public/kortix-brandmark-effect-full.svg diff --git a/frontend/public/kortix-brandmark-effect.svg b/apps/frontend/public/kortix-brandmark-effect.svg similarity index 100% rename from frontend/public/kortix-brandmark-effect.svg rename to apps/frontend/public/kortix-brandmark-effect.svg diff --git a/apps/frontend/public/kortix-computer-black.svg b/apps/frontend/public/kortix-computer-black.svg new file mode 100644 index 0000000000..a813023ade --- /dev/null +++ b/apps/frontend/public/kortix-computer-black.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/kortix-computer-white.svg b/apps/frontend/public/kortix-computer-white.svg new file mode 100644 index 0000000000..bdba1bfc55 --- /dev/null +++ b/apps/frontend/public/kortix-computer-white.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/kortix-logomark-white.svg b/apps/frontend/public/kortix-logomark-white.svg new file mode 100644 index 0000000000..a1715c58cb --- /dev/null +++ b/apps/frontend/public/kortix-logomark-white.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/kortix-symbol.svg b/apps/frontend/public/kortix-symbol.svg similarity index 100% rename from frontend/public/kortix-symbol.svg rename to apps/frontend/public/kortix-symbol.svg diff --git a/frontend/public/logo_black.png b/apps/frontend/public/logo_black.png similarity index 100% rename from frontend/public/logo_black.png rename to apps/frontend/public/logo_black.png diff --git a/frontend/public/logomark-white.svg b/apps/frontend/public/logomark-white.svg similarity index 100% rename from frontend/public/logomark-white.svg rename to apps/frontend/public/logomark-white.svg diff --git a/frontend/public/manifest.json b/apps/frontend/public/manifest.json similarity index 100% rename from frontend/public/manifest.json rename to apps/frontend/public/manifest.json diff --git a/frontend/public/plan-icons/basic.svg b/apps/frontend/public/plan-icons/basic.svg similarity index 100% rename from frontend/public/plan-icons/basic.svg rename to apps/frontend/public/plan-icons/basic.svg diff --git a/frontend/public/plan-icons/plus.svg b/apps/frontend/public/plan-icons/plus.svg similarity index 100% rename from frontend/public/plan-icons/plus.svg rename to apps/frontend/public/plan-icons/plus.svg diff --git a/frontend/public/plan-icons/pro.svg b/apps/frontend/public/plan-icons/pro.svg similarity index 100% rename from frontend/public/plan-icons/pro.svg rename to apps/frontend/public/plan-icons/pro.svg diff --git a/frontend/public/plan-icons/ultra.svg b/apps/frontend/public/plan-icons/ultra.svg similarity index 100% rename from frontend/public/plan-icons/ultra.svg rename to apps/frontend/public/plan-icons/ultra.svg diff --git a/frontend/public/robots.txt b/apps/frontend/public/robots.txt similarity index 100% rename from frontend/public/robots.txt rename to apps/frontend/public/robots.txt diff --git a/frontend/public/share-page/og-fallback.png b/apps/frontend/public/share-page/og-fallback.png similarity index 100% rename from frontend/public/share-page/og-fallback.png rename to apps/frontend/public/share-page/og-fallback.png diff --git a/frontend/public/showcase/data/dashboard.png b/apps/frontend/public/showcase/data/dashboard.png similarity index 100% rename from frontend/public/showcase/data/dashboard.png rename to apps/frontend/public/showcase/data/dashboard.png diff --git a/frontend/public/showcase/image/logo.png b/apps/frontend/public/showcase/image/logo.png similarity index 100% rename from frontend/public/showcase/image/logo.png rename to apps/frontend/public/showcase/image/logo.png diff --git a/frontend/public/showcase/image/mockup-board.png b/apps/frontend/public/showcase/image/mockup-board.png similarity index 100% rename from frontend/public/showcase/image/mockup-board.png rename to apps/frontend/public/showcase/image/mockup-board.png diff --git a/frontend/public/showcase/presentation/browser.png b/apps/frontend/public/showcase/presentation/browser.png similarity index 100% rename from frontend/public/showcase/presentation/browser.png rename to apps/frontend/public/showcase/presentation/browser.png diff --git a/frontend/public/showcase/presentation/slide1.png b/apps/frontend/public/showcase/presentation/slide1.png similarity index 100% rename from frontend/public/showcase/presentation/slide1.png rename to apps/frontend/public/showcase/presentation/slide1.png diff --git a/frontend/public/showcase/presentation/slide2.png b/apps/frontend/public/showcase/presentation/slide2.png similarity index 100% rename from frontend/public/showcase/presentation/slide2.png rename to apps/frontend/public/showcase/presentation/slide2.png diff --git a/apps/frontend/public/stores/app store black button.svg b/apps/frontend/public/stores/app store black button.svg new file mode 100644 index 0000000000..e55f9ed562 --- /dev/null +++ b/apps/frontend/public/stores/app store black button.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/frontend/public/stores/app store white button.svg b/apps/frontend/public/stores/app store white button.svg new file mode 100644 index 0000000000..474ac6aa6c --- /dev/null +++ b/apps/frontend/public/stores/app store white button.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/frontend/public/stores/google play black button.svg b/apps/frontend/public/stores/google play black button.svg new file mode 100644 index 0000000000..d4f9cf2c49 --- /dev/null +++ b/apps/frontend/public/stores/google play black button.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/frontend/public/stores/google play white button.svg b/apps/frontend/public/stores/google play white button.svg new file mode 100644 index 0000000000..9af79cb1e0 --- /dev/null +++ b/apps/frontend/public/stores/google play white button.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/public/wordmark.svg b/apps/frontend/public/wordmark.svg similarity index 100% rename from frontend/public/wordmark.svg rename to apps/frontend/public/wordmark.svg diff --git a/frontend/src/app/(dashboard)/admin/analytics/page.tsx b/apps/frontend/src/app/(dashboard)/admin/analytics/page.tsx similarity index 99% rename from frontend/src/app/(dashboard)/admin/analytics/page.tsx rename to apps/frontend/src/app/(dashboard)/admin/analytics/page.tsx index ff11bcc2d9..3896827d3e 100644 --- a/frontend/src/app/(dashboard)/admin/analytics/page.tsx +++ b/apps/frontend/src/app/(dashboard)/admin/analytics/page.tsx @@ -11,7 +11,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Pagination } from '@/components/agents/pagination'; import { DataTable, DataTableColumn } from '@/components/ui/data-table'; -import { toast } from 'sonner'; +import { toast } from '@/lib/toast'; import { Users, MessageSquare, @@ -32,6 +32,7 @@ import { Lock, Unlock, } from 'lucide-react'; +import { KortixLoader } from '@/components/ui/kortix-loader'; import { Calendar } from '@/components/ui/calendar'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { format } from 'date-fns'; @@ -3618,7 +3619,7 @@ export default function AdminAnalyticsPage() { {/* Loading indicator when searching for user */} {isSearchingUser && pendingUserEmail && (
-
+ Loading user: {pendingUserEmail}
)} diff --git a/frontend/src/app/(dashboard)/admin/billing/page.tsx b/apps/frontend/src/app/(dashboard)/admin/billing/page.tsx similarity index 100% rename from frontend/src/app/(dashboard)/admin/billing/page.tsx rename to apps/frontend/src/app/(dashboard)/admin/billing/page.tsx diff --git a/frontend/src/app/(dashboard)/admin/feedback/page.tsx b/apps/frontend/src/app/(dashboard)/admin/feedback/page.tsx similarity index 100% rename from frontend/src/app/(dashboard)/admin/feedback/page.tsx rename to apps/frontend/src/app/(dashboard)/admin/feedback/page.tsx diff --git a/frontend/src/app/(dashboard)/admin/notifications/page.tsx b/apps/frontend/src/app/(dashboard)/admin/notifications/page.tsx similarity index 98% rename from frontend/src/app/(dashboard)/admin/notifications/page.tsx rename to apps/frontend/src/app/(dashboard)/admin/notifications/page.tsx index 7c5cb2aeda..12fe81e86e 100644 --- a/frontend/src/app/(dashboard)/admin/notifications/page.tsx +++ b/apps/frontend/src/app/(dashboard)/admin/notifications/page.tsx @@ -8,7 +8,8 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Loader2, Send, Users, User, ChevronDown, Sparkles } from "lucide-react"; +import { Send, Users, User, ChevronDown, Sparkles } from "lucide-react"; +import { KortixLoader } from '@/components/ui/kortix-loader'; import { Collapsible, CollapsibleContent, @@ -21,7 +22,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Badge } from "@/components/ui/badge"; export default function NotificationManagementPage() { @@ -121,7 +122,7 @@ export default function NotificationManagementPage() { > {triggerWorkflowMutation.isPending ? ( <> - + Sending... ) : ( @@ -176,7 +177,7 @@ export default function NotificationManagementPage() { {loadingWorkflows ? (
- + Loading workflows...
diff --git a/apps/frontend/src/app/(dashboard)/admin/stress-test/page.tsx b/apps/frontend/src/app/(dashboard)/admin/stress-test/page.tsx new file mode 100644 index 0000000000..6c728e34aa --- /dev/null +++ b/apps/frontend/src/app/(dashboard)/admin/stress-test/page.tsx @@ -0,0 +1,722 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import Link from 'next/link'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Progress } from '@/components/ui/progress'; +import { Switch } from '@/components/ui/switch'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { + Play, + Square, + RefreshCw, + Zap, + Clock, + CheckCircle2, + XCircle, + Activity, + TrendingUp, + AlertTriangle, + Loader2, + ExternalLink, + Timer, + Info, +} from 'lucide-react'; +import { useStressTest, StressTestResult } from '@/hooks/admin/use-stress-test'; +import { cn } from '@/lib/utils'; + +export default function AdminStressTestPage() { + const [numRequestsInput, setNumRequestsInput] = useState('5'); + const numRequests = Math.min(200, Math.max(1, parseInt(numRequestsInput) || 5)); + + const { state, runStressTest, cancelTest, resetTest } = useStressTest(); + + const stats = useMemo(() => { + const done = state.results.filter(r => r.status === 'done').length; + const error = state.results.filter(r => r.status === 'error').length; + const running = state.results.filter(r => r.status === 'running').length; + const pending = state.results.filter(r => r.status === 'pending').length; + const completed = done + error; + const total = state.results.length || numRequests; + const progress = total > 0 ? (completed / total) * 100 : 0; + + return { done, error, running, pending, completed, total, progress }; + }, [state.results, numRequests]); + + const handleStart = () => { + runStressTest({ + num_requests: numRequests, + measure_ttft: true, + }); + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'done': + return ; + case 'error': + return ; + case 'running': + return ; + default: + return ; + } + }; + + const getStatusBadge = (status: string) => { + switch (status) { + case 'done': + return Done; + case 'error': + return Error; + case 'running': + return Running; + default: + return Pending; + } + }; + + // Get visible results (prioritize running and recent completed) + const visibleResults = useMemo(() => { + const running = state.results.filter(r => r.status === 'running'); + const completed = state.results + .filter(r => r.status === 'done' || r.status === 'error') + .sort((a, b) => (b.total_ttft || b.request_time || 0) - (a.total_ttft || a.request_time || 0)); + + const combined = [...running, ...completed.slice(0, Math.max(0, 20 - running.length))]; + return combined.sort((a, b) => a.request_id - b.request_id).slice(0, 20); + }, [state.results]); + + // Build thread URL + const getThreadUrl = (result: StressTestResult) => { + if (!result.thread_id || !result.project_id) return null; + return `/projects/${result.project_id}/thread/${result.thread_id}`; + }; + + return ( +
+
+ {/* Header */} +
+
+

+ + Stress Test - Admin +

+

+ Run load tests with detailed timing breakdown (bypasses concurrent run limits) +

+
+
+ + {/* Timing Explanation with Visual Diagram */} + + +
+
+ +

Timing Metrics Timeline

+
+ + {/* Visual Timeline Diagram */} +
+
+ {/* Timeline header */} +
+ User clicks "Send" +
+ First token visible +
+ + {/* Total TTFT bar */} +
+
+ Total TTFT +
+ Request Time + First Response +
+
+
+ + {/* Request Time + First Response breakdown */} +
+
+ {/* Request Time */} +
+ Request Time +
+ {/* First Response */} +
+ First Response +
+
+ + {/* Detailed breakdown */} +
+
+ {/* Request Time details */} +
+
+ HTTP Call +
+
+ Setup +
+
+ Thread +
+
+ Agent +
+
+ {/* First Response details */} +
+
+ Agent Setup +
+
+ LLM TTFT +
+
+
+ + {/* Legend */} +
+
+
+ Request Time + HTTP + setup +
+
+
+ First Response + Agent overhead +
+
+
+ LLM TTFT + Pure model latency +
+
+
+ Total TTFT + User wait time +
+
+
+
+ + {/* Formulas */} +
+
+

Key Formula:

+

Total TTFT = Request Time + First Response

+
+
+

Agent Overhead:

+

Agent Overhead = First Response - LLM TTFT

+
+
+
+ + + + {/* Configuration */} + + + Test Configuration + + +
+
+ + setNumRequestsInput(e.target.value)} + onFocus={(e) => e.target.select()} + onBlur={() => { + // Normalize to valid value on blur + const val = Math.min(200, Math.max(1, parseInt(numRequestsInput) || 5)); + setNumRequestsInput(String(val)); + }} + disabled={state.isRunning} + className="w-32" + /> +
+ +
+ {!state.isRunning ? ( + + ) : ( + + )} + + {(state.summary || state.error) && !state.isRunning && ( + + )} +
+
+
+
+ + {/* Progress and Stats */} + {(state.isRunning || state.results.length > 0) && ( + <> + {/* Progress Bar */} + + +
+
+ + {state.isRunning ? ( + <>Batch {state.currentBatch}/{state.totalBatches} (waiting for LLM response...) + ) : ( + 'Completed' + )} + + {stats.completed}/{stats.total} requests +
+ + + {/* Quick Stats */} +
+
+ + Running: {stats.running} +
+
+ + Done: {stats.done} +
+
+ + Failed: {stats.error} +
+
+ + Pending: {stats.pending} +
+
+
+
+
+ + {/* Results Table */} + + + + + Live Results + + + +
+ + + + + + + + + + + + + + + {visibleResults.map((result) => { + const threadUrl = getThreadUrl(result); + return ( + + + + + + + + + + + ); + })} + {visibleResults.length === 0 && ( + + + + )} + +
#StatusThreadRequest TimeFirst Response + LLM TTFT + Total TTFTError
{result.request_id} +
+ {getStatusIcon(result.status)} + {getStatusBadge(result.status)} +
+
+ {threadUrl ? ( + + {result.thread_id?.substring(0, 8)}... + + + ) : ( + - + )} + + {result.request_time > 0 ? `${result.request_time.toFixed(2)}s` : '-'} + + {result.time_to_first_response != null ? ( + {result.time_to_first_response.toFixed(2)}s + ) : result.status === 'running' ? ( + + ) : ( + '-' + )} + + {result.llm_ttft != null ? ( + {result.llm_ttft.toFixed(2)}s + ) : result.status === 'running' ? ( + + ) : ( + '-' + )} + + {result.total_ttft != null ? ( + {result.total_ttft.toFixed(2)}s + ) : result.status === 'running' ? ( + + ) : ( + '-' + )} + + {result.error ? ( + + + + + {result.error} + + + + {result.error} + + + + ) : ( + '-' + )} +
+ No results yet +
+
+ {stats.total > 20 && ( +

+ Showing {Math.min(20, visibleResults.length)} of {stats.total} requests +

+ )} +
+
+ + )} + + {/* Summary */} + {state.summary && ( + + + + + Test Summary + + + +
+
+

Total Requests

+

{state.summary.total_requests}

+
+
+

Successful

+

+ {state.summary.successful} + + ({((state.summary.successful / state.summary.total_requests) * 100).toFixed(1)}%) + +

+
+
+

Failed

+

+ {state.summary.failed} + + ({((state.summary.failed / state.summary.total_requests) * 100).toFixed(1)}%) + +

+
+
+

Total Test Time

+

{state.summary.total_time}s

+
+
+ + {/* Request Times */} +
+

+ + Request Times +

+

+ Time for HTTP request to complete (distributed across workers like real traffic) +

+
+
+

Min

+

{state.summary.min_request_time}s

+
+
+

Average

+

{state.summary.avg_request_time}s

+
+
+

Max

+

{state.summary.max_request_time}s

+
+
+
+ + {/* Time to First Response */} + {state.summary.first_response_measured > 0 && ( +
+

+ + Time to First Response + {state.summary.first_response_measured} measured +

+

+ Time from agent start until first LLM response chunk (includes MCP init, prompt building, LLM TTFT) +

+
+
+

Min

+

+ {state.summary.min_time_to_first_response != null ? `${state.summary.min_time_to_first_response}s` : '-'} +

+
+
+

Average

+

+ {state.summary.avg_time_to_first_response != null ? `${state.summary.avg_time_to_first_response}s` : '-'} +

+
+
+

Max

+

+ {state.summary.max_time_to_first_response != null ? `${state.summary.max_time_to_first_response}s` : '-'} +

+
+
+
+ )} + + {/* LLM TTFT (Pure LiteLLM call time) */} + {state.summary.llm_ttft_measured > 0 && ( +
+

+ + LLM TTFT (Pure Model Latency) + {state.summary.llm_ttft_measured} measured +

+

+ Actual time for the LLM API call to return first token (from litellm.acompletion call to first chunk) +

+
+
+

Min

+

+ {state.summary.min_llm_ttft != null ? `${state.summary.min_llm_ttft}s` : '-'} +

+
+
+

Average

+

+ {state.summary.avg_llm_ttft != null ? `${state.summary.avg_llm_ttft}s` : '-'} +

+
+
+

Max

+

+ {state.summary.max_llm_ttft != null ? `${state.summary.max_llm_ttft}s` : '-'} +

+
+
+
+ )} + + {/* Total TTFT */} + {state.summary.min_total_ttft != null && ( +
+

+ + Total TTFT (End-to-End) +

+

+ Complete time from user request until first response = Request Time + Time to First Response +

+
+
+

Min

+

+ {state.summary.min_total_ttft}s +

+
+
+

Average

+

+ {state.summary.avg_total_ttft}s +

+
+
+

Max

+

+ {state.summary.max_total_ttft}s +

+
+
+
+ )} + +
+

Throughput

+

{state.summary.throughput} req/s

+
+ + {Object.keys(state.summary.error_breakdown).length > 0 && ( +
+

+ + Error Breakdown +

+
+ {Object.entries(state.summary.error_breakdown).map(([error, count]) => ( +
+ {count}x + {error} +
+ ))} +
+
+ )} + + {/* Timing Breakdown Table */} + {state.summary.timing_breakdown && Object.keys(state.summary.timing_breakdown).length > 0 && ( +
+

+ + Request Timing Breakdown +

+

+ Detailed breakdown of time spent in each phase during thread/project creation +

+
+ + + + + + + + + + + {state.summary.timing_breakdown.load_config_ms && ( + + + + + + + )} + {state.summary.timing_breakdown.get_model_ms && ( + + + + + + + )} + {state.summary.timing_breakdown.create_project_ms && ( + + + + + + + )} + {state.summary.timing_breakdown.create_thread_ms && ( + + + + + + + )} + {state.summary.timing_breakdown.create_message_and_run_ms && ( + + + + + + + )} + {state.summary.timing_breakdown.total_setup_ms && ( + + + + + + + )} + +
PhaseMin (ms)Avg (ms)Max (ms)
Load Config{state.summary.timing_breakdown.load_config_ms.min}{state.summary.timing_breakdown.load_config_ms.avg}{state.summary.timing_breakdown.load_config_ms.max}
Get Model{state.summary.timing_breakdown.get_model_ms.min}{state.summary.timing_breakdown.get_model_ms.avg}{state.summary.timing_breakdown.get_model_ms.max}
Create Project{state.summary.timing_breakdown.create_project_ms.min}{state.summary.timing_breakdown.create_project_ms.avg}{state.summary.timing_breakdown.create_project_ms.max}
Create Thread{state.summary.timing_breakdown.create_thread_ms.min}{state.summary.timing_breakdown.create_thread_ms.avg}{state.summary.timing_breakdown.create_thread_ms.max}
Create Message + Run{state.summary.timing_breakdown.create_message_and_run_ms.min}{state.summary.timing_breakdown.create_message_and_run_ms.avg}{state.summary.timing_breakdown.create_message_and_run_ms.max}
Total Setup{state.summary.timing_breakdown.total_setup_ms.min}{state.summary.timing_breakdown.total_setup_ms.avg}{state.summary.timing_breakdown.total_setup_ms.max}
+
+
+ )} +
+
+ )} + + {/* Error Display */} + {state.error && ( + + +
+ + Error: {state.error} +
+
+
+ )} +
+
+ ); +} diff --git a/apps/frontend/src/app/(dashboard)/admin/utils/_components/constants.ts b/apps/frontend/src/app/(dashboard)/admin/utils/_components/constants.ts new file mode 100644 index 0000000000..8ecb8e9356 --- /dev/null +++ b/apps/frontend/src/app/(dashboard)/admin/utils/_components/constants.ts @@ -0,0 +1,11 @@ +import { Zap, Globe, Database, Shield } from "lucide-react"; + +export const AVAILABLE_SERVICES = [ + { id: 'agent-runner', label: 'Agent Runner', icon: Zap }, + { id: 'web-application', label: 'Web Application', icon: Globe }, + { id: 'database', label: 'Database', icon: Database }, + { id: 'authentication', label: 'Authentication', icon: Shield }, +] as const; + +export type ServiceId = typeof AVAILABLE_SERVICES[number]['id']; +export type ServiceLabel = typeof AVAILABLE_SERVICES[number]['label']; diff --git a/apps/frontend/src/app/(dashboard)/admin/utils/_components/date-time-picker.tsx b/apps/frontend/src/app/(dashboard)/admin/utils/_components/date-time-picker.tsx new file mode 100644 index 0000000000..0d12d84851 --- /dev/null +++ b/apps/frontend/src/app/(dashboard)/admin/utils/_components/date-time-picker.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useState } from "react"; +import { format } from "date-fns"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Calendar } from "@/components/ui/calendar"; +import { CalendarIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface DateTimePickerProps { + date: Date | undefined; + setDate: (date: Date | undefined) => void; + label: string; +} + +export function DateTimePicker({ date, setDate, label }: DateTimePickerProps) { + const [timeValue, setTimeValue] = useState( + date ? format(date, "HH:mm") : "00:00" + ); + + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + const handleDateSelect = (selectedDate: Date | undefined) => { + if (selectedDate) { + const [hours, minutes] = timeValue.split(":").map(Number); + selectedDate.setHours(hours, minutes); + setDate(selectedDate); + } else { + setDate(undefined); + } + }; + + const handleTimeChange = (e: React.ChangeEvent) => { + const newTime = e.target.value; + setTimeValue(newTime); + if (date) { + const [hours, minutes] = newTime.split(":").map(Number); + const newDate = new Date(date); + newDate.setHours(hours, minutes); + setDate(newDate); + } + }; + + return ( +
+
+ + {timezone} +
+ + + + + + +
+ + +
+
+
+
+ ); +} diff --git a/apps/frontend/src/app/(dashboard)/admin/utils/_components/index.ts b/apps/frontend/src/app/(dashboard)/admin/utils/_components/index.ts new file mode 100644 index 0000000000..37e75a3cd3 --- /dev/null +++ b/apps/frontend/src/app/(dashboard)/admin/utils/_components/index.ts @@ -0,0 +1,6 @@ +export { DateTimePicker } from './date-time-picker'; +export { MaintenanceDialog } from './maintenance-dialog'; +export { TechnicalIssueDialog } from './technical-issue-dialog'; +export { MaintenanceCard, TechnicalIssueCard } from './status-cards'; +export { AVAILABLE_SERVICES } from './constants'; +export type { ServiceId, ServiceLabel } from './constants'; diff --git a/apps/frontend/src/app/(dashboard)/admin/utils/_components/maintenance-dialog.tsx b/apps/frontend/src/app/(dashboard)/admin/utils/_components/maintenance-dialog.tsx new file mode 100644 index 0000000000..57e325d38c --- /dev/null +++ b/apps/frontend/src/app/(dashboard)/admin/utils/_components/maintenance-dialog.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Wrench, Loader2 } from "lucide-react"; +import { DateTimePicker } from "./date-time-picker"; + +interface MaintenanceDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + enabled: boolean; + setEnabled: (enabled: boolean) => void; + startDate: Date | undefined; + setStartDate: (date: Date | undefined) => void; + endDate: Date | undefined; + setEndDate: (date: Date | undefined) => void; + onSave: () => Promise; + isPending: boolean; +} + +export function MaintenanceDialog({ + open, + onOpenChange, + enabled, + setEnabled, + startDate, + setStartDate, + endDate, + setEndDate, + onSave, + isPending, +}: MaintenanceDialogProps) { + return ( + + + + + + Scheduled Maintenance + + + Show a banner to users about upcoming or ongoing maintenance + + + +
+
+ + +
+ + {enabled && ( + <> + + + + )} +
+ + + + + +
+
+ ); +} diff --git a/apps/frontend/src/app/(dashboard)/admin/utils/_components/status-cards.tsx b/apps/frontend/src/app/(dashboard)/admin/utils/_components/status-cards.tsx new file mode 100644 index 0000000000..1a4ba3416b --- /dev/null +++ b/apps/frontend/src/app/(dashboard)/admin/utils/_components/status-cards.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Wrench, AlertTriangle, Clock, AlertCircle } from "lucide-react"; + +interface MaintenanceCardProps { + enabled: boolean; + onClick: () => void; +} + +export function MaintenanceCard({ enabled, onClick }: MaintenanceCardProps) { + return ( + + +
+
+
+ +
+
+ Scheduled Maintenance + + Show maintenance window to users + +
+
+ {enabled ? ( + + + Active + + ) : ( + Off + )} +
+
+
+ ); +} + +interface TechnicalIssueCardProps { + enabled: boolean; + message?: string; + onClick: () => void; +} + +export function TechnicalIssueCard({ enabled, message, onClick }: TechnicalIssueCardProps) { + return ( + + +
+
+
+ +
+
+ Technical Issue Banner + + Alert users about ongoing issues + +
+
+ {enabled ? ( + + + Active + + ) : ( + Off + )} +
+
+ {enabled && message && ( + +

+ {message} +

+
+ )} +
+ ); +} diff --git a/apps/frontend/src/app/(dashboard)/admin/utils/_components/technical-issue-dialog.tsx b/apps/frontend/src/app/(dashboard)/admin/utils/_components/technical-issue-dialog.tsx new file mode 100644 index 0000000000..07d1c4be9c --- /dev/null +++ b/apps/frontend/src/app/(dashboard)/admin/utils/_components/technical-issue-dialog.tsx @@ -0,0 +1,209 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; +import { AlertTriangle, AlertCircle, XCircle, Wrench, Loader2 } from "lucide-react"; +import { AVAILABLE_SERVICES } from "./constants"; + +type Severity = 'degraded' | 'outage' | 'maintenance'; + +interface TechnicalIssueDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + enabled: boolean; + setEnabled: (enabled: boolean) => void; + message: string; + setMessage: (message: string) => void; + severity: Severity; + setSeverity: (severity: Severity) => void; + description: string; + setDescription: (description: string) => void; + resolution: string; + setResolution: (resolution: string) => void; + services: string[]; + toggleService: (service: string) => void; + statusUrl: string; + setStatusUrl: (url: string) => void; + onSave: () => Promise; + isPending: boolean; +} + +export function TechnicalIssueDialog({ + open, + onOpenChange, + enabled, + setEnabled, + message, + setMessage, + severity, + setSeverity, + description, + setDescription, + resolution, + setResolution, + services, + toggleService, + statusUrl, + setStatusUrl, + onSave, + isPending, +}: TechnicalIssueDialogProps) { + return ( + + + + + + Technical Issue Banner + + + Alert users about ongoing issues or degraded performance + + + +
+
+ + +
+ + {enabled && ( + <> +
+ + +
+ +
+ + setMessage(e.target.value)} + /> +
+ +
+ +