diff --git a/.depcheckrc.json b/.depcheckrc.json deleted file mode 100644 index 221e718c..00000000 --- a/.depcheckrc.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "ignoreMatches": [ - "@types/*", - "eslint-*", - "prettier*", - "husky", - "rimraf", - "vitest", - "vite", - "typescript", - "wrangler", - "electron*" - ], - "ignoreDirs": ["dist", "build", "node_modules", ".git"], - "skipMissing": false, - "ignorePatterns": ["*.d.ts", "*.test.ts", "*.test.tsx", "*.spec.ts", "*.spec.tsx"] -} diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 2f8f89bc..00000000 --- a/.dockerignore +++ /dev/null @@ -1,26 +0,0 @@ -# Ignore Git and GitHub files -.git -.github/ - -# Ignore Husky configuration files -.husky/ - -# Ignore documentation and metadata files -CONTRIBUTING.md -LICENSE -README.md - -# Ignore environment examples and sensitive info -.env -*.local -*.example - -# Ignore node modules, logs and cache files -**/*.log -**/node_modules -**/dist -**/build -**/.cache -logs -dist-ssr -.DS_Store diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 5274ff01..00000000 --- a/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -root = true - -[*] -indent_style = space -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true -max_line_length = 120 -indent_size = 2 - -[*.md] -trim_trailing_whitespace = false diff --git a/.env.example b/.env.example deleted file mode 100644 index 47fb7aaf..00000000 --- a/.env.example +++ /dev/null @@ -1,142 +0,0 @@ -# shellcheck disable=SC2034,SC2148 - -# Rename this file to .env once you have filled in the below environment variables! - -# Get your GROQ API Key here - -# https://console.groq.com/keys -# You only need this environment variable set if you want to use Groq models -GROQ_API_KEY= - -# Get your HuggingFace API Key here - -# https://huggingface.co/settings/tokens -# You only need this environment variable set if you want to use HuggingFace models -HuggingFace_API_KEY= - - -# Get your Open AI API Key by following these instructions - -# https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key -# You only need this environment variable set if you want to use GPT models -OPENAI_API_KEY= - -# Get your Anthropic API Key in your account settings - -# https://console.anthropic.com/settings/keys -# You only need this environment variable set if you want to use Claude models -ANTHROPIC_API_KEY= - -# Get your OpenRouter API Key in your account settings - -# https://openrouter.ai/settings/keys -# You only need this environment variable set if you want to use OpenRouter models -OPEN_ROUTER_API_KEY= - -# Get your Google Generative AI API Key by following these instructions - -# https://console.cloud.google.com/apis/credentials -# You only need this environment variable set if you want to use Google Generative AI models -GOOGLE_GENERATIVE_AI_API_KEY= - -# You only need this environment variable set if you want to use oLLAMA models -# DONT USE http://localhost:11434 due to IPV6 issues -# USE EXAMPLE http://127.0.0.1:11434 -OLLAMA_API_BASE_URL= - -# You only need this environment variable set if you want to use OpenAI Like models -OPENAI_LIKE_API_BASE_URL= - -# You only need this environment variable set if you want to use Together AI models -TOGETHER_API_BASE_URL= - -# You only need this environment variable set if you want to use DeepSeek models through their API -DEEPSEEK_API_KEY= - -# Get your OpenAI Like API Key -OPENAI_LIKE_API_KEY= - -# Get your Together API Key -TOGETHER_API_KEY= - -# You only need this environment variable set if you want to use Hyperbolic models -#Get your Hyperbolics API Key at https://app.hyperbolic.xyz/settings -#baseURL="https://api.hyperbolic.xyz/v1/chat/completions" -HYPERBOLIC_API_KEY= -HYPERBOLIC_API_BASE_URL= - -# Get your Mistral API Key by following these instructions - -# https://console.mistral.ai/api-keys/ -# You only need this environment variable set if you want to use Mistral models -MISTRAL_API_KEY= - -# Get the Cohere Api key by following these instructions - -# https://dashboard.cohere.com/api-keys -# You only need this environment variable set if you want to use Cohere models -COHERE_API_KEY= - -# Get LMStudio Base URL from LM Studio Developer Console -# Make sure to enable CORS -# DONT USE http://localhost:1234 due to IPV6 issues -# Example: http://127.0.0.1:1234 -LMSTUDIO_API_BASE_URL= - -# Get your xAI API key -# https://x.ai/api -# You only need this environment variable set if you want to use xAI models -XAI_API_KEY= - -# Get your Perplexity API Key here - -# https://www.perplexity.ai/settings/api -# You only need this environment variable set if you want to use Perplexity models -PERPLEXITY_API_KEY= - -# Get your AWS configuration -# https://console.aws.amazon.com/iam/home -# The JSON should include the following keys: -# - region: The AWS region where Bedrock is available. -# - accessKeyId: Your AWS access key ID. -# - secretAccessKey: Your AWS secret access key. -# - sessionToken (optional): Temporary session token if using an IAM role or temporary credentials. -# Example JSON: -# {"region": "us-east-1", "accessKeyId": "yourAccessKeyId", "secretAccessKey": "yourSecretAccessKey", "sessionToken": "yourSessionToken"} -AWS_BEDROCK_CONFIG= - -# Include this environment variable if you want more logging for debugging locally -VITE_LOG_LEVEL=debug - -# Get your GitHub Personal Access Token here - -# https://github.com/settings/tokens -# This token is used for: -# 1. Importing/cloning GitHub repositories without rate limiting -# 2. Accessing private repositories -# 3. Automatic GitHub authentication (no need to manually connect in the UI) -# -# For classic tokens, ensure it has these scopes: repo, read:org, read:user -# For fine-grained tokens, ensure it has Repository and Organization access -VITE_GITHUB_ACCESS_TOKEN= - -# Specify the type of GitHub token you're using -# Can be 'classic' or 'fine-grained' -# Classic tokens are recommended for broader access -VITE_GITHUB_TOKEN_TYPE=classic - -# Example Context Values for qwen2.5-coder:32b -# -# DEFAULT_NUM_CTX=32768 # Consumes 36GB of VRAM -# DEFAULT_NUM_CTX=24576 # Consumes 32GB of VRAM -# DEFAULT_NUM_CTX=12288 # Consumes 26GB of VRAM -# DEFAULT_NUM_CTX=6144 # Consumes 24GB of VRAM -DEFAULT_NUM_CTX= - -# LangSmith Tracing Configuration for Claude Code -# These variables enable OpenTelemetry tracing to LangSmith for monitoring Claude Code operations -# See langsmith-tracing.md for detailed setup instructions -# -# Get your LangSmith API Key from https://smith.langchain.com -# You only need these environment variables if you want to trace Claude Code operations -LANGSMITH_API_KEY= -LANGSMITH_PROJECT=claude-code-trace - -# Claude Code Telemetry (set to 1 to enable) -CLAUDE_CODE_ENABLE_TELEMETRY= - -# OpenTelemetry Configuration (uncomment to enable) -# OTEL_LOGS_EXPORTER=otlp -# OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/json -# OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://api.smith.langchain.com/otel/v1/claude_code -# OTEL_LOG_USER_PROMPTS=1 diff --git a/.env.production b/.env.production deleted file mode 100644 index b6f9b4f8..00000000 --- a/.env.production +++ /dev/null @@ -1,117 +0,0 @@ -# shellcheck disable=SC2034,SC2148 - -# Rename this file to .env once you have filled in the below environment variables! - -# Get your GROQ API Key here - -# https://console.groq.com/keys -# You only need this environment variable set if you want to use Groq models -GROQ_API_KEY= - -# Get your HuggingFace API Key here - -# https://huggingface.co/settings/tokens -# You only need this environment variable set if you want to use HuggingFace models -HuggingFace_API_KEY= - -# Get your Open AI API Key by following these instructions - -# https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key -# You only need this environment variable set if you want to use GPT models -OPENAI_API_KEY= - -# Get your Anthropic API Key in your account settings - -# https://console.anthropic.com/settings/keys -# You only need this environment variable set if you want to use Claude models -ANTHROPIC_API_KEY= - -# Get your OpenRouter API Key in your account settings - -# https://openrouter.ai/settings/keys -# You only need this environment variable set if you want to use OpenRouter models -OPEN_ROUTER_API_KEY= - -# Get your Google Generative AI API Key by following these instructions - -# https://console.cloud.google.com/apis/credentials -# You only need this environment variable set if you want to use Google Generative AI models -GOOGLE_GENERATIVE_AI_API_KEY= - -# You only need this environment variable set if you want to use oLLAMA models -# DONT USE http://localhost:11434 due to IPV6 issues -# USE EXAMPLE http://127.0.0.1:11434 -OLLAMA_API_BASE_URL= - -# You only need this environment variable set if you want to use OpenAI Like models -OPENAI_LIKE_API_BASE_URL= - -# You only need this environment variable set if you want to use Together AI models -TOGETHER_API_BASE_URL= - -# You only need this environment variable set if you want to use DeepSeek models through their API -DEEPSEEK_API_KEY= - -# Get your OpenAI Like API Key -OPENAI_LIKE_API_KEY= - -# Get your Together API Key -TOGETHER_API_KEY= - -# You only need this environment variable set if you want to use Hyperbolic models -HYPERBOLIC_API_KEY= -HYPERBOLIC_API_BASE_URL= - -# Get your Mistral API Key by following these instructions - -# https://console.mistral.ai/api-keys/ -# You only need this environment variable set if you want to use Mistral models -MISTRAL_API_KEY= - -# Get the Cohere Api key by following these instructions - -# https://dashboard.cohere.com/api-keys -# You only need this environment variable set if you want to use Cohere models -COHERE_API_KEY= - -# Get LMStudio Base URL from LM Studio Developer Console -# Make sure to enable CORS -# DONT USE http://localhost:1234 due to IPV6 issues -# Example: http://127.0.0.1:1234 -LMSTUDIO_API_BASE_URL= - -# Get your xAI API key -# https://x.ai/api -# You only need this environment variable set if you want to use xAI models -XAI_API_KEY= - -# Get your Perplexity API Key here - -# https://www.perplexity.ai/settings/api -# You only need this environment variable set if you want to use Perplexity models -PERPLEXITY_API_KEY= - -# Get your AWS configuration -# https://console.aws.amazon.com/iam/home -AWS_BEDROCK_CONFIG= - -# Include this environment variable if you want more logging for debugging locally -VITE_LOG_LEVEL= - -# Get your GitHub Personal Access Token here - -# https://github.com/settings/tokens -# This token is used for: -# 1. Importing/cloning GitHub repositories without rate limiting -# 2. Accessing private repositories -# 3. Automatic GitHub authentication (no need to manually connect in the UI) -# -# For classic tokens, ensure it has these scopes: repo, read:org, read:user -# For fine-grained tokens, ensure it has Repository and Organization access -VITE_GITHUB_ACCESS_TOKEN= - -# Specify the type of GitHub token you're using -# Can be 'classic' or 'fine-grained' -# Classic tokens are recommended for broader access -VITE_GITHUB_TOKEN_TYPE= - -# Netlify Authentication -VITE_NETLIFY_ACCESS_TOKEN= - -# Example Context Values for qwen2.5-coder:32b -# -# DEFAULT_NUM_CTX=32768 # Consumes 36GB of VRAM -# DEFAULT_NUM_CTX=24576 # Consumes 32GB of VRAM -# DEFAULT_NUM_CTX=12288 # Consumes 26GB of VRAM -# DEFAULT_NUM_CTX=6144 # Consumes 24GB of VRAM -DEFAULT_NUM_CTX= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index c47f8528..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true - }, - "extends": ["eslint:recommended", "plugin:prettier/recommended"], - "rules": { - "no-console": "off" - } -} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index b1074c2d..00000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -@Gerome-Elassaad \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index ab1b5981..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: codinit-dev diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 4c129002..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: 'Bug report' -description: Help us fix an issue -body: - - type: markdown - attributes: - value: Thanks for reporting an issue with [codinit.dev](https://codinit.dev)! - - - type: textarea - id: description - attributes: - label: What happened? - placeholder: | - **Bug:** … - **Expected:** … - validations: - required: true - - - type: textarea - id: steps - attributes: - label: How to reproduce - placeholder: | - 1. … - 2. … - 3. … - validations: - required: true - - - type: textarea - id: environment - attributes: - label: Environment - placeholder: | - - Browser: - - OS: - - Provider/Model: - - - type: textarea - id: additional - attributes: - label: Extra info - placeholder: Screenshots, links, etc. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index c0d44d6f..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: codinit.dev related issues - url: https://github.com/codinit-dev/codinit-dev/issues/new/choose - about: Report issues related to codinit.dev diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md deleted file mode 100644 index 9d6719cf..00000000 --- a/.github/ISSUE_TEMPLATE/feature.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Issue -title: '' -labels: - - feature -assignees: '' ---- - -# Motivation - - - -# Proposed Solution - - - -# Additional Context - - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 45afc677..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Feature request -about: Submit a feature request that has been denied by other app dev platforms -title: '' -labels: '' -assignees: '' ---- - -**Describe the solution you'd like:** - - - -**Have you requested this from a different platform and been denied?:** - - - -**If you answered yes to the last question please write the platform that refused your suggestion.:** - - - -**Additional context:** - - diff --git a/.github/actions/setup-and-build/action.yaml b/.github/actions/setup-and-build/action.yaml deleted file mode 100644 index 8004384d..00000000 --- a/.github/actions/setup-and-build/action.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Setup and Build -description: Generic setup action -inputs: - pnpm-version: - description: 'The version of pnpm to use' - required: false - default: '9.15.9' - node-version: - description: 'The version of Node.js to use' - required: false - default: '20.15.1' -runs: - using: composite - - steps: - - uses: pnpm/action-setup@v4 - with: - version: ${{ inputs.pnpm-version }} - run_install: false - - - name: Set Node.js version to ${{ inputs.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ inputs.node-version }} - cache: pnpm - - - name: Install dependencies and build project - shell: bash - run: | - pnpm install - pnpm run build \ No newline at end of file diff --git a/.github/actions/setup-and-build/security.yaml b/.github/actions/setup-and-build/security.yaml deleted file mode 100644 index 2f445234..00000000 --- a/.github/actions/setup-and-build/security.yaml +++ /dev/null @@ -1,120 +0,0 @@ -name: Security Analysis - -on: - push: - branches: [main, stable] - pull_request: - branches: [main] - schedule: - # Run weekly security scan on Sundays at 2 AM - - cron: '0 2 * * 0' - -permissions: - actions: read - contents: read - security-events: read - -jobs: - codeql: - name: CodeQL Analysis - runs-on: ubuntu-latest - timeout-minutes: 45 - - strategy: - fail-fast: false - matrix: - language: ['javascript', 'typescript'] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-extended,security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" - upload: false - output: "codeql-results" - - - name: Upload CodeQL results as artifact - uses: actions/upload-artifact@v4 - if: always() - with: - name: codeql-results-${{ matrix.language }} - path: codeql-results - - dependency-scan: - name: Dependency Vulnerability Scan - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.18.0' - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: '9.15.9' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run npm audit - run: pnpm audit --audit-level moderate - continue-on-error: true - - - name: Generate SBOM - uses: anchore/sbom-action@v0 - with: - path: ./ - format: spdx-json - artifact-name: sbom.spdx.json - - - name: Upload SBOM as artifact - uses: actions/upload-artifact@v4 - if: always() - with: - name: sbom-results - path: | - sbom.spdx.json - **/sbom.spdx.json - - secrets-scan: - name: Secrets Detection - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Run Trivy secrets scan - uses: aquasecurity/trivy-action@master - with: - scan-type: 'fs' - scan-ref: '.' - format: 'sarif' - output: 'trivy-secrets-results.sarif' - scanners: 'secret' - - - name: Upload Trivy secrets results as artifact - uses: actions/upload-artifact@v4 - if: always() - with: - name: trivy-secrets-results - path: trivy-secrets-results.sarif diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 8ab236d5..00000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: CI/CD - -on: - push: - branches: - - master - pull_request: - -jobs: - test: - name: Test - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup and Build - uses: ./.github/actions/setup-and-build - - - name: Run type check - run: pnpm run typecheck - - # - name: Run ESLint - # run: pnpm run lint - - - name: Run tests - run: pnpm run test diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml deleted file mode 100644 index f6f0e6d0..00000000 --- a/.github/workflows/docker.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: Docker Publish - -on: - push: - branches: [main, stable] - tags: ["v*", "*.*.*"] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - packages: write - contents: read - -env: - REGISTRY: ghcr.io - IMAGE_NAME: codinit-dev/codinit-dev - -jobs: - docker-build-publish: - runs-on: ubuntu-latest - # timeout-minutes: 30 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker image - id: meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' }} - type=ref,event=tag - type=sha,format=short - type=raw,value=${{ github.ref_name }},enable=${{ startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/stable' }} - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - target: codinit-dev-production - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Check manifest - run: | - IMAGE_NAME_LOWER=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') - docker buildx imagetools inspect ${{ env.REGISTRY }}/${IMAGE_NAME_LOWER}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml deleted file mode 100644 index 63ef8902..00000000 --- a/.github/workflows/electron.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: Electron Build and Release - -on: - workflow_dispatch: - inputs: - tag: - description: 'Tag for the release (e.g., v1.0.0). Leave empty if not applicable.' - required: false - os: - description: 'Operating system to build for (all, ubuntu-latest, windows-latest, macos-latest)' - required: false - default: 'all' - push: - branches: - - electron - tags: - - 'v*' - -permissions: - contents: write - -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [20.15.1] - fail-fast: false - - steps: - - name: Check out Git repository - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 9.15.9 - run_install: false - - - name: Get pnpm store directory - id: get-pnpm-store-path - shell: bash - run: | - echo "store_path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.get-pnpm-store-path.outputs.store_path }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') || 'no-lock' }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install - - - name: Install Linux dependencies - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install -y rpm - - - name: Build Electron app - env: - GH_TOKEN: ${{ secrets.TOKEN }} - NODE_OPTIONS: "--max_old_space_size=4096" - run: | - if [ "$RUNNER_OS" == "Windows" ]; then - pnpm run electron:build:win - elif [ "$RUNNER_OS" == "macOS" ]; then - pnpm run electron:build:mac - else - pnpm run electron:build:linux - fi - shell: bash - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: electron-${{ runner.os }}-artifacts - path: | - dist/*.exe - dist/*.dmg - dist/*.deb - dist/*.AppImage - dist/*.zip - dist/*.blockmap - dist/*.yml - retention-days: 1 - if-no-files-found: warn - - release: - needs: build - runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref_type == 'tag' - - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Display structure of downloaded files - run: ls -R artifacts - - - name: Create Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - draft: false - name: Release ${{ github.ref_name }} - files: | - artifacts/**/*.exe - artifacts/**/*.dmg - artifacts/**/*.deb - artifacts/**/*.AppImage - artifacts/**/*.zip - artifacts/**/*.blockmap - artifacts/**/*.yml - env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pr-release-validation.yaml b/.github/workflows/pr-release-validation.yaml deleted file mode 100644 index 9c5787e2..00000000 --- a/.github/workflows/pr-release-validation.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: PR Validation - -on: - pull_request: - types: [opened, synchronize, reopened, labeled, unlabeled] - branches: - - main - -jobs: - validate: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Validate PR Labels - run: | - if [[ "${{ contains(github.event.pull_request.labels.*.name, 'stable-release') }}" == "true" ]]; then - echo "✓ PR has stable-release label" - - # Check version bump labels - if [[ "${{ contains(github.event.pull_request.labels.*.name, 'major') }}" == "true" ]]; then - echo "✓ Major version bump requested" - elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'minor') }}" == "true" ]]; then - echo "✓ Minor version bump requested" - else - echo "✓ Patch version bump will be applied" - fi - else - echo "This PR doesn't have the stable-release label. No release will be created." - fi diff --git a/.github/workflows/update-stable.yml b/.github/workflows/update-stable.yml deleted file mode 100644 index c2df1b1b..00000000 --- a/.github/workflows/update-stable.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Update Stable Branch - -# DISABLED: Using manual tag workflow instead -# To re-enable, uncomment the 'on:' section below -on: - workflow_dispatch: - -# on: -# push: -# branches: -# - main - -permissions: - contents: write - -jobs: - prepare-release: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Configure Git - run: | - git config --global user.name 'github-actions[bot]' - git config --global user.email 'github-actions[bot]@users.noreply.github.com' - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: latest - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Get Current Version - id: current_version - run: | - CURRENT_VERSION=$(node -p "require('./package.json').version") - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT - - - name: Install semver - run: pnpm add -g semver - - - name: Determine Version Bump - id: version_bump - run: | - # Default to patch since this workflow is now manual only - echo "bump=patch" >> $GITHUB_OUTPUT - - - name: Bump Version - id: bump_version - run: | - NEW_VERSION=$(semver -i ${{ steps.version_bump.outputs.bump }} ${{ steps.current_version.outputs.version }}) - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - - - name: Update Package.json - run: | - NEW_VERSION=${{ steps.bump_version.outputs.new_version }} - pnpm version $NEW_VERSION --no-git-tag-version --allow-same-version - - - name: Get the latest commit hash and version tag - run: | - echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV - echo "NEW_VERSION=${{ steps.bump_version.outputs.new_version }}" >> $GITHUB_ENV - - - name: Commit and Tag Release - run: | - git pull - git add package.json pnpm-lock.yaml - git commit -m "chore: release version ${{ steps.bump_version.outputs.new_version }}" - git tag "v${{ steps.bump_version.outputs.new_version }}" - git push - git push --tags - - - name: Update Stable Branch - run: | - if ! git checkout stable 2>/dev/null; then - echo "Creating new stable branch..." - git checkout -b stable - fi - git merge main --no-ff -m "chore: release version ${{ steps.bump_version.outputs.new_version }}" - git push --set-upstream origin stable --force diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e2609f4e..00000000 --- a/.gitignore +++ /dev/null @@ -1,55 +0,0 @@ -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -.vscode/* -.vscode/launch.json -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -/.history -/.cache -/build -functions/build/ -.env.local -.env -.dev.vars -*.vars -.wrangler -_worker.bundle - -Modelfile -modelfiles - -# docs ignore -site - -# commit file ignore -app/commit.json -changelogUI.md -docs/instructions/Roadmap.md -.cursorrules -*.md -.qodo -CLAUDE.md -.roo - -AGENTS.md* -.mcp.json -.claude -backend \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 5f5c2b9e..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh - -echo "🔍 Running pre-commit hook to check the code looks good... 🔍" - -# Load NVM if available (useful for managing Node.js versions) -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - -# Ensure `pnpm` is available -echo "Checking if pnpm is available..." -if ! command -v pnpm >/dev/null 2>&1; then - echo "❌ pnpm not found! Please ensure pnpm is installed and available in PATH." - exit 1 -fi - -# Run typecheck -echo "Running typecheck..." -if ! pnpm typecheck; then - echo "❌ Type checking failed! Please review TypeScript types." - echo "Once you're done, don't forget to add your changes to the commit! 🚀" - exit 1 -fi - -# Run lint -echo "Running lint..." -if ! pnpm lint; then - echo "❌ Linting failed! Run 'pnpm lint:fix' to fix the easy issues." - echo "Once you're done, don't forget to add your beautification to the commit! 🤩" - exit 1 -fi - -echo "👍 All checks passed! Committing changes..." diff --git a/.lighthouserc.json b/.lighthouserc.json deleted file mode 100644 index af1207d8..00000000 --- a/.lighthouserc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "ci": { - "collect": { - "url": ["http://localhost:5173/"], - "startServerCommand": "pnpm run start", - "numberOfRuns": 3 - }, - "assert": { - "assertions": { - "categories:performance": ["warn", { "minScore": 0.8 }], - "categories:accessibility": ["warn", { "minScore": 0.9 }], - "categories:best-practices": ["warn", { "minScore": 0.8 }], - "categories:seo": ["warn", { "minScore": 0.8 }] - } - }, - "upload": { - "target": "temporary-public-storage" - } - } -} diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/.npmrc b/.npmrc deleted file mode 100644 index bf2e7648..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -shamefully-hoist=true diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index bd5535a6..00000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -pnpm-lock.yaml diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 8d3dfb04..00000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 120, - "singleQuote": true, - "useTabs": false, - "tabWidth": 2, - "semi": true, - "bracketSpacing": true -} diff --git a/404.html b/404.html new file mode 100644 index 00000000..aa259219 --- /dev/null +++ b/404.html @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + Docs CodinIT.dev + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ +

404 - Not found

+ +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index f96d45a7..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -contact@codinit.dev. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 6a5bdfaf..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,125 +0,0 @@ -# Contributing to CodinIT (codinit-dev) - -Download desktop app --> [latest release](https://github.com/codinit-dev/codinit-dev/releases/latest) - -Thanks for your interest in contributing! CodinIT is an open-source AI app builder (web + desktop) and we welcome contributions of all kinds — bug reports, documentation fixes, examples, tests, design improvements, and code changes. - -Please take a quick look through this document to find the best way to contribute. - ---- - -## Table of Contents - -* [Code of Conduct](#code-of-conduct) -* [How can I contribute?](#how-can-i-contribute) - * [Reporting bugs](#reporting-bugs) - * [Requesting features](#requesting-features) - * [Working on issues / submitting PRs](#working-on-issues--submitting-prs) -* [Development setup](#development-setup) -* [Branching & commits](#branching--commits) -* [Formatting, linting & tests](#formatting-linting--tests) -* [Pull request checklist](#pull-request-checklist) -* [Security](#security) -* [License & thanks](#license--thanks) - ---- - -## [Code of Conduct](/CODE_OF_CONDUCT.md) - -We expect all contributors to follow a friendly, respectful, and constructive Code of Conduct. If the project does not already include one, please follow GitHub's Community Guidelines and be courteous in issues, PRs, and discussions. - -If you encounter harassment or problematic behavior, please contact the repository owners privately (see the repository `SECURITY.yaml`). - ---- - -## How can I contribute? - -### Reporting bugs - -If you find a bug: - -1. Search existing issues to see whether it's already been reported. -2. If not found, open a new issue with a clear title and the following details: - - * **What you expected to happen** - * **What actually happened** - * **Steps to reproduce** (minimum reproducible example is ideal) - * **Environment** (OS, Node/pnpm/yarn version, Electron if applicable) - * Any error output or stack traces - -A good bug report helps maintainers reproduce and fix problems quickly. - -### Requesting features - -Open an issue labeled **enhancement** or **feature request** describing the desired behavior, the user story it supports, and any ideas for implementation. If you plan to implement the feature yourself, say so and link to the issue from your pull request. - -### Working on issues / submitting PRs - -1. Claim the issue by commenting on it, or start a PR and reference the issue using `Fixes #ISSUE`. -2. Create a branch from `main` named using the pattern: `feature/short-description` or `fix/short-description`. -3. Follow the commit style described below and open a PR describing the change, why it’s needed, and any migration or configuration steps. -4. Keep PRs focused: small, reviewable changes are easiest to land. - ---- - -## Development setup (for contributors) - -This section is intentionally minimal and focused on contributing. - -1. Fork the repository on GitHub. -2. Clone your fork and install dependencies using the package manager defined in `package.json` (pnpm preferred). -3. Create a `.env.local` file based on `.env.example` and provide valid credentials for any services required by the area you are working on (e.g. AI providers, database, auth). -4. Run the development environment and confirm the app starts before making changes. - -If setup instructions change, the project README is the source of truth. Keep this file focused on contribution rules. - ---- - -## Branching & commits - -* Branch from `main` for new work. -* Use descriptive branch names: `feature/`, `fix/`, `chore/`. -* Use conventional commits where possible, for example: - - * `feat: add X` - * `fix: correct Y` - * `chore: update deps` - ---- - -## Formatting, linting & tests - -This project uses formatting and linting tools (Prettier, ESLint, etc.). Please: - -* Run linters and formatters before creating a PR. -* Add or update tests for new/changed behavior where applicable. There is a `__tests__` folder and Playwright configuration for end-to-end/preview tests. -* If you are unsure which scripts to run, check `package.json` for `test`, `lint`, `format`, and `dev` scripts. - ---- - -## Pull request checklist - -Before requesting review, please ensure: - -* [ ] The PR targets the `main` branch (or the branch noted in the issue) -* [ ] Your code builds and runs locally (`pnpm run dev` / `pnpm build`) -* [ ] Linting and formatting pass -* [ ] Relevant tests are added/updated and pass -* [ ] You’ve updated documentation when applicable (README, inline comments, etc.) - ---- - -## Security - -If you discover a security vulnerability, please follow the repository's `SECURITY.yaml` guidance and do **not** disclose sensitive details in public issues. Prefer private contact to the maintainers as described in the repository's security policy. - ---- - -## License & thanks - -By contributing, you agree that your contributions will be licensed under the repository’s MIT license. - -As well as our [Terms](https://codinit.dev/terms) & [Privacy Policy](https://codinit.dev/privacy) any violations relating to our terms & conditions will be deemed punishable by law and legal action will be taken against you. - -Thanks for helping make CodinIT better! If you have any questions about contributing, open an issue marked `help wanted` or reach out via the repository discussions. - diff --git a/CONTRIBUTING/index.html b/CONTRIBUTING/index.html new file mode 100644 index 00000000..d1f52bc1 --- /dev/null +++ b/CONTRIBUTING/index.html @@ -0,0 +1,1005 @@ + + + + + + + + + + + + + + + + + + + + + + + Contribution Guidelines - Docs CodinIT.dev + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Contribution Guidelines

+

Welcome! This guide provides all the details you need to contribute effectively to the project. Thank you for helping us make codinit.dev a better tool for developers worldwide. 💡

+
+

📋 Table of Contents

+
    +
  1. Code of Conduct
  2. +
  3. How Can I Contribute?
  4. +
  5. Pull Request Guidelines
  6. +
  7. Coding Standards
  8. +
  9. Development Setup
  10. +
  11. Testing
  12. +
  13. Deployment
  14. +
  15. Docker Deployment
  16. +
  17. VS Code Dev Containers Integration
  18. +
+
+

🛡️ Code of Conduct

+

This project is governed by our Code of Conduct. By participating, you agree to uphold this code. Report unacceptable behavior to the project maintainers.

+
+

🛠️ How Can I Contribute?

+

1️⃣ Reporting Bugs or Feature Requests

+
    +
  • Check the issue tracker to avoid duplicates.
  • +
  • Use issue templates (if available).
  • +
  • Provide detailed, relevant information and steps to reproduce bugs.
  • +
+

2️⃣ Code Contributions

+
    +
  1. Fork the repository.
  2. +
  3. Create a feature or fix branch.
  4. +
  5. Write and test your code.
  6. +
  7. Submit a pull request (PR).
  8. +
+

3️⃣ Join as a Core Contributor

+

Interested in maintaining and growing the project? Fill out our Contributor Application Form.

+
+

✅ Pull Request Guidelines

+

PR Checklist

+
    +
  • Branch from the main branch.
  • +
  • Update documentation, if needed.
  • +
  • Test all functionality manually.
  • +
  • Focus on one feature/bug per PR.
  • +
+

Review Process

+
    +
  1. Manual testing by reviewers.
  2. +
  3. At least one maintainer review required.
  4. +
  5. Address review comments.
  6. +
  7. Maintain a clean commit history.
  8. +
+
+

📏 Coding Standards

+

General Guidelines

+
    +
  • Follow existing code style.
  • +
  • Comment complex logic.
  • +
  • Keep functions small and focused.
  • +
  • Use meaningful variable names.
  • +
+
+

🖥️ Development Setup

+

1️⃣ Initial Setup

+
    +
  • Clone the repository: +
    git clone https://github.com/Gerome-Elassaad/codinit-app.git
    +cd codinit-app
    +
  • +
  • Install dependencies: +
    pnpm install
    +
  • +
  • Set up environment variables:
  • +
  • Rename .env.example to .env.local.
  • +
  • Add your API keys for providers you want to use: +
    GROQ_API_KEY=XXX
    +OPENAI_API_KEY=XXX
    +ANTHROPIC_API_KEY=XXX
    +GOOGLE_GENERATIVE_AI_API_KEY=XXX
    +MOONSHOT_API_KEY=XXX
    +XAI_API_KEY=XXX
    +# ... add other provider keys as needed
    +
  • +
  • For Docker users: Run the setup script or manually copy .env.local to .env: +
    # Option 1: Use the setup script
    +./scripts/setup-env.sh
    +
    +# Option 2: Manual copy
    +cp .env.local .env
    +
    + Docker Compose requires .env for variable substitution.
  • +
  • Optionally set:
      +
    • Debug level: VITE_LOG_LEVEL=debug
    • +
    • Context size: DEFAULT_NUM_CTX=32768
    • +
    • Local provider base URLs (Ollama, LM Studio): Use 127.0.0.1 instead of localhost
    • +
    +
  • +
+

Note: Never commit your .env.local or .env files to version control. They're already in .gitignore.

+

2️⃣ Run Development Server

+
pnpm run dev
+
+

Tip: Use Google Chrome Canary for local testing.

+
+

🧪 Testing

+

Run the test suite with:

+
pnpm test
+
+
+

🚀 Deployment

+

Deploy to Cloudflare Pages

+
pnpm run deploy
+
+

Ensure you have required permissions and that Wrangler is configured.

+
+

🐳 Docker Deployment

+

This section outlines the methods for deploying the application using Docker. The processes for Development and Production are provided separately for clarity.

+
+

🧑‍💻 Development Environment

+

Build Options

+

Option 1: Helper Scripts

+
# Development build
+npm run dockerbuild
+
+

Option 2: Direct Docker Build Command

+
docker build . --target codinit-ai-development
+
+

Option 3: Docker Compose Profile

+
docker compose --profile development up
+
+

Running the Development Container

+
docker run -p 5173:5173 --env-file .env.local codinit-ai:development
+
+
+

🏭 Production Environment

+

Build Options

+

Option 1: Helper Scripts

+
# Production build
+npm run dockerbuild:prod
+
+

Option 2: Direct Docker Build Command

+
docker build . --target codinit-ai-production
+
+

Option 3: Docker Compose Profile

+
docker compose --profile production up
+
+

Running the Production Container

+
docker run -p 5173:5173 --env-file .env.local codinit-ai:production
+
+
+

Coolify Deployment

+

For an easy deployment process, use Coolify:

+
    +
  1. Import your Git repository into Coolify.
  2. +
  3. Choose Docker Compose as the build pack.
  4. +
  5. Configure environment variables (e.g., API keys).
  6. +
  7. Set the start command: +
    docker compose --profile production up
    +
  8. +
+
+

🛠️ VS Code Dev Containers Integration

+

The docker-compose.yaml configuration is compatible with VS Code Dev Containers, making it easy to set up a development environment directly in Visual Studio Code.

+

Steps to Use Dev Containers

+
    +
  1. Open the command palette in VS Code (Ctrl+Shift+P or Cmd+Shift+P on macOS).
  2. +
  3. Select Dev Containers: Reopen in Container.
  4. +
  5. Choose the development profile when prompted.
  6. +
  7. VS Code will rebuild the container and open it with the pre-configured environment.
  8. +
+
+

🔑 Environment Variables

+

Ensure .env.local is configured correctly with:

+
    +
  • API keys.
  • +
  • Context-specific configurations.
  • +
+

Example for the DEFAULT_NUM_CTX variable:

+
DEFAULT_NUM_CTX=24576 # Uses 32GB VRAM
+
+ + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ +
+ + +
+ +
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 47a18bf3..00000000 --- a/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -ARG BASE=node:20.18.0 -FROM ${BASE} AS base - -WORKDIR /app - -# Install dependencies (this step is cached as long as the dependencies don't change) -COPY package.json pnpm-lock.yaml ./ - -#RUN npm install -g corepack@latest - -#RUN corepack enable pnpm && pnpm install -RUN npm install -g pnpm && pnpm install - -# Copy the rest of your app's source code -COPY . . - -# Expose the port the app runs on -EXPOSE 5173 - -# Production image -FROM base AS codinit-dev-production - -# Define non-sensitive configuration variables -ARG OLLAMA_API_BASE_URL -ARG TOGETHER_API_BASE_URL -ARG VITE_LOG_LEVEL=debug -ARG DEFAULT_NUM_CTX - -# Set non-sensitive environment variables -# NOTE: API keys should be passed at runtime using -e flags or --env-file -# Example: docker run -e OPENAI_API_KEY=sk-... or docker run --env-file .env -ENV WRANGLER_SEND_METRICS=false \ - OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \ - TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \ - VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \ - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} \ - RUNNING_IN_DOCKER=true - -# Pre-configure wrangler to disable metrics -RUN mkdir -p /root/.config/.wrangler && \ - echo '{"enabled":false}' > /root/.config/.wrangler/metrics.json - -RUN pnpm run build - -CMD [ "pnpm", "run", "dockerstart"] - -# Development image -FROM base AS codinit-dev-development - -# Define non-sensitive configuration variables -ARG OLLAMA_API_BASE_URL -ARG TOGETHER_API_BASE_URL -ARG VITE_LOG_LEVEL=debug -ARG DEFAULT_NUM_CTX - -# Set non-sensitive environment variables -# NOTE: API keys should be passed at runtime using -e flags or --env-file -# Example: docker run -e OPENAI_API_KEY=sk-... or docker run --env-file .env -ENV OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \ - TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \ - VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \ - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} \ - RUNNING_IN_DOCKER=true - -RUN mkdir -p ${WORKDIR}/run -CMD pnpm run dev --host diff --git a/FAQ/index.html b/FAQ/index.html new file mode 100644 index 00000000..888414df --- /dev/null +++ b/FAQ/index.html @@ -0,0 +1,1219 @@ + + + + + + + + + + + + + + + + + + + + + Frequently Asked Questions (FAQ) - Docs CodinIT.dev + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + + +

Frequently Asked Questions (FAQ)

+

Models and Setup

+
+What are the best models for CodinIT.dev? +
+

For the best experience with CodinIT.dev, we recommend using the following models from our 19+ supported providers:

+
**Top Recommended Models:**
+- **Claude 3.5 Sonnet** (Anthropic): Best overall coder, excellent for complex applications
+- **GPT-4o** (OpenAI): Strong alternative with great performance across all use cases
+- **Claude 4 Opus** (Anthropic): Latest flagship model with enhanced capabilities
+- **Gemini 2.0 Flash** (Google): Exceptional speed for rapid development
+- **DeepSeekCoder V3** (DeepSeek): Best open-source model for coding tasks
+
+**Self-Hosting Options:**
+- **DeepSeekCoder V2 236b**: Powerful self-hosted option
+- **Qwen 2.5 Coder 32b**: Best for moderate hardware requirements
+- **Ollama models**: Local inference with various model sizes
+
+**Latest Specialized Models:**
+- **Moonshot AI (Kimi)**: Kimi K2 models with advanced reasoning capabilities
+- **xAI Grok 4**: Latest Grok model with 256K context window
+- **Anthropic Claude 4 Opus**: Latest flagship model from Anthropic
+
+!!! tip "Model Selection Tips"
+    - Use larger models (7B+ parameters) for complex applications
+    - Claude models excel at structured code generation
+    - GPT-4o provides excellent general-purpose coding assistance
+    - Gemini models offer the fastest response times
+
+
+How do I configure API keys for different providers? +
+

You can configure API keys in two ways:

+
**Option 1: Environment Variables (Recommended for production)**
+Create a `.env.local` file in your project root:
+```bash
+ANTHROPIC_API_KEY=your_anthropic_key_here
+OPENAI_API_KEY=your_openai_key_here
+GOOGLE_GENERATIVE_AI_API_KEY=your_google_key_here
+MOONSHOT_API_KEY=your_moonshot_key_here
+XAI_API_KEY=your_xai_key_here
+```
+
+**Option 2: In-App Configuration**
+- Click the settings icon (⚙️) in the sidebar
+- Navigate to the "Providers" tab
+- Switch between "Cloud Providers" and "Local Providers" tabs
+- Click on a provider card to expand its configuration
+- Click on the "API Key" field to enter edit mode
+- Paste your API key and press Enter to save
+- Look for the green checkmark to confirm proper configuration
+
+!!! note "Security Note"
+    Never commit API keys to version control. The `.env.local` file is already in `.gitignore`.
+
+
+How do I add a new LLM provider? +
+

CodinIT.dev uses a modular provider architecture. To add a new provider:

+
1. **Create a Provider Class** in `app/lib/modules/llm/providers/your-provider.ts`
+2. **Implement the BaseProvider interface** with your provider's specific logic
+3. **Register the provider** in `app/lib/modules/llm/registry.ts`
+4. **The system automatically detects** and registers your new provider
+
+See the [Adding New LLMs](../#adding-new-llms) section for complete implementation details.
+
+
+How do I set up Moonshot AI (Kimi) provider? +
+

Moonshot AI provides access to advanced Kimi models with excellent reasoning capabilities:

+
**Setup Steps:**
+1. Visit [Moonshot AI Platform](https://platform.moonshot.ai/console/api-keys)
+2. Create an account and generate an API key
+3. Add `MOONSHOT_API_KEY=your_key_here` to your `.env.local` file
+4. Or configure it directly in Settings → Providers → Cloud Providers → Moonshot
+
+**Available Models:**
+- **Kimi K2 Preview**: Latest Kimi model with 128K context
+- **Kimi K2 Turbo**: Fast inference optimized version
+- **Kimi Thinking**: Specialized for complex reasoning tasks
+- **Moonshot v1 series**: Legacy models with vision capabilities
+
+!!! tip "Moonshot AI Features"
+    - Excellent for Chinese language tasks
+    - Strong reasoning capabilities
+    - Vision-enabled models available
+    - Competitive pricing
+
+
+What are the latest xAI Grok models? +
+

xAI has released several new Grok models with enhanced capabilities:

+
**Latest Models:**
+- **Grok 4**: Most advanced model with 256K context window
+- **Grok 4 (07-09)**: Specialized variant for specific tasks
+- **Grok 3 Beta**: Previous generation with 131K context
+- **Grok 3 Mini variants**: Optimized for speed and efficiency
+
+**Setup:**
+1. Get your API key from [xAI Platform](https://docs.x.ai/docs/quickstart#creating-an-api-key)
+2. Add `XAI_API_KEY=your_key_here` to your `.env.local` file
+3. Models will be available in the provider selection
+
+

Best Practices

+
+How do I access help and documentation? +
+

CodinIT.dev provides multiple ways to access help and documentation:

+
**Help Icon in Sidebar:**
+- Look for the question mark (?) icon in the sidebar
+- Click it to open the full documentation in a new tab
+- Provides instant access to guides, troubleshooting, and FAQs
+
+**Documentation Resources:**
+- **Main Documentation**: Complete setup and feature guides
+- **FAQ Section**: Answers to common questions
+- **Troubleshooting**: Solutions for common issues
+- **Best Practices**: Tips for optimal usage
+
+**Community Support:**
+- **GitHub Issues**: Report bugs and request features
+
+
+How do I get the best results with CodinIT.dev? +
+

Follow these proven strategies for optimal results:

+
**Project Setup:**
+- **Be specific about your stack**: Mention frameworks/libraries (Astro, Tailwind, ShadCN, Next.js) in your initial prompt
+- **Choose appropriate templates**: Use our 15+ project templates for quick starts
+- **Configure providers properly**: Set up your preferred LLM providers before starting
+
+**Development Workflow:**
+- **Use the enhance prompt icon**: Click the enhance icon to let AI refine your prompts before submitting
+- **Scaffold basics first**: Build foundational structure before adding advanced features
+- **Batch simple instructions**: Combine tasks like *"Change colors, add mobile responsiveness, restart dev server"*
+
+**Advanced Features:**
+- **Leverage MCP tools**: Use Model Context Protocol for enhanced AI capabilities
+- **Connect databases**: Integrate Supabase for backend functionality
+- **Use Git integration**: Version control your projects with GitHub
+- **Deploy easily**: Use built-in Vercel, Netlify, or GitHub Pages deployment
+
+
+What is MCP and why should I use it? +
+

MCP (Model Context Protocol) is an open protocol that extends CodinIT.dev's AI capabilities by allowing it to interact with external tools and data sources:

+
**What MCP Enables:**
+- **Database Access**: Query SQL databases, MongoDB, Redis, and more
+- **File Operations**: Read/write files with proper permissions
+- **API Integrations**: Connect to REST APIs, GraphQL endpoints
+- **Custom Tools**: Build domain-specific tools for your workflow
+- **Real-time Data**: Access live data during AI conversations
+
+**Why Use MCP:**
+- Makes the AI aware of your specific data and context
+- Automates complex workflows with multiple tool calls
+- Securely connects to enterprise systems
+- Standardized protocol supported by multiple AI platforms
+
+
+How do I set up MCP servers in CodinIT.dev? +
+

Setting up MCP servers is straightforward:

+
**Step-by-Step Setup:**
+1. **Open Settings**: Click the settings icon (⚙️) in the sidebar
+2. **Navigate to MCP Tab**: Select "MCP" from the settings menu
+3. **Add Server**: Click "Add Server" or "Configure Server"
+4. **Choose Server Type**:
+   - **STDIO**: For local command-line tools
+   - **SSE**: For Server-Sent Events servers
+   - **HTTP**: For HTTP-based MCP servers
+5. **Configure Connection**:
+   - Enter server name and description
+   - Set command/URL based on server type
+   - Add required environment variables or headers
+6. **Save and Enable**: Save configuration and enable the server
+
+**Example STDIO Configuration (PostgreSQL)**:
+```json
+{
+  "name": "postgres-db",
+  "type": "stdio",
+  "command": "npx",
+  "args": ["-y", "@modelcontextprotocol/server-postgres"],
+  "env": {
+    "DATABASE_URL": "postgresql://user:pass@localhost/mydb"
+  }
+}
+```
+
+**Example SSE Configuration (Remote API)**:
+```json
+{
+  "name": "my-api",
+  "type": "sse",
+  "url": "https://api.example.com/mcp",
+  "headers": {
+    "Authorization": "Bearer YOUR_API_TOKEN"
+  }
+}
+```
+
+
+What MCP server types are supported? +
+

CodinIT.dev supports three types of MCP server connections:

+
**1. STDIO Servers (Local Tools)**
+- Run as local command-line processes
+- Communicate via standard input/output
+- Best for: Local databases, file systems, CLI tools
+- Examples: PostgreSQL server, filesystem server, git tools
+
+**2. SSE Servers (Server-Sent Events)**
+- Connect to remote servers via HTTP
+- Real-time streaming with Server-Sent Events
+- Best for: Remote APIs, cloud services
+- Examples: Cloud database services, third-party APIs
+
+**3. Streamable HTTP Servers**
+- HTTP-based protocol with streaming support
+- Flexible connection options
+- Best for: Custom services, enterprise systems
+- Examples: Internal APIs, custom business logic
+
+Each type has different setup requirements and use cases. Choose based on your needs.
+
+
+How do MCP tools work during conversations? +
+

When MCP servers are configured, their tools become available to the AI:

+
**Tool Discovery:**
+- CodinIT.dev automatically detects all tools from connected servers
+- Tools appear in the AI's available tool list
+- Tool descriptions help the AI understand when to use them
+
+**Tool Execution Flow:**
+1. **AI Decides**: Based on your prompt, AI determines which tool to use
+2. **User Approval**: You review and approve the tool execution for security
+3. **Tool Runs**: The MCP server executes the tool with provided parameters
+4. **Results Return**: Tool output is sent back to the AI
+5. **AI Responds**: AI incorporates tool results into its response
+
+**Approval States:**
+- **APPROVE**: Allow the tool to execute
+- **REJECT**: Deny the tool execution
+- **ERROR**: Tool execution failed
+
+**Security Features:**
+- All tool executions require explicit user approval
+- Tool parameters are shown before execution
+- Failed executions are logged with error details
+
+
+What are common MCP use cases? +
+

MCP enables many powerful workflows:

+
**Development & DevOps:**
+- Query database schemas and generate migrations
+- Read/write code files for automated refactoring
+- Execute git commands for version control
+- Deploy applications to cloud platforms
+- Run test suites and analyze results
+
+**Data Analysis:**
+- Query SQL databases for business intelligence
+- Process CSV/JSON files for data transformation
+- Generate charts and visualizations
+- Fetch real-time data from APIs
+- Aggregate data from multiple sources
+
+**Business Operations:**
+- Integrate with CRM systems (Salesforce, HubSpot)
+- Manage customer support tickets
+- Access inventory and order management systems
+- Generate reports from business data
+- Automate routine administrative tasks
+
+**Content Management:**
+- Read/write documentation files
+- Manage blog posts and articles
+- Update website content
+- Process and optimize images
+- Version control content changes
+
+
+How do I troubleshoot MCP connection issues? +
+

Common MCP issues and solutions:

+
**Server Won't Connect:**
+- Verify server endpoint/command is correct
+- Check authentication credentials (API keys, tokens)
+- Ensure network connectivity for remote servers
+- Review server logs for specific error messages
+- Confirm MCP protocol version compatibility
+
+**Tools Not Appearing:**
+- Restart the MCP server to refresh tool list
+- Check server configuration in Settings → MCP
+- Verify server is enabled (toggle switch on)
+- Look for errors in browser console (F12)
+- Confirm server implements tool discovery correctly
+
+**Tool Execution Failures:**
+- Check tool parameters are valid
+- Ensure required permissions are granted
+- Verify environment variables are set correctly
+- Review tool-specific error messages
+- Test the tool outside CodinIT.dev first
+
+**Performance Issues:**
+- Limit number of concurrent tool calls
+- Use smaller result sets when querying databases
+- Implement caching in your MCP server
+- Monitor server response times
+- Consider using local servers for better performance
+
+**Conflict Resolution:**
+- If multiple servers provide the same tool name, CodinIT.dev will detect the conflict
+- Rename tools in server configuration to avoid conflicts
+- Disable unused servers to reduce tool namespace pollution
+
+
+Can I build my own MCP servers? +
+

Yes! You can create custom MCP servers for your specific needs:

+
**Building Custom Servers:**
+- Use the official MCP SDK: `@modelcontextprotocol/sdk`
+- Implement server in TypeScript, Python, or other languages
+- Define custom tools with input schemas and handlers
+- Deploy as STDIO, SSE, or HTTP server
+
+**Basic Server Example (TypeScript)**:
+```typescript
+import { Server } from '@modelcontextprotocol/sdk/server/index.js';
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
+
+const server = new Server({
+  name: 'my-custom-server',
+  version: '1.0.0',
+});
+
+// Register a custom tool
+server.setRequestHandler('tools/list', async () => ({
+  tools: [{
+    name: 'get_user_data',
+    description: 'Fetch user data from database',
+    inputSchema: {
+      type: 'object',
+      properties: {
+        userId: { type: 'string' }
+      }
+    }
+  }]
+}));
+
+// Start the server
+const transport = new StdioServerTransport();
+await server.connect(transport);
+```
+
+**Resources:**
+- MCP Documentation: [modelcontextprotocol.io](https://modelcontextprotocol.io)
+- Example Servers: GitHub MCP organization
+- SDK Reference: @modelcontextprotocol/sdk package
+
+Custom servers allow you to integrate any system or data source with CodinIT.dev!
+
+
+How do I deploy my CodinIT.dev projects? +
+

CodinIT.dev supports one-click deployment to multiple platforms:

+
**Supported Platforms:**
+- **Vercel**: Go to Settings → Connections → Vercel, then deploy with one click
+- **Netlify**: Connect your Netlify account and deploy instantly
+- **GitHub Pages**: Push to GitHub and enable Pages in repository settings
+
+**Deployment Features:**
+- Automatic build configuration for popular frameworks
+- Environment variable management
+- Custom domain support
+- Preview deployments for testing
+
+
+How do I use Git integration features? +
+

CodinIT.dev provides comprehensive Git integration with GitHub and GitLab:

+
**Basic Git Operations:**
+- Import existing repositories by URL
+- Create new repositories on GitHub or GitLab
+- Automatic commits for major changes
+- Push/pull changes seamlessly
+
+**GitHub Integration:**
+- Connect GitHub account in Settings → Connections → GitHub
+- Import from your connected repositories
+- Create and manage branches
+- View repository statistics
+
+**GitLab Integration:**
+- Connect GitLab account in Settings → Connections → GitLab
+- Browse and import GitLab projects
+- Manage GitLab branches
+- Access project metadata
+
+**Advanced Features:**
+- Version control with diff visualization
+- Collaborative development support
+- Bug report generation with automatic system information
+
+

Project Information

+
+How do I contribute to CodinIT.dev? +
+

Check out our Contribution Guide for more details on how to get involved!

+
+Why are there so many open issues/pull requests? +

We're forming a team of maintainers to manage demand and streamline issue resolution. The maintainers are rockstars, and we're also exploring partnerships to help the project thrive.

+
+

New Features & Technologies

+
+What's new in CodinIT.dev? +
+

Recent major additions to CodinIT.dev include:

+
**Advanced AI Capabilities:**
+- **19 LLM Providers**: Support for Anthropic, OpenAI, Google, DeepSeek, Cohere, and more
+- **MCP Integration**: Model Context Protocol for enhanced AI tool calling
+- **Dynamic Model Loading**: Automatic model discovery from provider APIs
+
+**Development Tools:**
+- **WebContainer**: Secure sandboxed development environment
+- **Live Preview**: Real-time application previews without leaving the editor
+- **Project Templates**: 15+ starter templates for popular frameworks
+
+**Version Control & Collaboration:**
+- **Git Integration**: Import/export projects with GitHub and GitLab
+- **Automatic Commits**: Smart version control for project changes
+- **Diff Visualization**: See code changes clearly
+- **Bug Reporting**: Built-in bug report generation and tracking
+
+**Backend & Database:**
+- **Supabase Integration**: Built-in database and authentication
+- **API Integration**: Connect to external services and databases
+
+**Deployment & Production:**
+- **One-Click Deployment**: Vercel, Netlify, and GitHub Pages support
+- **Environment Management**: Production-ready configuration
+- **Build Optimization**: Automatic configuration for popular frameworks
+
+
+How do I use the new project templates? +
+

CodinIT.dev offers templates for popular frameworks and technologies:

+
**Getting Started:**
+1. Start a new project in CodinIT.dev
+2. Browse available templates in the starter selection
+3. Choose your preferred technology stack
+4. The AI will scaffold your project with best practices
+
+**Available Templates:**
+- **Frontend**: React, Vue, Angular, Svelte, SolidJS
+- **Full-Stack**: Next.js, Astro, Qwik, Remix, Nuxt
+- **Mobile**: Expo, React Native
+- **Content**: Slidev presentations, Astro blogs
+- **Vanilla**: Vite with TypeScript/JavaScript
+
+Templates include pre-configured tooling, linting, and build processes.
+
+
+How does WebContainer work? +
+

WebContainer provides a secure development environment:

+
**Features:**
+- **Isolated Environment**: Secure sandbox for running code
+- **Full Node.js Support**: Run npm, build tools, and dev servers
+- **Live File System**: Direct manipulation of project files
+- **Terminal Integration**: Execute commands with real-time output
+
+**Supported Technologies:**
+- All major JavaScript frameworks (React, Vue, Angular, etc.)
+- Build tools (Vite, Webpack, Parcel)
+- Package managers (npm, pnpm, yarn)
+
+
+How do I connect external databases? +
+

Use Supabase for backend database functionality:

+
**Setup Process:**
+1. Create a Supabase project at supabase.com
+2. Get your project URL and API keys
+3. Configure the connection in your CodinIT.dev project
+4. Use Supabase tools to interact with your database
+
+**Available Features:**
+- Real-time subscriptions
+- Built-in authentication
+- Row Level Security (RLS)
+- Automatic API generation
+- Database migrations
+
+

Model Comparisons

+
+How do local LLMs compare to larger models like Claude 3.5 Sonnet for CodinIT.dev? +
+

While local LLMs are improving rapidly, larger models still offer the best results for complex applications. Here's the current landscape:

+
**Recommended for Production:**
+- **Claude 4 Opus**: Latest flagship model with enhanced reasoning (200K context)
+- **Claude 3.5 Sonnet**: Proven excellent performance across all tasks
+- **GPT-4o**: Strong general-purpose coding with great reliability
+- **xAI Grok 4**: Latest Grok with 256K context window
+
+**Fast & Efficient:**
+- **Gemini 2.0 Flash**: Exceptional speed for rapid development
+- **Claude 3 Haiku**: Cost-effective for simpler tasks
+- **xAI Grok 3 Mini Fast**: Optimized for speed and efficiency
+
+**Advanced Reasoning:**
+- **Moonshot AI Kimi K2**: Advanced reasoning with 128K context
+- **Moonshot AI Kimi Thinking**: Specialized for complex reasoning tasks
+
+**Open Source & Self-Hosting:**
+- **DeepSeekCoder V3**: Best open-source model available
+- **DeepSeekCoder V2 236b**: Powerful self-hosted option
+- **Qwen 2.5 Coder 32b**: Good balance of performance and resource usage
+
+**Local Models (Ollama):**
+- Best for privacy and offline development
+- Use 7B+ parameter models for reasonable performance
+- Still experimental for complex, large-scale applications
+
+!!! tip "Model Selection Guide"
+    - Use Claude/GPT-4o for complex applications
+    - Use Gemini for fast prototyping
+    - Use local models for privacy/offline development
+    - Always test with your specific use case
+
+

Troubleshooting

+
+There was an error processing this request +
+

This generic error message means something went wrong. Check these locations:

+
- **Terminal output**: If you started with Docker or `pnpm`
+- **Browser developer console**: Press `F12` → Console tab
+- **Server logs**: Check for any backend errors
+- **Network tab**: Verify API calls are working
+
+
+x-api-key header missing +
+

This authentication error can be resolved by:

+
- **Restarting the container**: `docker compose restart` (if using Docker)
+- **Switching run methods**: Try `pnpm` if using Docker, or vice versa
+- **Checking API keys**: Verify your API keys are properly configured
+- **Clearing browser cache**: Sometimes cached authentication causes issues
+
+
+Blank preview when running the app +
+

Blank previews usually indicate code generation issues:

+
- **Check developer console** for JavaScript errors
+- **Verify WebContainer is running** properly
+- **Try refreshing** the preview pane
+- **Check for hallucinated code** in the generated files
+- **Restart the development server** if issues persist
+
+
+MCP server connection failed +
+

If you're having trouble with MCP integrations:

+
- **Verify server configuration** in Settings → MCP
+- **Check server endpoints** and authentication credentials
+- **Test server connectivity** outside of CodinIT.dev
+- **Review MCP server logs** for specific error messages
+- **Ensure server supports** the MCP protocol version
+
+
+Git integration not working +
+

Common Git-related issues and solutions:

+
- **GitHub connection failed**: Verify your GitHub token has correct permissions
+- **Repository not found**: Check repository URL and access permissions
+- **Push/pull failed**: Ensure you have write access to the repository
+- **Merge conflicts**: Resolve conflicts manually or use the diff viewer
+- **Large files blocked**: Check GitHub's file size limits
+
+
+Deployment failed +
+

Deployment issues can be resolved by:

+
- **Checking build logs** for specific error messages
+- **Verifying environment variables** are set correctly
+- **Testing locally** before deploying
+- **Checking platform-specific requirements** (Node version, build commands)
+- **Reviewing deployment configuration** in platform settings
+
+
+Everything works, but the results are bad +
+

For suboptimal AI responses, try these solutions:

+
- **Switch to a more capable model**: Use Claude 3.5 Sonnet, GPT-4o, or Claude 4 Opus
+- **Be more specific** in your prompts about requirements and technologies
+- **Use the enhance prompt feature** to refine your requests
+- **Break complex tasks** into smaller, focused prompts
+- **Provide context** about your project structure and goals
+
+
+WebContainer preview not loading +
+

If the live preview isn't working:

+
- **Check WebContainer status** in the terminal
+- **Verify Node.js compatibility** with your project
+- **Restart the development environment**
+- **Clear browser cache** and reload
+- **Check for conflicting ports** (default is 5173)
+
+
+Received structured exception #0xc0000005: access violation +
+

Windows-specific issue: Update the Visual C++ Redistributable

+
+Miniflare or Wrangler errors in Windows +
+

Windows development environment: Install Visual Studio C++ (version 14.40.33816 or later). More details in GitHub Issues

+
+Provider not showing up after adding it +
+

If your custom LLM provider isn't appearing:

+
- **Restart the development server** to reload providers
+- **Check the provider registry** in `app/lib/modules/llm/registry.ts`
+- **Verify the provider class** extends `BaseProvider` correctly
+- **Check browser console** for provider loading errors
+- **Ensure proper TypeScript compilation** without errors
+
+
+

Get Help & Support

+
+

Report Issues

+
+

Open an Issue in our GitHub Repository

+ + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ +
+ + +
+ +
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 77703be1..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 CodinIT.dev - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index c3eb2cc0..00000000 --- a/README.md +++ /dev/null @@ -1,129 +0,0 @@ -

- CodinIT.dev Hero -

- -

- - Fazier badge - -     -     - - CodinIT.dev badge - -

- -

CodinIT.dev — Open‑Source AI App Builder

- -

- Build, manage, and deploy intelligent applications faster — directly from your browser or desktop. -

- ---- - -## Overview - -CodinIT.dev is an open‑source, AI full‑stack development platform designed to help developers build modern Node.js applications with speed and precision. It combines code generation, project management, and deployment tools into a single workflow, powered by your choice of AI providers. - -Whether you are prototyping, scaling a SaaS product, or experimenting with local LLMs, CodinIT.dev adapts to your stack and workflow. - ---- - -## Quick Start - -### Run as a Desktop App - -Download the latest prebuilt release for macOS, Windows, and Linux. - -[Download Latest Release](https://github.com/codinit-dev/codinit-dev/releases/latest) - -Get up and running in minutes. - -### 1. Clone the Repository - -```bash -git clone https://github.com/codinit-dev/codinit-dev.git -cd codinit-dev - -``` - -### 2. Install Dependencies - -```bash -# npm -npm install - -# or pnpm -pnpm install - -``` - -### 3. Configure Environment - -Create a `.env` file and add your preferred AI provider keys. You can mix and match multiple providers depending on your requirements. - -### 4. Run the Development Server - -```bash -pnpm run dev - -``` - -The application will be available at: http://localhost:5173 - ---- - -## Core Capabilities - -- **Automated Full-Stack Engineering:** Streamline the creation and management of complex Node.js architectures using intelligent generation. -- **Universal Model Integration:** Seamlessly connect with over 19 cloud and local AI providers. -- **Hybrid Environment Support:** native compatibility for both Web browsers and Desktop (Electron) environments. -- **Production-Ready Containerization:** Fully Dockerized workflow with preset configurations for Vercel, Netlify, and GitHub Pages. -- **Integrated Development Suite:** Includes robust utilities such as semantic search, diff visualization, and concurrency file-locking. -- **Expanded Ecosystem Connectivity:** Native integration with Supabase, real-time data visualization tools, and voice-command interfaces. -- **Vendor-Neutral Infrastructure:** A flexible architecture designed to prevent vendor lock-in, allowing dynamic switching between backend providers. - ---- - -## Supported AI Providers - -CodinIT.dev allows you to use one provider or switch dynamically per task. - -### Cloud Providers - -OpenAI, Anthropic, Google, Groq, xAI, DeepSeek, Cohere, Mistral, Together, Perplexity, HuggingFace, OpenRouter, and more. - -### Local Providers - -Ollama, LM Studio, and OpenAI‑compatible local endpoints. - ---- - -## Deployment & Desktop Usage - -### Run with Docker - -```bash -npm run dockerbuild -docker compose --profile development up - -``` - - diff --git a/__tests__/config/setup.ts b/__tests__/config/setup.ts deleted file mode 100644 index a3ce7f82..00000000 --- a/__tests__/config/setup.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { expect, afterEach, beforeAll, vi } from 'vitest'; -import { cleanup } from '@testing-library/react'; -import * as matchers from '@testing-library/jest-dom/matchers'; - -// Extend expect with jest-dom matchers -expect.extend(matchers); - -// Clean up after each test -afterEach(() => { - cleanup(); -}); - -// Mock environment variables -beforeAll(() => { - // Set up test environment variables - process.env.NODE_ENV = 'test'; - process.env.VITE_APP_ENV = 'test'; - - // Mock console methods to reduce noise in tests - const originalConsoleError = console.error; - const originalConsoleWarn = console.warn; - - console.error = (...args: any[]) => { - // Only show errors that aren't from React testing library - if (!args[0]?.includes?.('Warning: ReactDOMTestUtils')) { - originalConsoleError(...args); - } - }; - - console.warn = (...args: any[]) => { - // Only show warnings that aren't from React testing library - if (!args[0]?.includes?.('Warning: ReactDOMTestUtils')) { - originalConsoleWarn(...args); - } - }; -}); - -// Mock fetch globally -(globalThis as any).fetch = vi.fn(); - -// Mock ResizeObserver -(globalThis as any).ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -// Mock IntersectionObserver -(globalThis as any).IntersectionObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -// Mock matchMedia -Object.defineProperty(window, 'matchMedia', { - writable: true, - value: vi.fn().mockImplementation((query: string) => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), // deprecated - removeListener: vi.fn(), // deprecated - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), -}); - -// Mock localStorage -const localStorageMock = { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn(), - length: 0, - key: vi.fn(), -} as Storage; -(globalThis as any).localStorage = localStorageMock; - -// Mock sessionStorage -const sessionStorageMock = { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn(), - length: 0, - key: vi.fn(), -} as Storage; -(globalThis as any).sessionStorage = sessionStorageMock; - -// Skip WebSocket mocking for now to avoid type issues diff --git a/__tests__/config/vitest.config.ts b/__tests__/config/vitest.config.ts deleted file mode 100644 index 12a3385b..00000000 --- a/__tests__/config/vitest.config.ts +++ /dev/null @@ -1,52 +0,0 @@ -/// -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import { resolve } from 'path'; - -export default defineConfig({ - plugins: [react(), tsconfigPaths()], - test: { - environment: 'jsdom', - setupFiles: ['./__tests__/config/setup.ts'], - globals: true, - include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - exclude: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/electron/**', '**/public/**'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html'], - exclude: [ - 'node_modules/', - 'dist/', - 'build/', - 'electron/', - 'public/', - '**/*.d.ts', - '**/*.config.{js,ts}', - '**/*.{test,spec}.{js,ts,tsx}', - 'app/entry.client.tsx', - 'app/entry.server.tsx', - 'app/root.tsx', - ], - thresholds: { - global: { - branches: 70, - functions: 70, - lines: 70, - statements: 70, - }, - }, - }, - browser: { - enabled: true, - headless: true, - provider: 'playwright', - name: 'chromium', - }, - }, - resolve: { - alias: { - '~': resolve(__dirname, 'app'), - }, - }, -}); diff --git a/__tests__/fixtures/api-responses.ts b/__tests__/fixtures/api-responses.ts deleted file mode 100644 index 68ea6055..00000000 --- a/__tests__/fixtures/api-responses.ts +++ /dev/null @@ -1,188 +0,0 @@ -// Test fixtures for API responses and mock data - -export const mockOpenAIModels = { - data: [ - { - id: 'gpt-4o', - object: 'model', - created: 1687882411, - owned_by: 'openai', - }, - { - id: 'gpt-4o-mini', - object: 'model', - created: 1687882410, - owned_by: 'openai', - }, - { - id: 'gpt-3.5-turbo', - object: 'model', - created: 1677610602, - owned_by: 'openai', - }, - ], -}; - -export const mockAnthropicModels = { - data: [ - { - id: 'claude-3-5-sonnet-20241022', - display_name: 'Claude 3.5 Sonnet', - type: 'model', - }, - { - id: 'claude-3-haiku-20240307', - display_name: 'Claude 3 Haiku', - type: 'model', - }, - ], -}; - -export const mockModelInfo = { - openai: [ - { - name: 'gpt-4o', - label: 'GPT-4o', - provider: 'OpenAI', - maxTokenAllowed: 128000, - maxCompletionTokens: 4096, - }, - { - name: 'gpt-4o-mini', - label: 'GPT-4o Mini', - provider: 'OpenAI', - maxTokenAllowed: 128000, - maxCompletionTokens: 4096, - }, - ], - anthropic: [ - { - name: 'claude-3-5-sonnet-20241022', - label: 'Claude 3.5 Sonnet', - provider: 'Anthropic', - maxTokenAllowed: 200000, - maxCompletionTokens: 8192, - }, - ], -}; - -export const mockProviderInfo = [ - { - name: 'OpenAI', - staticModels: mockModelInfo.openai, - getApiKeyLink: 'https://platform.openai.com/api-keys', - labelForGetApiKey: 'Get API Key', - icon: '/thirdparty/logos/openai.svg', - }, - { - name: 'Anthropic', - staticModels: mockModelInfo.anthropic, - getApiKeyLink: 'https://console.anthropic.com/', - labelForGetApiKey: 'Get API Key', - icon: '/thirdparty/logos/anthropic.svg', - }, -]; - -export const mockApiKeys = { - openai: 'sk-test123456789', - anthropic: 'sk-ant-test123456789', -}; - -export const mockChatMessages = [ - { - id: '1', - role: 'user', - content: 'Hello, can you help me write a React component?', - }, - { - id: '2', - role: 'assistant', - content: - "Of course! I'd be happy to help you write a React component. What kind of component are you looking to create?", - }, -]; - -export const mockFileSystem = { - '/app': { - type: 'directory', - children: { - src: { - type: 'directory', - children: { - components: { - type: 'directory', - children: { - 'Button.tsx': { - type: 'file', - content: 'export const Button = () => ;', - }, - 'Input.tsx': { - type: 'file', - content: 'export const Input = () => ;', - }, - }, - }, - 'App.tsx': { - type: 'file', - content: 'import React from "react";\n\nexport const App = () =>
Hello World
;', - }, - }, - }, - 'package.json': { - type: 'file', - content: JSON.stringify( - { - name: 'test-app', - version: '1.0.0', - dependencies: { - react: '^18.0.0', - }, - }, - null, - 2, - ), - }, - }, - }, -}; - -export const mockDeploymentConfig = { - netlify: { - siteId: 'test-site-id', - buildCommand: 'npm run build', - publishDirectory: 'dist', - environment: { - NODE_ENV: 'production', - }, - }, - vercel: { - projectId: 'test-project-id', - orgId: 'test-org-id', - buildCommand: 'npm run build', - outputDirectory: 'dist', - }, -}; - -export const mockGitInfo = { - branch: 'main', - commitHash: 'abc123def456', - remoteUrl: 'https://github.com/user/repo.git', - author: 'Test Author', - email: 'test@example.com', -}; - -export const mockSystemInfo = { - platform: 'darwin', - arch: 'arm64', - version: '22.1.0', - hostname: 'test-machine', - cpus: 8, - memory: { - total: 16 * 1024 * 1024 * 1024, // 16GB - free: 8 * 1024 * 1024 * 1024, // 8GB - }, - disk: { - total: 500 * 1024 * 1024 * 1024, // 500GB - free: 200 * 1024 * 1024 * 1024, // 200GB - }, -}; diff --git a/__tests__/helpers/test-utils.tsx b/__tests__/helpers/test-utils.tsx deleted file mode 100644 index 0e7c63ac..00000000 --- a/__tests__/helpers/test-utils.tsx +++ /dev/null @@ -1,141 +0,0 @@ -// Test helpers and utilities - -import { render, type RenderOptions } from '@testing-library/react'; -import { type ReactElement } from 'react'; -import { BrowserRouter } from 'react-router-dom'; -import { vi } from 'vitest'; - -// Custom render function that includes common providers -const AllTheProviders = ({ children }: { children: React.ReactNode }) => { - return {children}; -}; - -const customRender = (ui: ReactElement, options?: Omit) => - render(ui, { wrapper: AllTheProviders, ...options }); - -// Mock LLM provider responses -export const mockLLMResponse = (content: string, model = 'gpt-4o') => ({ - id: 'chatcmpl-test', - object: 'chat.completion', - created: Date.now(), - model, - choices: [ - { - index: 0, - message: { - role: 'assistant', - content, - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 20, - total_tokens: 30, - }, -}); - -// Mock fetch responses -export const mockFetchResponse = (data: any, status = 200) => ({ - ok: status >= 200 && status < 300, - status, - json: () => Promise.resolve(data), - text: () => Promise.resolve(JSON.stringify(data)), - headers: new Headers({ 'content-type': 'application/json' }), -}); - -// Create mock file system for testing -export const createMockFileSystem = (structure: Record) => { - const mockFS: Record = {}; - - const buildFS = (obj: Record, path = '') => { - for (const [key, value] of Object.entries(obj)) { - const fullPath = path ? `${path}/${key}` : key; - - if (value && typeof value === 'object' && value.type === 'file') { - mockFS[fullPath] = value.content; - } else if (value && typeof value === 'object' && value.type === 'directory') { - buildFS(value.children, fullPath); - } - } - }; - - buildFS(structure); - - return mockFS; -}; - -// Generate test IDs for components -export const testIds = { - modelSelector: 'model-selector', - providerDropdown: 'provider-dropdown', - modelDropdown: 'model-dropdown', - chatInput: 'chat-input', - sendButton: 'send-button', - messageList: 'message-list', - fileTree: 'file-tree', - editor: 'editor', - terminal: 'terminal', - settingsPanel: 'settings-panel', -}; - -// Common test data generators -export const generateTestMessage = (overrides = {}) => ({ - id: `msg-${Date.now()}`, - role: 'user', - content: 'Test message', - createdAt: new Date(), - ...overrides, -}); - -export const generateTestModel = (overrides = {}) => ({ - name: 'gpt-4o', - label: 'GPT-4o', - provider: 'OpenAI', - maxTokenAllowed: 128000, - maxCompletionTokens: 4096, - ...overrides, -}); - -export const generateTestProvider = (overrides = {}) => ({ - name: 'OpenAI', - staticModels: [], - getApiKeyLink: 'https://platform.openai.com/api-keys', - labelForGetApiKey: 'Get API Key', - icon: '/thirdparty/logos/openai.svg', - ...overrides, -}); - -// Wait for async operations -export const waitForAsync = () => new Promise((resolve) => setImmediate(resolve)); - -// Mock console methods for cleaner test output -export const mockConsole = () => { - const originalConsole = { ...console }; - const mockMethods: (keyof typeof console)[] = ['log', 'warn', 'error', 'info', 'debug']; - - mockMethods.forEach((method) => { - (console[method] as any) = vi.fn(); - }); - - return { - restore: () => { - mockMethods.forEach((method) => { - (console[method] as any) = originalConsole[method]; - }); - }, - }; -}; - -// Create test wrapper with common setup -export const createTestWrapper = (component: React.ComponentType) => { - const Component = component; - return (props: any) => ( - - - - ); -}; - -export { customRender as render }; diff --git a/__tests__/unit/providers/manager.test.ts b/__tests__/unit/providers/manager.test.ts deleted file mode 100644 index 6aeb0f15..00000000 --- a/__tests__/unit/providers/manager.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { LLMManager } from '~/lib/modules/llm/manager'; -import { mockOpenAIModels, mockApiKeys } from '../../fixtures/api-responses'; - -describe('LLMManager', () => { - let manager: LLMManager; - - beforeEach(() => { - // Reset the singleton instance for each test - (LLMManager as any)._instance = null; - manager = LLMManager.getInstance(); - - // Silence console for cleaner test output - vi.spyOn(console, 'log').mockImplementation(() => { }); - vi.spyOn(console, 'error').mockImplementation(() => { }); - vi.spyOn(console, 'warn').mockImplementation(() => { }); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('getInstance', () => { - it('should return a singleton instance', () => { - const instance1 = LLMManager.getInstance(); - const instance2 = LLMManager.getInstance(); - expect(instance1).toBe(instance2); - }); - }); - - describe('getAllProviders', () => { - it('should return all registered providers', () => { - const providers = manager.getAllProviders(); - expect(providers).toBeDefined(); - expect(Array.isArray(providers)).toBe(true); - expect(providers.length).toBeGreaterThan(0); - - // Check that providers have required properties - providers.forEach((provider) => { - expect(provider).toHaveProperty('name'); - expect(provider).toHaveProperty('staticModels'); - expect(typeof provider.name).toBe('string'); - expect(Array.isArray(provider.staticModels)).toBe(true); - }); - }); - }); - - describe('getProvider', () => { - it('should return a provider by name', () => { - const provider = manager.getProvider('OpenAI'); - expect(provider).toBeDefined(); - expect(provider?.name).toBe('OpenAI'); - }); - - it('should return undefined for non-existent provider', () => { - const provider = manager.getProvider('NonExistentProvider'); - expect(provider).toBeUndefined(); - }); - }); - - describe('getDefaultProvider', () => { - it('should return a default provider', () => { - const provider = manager.getDefaultProvider(); - expect(provider).toBeDefined(); - expect(provider.name).toBeDefined(); - }); - }); - - describe('getStaticModelList', () => { - it('should return all static models from all providers', () => { - const models = manager.getStaticModelList(); - expect(Array.isArray(models)).toBe(true); - expect(models.length).toBeGreaterThan(0); - - // Check model structure - models.forEach((model) => { - expect(model).toHaveProperty('name'); - expect(model).toHaveProperty('label'); - expect(model).toHaveProperty('provider'); - expect(model).toHaveProperty('maxTokenAllowed'); - }); - }); - }); - - describe('updateModelList', () => { - it('should update model list with API keys', async () => { - const mockFetch = vi.fn(); - global.fetch = mockFetch; - - // Mock successful API responses for providers - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ data: mockOpenAIModels.data }), - }); - - const options = { - apiKeys: mockApiKeys, - providerSettings: {}, - serverEnv: {}, - }; - - const models = await manager.updateModelList(options); - - expect(Array.isArray(models)).toBe(true); - expect(models.length).toBeGreaterThan(0); - }); - - it('should handle provider errors gracefully', async () => { - const mockFetch = vi.fn(); - global.fetch = mockFetch; - - // Mock failed API response - mockFetch.mockRejectedValueOnce(new Error('API Error')); - - const options = { - apiKeys: mockApiKeys, - providerSettings: {}, - serverEnv: {}, - }; - - const models = await manager.updateModelList(options); - - // Should still return static models even if dynamic fetch fails - expect(Array.isArray(models)).toBe(true); - expect(models.length).toBeGreaterThan(0); - }); - }); - - describe('getModelListFromProvider', () => { - it('should return models for a specific provider', async () => { - const provider = manager.getProvider('OpenAI'); - expect(provider).toBeDefined(); - - if (provider) { - const mockFetch = vi.fn(); - global.fetch = mockFetch; - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ data: mockOpenAIModels.data }), - }); - - const options = { - apiKeys: mockApiKeys, - providerSettings: {}, - serverEnv: {}, - }; - - const models = await manager.getModelListFromProvider(provider, options); - - expect(Array.isArray(models)).toBe(true); - models.forEach((model) => { - expect(model.provider).toBe('OpenAI'); - }); - } - }); - - it('should return static models when API call fails', async () => { - const provider = manager.getProvider('OpenAI'); - expect(provider).toBeDefined(); - - if (provider) { - const mockFetch = vi.fn(); - global.fetch = mockFetch; - - mockFetch.mockRejectedValueOnce(new Error('API Error')); - - const options = { - apiKeys: {}, - providerSettings: {}, - serverEnv: {}, - }; - - const models = await manager.getModelListFromProvider(provider, options); - - expect(Array.isArray(models)).toBe(true); - expect(models.length).toBeGreaterThan(0); - - // Should return static models - models.forEach((model) => { - expect(model.provider).toBe('OpenAI'); - }); - } - }); - }); -}); diff --git a/__tests__/unit/runtime/code-validator.test.ts b/__tests__/unit/runtime/code-validator.test.ts deleted file mode 100644 index 01749903..00000000 --- a/__tests__/unit/runtime/code-validator.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { validateCode } from '~/lib/runtime/code-validator'; - -describe('code-validator', () => { - beforeEach(() => { - vi.spyOn(console, 'log').mockImplementation(() => undefined); - vi.spyOn(console, 'warn').mockImplementation(() => undefined); - vi.spyOn(console, 'debug').mockImplementation(() => undefined); - vi.spyOn(console, 'error').mockImplementation(() => undefined); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - describe('validateCode - General', () => { - it('should detect placeholder comments', () => { - const code = ` -function test() { - // rest of code here - return true; -} - `; - const result = validateCode('test.js', code); - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Code contains placeholder comment "// rest of code here"'); - }); - - it('should detect ellipsis placeholders', () => { - const code = ` -function test() { - const x = 1; - /* ... */ - return x; -} - `; - const result = validateCode('test.js', code); - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Code contains ellipsis placeholders'); - }); - - it('should warn about markdown code blocks', () => { - const code = '```javascript\nconst x = 1;\n```'; - const result = validateCode('test.js', code); - expect(result.warnings).toContain('File contains markdown code block syntax (```)'); - }); - - it('should pass valid code without placeholders', () => { - const code = ` -function add(a, b) { - return a + b; -} - `; - const result = validateCode('test.js', code); - expect(result.isValid).toBe(true); - expect(result.errors).toHaveLength(0); - }); - }); - - describe('validateCode - JavaScript/TypeScript', () => { - it('should detect mismatched braces', () => { - const code = ` -function test() { - if (true) { - console.log('test'); - // missing closing brace -} - `; - const result = validateCode('test.js', code); - expect(result.isValid).toBe(false); - expect(result.errors.some((e) => e.includes('Mismatched braces'))).toBe(true); - }); - - it('should detect mismatched parentheses', () => { - const code = 'function test(a, b {\n return a + b;\n}'; - const result = validateCode('test.js', code); - expect(result.isValid).toBe(false); - expect(result.errors.some((e) => e.includes('parentheses'))).toBe(true); - }); - - it('should detect mismatched brackets', () => { - const code = 'const arr = [1, 2, 3;'; - const result = validateCode('test.js', code); - expect(result.isValid).toBe(false); - expect(result.errors.some((e) => e.includes('brackets'))).toBe(true); - }); - - it('should pass valid JavaScript', () => { - const code = ` -function fibonacci(n) { - if (n <= 1) return n; - return fibonacci(n - 1) + fibonacci(n - 2); -} - -const result = fibonacci(10); -console.log(result); - `; - const result = validateCode('fib.js', code); - expect(result.isValid).toBe(true); - }); - - it('should pass valid TypeScript', () => { - const code = ` -interface User { - name: string; - age: number; -} - -function greet(user: User): string { - return \`Hello, \${user.name}!\`; -} - `; - const result = validateCode('greet.ts', code); - expect(result.isValid).toBe(true); - }); - - it('should detect mixed Python/JS syntax', () => { - const code = 'import from http'; - const result = validateCode('test.js', code); - expect(result.isValid).toBe(false); - expect(result.errors.some((e) => e.includes('mixing JavaScript and Python'))).toBe(true); - }); - - it('should ignore comments when counting braces', () => { - const code = ` -function test() { - // This is a comment with {braces} - /* Another comment { with } braces */ - return true; -} - `; - const result = validateCode('test.js', code); - expect(result.isValid).toBe(true); - }); - }); - - describe('validateCode - Python', () => { - it('should warn about tabs instead of spaces', () => { - const code = 'def test():\n\treturn True'; - const result = validateCode('test.py', code); - expect(result.warnings.some((w) => w.includes('tabs instead of spaces'))).toBe(true); - }); - - it('should detect JavaScript keywords in Python', () => { - const code = ` -const x = 5 -let y = 10 -var z = 15 - `; - const result = validateCode('test.py', code); - expect(result.isValid).toBe(false); - expect(result.errors.some((e) => e.includes('JavaScript variable declarations'))).toBe(true); - }); - - it('should warn about function keyword', () => { - const code = 'function greet(name):\n return f"Hello {name}"'; - const result = validateCode('test.py', code); - expect(result.warnings.some((w) => w.includes('function'))).toBe(true); - }); - - it('should pass valid Python code', () => { - const code = ` -def factorial(n): - if n <= 1: - return 1 - return n * factorial(n - 1) - -result = factorial(5) -print(result) - `; - const result = validateCode('factorial.py', code); - expect(result.isValid).toBe(true); - }); - - it('should handle Python classes', () => { - const code = ` -class User: - def __init__(self, name, age): - self.name = name - self.age = age - - def greet(self): - return f"Hello, {self.name}!" - `; - const result = validateCode('user.py', code); - expect(result.isValid).toBe(true); - }); - }); - - describe('validateCode - JSON', () => { - it('should detect invalid JSON', () => { - const code = '{ "name": "test", invalid }'; - const result = validateCode('config.json', code); - expect(result.isValid).toBe(false); - expect(result.errors.some((e) => e.includes('Invalid JSON'))).toBe(true); - }); - - it('should pass valid JSON', () => { - const code = `{ - "name": "test-project", - "version": "1.0.0", - "dependencies": { - "react": "^18.0.0" - } -}`; - const result = validateCode('package.json', code); - expect(result.isValid).toBe(true); - }); - - it('should detect trailing commas', () => { - const code = '{ "name": "test", }'; - const result = validateCode('config.json', code); - expect(result.isValid).toBe(false); - }); - }); - - describe('validateCode - Complex scenarios', () => { - it('should handle nested structures', () => { - const code = ` -const obj = { - nested: { - deep: { - value: [1, 2, [3, 4]] - } - } -}; - `; - const result = validateCode('nested.js', code); - expect(result.isValid).toBe(true); - }); - - it('should handle JSX syntax', () => { - const code = ` -export function Button({ children, onClick }) { - return ( - - ); -} - `; - const result = validateCode('Button.jsx', code); - expect(result.isValid).toBe(true); - }); - - it('should detect multiple errors', () => { - const code = ` -function test( { - const x = [1, 2, 3; - // rest of code here -} - `; - const result = validateCode('test.js', code); - expect(result.isValid).toBe(false); - expect(result.errors.length).toBeGreaterThan(1); - }); - }); -}); diff --git a/__tests__/unit/runtime/message-parser.test.ts b/__tests__/unit/runtime/message-parser.test.ts deleted file mode 100644 index 50aca263..00000000 --- a/__tests__/unit/runtime/message-parser.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; - -function cleanoutMarkdownSyntax(content: string) { - const codeBlockRegex = /^\s*```[\w-]*\s*\n?([\s\S]*?)\n?\s*```\s*$/; - const match = content.match(codeBlockRegex); - - if (match) { - return match[1].trim(); - } - - const multilineCodeBlockRegex = /```[\w-]*\s*\n([\s\S]*?)```/g; - let cleaned = content.replace(multilineCodeBlockRegex, (_match, code) => code.trim()); - - const inlineCodeBlockRegex = /^```[\w-]*\s*\n?|```\s*$/gm; - cleaned = cleaned.replace(inlineCodeBlockRegex, ''); - - return cleaned.trim() !== content.trim() ? cleaned.trim() : content; -} - -describe('message-parser - cleanoutMarkdownSyntax', () => { - beforeEach(() => { - vi.spyOn(console, 'log').mockImplementation(() => { }); - vi.spyOn(console, 'error').mockImplementation(() => { }); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('Basic markdown removal', () => { - it('should remove markdown code block with language', () => { - const input = '```javascript\nconst x = 1;\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('const x = 1;'); - }); - - it('should remove markdown code block without language', () => { - const input = '```\nconst x = 1;\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('const x = 1;'); - }); - - it('should handle code with no markdown', () => { - const input = 'const x = 1;\nconsole.log(x);'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe(input); - }); - - it('should preserve newlines in code', () => { - const input = '```javascript\nfunction test() {\n return true;\n}\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('function test() {\n return true;\n}'); - }); - }); - - describe('Language identifiers', () => { - it('should handle typescript language', () => { - const input = '```typescript\ninterface User { name: string; }\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('interface User { name: string; }'); - }); - - it('should handle python language', () => { - const input = '```python\ndef hello():\n print("Hello")\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('def hello():\n print("Hello")'); - }); - - it('should handle language with hyphens', () => { - const input = '```type-script\nconst x: number = 1;\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('const x: number = 1;'); - }); - - it('should handle jsx/tsx', () => { - const input = '```tsx\nexport const Button = () => ;\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('export const Button = () => ;'); - }); - }); - - describe('Whitespace handling', () => { - it('should trim leading whitespace', () => { - const input = ' ```javascript\nconst x = 1;\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('const x = 1;'); - }); - - it('should trim trailing whitespace', () => { - const input = '```javascript\nconst x = 1;\n``` '; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('const x = 1;'); - }); - - it('should handle extra newlines', () => { - const input = '```javascript\n\nconst x = 1;\n\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('const x = 1;'); - }); - - it('should handle no newline after opening backticks', () => { - const input = '```javascriptconst x = 1;\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toContain('x = 1;'); - }); - }); - - describe('Complex scenarios', () => { - it('should handle multiline code', () => { - const input = `\`\`\`javascript -function fibonacci(n) { - if (n <= 1) return n; - return fibonacci(n - 1) + fibonacci(n - 2); -} -\`\`\``; - const result = cleanoutMarkdownSyntax(input); - expect(result).toContain('function fibonacci'); - expect(result).toContain('return fibonacci'); - }); - - it('should handle code with backticks inside', () => { - const input = '```javascript\nconst str = `template ${var}`;\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('const str = `template ${var}`;'); - }); - - it('should handle nested structures', () => { - const input = `\`\`\`typescript -const obj = { - nested: { - value: [1, 2, 3] - } -}; -\`\`\``; - const result = cleanoutMarkdownSyntax(input); - expect(result).toContain('const obj = {'); - expect(result).toContain('nested:'); - }); - - it('should handle incomplete markdown (no closing backticks)', () => { - const input = '```javascript\nconst x = 1;'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('const x = 1;'); - }); - - it('should handle incomplete markdown (no opening backticks)', () => { - const input = 'const x = 1;\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe('const x = 1;'); - }); - }); - - describe('Multiple code blocks', () => { - it('should handle multiple code blocks in sequence', () => { - const input = `\`\`\`javascript -const a = 1; -\`\`\` - -\`\`\`python -b = 2 -\`\`\``; - const result = cleanoutMarkdownSyntax(input); - expect(result).toContain('const a = 1;'); - expect(result).toContain('b = 2'); - }); - - it('should clean multiple inline code blocks', () => { - const input = 'Some text ```js\ncode1\n``` more text ```py\ncode2\n``` end'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toContain('code1'); - expect(result).toContain('code2'); - }); - }); - - describe('Edge cases', () => { - it('should handle empty code block', () => { - const input = '```javascript\n\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe(''); - }); - - it('should handle just backticks', () => { - const input = '```\n```'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe(''); - }); - - it('should preserve code with no markdown at all', () => { - const input = 'function test() { return 42; }'; - const result = cleanoutMarkdownSyntax(input); - expect(result).toBe(input); - }); - - it('should handle very long code blocks', () => { - const code = 'const x = 1;\n'.repeat(100); - const input = `\`\`\`javascript\n${code}\`\`\``; - const result = cleanoutMarkdownSyntax(input); - expect(result.split('\n').length).toBe(100); - }); - }); - - describe('Real-world scenarios', () => { - it('should clean AI-generated React component', () => { - const input = `\`\`\`tsx -import React from 'react'; - -export function Button({ children, onClick }: ButtonProps) { - return ( - - ); -} -\`\`\``; - const result = cleanoutMarkdownSyntax(input); - expect(result).toContain('import React'); - expect(result).toContain('export function Button'); - expect(result).toContain(' - )} - - {showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'} - - - -
- {/* Interface Mode Toggle */} -
- -
- - {/* Developer Mode Toggle */} -
- -
- - {/* Avatar and Dropdown */} -
- -
- - {/* Close Button */} - -
- - - {/* Content */} -
- - {showTabManagement ? ( - - ) : activeTab ? ( - getTabComponent(activeTab) - ) : useSearchInterface ? ( - { - // Handle setting changes from search interface - console.log('Setting changed:', settingId, value); - - // You could dispatch to stores here if needed - }} - /> - ) : ( - - - {(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => ( - - handleTabClick(tab.id as TabType)} - isActive={activeTab === tab.id} - hasUpdate={getTabUpdateStatus(tab.id)} - statusMessage={getStatusMessage(tab.id)} - description={TAB_DESCRIPTIONS[tab.id]} - isLoading={loadingTab === tab.id} - className="h-full relative" - > - {BETA_TABS.has(tab.id) && } - - - ))} - - - )} - -
- - - - - - - - ); -}; diff --git a/app/components/@settings/core/ControlPanelDialog/ControlPanelDialog.tsx b/app/components/@settings/core/ControlPanelDialog/ControlPanelDialog.tsx deleted file mode 100644 index 5b4905eb..00000000 --- a/app/components/@settings/core/ControlPanelDialog/ControlPanelDialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { motion } from 'framer-motion'; -import * as RadixDialog from '@radix-ui/react-dialog'; -import { classNames } from '~/utils/classNames'; -import type { TabType } from '~/components/@settings/core/types'; -import { ControlPanelSidebar } from './components/ControlPanelSidebar'; -import { ControlPanelContent } from './components/ControlPanelContent'; -import { useControlPanelDialog } from './hooks/useControlPanelDialog'; - -interface ControlPanelDialogProps { - isOpen: boolean; - onClose: () => void; - initialTab?: TabType; -} - -export function ControlPanelDialog({ isOpen, onClose, initialTab = 'settings' }: ControlPanelDialogProps) { - const { activeTab, setActiveTab, visibleTabs } = useControlPanelDialog(initialTab); - - return ( - - - - - - -
- - - {/* Close button */} - - - - - {/* Sidebar */} - - - {/* Main Content */} - - - -
-
-
- ); -} diff --git a/app/components/@settings/core/ControlPanelDialog/components/ControlPanelContent.tsx b/app/components/@settings/core/ControlPanelDialog/components/ControlPanelContent.tsx deleted file mode 100644 index 63531f94..00000000 --- a/app/components/@settings/core/ControlPanelDialog/components/ControlPanelContent.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Suspense, lazy } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { classNames } from '~/utils/classNames'; -import { TAB_LABELS } from '~/components/@settings/core/constants'; -import type { TabType } from '~/components/@settings/core/types'; -import { TextShimmer } from '~/components/ui/text-shimmer'; - -const ProfileTab = lazy(() => - import('~/components/@settings/tabs/profile/ProfileTab').then((module) => ({ default: module.default })), -); -const SettingsTab = lazy(() => - import('~/components/@settings/tabs/settings/SettingsTab').then((module) => ({ default: module.default })), -); -const NotificationsTab = lazy(() => - import('~/components/@settings/tabs/notifications/NotificationsTab').then((module) => ({ default: module.default })), -); -const FeaturesTab = lazy(() => - import('~/components/@settings/tabs/features/FeaturesTab').then((module) => ({ default: module.default })), -); -const DataTab = lazy(() => - import('~/components/@settings/tabs/data/DataTab').then((module) => ({ default: module.DataTab })), -); -const CloudProvidersTab = lazy(() => - import('~/components/@settings/tabs/providers/cloud/CloudProvidersTab').then((module) => ({ - default: module.default, - })), -); -const LocalProvidersTab = lazy(() => - import('~/components/@settings/tabs/providers/local/LocalProvidersTab').then((module) => ({ - default: module.default, - })), -); -const ServiceStatusTab = lazy(() => - import('~/components/@settings/tabs/providers/status/ServiceStatusTab').then((module) => ({ - default: module.default, - })), -); -const ConnectionsTab = lazy(() => - import('~/components/@settings/tabs/connections/ConnectionsTab').then((module) => ({ default: module.default })), -); -const DebugTab = lazy(() => - import('~/components/@settings/tabs/debug/DebugTab').then((module) => ({ default: module.default })), -); -const UpdateTab = lazy(() => - import('~/components/@settings/tabs/update/UpdateTab').then((module) => ({ default: module.default })), -); -const ApiKeysTab = lazy(() => - import('~/components/@settings/tabs/api-keys/APIKeysTab').then((module) => ({ default: module.default })), -); - -interface ControlPanelContentProps { - activeTab: TabType; -} - -function LoadingFallback() { - return ( -
-
-
- Loading... -
-
- ); -} - -function TabContent({ tab }: { tab: TabType }) { - switch (tab) { - case 'profile': - return ; - case 'settings': - return ; - case 'notifications': - return ; - case 'features': - return ; - case 'data': - return ; - case 'cloud-providers': - return ; - case 'local-providers': - return ; - case 'service-status': - return ; - case 'connection': - return ; - case 'debug': - return ; - case 'update': - return ; - case 'api-keys': - return ; - default: - return ( -
-
-
-

Tab not found

-
-
- ); - } -} - -export function ControlPanelContent({ activeTab }: ControlPanelContentProps) { - return ( -
- {/* Header */} -
-
-

- {TAB_LABELS[activeTab]} -

-
-
- - {/* Content */} -
- - - }> - - - - -
-
- ); -} diff --git a/app/components/@settings/core/ControlPanelDialog/components/ControlPanelSidebar.tsx b/app/components/@settings/core/ControlPanelDialog/components/ControlPanelSidebar.tsx deleted file mode 100644 index 2b61f14e..00000000 --- a/app/components/@settings/core/ControlPanelDialog/components/ControlPanelSidebar.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { classNames } from '~/utils/classNames'; -import { TAB_ICONS, TAB_LABELS } from '~/components/@settings/core/constants'; -import type { TabType, TabVisibilityConfig } from '~/components/@settings/core/types'; - -interface ControlPanelSidebarProps { - activeTab: TabType; - onTabChange: (tab: TabType) => void; - tabs: TabVisibilityConfig[]; -} - -export function ControlPanelSidebar({ activeTab, onTabChange, tabs }: ControlPanelSidebarProps) { - // Group tabs into primary and secondary sections - const primaryTabs = tabs.slice(0, 8); // First 8 tabs as primary - const secondaryTabs = tabs.slice(8); // Rest as secondary - - const renderTabButton = (tab: TabVisibilityConfig) => { - const isActive = activeTab === tab.id; - const iconClass = TAB_ICONS[tab.id]; - const label = TAB_LABELS[tab.id]; - - return ( -
  • - -
  • - ); - }; - - return ( - - ); -} diff --git a/app/components/@settings/core/ControlPanelDialog/hooks/useControlPanelDialog.ts b/app/components/@settings/core/ControlPanelDialog/hooks/useControlPanelDialog.ts deleted file mode 100644 index 74bd7cbf..00000000 --- a/app/components/@settings/core/ControlPanelDialog/hooks/useControlPanelDialog.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useState, useEffect, useMemo } from 'react'; -import { useStore } from '@nanostores/react'; -import { tabConfigurationStore, developerModeStore } from '~/lib/stores/settings'; -import type { TabType, TabVisibilityConfig } from '~/components/@settings/core/types'; - -export function useControlPanelDialog(initialTab: TabType = 'settings') { - const [activeTab, setActiveTab] = useState(initialTab); - const tabConfiguration = useStore(tabConfigurationStore); - const isDeveloperMode = useStore(developerModeStore); - - // Get visible tabs based on current configuration - const visibleTabs = useMemo(() => { - const currentWindow = isDeveloperMode ? 'developer' : 'user'; - const tabsArray = currentWindow === 'developer' ? tabConfiguration.developerTabs : tabConfiguration.userTabs; - - return tabsArray - .filter((tab: TabVisibilityConfig) => tab.visible) - .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => a.order - b.order); - }, [tabConfiguration, isDeveloperMode]); - - // Ensure active tab is valid when configuration changes - useEffect(() => { - if (!visibleTabs.find((tab: TabVisibilityConfig) => tab.id === activeTab)) { - const firstVisibleTab = visibleTabs[0]; - - if (firstVisibleTab) { - setActiveTab(firstVisibleTab.id); - } - } - }, [visibleTabs, activeTab]); - - // Reset to initial tab when dialog opens - - useEffect(() => { - if (visibleTabs.find((tab: TabVisibilityConfig) => tab.id === initialTab)) { - setActiveTab(initialTab); - } - }, [initialTab, visibleTabs]); - - return { - activeTab, - setActiveTab, - visibleTabs, - isDeveloperMode, - }; -} diff --git a/app/components/@settings/core/ControlPanelDialog/index.ts b/app/components/@settings/core/ControlPanelDialog/index.ts deleted file mode 100644 index 77f1a56a..00000000 --- a/app/components/@settings/core/ControlPanelDialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ControlPanelDialog } from './ControlPanelDialog'; diff --git a/app/components/@settings/core/constants.ts b/app/components/@settings/core/constants.ts deleted file mode 100644 index f342ad10..00000000 --- a/app/components/@settings/core/constants.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { TabType } from './types'; - -export const TAB_ICONS: Record = { - profile: 'i-ph:user-circle-fill', - settings: 'i-ph:gear-six-fill', - notifications: 'i-ph:bell-fill', - features: 'i-ph:star-fill', - data: 'i-ph:database-fill', - 'cloud-providers': 'i-ph:cloud-fill', - 'local-providers': 'i-ph:desktop-fill', - 'service-status': 'i-ph:activity-bold', - connection: 'i-ph:wifi-high-fill', - debug: 'i-ph:bug-fill', - 'event-logs': 'i-ph:list-bullets-fill', - update: 'i-ph:arrow-clockwise-fill', - 'task-manager': 'i-ph:chart-line-fill', - 'tab-management': 'i-ph:squares-four-fill', - 'api-keys': 'i-ph:key-fill', -}; - -export const TAB_LABELS: Record = { - profile: 'Profile', - settings: 'Settings', - notifications: 'Notifications', - features: 'Features', - data: 'Data Management', - 'cloud-providers': 'Cloud Providers', - 'local-providers': 'Local Providers', - 'service-status': 'Service Status', - connection: 'Connection', - debug: 'Debug', - 'event-logs': 'Event Logs', - update: 'Updates', - 'task-manager': 'Task Manager', - 'tab-management': 'Tab Management', - 'api-keys': 'API Keys', -}; - -export const TAB_DESCRIPTIONS: Record = { - profile: 'Manage your profile and account settings', - settings: 'Configure application preferences', - notifications: 'View and manage your notifications', - features: 'Explore new and upcoming features', - data: 'Manage your data and storage', - 'cloud-providers': 'Configure cloud AI providers and models', - 'local-providers': 'Configure local AI providers and models', - 'service-status': 'Monitor cloud LLM service status', - connection: 'Check connection status and settings', - debug: 'Debug tools and system information', - 'event-logs': 'View system events and logs', - update: 'Check for updates and release notes', - 'task-manager': 'Monitor system resources and processes', - 'tab-management': 'Configure visible tabs and their order', - 'api-keys': 'Manage API keys for AI providers', -}; - -export const DEFAULT_TAB_CONFIG = [ - // User Window Tabs (Always visible by default) - { id: 'features', visible: true, window: 'user' as const, order: 0 }, - { id: 'data', visible: true, window: 'user' as const, order: 1 }, - { id: 'cloud-providers', visible: true, window: 'user' as const, order: 2 }, - { id: 'local-providers', visible: true, window: 'user' as const, order: 3 }, - { id: 'connection', visible: true, window: 'user' as const, order: 4 }, - { id: 'notifications', visible: true, window: 'user' as const, order: 5 }, - - // User Window Tabs (In dropdown, initially hidden) - { id: 'profile', visible: true, window: 'user' as const, order: 7 }, - { id: 'settings', visible: true, window: 'user' as const, order: 8 }, - { id: 'api-keys', visible: true, window: 'user' as const, order: 9 }, - { id: 'service-status', visible: true, window: 'user' as const, order: 11 }, - - // User Window Tabs (Hidden, controlled by TaskManagerTab) - { id: 'debug', visible: false, window: 'user' as const, order: 12 }, - { id: 'update', visible: true, window: 'user' as const, order: 13 }, - - // Developer Window Tabs (All visible by default) - { id: 'features', visible: true, window: 'developer' as const, order: 0 }, - { id: 'data', visible: true, window: 'developer' as const, order: 1 }, - { id: 'cloud-providers', visible: true, window: 'developer' as const, order: 2 }, - { id: 'local-providers', visible: true, window: 'developer' as const, order: 3 }, - { id: 'connection', visible: true, window: 'developer' as const, order: 4 }, - { id: 'notifications', visible: true, window: 'developer' as const, order: 5 }, - { id: 'profile', visible: true, window: 'developer' as const, order: 7 }, - { id: 'settings', visible: true, window: 'developer' as const, order: 8 }, - { id: 'api-keys', visible: true, window: 'developer' as const, order: 9 }, - { id: 'service-status', visible: true, window: 'developer' as const, order: 11 }, - { id: 'debug', visible: true, window: 'developer' as const, order: 12 }, - { id: 'update', visible: true, window: 'developer' as const, order: 13 }, -]; diff --git a/app/components/@settings/core/types.ts b/app/components/@settings/core/types.ts deleted file mode 100644 index 356d3836..00000000 --- a/app/components/@settings/core/types.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { ReactNode } from 'react'; - -export type SettingCategory = 'profile' | 'file_sharing' | 'connectivity' | 'system' | 'services' | 'preferences'; - -export type TabType = - | 'profile' - | 'settings' - | 'notifications' - | 'features' - | 'data' - | 'cloud-providers' - | 'local-providers' - | 'service-status' - | 'connection' - | 'debug' - | 'event-logs' - | 'update' - | 'task-manager' - | 'tab-management' - | 'api-keys'; - -export type WindowType = 'user' | 'developer'; - -export interface UserProfile { - nickname: any; - name: string; - email: string; - avatar?: string; - theme: 'light' | 'dark' | 'system'; - notifications: boolean; - password?: string; - bio?: string; - language: string; - timezone: string; -} - -export interface SettingItem { - id: TabType; - label: string; - icon: string; - category: SettingCategory; - description?: string; - component: () => ReactNode; - badge?: string; - keywords?: string[]; -} - -export interface TabVisibilityConfig { - id: TabType; - visible: boolean; - window: WindowType; - order: number; - isExtraDevTab?: boolean; - locked?: boolean; -} - -export interface DevTabConfig extends TabVisibilityConfig { - window: 'developer'; -} - -export interface UserTabConfig extends TabVisibilityConfig { - window: 'user'; -} - -export interface TabWindowConfig { - userTabs: UserTabConfig[]; - developerTabs: DevTabConfig[]; -} - -export const TAB_LABELS: Record = { - profile: 'Profile', - settings: 'Settings', - notifications: 'Notifications', - features: 'Features', - data: 'Data Management', - 'cloud-providers': 'Cloud Providers', - 'local-providers': 'Local Providers', - 'service-status': 'Service Status', - connection: 'Connections', - debug: 'Debug', - 'event-logs': 'Event Logs', - update: 'Updates', - 'task-manager': 'Task Manager', - 'tab-management': 'Tab Management', - 'api-keys': 'API Keys', -}; - -export const categoryLabels: Record = { - profile: 'Profile & Account', - file_sharing: 'File Sharing', - connectivity: 'Connectivity', - system: 'System', - services: 'Services', - preferences: 'Preferences', -}; - -export const categoryIcons: Record = { - profile: 'i-ph:user-circle', - file_sharing: 'i-ph:folder-simple', - connectivity: 'i-ph:wifi-high', - system: 'i-ph:gear', - services: 'i-ph:cube', - preferences: 'i-ph:sliders', -}; - -export interface Profile { - username?: string; - bio?: string; - avatar?: string; - preferences?: { - notifications?: boolean; - theme?: 'light' | 'dark' | 'system'; - language?: string; - timezone?: string; - }; -} diff --git a/app/components/@settings/index.ts b/app/components/@settings/index.ts deleted file mode 100644 index 3b98f1ce..00000000 --- a/app/components/@settings/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Core exports -export { ControlPanel } from './core/ControlPanel'; -export { ControlPanelDialog } from './core/ControlPanelDialog'; -export type { TabType, TabVisibilityConfig } from './core/types'; - -// Constants -export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constants'; - -// Shared components -export { TabTile } from './shared/components/TabTile'; -export { TabManagement } from './shared/components/TabManagement'; - -// Utils -export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers'; -export * from './utils/animations'; diff --git a/app/components/@settings/shared/components/DraggableTabList.tsx b/app/components/@settings/shared/components/DraggableTabList.tsx deleted file mode 100644 index a0f0d37c..00000000 --- a/app/components/@settings/shared/components/DraggableTabList.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { useDrag, useDrop } from 'react-dnd'; -import { motion } from 'framer-motion'; -import { classNames } from '~/utils/classNames'; -import type { TabVisibilityConfig } from '~/components/@settings/core/types'; -import { TAB_LABELS } from '~/components/@settings/core/types'; -import { Switch } from '~/components/ui/Switch'; - -interface DraggableTabListProps { - tabs: TabVisibilityConfig[]; - onReorder: (tabs: TabVisibilityConfig[]) => void; - onWindowChange?: (tab: TabVisibilityConfig, window: 'user' | 'developer') => void; - onVisibilityChange?: (tab: TabVisibilityConfig, visible: boolean) => void; - showControls?: boolean; -} - -interface DraggableTabItemProps { - tab: TabVisibilityConfig; - index: number; - moveTab: (dragIndex: number, hoverIndex: number) => void; - showControls?: boolean; - onWindowChange?: (tab: TabVisibilityConfig, window: 'user' | 'developer') => void; - onVisibilityChange?: (tab: TabVisibilityConfig, visible: boolean) => void; -} - -interface DragItem { - type: string; - index: number; - id: string; -} - -const DraggableTabItem = ({ - tab, - index, - moveTab, - showControls, - onWindowChange, - onVisibilityChange, -}: DraggableTabItemProps) => { - const [{ isDragging }, dragRef] = useDrag({ - type: 'tab', - item: { type: 'tab', index, id: tab.id }, - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), - }); - - const [, dropRef] = useDrop({ - accept: 'tab', - hover: (item: DragItem, monitor) => { - if (!monitor.isOver({ shallow: true })) { - return; - } - - if (item.index === index) { - return; - } - - if (item.id === tab.id) { - return; - } - - moveTab(item.index, index); - item.index = index; - }, - }); - - const ref = (node: HTMLDivElement | null) => { - dragRef(node); - dropRef(node); - }; - - return ( - -
    -
    -
    -
    -
    -
    {TAB_LABELS[tab.id]}
    - {showControls && ( -
    - Order: {tab.order}, Window: {tab.window} -
    - )} -
    -
    - {showControls && !tab.locked && ( -
    -
    - onVisibilityChange?.(tab, checked)} - className="data-[state=checked]:bg-blue-500" - aria-label={`Toggle ${TAB_LABELS[tab.id]} visibility`} - /> - -
    -
    - - onWindowChange?.(tab, checked ? 'developer' : 'user')} - className="data-[state=checked]:bg-blue-500" - aria-label={`Toggle ${TAB_LABELS[tab.id]} window assignment`} - /> - -
    -
    - )} - - ); -}; - -export const DraggableTabList = ({ - tabs, - onReorder, - onWindowChange, - onVisibilityChange, - showControls = false, -}: DraggableTabListProps) => { - const moveTab = (dragIndex: number, hoverIndex: number) => { - const items = Array.from(tabs); - const [reorderedItem] = items.splice(dragIndex, 1); - items.splice(hoverIndex, 0, reorderedItem); - - // Update order numbers based on position - const reorderedTabs = items.map((tab, index) => ({ - ...tab, - order: index + 1, - })); - - onReorder(reorderedTabs); - }; - - return ( -
    - {tabs.map((tab, index) => ( - - ))} -
    - ); -}; diff --git a/app/components/@settings/shared/components/SearchInput.tsx b/app/components/@settings/shared/components/SearchInput.tsx deleted file mode 100644 index 4daaa430..00000000 --- a/app/components/@settings/shared/components/SearchInput.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { classNames } from '~/utils/classNames'; - -interface SearchInputProps { - onQueryChange: (query: string) => void; - placeholder?: string; - autoFocus?: boolean; - className?: string; -} - -export const SearchInput: React.FC = ({ - onQueryChange, - placeholder = 'Search settings...', - autoFocus = true, - className, -}) => { - const [query, setQuery] = useState(''); - const inputRef = useRef(null); - - useEffect(() => { - if (autoFocus && inputRef.current) { - inputRef.current.focus(); - } - }, [autoFocus]); - - useEffect(() => { - onQueryChange(query); - }, [query, onQueryChange]); - - const handleInputChange = (e: React.ChangeEvent) => { - setQuery(e.target.value); - }; - - const handleClear = () => { - setQuery(''); - - if (inputRef.current) { - inputRef.current.focus(); - } - }; - - return ( -
    -
    -
    -
    - - - - {query && ( - - )} -
    - ); -}; diff --git a/app/components/@settings/shared/components/SearchInterface.tsx b/app/components/@settings/shared/components/SearchInterface.tsx deleted file mode 100644 index de51d8a4..00000000 --- a/app/components/@settings/shared/components/SearchInterface.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { motion } from 'framer-motion'; -import { classNames } from '~/utils/classNames'; -import { SearchInput } from './SearchInput'; -import { SearchResults } from './SearchResults'; -import { toast } from 'react-toastify'; -import { - searchSettings, - updateSettingValue, - getRecentSettings, - type SearchableSetting, -} from '~/components/@settings/shared/utils/settingsSearch'; - -interface SearchInterfaceProps { - userProfile: any; // From the profile store - onSettingChange?: (settingId: string, value: any) => void; - className?: string; -} - -export const SearchInterface: React.FC = ({ - userProfile: _userProfile, - onSettingChange, - className, -}) => { - const [query, setQuery] = useState(''); - const [results, setResults] = useState([]); - const [selectedIndex, setSelectedIndex] = useState(-1); - const [isLoading, setIsLoading] = useState(false); - - // Initialize with recent settings - useEffect(() => { - const recent = getRecentSettings(); - setResults(recent.length > 0 ? recent : []); - }, []); - - // Handle search query changes - const handleQueryChange = useCallback((newQuery: string) => { - setQuery(newQuery); - setSelectedIndex(-1); - - if (newQuery.trim()) { - setIsLoading(true); - - // Debounce search for better performance - setTimeout(() => { - const searchResults = searchSettings(newQuery); - setResults(searchResults); - setIsLoading(false); - }, 150); - } else { - // Show recent settings when no query - const recent = getRecentSettings(); - setResults(recent.length > 0 ? recent : []); - setIsLoading(false); - } - }, []); - - // Handle setting changes - const handleSettingChange = useCallback( - (settingId: string, value: any) => { - updateSettingValue(settingId, value); - - // Call external handler if provided - onSettingChange?.(settingId, value); - - // Show success feedback - const setting = results.find((r) => r.id === settingId); - - if (setting) { - toast.success(`${setting.title} updated`); - } - }, - [results, onSettingChange], - ); - - // Handle action execution - const handleActionExecute = useCallback((setting: SearchableSetting) => { - if (setting.action) { - try { - const result = setting.action(); - - if (result instanceof Promise) { - result - .then(() => { - toast.success(`${setting.title} executed`); - }) - .catch((error) => { - console.error('Action failed:', error); - toast.error(`Failed to execute ${setting.title}`); - }); - } else { - toast.success(`${setting.title} executed`); - } - } catch (error) { - console.error('Action failed:', error); - toast.error(`Failed to execute ${setting.title}`); - } - } - }, []); - - // Keyboard navigation - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (results.length === 0) { - return; - } - - switch (e.key) { - case 'ArrowDown': - e.preventDefault(); - setSelectedIndex((prev) => (prev < results.length - 1 ? prev + 1 : 0)); - break; - case 'ArrowUp': - e.preventDefault(); - setSelectedIndex((prev) => (prev > 0 ? prev - 1 : results.length - 1)); - break; - case 'Enter': - e.preventDefault(); - - if (selectedIndex >= 0 && selectedIndex < results.length) { - const setting = results[selectedIndex]; - - if (setting.type === 'action') { - handleActionExecute(setting); - } else if (setting.type === 'toggle') { - handleSettingChange(setting.id, !setting.value); - } - } - - break; - case 'Escape': - setQuery(''); - setSelectedIndex(-1); - break; - } - }; - - document.addEventListener('keydown', handleKeyDown); - - return () => document.removeEventListener('keydown', handleKeyDown); - }, [results, selectedIndex, handleSettingChange, handleActionExecute]); - - return ( -
    - {/* Search Input */} -
    - -
    - - {/* Results */} - - {query && ( -
    -

    - {results.length} result{results.length !== 1 ? 's' : ''} for "{query}" -

    -
    - )} - - {!query && results.length > 0 && ( -
    -

    Recent Settings

    -
    - )} - - {isLoading ? ( -
    -
    -
    - ) : ( - - )} - - {!query && results.length === 0 && ( -
    -
    -
    -
    -

    Start typing to search settings

    -

    - Search by name, description, or category to find what you need. -

    -
    - )} - - - {/* Keyboard shortcuts hint */} - -
    - ↑↓ Navigate - ↵ Select - ⎋ Clear -
    -
    -
    - ); -}; diff --git a/app/components/@settings/shared/components/SearchResults.tsx b/app/components/@settings/shared/components/SearchResults.tsx deleted file mode 100644 index ef262b10..00000000 --- a/app/components/@settings/shared/components/SearchResults.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { classNames } from '~/utils/classNames'; -import { SettingResultItem } from './SettingResultItem'; -import type { SearchableSetting } from '~/components/@settings/shared/utils/settingsSearch'; - -interface SearchResultsProps { - results: SearchableSetting[]; - onSettingChange: (settingId: string, value: any) => void; - onActionExecute: (setting: SearchableSetting) => void; - selectedIndex?: number; - className?: string; -} - -export const SearchResults: React.FC = ({ - results, - onSettingChange, - onActionExecute, - selectedIndex = -1, - className, -}) => { - if (results.length === 0) { - return ( - -
    -
    -
    -

    No settings found

    -

    - Try adjusting your search terms or browse categories below. -

    - - ); - } - - return ( -
    - - {results.map((setting, index) => ( - - onSettingChange(settingId, value)} - onSelect={(settingId, value) => onSettingChange(settingId, value)} - onAction={onActionExecute} - isSelected={selectedIndex === index} - /> - - ))} - -
    - ); -}; diff --git a/app/components/@settings/shared/components/SettingResultItem.tsx b/app/components/@settings/shared/components/SettingResultItem.tsx deleted file mode 100644 index 88007863..00000000 --- a/app/components/@settings/shared/components/SettingResultItem.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import React from 'react'; -import { motion } from 'framer-motion'; -import { classNames } from '~/utils/classNames'; -import { Switch } from '~/components/ui/Switch'; -import type { SearchableSetting } from '~/components/@settings/shared/utils/settingsSearch'; - -interface SettingResultItemProps { - setting: SearchableSetting; - onToggle?: (settingId: string, value: boolean) => void; - onSelect?: (settingId: string, value: string) => void; - onAction?: (setting: SearchableSetting) => void; - isSelected?: boolean; - className?: string; -} - -export const SettingResultItem: React.FC = ({ - setting, - onToggle, - onSelect, - onAction, - isSelected = false, - className, -}) => { - const handleClick = () => { - if (setting.type === 'action' && onAction) { - onAction(setting); - } - }; - - const renderControl = () => { - switch (setting.type) { - case 'toggle': - return ( - onToggle?.(setting.id, checked)} /> - ); - - case 'select': - return ( - - ); - - case 'action': - return ( - - ); - - default: - return null; - } - }; - - const getSelectOptions = (settingId: string) => { - switch (settingId) { - case 'language': - return ( - <> - - - - - - - - - - - - ); - case 'timezone': - return ( - - ); - default: - return ; - } - }; - - const getCategoryColor = (category: string) => { - switch (category) { - case 'Preferences': - return 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'; - case 'Features': - return 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'; - case 'Actions': - return 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300'; - default: - return 'bg-gray-100 dark:bg-gray-900/30 text-gray-700 dark:text-gray-300'; - } - }; - - return ( - -
    - {/* Icon */} -
    -
    -
    - - {/* Content */} -
    -
    -

    - {setting.title} -

    - - {setting.category} - -
    -

    {setting.description}

    - {setting.keywords.length > 0 && ( -
    - {setting.keywords.slice(0, 3).map((keyword, index) => ( - - {keyword} - - ))} -
    - )} -
    -
    - - {/* Control */} -
    {renderControl()}
    - - ); -}; diff --git a/app/components/@settings/shared/components/SettingsCard.tsx b/app/components/@settings/shared/components/SettingsCard.tsx deleted file mode 100644 index ee9eb0a3..00000000 --- a/app/components/@settings/shared/components/SettingsCard.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react'; -import { motion } from 'framer-motion'; -import { classNames } from '~/utils/classNames'; - -interface SettingsCardProps { - children: React.ReactNode; - variant?: 'default' | 'elevated' | 'gradient' | 'compact'; - className?: string; - delay?: number; -} - -export const SettingsCard: React.FC = ({ children, variant = 'default', className, delay = 0 }) => { - const getVariantClasses = () => { - switch (variant) { - case 'elevated': - return classNames( - 'bg-white dark:bg-gray-800/50', - 'border border-[#E5E5E5] dark:border-[#2A2A2A]', - 'shadow-lg dark:shadow-xl', - 'hover:shadow-xl dark:hover:shadow-2xl', - 'hover:border-blue-200 dark:hover:border-blue-800/30', - 'rounded-xl', - 'backdrop-blur-sm', - ); - case 'gradient': - return classNames( - 'bg-gradient-to-br from-white to-gray-50/50 dark:from-[#0F0F0F] dark:to-[#999999]', - 'border border-[#E5E5E5] dark:border-[#2A2A2A]', - 'shadow-md dark:shadow-lg', - 'hover:shadow-lg dark:hover:shadow-xl', - 'hover:border-blue-300 dark:hover:border-blue-700/40', - 'rounded-2xl', - 'relative overflow-hidden', - 'before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-500/5 before:to-purple-500/5 before:opacity-0 hover:before:opacity-100 before:transition-opacity before:duration-300', - ); - case 'compact': - return classNames( - 'bg-white dark:bg-gray-800/50', - 'border border-[#E5E5E5] dark:border-[#999999]', - 'shadow-sm dark:shadow-none', - 'hover:shadow-md dark:hover:shadow-lg', - 'hover:border-blue-200 dark:hover:border-blue-800/20', - 'rounded-lg', - ); - default: - return classNames( - 'bg-white dark:bg-gray-800/50', - 'border border-[#E5E5E5] dark:border-[#999999]', - 'shadow-sm dark:shadow-none', - 'hover:shadow-md dark:hover:shadow-lg', - 'hover:border-blue-200 dark:hover:border-blue-800/20', - 'rounded-xl', - 'transition-all duration-300 ease-out', - ); - } - }; - - return ( - - {children} - - ); -}; - -interface SettingsSectionProps { - title: string; - description?: string; - icon?: string; - children: React.ReactNode; - className?: string; - delay?: number; -} - -export const SettingsSection: React.FC = ({ - title, - description, - icon, - children, - className, - delay = 0, -}) => { - return ( - -
    - {icon &&
    } -
    -

    {title}

    - {description &&

    {description}

    } -
    -
    - {children} - - ); -}; diff --git a/app/components/@settings/shared/components/SettingsPanel.tsx b/app/components/@settings/shared/components/SettingsPanel.tsx deleted file mode 100644 index 711b79cf..00000000 --- a/app/components/@settings/shared/components/SettingsPanel.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import React from 'react'; -import { motion } from 'framer-motion'; -import { classNames } from '~/utils/classNames'; - -interface SettingsPanelProps { - children: React.ReactNode; - variant?: 'section' | 'subsection' | 'compact' | 'highlight'; - className?: string; - delay?: number; -} - -export const SettingsPanel: React.FC = ({ - children, - variant = 'section', - className, - delay = 0, -}) => { - const getVariantClasses = () => { - switch (variant) { - case 'subsection': - return classNames( - 'bg-gray-50/50 dark:bg-gray-800/30', - 'border border-gray-200/60 dark:border-gray-700/40', - 'rounded-xl', - 'backdrop-blur-sm', - ); - case 'highlight': - return classNames( - 'bg-gradient-to-r from-blue-50/60 to-purple-50/40 dark:from-blue-950/20 dark:to-purple-950/10', - 'border border-blue-200/40 dark:border-blue-800/30', - 'rounded-xl', - 'backdrop-blur-sm', - ); - case 'compact': - return classNames( - 'bg-gray-50/30 dark:bg-gray-800/20', - 'border border-gray-200/40 dark:border-gray-700/30', - 'rounded-lg', - ); - default: // section - return classNames( - 'bg-white/60 dark:bg-gray-900/40', - 'border border-gray-200/50 dark:border-gray-700/30', - 'rounded-xl', - 'backdrop-blur-sm', - 'shadow-sm dark:shadow-none', - ); - } - }; - - return ( - - {children} - - ); -}; - -interface SettingsListProps { - children: React.ReactNode; - className?: string; - delay?: number; -} - -export const SettingsList: React.FC = ({ children, className, delay = 0 }) => { - return ( - - {children} - - ); -}; - -interface SettingsListItemProps { - children: React.ReactNode; - className?: string; - onClick?: () => void; - disabled?: boolean; -} - -export const SettingsListItem: React.FC = ({ - children, - className, - onClick, - disabled = false, -}) => { - return ( - - {children} - - ); -}; - -interface SettingsGroupProps { - title?: string; - description?: string; - icon?: string; - children: React.ReactNode; - className?: string; - delay?: number; - layout?: 'grid' | 'flex' | 'block'; - columns?: 1 | 2 | 3 | 4; -} - -export const SettingsGroup: React.FC = ({ - title, - description, - icon, - children, - className, - delay = 0, - layout = 'block', - columns = 1, -}) => { - const getLayoutClasses = () => { - switch (layout) { - case 'grid': - return `grid grid-cols-1 md:grid-cols-${columns} gap-6`; - case 'flex': - return 'flex flex-wrap gap-6'; - default: - return 'space-y-4'; - } - }; - - return ( - - {(title || description) && ( -
    - {icon &&
    } -
    - {title &&

    {title}

    } - {description &&

    {description}

    } -
    -
    - )} -
    {children}
    - - ); -}; diff --git a/app/components/@settings/shared/components/TabManagement.tsx b/app/components/@settings/shared/components/TabManagement.tsx deleted file mode 100644 index 4eb3580f..00000000 --- a/app/components/@settings/shared/components/TabManagement.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; -import { useStore } from '@nanostores/react'; -import { Switch } from '~/components/ui/Switch'; -import { classNames } from '~/utils/classNames'; -import { tabConfigurationStore } from '~/lib/stores/settings'; -import { TAB_LABELS } from '~/components/@settings/core/constants'; -import type { TabType } from '~/components/@settings/core/types'; -import { toast } from 'react-toastify'; -import { useSettingsStore } from '~/lib/stores/settings'; - -// Define tab icons mapping -const TAB_ICONS: Record = { - profile: 'i-ph:user-circle-fill', - settings: 'i-ph:gear-six-fill', - notifications: 'i-ph:bell-fill', - features: 'i-ph:star-fill', - data: 'i-ph:database-fill', - 'cloud-providers': 'i-ph:cloud-fill', - 'local-providers': 'i-ph:desktop-fill', - 'service-status': 'i-ph:activity-fill', - connection: 'i-ph:wifi-high-fill', - debug: 'i-ph:bug-fill', - 'event-logs': 'i-ph:list-bullets-fill', - update: 'i-ph:arrow-clockwise-fill', - 'task-manager': 'i-ph:chart-line-fill', - 'tab-management': 'i-ph:squares-four-fill', - 'api-keys': 'i-ph:key-fill', -}; - -// Define which tabs are default in user mode -const DEFAULT_USER_TABS: TabType[] = [ - 'features', - 'data', - 'cloud-providers', - 'api-keys', - 'local-providers', - 'connection', - 'notifications', - 'event-logs', -]; - -// Define which tabs can be added to user mode -const OPTIONAL_USER_TABS: TabType[] = ['profile', 'settings', 'task-manager', 'service-status', 'debug', 'update']; - -// All available tabs for user mode -const ALL_USER_TABS = [...DEFAULT_USER_TABS, ...OPTIONAL_USER_TABS]; - -// Define which tabs are beta -const BETA_TABS = new Set(['task-manager', 'service-status', 'update', 'local-providers']); - -// Beta label component -const BetaLabel = () => ( - BETA -); - -export const TabManagement = () => { - const [searchQuery, setSearchQuery] = useState(''); - const tabConfiguration = useStore(tabConfigurationStore); - const { setSelectedTab } = useSettingsStore(); - - const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => { - // Get current tab configuration - const currentTab = tabConfiguration.userTabs.find((tab) => tab.id === tabId); - - // If tab doesn't exist in configuration, create it - if (!currentTab) { - const newTab = { - id: tabId, - visible: checked, - window: 'user' as const, - order: tabConfiguration.userTabs.length, - }; - - const updatedTabs = [...tabConfiguration.userTabs, newTab]; - - tabConfigurationStore.set({ - ...tabConfiguration, - userTabs: updatedTabs, - }); - - toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`); - - return; - } - - // Check if tab can be enabled in user mode - const canBeEnabled = DEFAULT_USER_TABS.includes(tabId) || OPTIONAL_USER_TABS.includes(tabId); - - if (!canBeEnabled && checked) { - toast.error('This tab cannot be enabled in user mode'); - return; - } - - // Update tab visibility - const updatedTabs = tabConfiguration.userTabs.map((tab) => { - if (tab.id === tabId) { - return { ...tab, visible: checked }; - } - - return tab; - }); - - // Update store - tabConfigurationStore.set({ - ...tabConfiguration, - userTabs: updatedTabs, - }); - - // Show success message - toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`); - }; - - // Create a map of existing tab configurations - const tabConfigMap = new Map(tabConfiguration.userTabs.map((tab) => [tab.id, tab])); - - // Generate the complete list of tabs, including those not in the configuration - const allTabs = ALL_USER_TABS.map((tabId) => { - return ( - tabConfigMap.get(tabId) || { - id: tabId, - visible: false, - window: 'user' as const, - order: -1, - } - ); - }); - - // Filter tabs based on search query - const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase())); - - useEffect(() => { - // Reset to first tab when component unmounts - return () => { - setSelectedTab('user'); // Reset to user tab when unmounting - }; - }, [setSelectedTab]); - - return ( -
    - - {/* Header */} -
    -
    -
    -
    -
    -
    -

    Tab Management

    -

    Configure visible tabs and their order

    -
    -
    - - {/* Search */} -
    -
    -
    -
    - setSearchQuery(e.target.value)} - placeholder="Search tabs..." - className={classNames( - 'w-full pl-10 pr-4 py-2 rounded-lg', - 'bg-codinit-elements-background-depth-2', - 'border border-codinit-elements-borderColor', - 'text-codinit-elements-textPrimary', - 'placeholder-codinit-elements-textTertiary', - 'focus:outline-none focus:ring-2 focus:ring-blue-500/30', - 'transition-all duration-200', - )} - /> -
    -
    - - {/* Tab Grid */} -
    - {/* Default Section Header */} - {filteredTabs.some((tab) => DEFAULT_USER_TABS.includes(tab.id)) && ( -
    -
    - Default Tabs -
    - )} - - {/* Default Tabs */} - {filteredTabs - .filter((tab) => DEFAULT_USER_TABS.includes(tab.id)) - .map((tab, index) => ( - - {/* Status Badges */} -
    - - Default - -
    - -
    - -
    -
    -
    - - -
    -
    -
    -
    -

    - {TAB_LABELS[tab.id]} -

    - {BETA_TABS.has(tab.id) && } -
    -

    - {tab.visible ? 'Visible in user mode' : 'Hidden in user mode'} -

    -
    - { - const isDisabled = - !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id); - - if (!isDisabled) { - handleTabVisibilityChange(tab.id, checked); - } - }} - className={classNames('data-[state=checked]:bg-blue-500 ml-4', { - 'opacity-50 pointer-events-none': - !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id), - })} - /> -
    -
    -
    - - - - ))} - - {/* Optional Section Header */} - {filteredTabs.some((tab) => OPTIONAL_USER_TABS.includes(tab.id)) && ( -
    -
    - Optional Tabs -
    - )} - - {/* Optional Tabs */} - {filteredTabs - .filter((tab) => OPTIONAL_USER_TABS.includes(tab.id)) - .map((tab, index) => ( - - {/* Status Badges */} -
    - - Optional - -
    - -
    - -
    -
    -
    - - -
    -
    -
    -
    -

    - {TAB_LABELS[tab.id]} -

    - {BETA_TABS.has(tab.id) && } -
    -

    - {tab.visible ? 'Visible in user mode' : 'Hidden in user mode'} -

    -
    - { - const isDisabled = - !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id); - - if (!isDisabled) { - handleTabVisibilityChange(tab.id, checked); - } - }} - className={classNames('data-[state=checked]:bg-blue-500 ml-4', { - 'opacity-50 pointer-events-none': - !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id), - })} - /> -
    -
    -
    - - - - ))} -
    -
    -
    - ); -}; diff --git a/app/components/@settings/shared/components/TabTile.tsx b/app/components/@settings/shared/components/TabTile.tsx deleted file mode 100644 index d9e653f5..00000000 --- a/app/components/@settings/shared/components/TabTile.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { motion } from 'framer-motion'; -import * as Tooltip from '@radix-ui/react-tooltip'; -import { classNames } from '~/utils/classNames'; -import type { TabVisibilityConfig } from '~/components/@settings/core/types'; -import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants'; - -interface TabTileProps { - tab: TabVisibilityConfig; - onClick?: () => void; - isActive?: boolean; - hasUpdate?: boolean; - statusMessage?: string; - description?: string; - isLoading?: boolean; - className?: string; - children?: React.ReactNode; -} - -export const TabTile: React.FC = ({ - tab, - onClick, - isActive, - hasUpdate, - statusMessage, - description, - isLoading, - className, - children, -}: TabTileProps) => { - return ( - - - - - {/* Background gradient overlay for active state */} - {isActive && ( -
    - )} - - {/* Main Content */} -
    - {/* Icon */} - - - - - {/* Label and Description */} -
    -

    - {TAB_LABELS[tab.id]} -

    - {description && ( -

    - {description} -

    - )} -
    -
    - - {/* Update Indicator with Enhanced Tooltip */} - {hasUpdate && ( - <> - - - -
    -
    - {statusMessage} -
    - - - - - )} - - {/* Children (e.g. Beta Label) */} - {children} - - - - - ); -}; diff --git a/app/components/@settings/shared/utils/settingsSearch.ts b/app/components/@settings/shared/utils/settingsSearch.ts deleted file mode 100644 index 55ebf60c..00000000 --- a/app/components/@settings/shared/utils/settingsSearch.ts +++ /dev/null @@ -1,295 +0,0 @@ -import type { UserProfile } from '~/components/@settings/core/types'; - -export interface SearchableSetting { - id: string; - title: string; - description: string; - category: string; - type: 'toggle' | 'select' | 'input' | 'action'; - value: any; - keywords: string[]; - icon: string; - lastModified?: Date; - path?: string; // For navigation if needed - action?: () => void | Promise; // For action type settings -} - -export interface RecentSetting extends SearchableSetting { - lastAccessed: Date; -} - -let settingsRegistry: SearchableSetting[] = []; -let recentSettings: RecentSetting[] = []; - -// Initialize the search index -export const initializeSearchIndex = (userProfile: UserProfile) => { - settingsRegistry = buildSettingsRegistry(userProfile); - loadRecentSettings(); -}; - -// Build the complete settings registry -const buildSettingsRegistry = (userProfile: UserProfile): SearchableSetting[] => { - const settings: SearchableSetting[] = [ - // Language & Localization - { - id: 'language', - title: 'Language', - description: 'Change the application language', - category: 'Preferences', - type: 'select', - value: userProfile.language || 'en', - keywords: ['locale', 'i18n', 'internationalization', 'language', 'lang'], - icon: 'i-ph:translate-fill', - path: 'settings', - }, - - // Notifications - { - id: 'notifications', - title: 'Notifications', - description: 'Enable or disable push notifications', - category: 'Preferences', - type: 'toggle', - value: userProfile.notifications ?? true, - keywords: ['alerts', 'push', 'notification', 'notify', 'alert'], - icon: 'i-ph:bell-fill', - path: 'settings', - }, - - // Timezone - { - id: 'timezone', - title: 'Timezone', - description: 'Set your local timezone', - category: 'Preferences', - type: 'select', - value: userProfile.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, - keywords: ['time', 'zone', 'local', 'region', 'clock'], - icon: 'i-ph:globe-fill', - path: 'settings', - }, - - // Feature Toggles - { - id: 'latestBranch', - title: 'Main Branch Updates', - description: 'Get the latest updates from the main branch', - category: 'Features', - type: 'toggle', - value: true, // Default value - keywords: ['updates', 'branch', 'main', 'latest', 'version', 'git'], - icon: 'i-ph:git-branch', - path: 'features', - }, - - { - id: 'autoSelectTemplate', - title: 'Auto Select Template', - description: 'Automatically select starter template', - category: 'Features', - type: 'toggle', - value: true, - keywords: ['template', 'auto', 'select', 'starter', 'project'], - icon: 'i-ph:selection', - path: 'features', - }, - - { - id: 'contextOptimization', - title: 'Context Optimization', - description: 'Optimize context for better responses', - category: 'Features', - type: 'toggle', - value: true, - keywords: ['context', 'optimize', 'ai', 'response', 'performance'], - icon: 'i-ph:brain', - path: 'features', - }, - - { - id: 'eventLogs', - title: 'Event Logging', - description: 'Enable detailed event logging and history', - category: 'Features', - type: 'toggle', - value: true, - keywords: ['logs', 'events', 'history', 'debug', 'tracking'], - icon: 'i-ph:list-bullets', - path: 'features', - }, - - // Quick Actions - { - id: 'resetSettings', - title: 'Reset All Settings', - description: 'Reset all settings to their default values', - category: 'Actions', - type: 'action', - value: null, - keywords: ['reset', 'default', 'clear', 'restore', 'factory'], - icon: 'i-ph:arrow-counter-clockwise', - action: () => { - // Implementation will be added - console.log('Reset settings action'); - }, - }, - - { - id: 'exportSettings', - title: 'Export Settings', - description: 'Export your settings to a file', - category: 'Actions', - type: 'action', - value: null, - keywords: ['export', 'backup', 'save', 'file', 'download'], - icon: 'i-ph:download', - action: () => { - // Implementation will be added - console.log('Export settings action'); - }, - }, - - { - id: 'importSettings', - title: 'Import Settings', - description: 'Import settings from a file', - category: 'Actions', - type: 'action', - value: null, - keywords: ['import', 'restore', 'load', 'file', 'upload'], - icon: 'i-ph:upload', - action: () => { - // Implementation will be added - console.log('Import settings action'); - }, - }, - ]; - - return settings; -}; - -// Simple search functionality (can be enhanced with Fuse.js later) -export const searchSettings = (query: string): SearchableSetting[] => { - if (!query.trim()) { - return getDefaultResults(); - } - - const searchTerm = query.toLowerCase(); - const results = settingsRegistry.filter((setting) => { - const searchableText = [setting.title, setting.description, ...setting.keywords, setting.category] - .join(' ') - .toLowerCase(); - - return searchableText.includes(searchTerm); - }); - - // Sort by relevance (title matches first, then description, then keywords) - return results.sort((a, b) => { - const aTitle = a.title.toLowerCase().includes(searchTerm); - const bTitle = b.title.toLowerCase().includes(searchTerm); - - if (aTitle && !bTitle) { - return -1; - } - - if (!aTitle && bTitle) { - return 1; - } - - return 0; - }); -}; - -// Get default results when no search query -const getDefaultResults = (): SearchableSetting[] => { - const recent = getRecentSettings(); - - if (recent.length > 0) { - return recent; - } - - // Return popular/quick access settings - return settingsRegistry.filter((setting) => - ['language', 'notifications', 'contextOptimization', 'autoSelectTemplate'].includes(setting.id), - ); -}; - -// Recent settings management -const RECENT_SETTINGS_KEY = 'codinit_recent_settings'; - -const loadRecentSettings = () => { - try { - const stored = localStorage.getItem(RECENT_SETTINGS_KEY); - - if (stored) { - const parsed = JSON.parse(stored); - recentSettings = parsed.map((item: any) => ({ - ...item, - lastAccessed: new Date(item.lastAccessed), - })); - } - } catch (error) { - console.warn('Failed to load recent settings:', error); - recentSettings = []; - } -}; - -const saveRecentSettings = () => { - try { - localStorage.setItem(RECENT_SETTINGS_KEY, JSON.stringify(recentSettings)); - } catch (error) { - console.warn('Failed to save recent settings:', error); - } -}; - -export const addToRecentSettings = (settingId: string) => { - const setting = settingsRegistry.find((s) => s.id === settingId); - - if (!setting) { - return; - } - - // Remove if already exists - recentSettings = recentSettings.filter((s) => s.id !== settingId); - - // Add to beginning - recentSettings.unshift({ - ...setting, - lastAccessed: new Date(), - }); - - // Keep only last 10 - recentSettings = recentSettings.slice(0, 10); - - saveRecentSettings(); -}; - -export const getRecentSettings = (): SearchableSetting[] => { - return recentSettings.slice(0, 5); // Return top 5 recent settings -}; - -// Update setting value -export const updateSettingValue = (settingId: string, newValue: any) => { - const setting = settingsRegistry.find((s) => s.id === settingId); - - if (setting) { - setting.value = newValue; - setting.lastModified = new Date(); - addToRecentSettings(settingId); - } -}; - -// Get setting by ID -export const getSettingById = (settingId: string): SearchableSetting | undefined => { - return settingsRegistry.find((s) => s.id === settingId); -}; - -// Get all settings (for debugging) -export const getAllSettings = (): SearchableSetting[] => { - return [...settingsRegistry]; -}; - -// Get settings by category -export const getSettingsByCategory = (category: string): SearchableSetting[] => { - return settingsRegistry.filter((s) => s.category === category); -}; diff --git a/app/components/@settings/tabs/api-keys/APIKeysTab.tsx b/app/components/@settings/tabs/api-keys/APIKeysTab.tsx deleted file mode 100644 index 9252247f..00000000 --- a/app/components/@settings/tabs/api-keys/APIKeysTab.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { useState, useEffect } from 'react'; -import { classNames } from '~/utils/classNames'; -import { toast } from 'react-toastify'; -import Cookies from 'js-cookie'; -import { SettingsSection } from '~/components/@settings/shared/components/SettingsCard'; -import { SettingsPanel, SettingsList, SettingsListItem } from '~/components/@settings/shared/components/SettingsPanel'; - -interface ProviderConfig { - name: string; - displayName: string; - icon: string; - apiKeyPlaceholder: string; - getApiKeyUrl?: string; - category: 'cloud' | 'local'; -} - -const PROVIDERS: ProviderConfig[] = [ - { - name: 'Anthropic', - displayName: 'Anthropic (Claude)', - icon: '/thirdparty/logos/anthropic.svg', - apiKeyPlaceholder: 'sk-ant-...', - getApiKeyUrl: 'https://console.anthropic.com/', - category: 'cloud', - }, - { - name: 'OpenAI', - displayName: 'OpenAI', - icon: '/thirdparty/logos/openai.svg', - apiKeyPlaceholder: 'sk-...', - getApiKeyUrl: 'https://platform.openai.com/api-keys', - category: 'cloud', - }, - { - name: 'Google', - displayName: 'Google (Gemini)', - icon: '/thirdparty/logos/gemini.svg', - apiKeyPlaceholder: 'AI...', - getApiKeyUrl: 'https://aistudio.google.com/app/apikey', - category: 'cloud', - }, - { - name: 'Groq', - displayName: 'Groq', - icon: '/thirdparty/logos/groq.svg', - apiKeyPlaceholder: 'gsk_...', - getApiKeyUrl: 'https://console.groq.com/keys', - category: 'cloud', - }, - { - name: 'xAI', - displayName: 'xAI (Grok)', - icon: '/thirdparty/logos/xai.svg', - apiKeyPlaceholder: 'xai-...', - getApiKeyUrl: 'https://console.x.ai/', - category: 'cloud', - }, - { - name: 'Deepseek', - displayName: 'Deepseek', - icon: '/thirdparty/logos/deepseek.svg', - apiKeyPlaceholder: 'sk-...', - getApiKeyUrl: 'https://platform.deepseek.com/api_keys', - category: 'cloud', - }, - { - name: 'Mistral', - displayName: 'Mistral AI', - icon: '/thirdparty/logos/mistral.svg', - apiKeyPlaceholder: '...', - getApiKeyUrl: 'https://console.mistral.ai/api-keys', - category: 'cloud', - }, - { - name: 'Moonshot', - displayName: 'Moonshot AI', - icon: '/thirdparty/logos/moonshot.svg', - apiKeyPlaceholder: 'sk-...', - getApiKeyUrl: 'https://platform.moonshot.cn/console/api-keys', - category: 'cloud', - }, - { - name: 'OpenRouter', - displayName: 'OpenRouter', - icon: '/thirdparty/logos/openrouter.svg', - apiKeyPlaceholder: 'sk-or-...', - getApiKeyUrl: 'https://openrouter.ai/keys', - category: 'cloud', - }, - { - name: 'Perplexity', - displayName: 'Perplexity', - icon: '/thirdparty/logos/perplexity.svg', - apiKeyPlaceholder: 'pplx-...', - getApiKeyUrl: 'https://www.perplexity.ai/settings/api', - category: 'cloud', - }, - { - name: 'Together', - displayName: 'Together AI', - icon: '/thirdparty/logos/togetherai.svg', - apiKeyPlaceholder: '...', - getApiKeyUrl: 'https://api.together.xyz/settings/api-keys', - category: 'cloud', - }, - { - name: 'HuggingFace', - displayName: 'Hugging Face', - icon: '/thirdparty/logos/huggingface.svg', - apiKeyPlaceholder: 'hf_...', - getApiKeyUrl: 'https://huggingface.co/settings/tokens', - category: 'cloud', - }, - { - name: 'Hyperbolic', - displayName: 'Hyperbolic', - icon: '/thirdparty/logos/hyperbolic.svg', - apiKeyPlaceholder: '...', - getApiKeyUrl: 'https://app.hyperbolic.xyz/settings', - category: 'cloud', - }, - { - name: 'AmazonBedrock', - displayName: 'Amazon Bedrock', - icon: '/thirdparty/logos/bedrock.svg', - apiKeyPlaceholder: 'Access Key ID', - getApiKeyUrl: 'https://console.aws.amazon.com/iam/home#/security_credentials', - category: 'cloud', - }, - { - name: 'Github', - displayName: 'GitHub Models', - icon: '/thirdparty/logos/github.svg', - apiKeyPlaceholder: 'ghp_...', - getApiKeyUrl: 'https://github.com/settings/tokens', - category: 'cloud', - }, -]; - -export default function ApiKeysTab() { - const [apiKeys, setApiKeys] = useState>({}); - const [visibleKeys, setVisibleKeys] = useState>({}); - const [editingKeys, setEditingKeys] = useState>({}); - - useEffect(() => { - const savedKeys = Cookies.get('apiKeys'); - - if (savedKeys) { - try { - setApiKeys(JSON.parse(savedKeys)); - } catch (e) { - console.error('Failed to parse API keys from cookies:', e); - } - } - }, []); - - const handleSaveKey = (providerName: string, key: string) => { - const updatedKeys = { ...apiKeys, [providerName]: key }; - setApiKeys(updatedKeys); - Cookies.set('apiKeys', JSON.stringify(updatedKeys), { expires: 365 }); - setEditingKeys({ ...editingKeys, [providerName]: false }); - toast.success(`${providerName} API key saved`); - }; - - const handleDeleteKey = (providerName: string) => { - const updatedKeys = { ...apiKeys }; - delete updatedKeys[providerName]; - setApiKeys(updatedKeys); - Cookies.set('apiKeys', JSON.stringify(updatedKeys), { expires: 365 }); - toast.success(`${providerName} API key removed`); - }; - - const toggleVisibility = (providerName: string) => { - setVisibleKeys({ ...visibleKeys, [providerName]: !visibleKeys[providerName] }); - }; - - const toggleEditing = (providerName: string) => { - setEditingKeys({ ...editingKeys, [providerName]: !editingKeys[providerName] }); - }; - - const maskApiKey = (key: string): string => { - if (key.length <= 8) { - return '•'.repeat(key.length); - } - - return key.slice(0, 3) + '•'.repeat(key.length - 7) + key.slice(-4); - }; - - const cloudProviders = PROVIDERS.filter((p) => p.category === 'cloud'); - - return ( -
    - - -
    -
    -
    -
    -

    API Key Storage

    -

    - API keys are stored securely in browser cookies and are never sent to our servers. They are only used - to authenticate with the respective AI providers. -

    -
    -
    -
    - - - {cloudProviders.map((provider) => { - const hasKey = !!apiKeys[provider.name]; - const isVisible = visibleKeys[provider.name]; - const isEditing = editingKeys[provider.name]; - - return ( - -
    -
    - {provider.displayName} -
    -
    -
    -

    - {provider.displayName} -

    - {hasKey && !isEditing && ( - - Configured - - )} -
    - - {isEditing || !hasKey ? ( -
    - { - if (e.key === 'Enter') { - handleSaveKey(provider.name, e.currentTarget.value); - } - }} - onBlur={(e) => { - if (e.target.value) { - handleSaveKey(provider.name, e.target.value); - } else if (!hasKey) { - setEditingKeys({ ...editingKeys, [provider.name]: false }); - } - }} - /> - -
    - ) : ( -
    - - {maskApiKey(apiKeys[provider.name])} - - - -
    - )} - - {provider.getApiKeyUrl && ( - - Get API Key -
    - - )} -
    -
    - - ); - })} - - - -
    - ); -} diff --git a/app/components/@settings/tabs/connections/CloudflareConnection.tsx b/app/components/@settings/tabs/connections/CloudflareConnection.tsx deleted file mode 100644 index 369b47dc..00000000 --- a/app/components/@settings/tabs/connections/CloudflareConnection.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; -import { toast } from 'react-toastify'; -import { useStore } from '@nanostores/react'; -import { logStore } from '~/lib/stores/logs'; -import { classNames } from '~/utils/classNames'; -import { - cloudflareConnection, - isConnecting, - isFetchingStats, - updateCloudflareConnection, - fetchCloudflareStats, -} from '~/lib/stores/cloudflare'; - -export default function CloudflareConnection() { - const connection = useStore(cloudflareConnection); - const connecting = useStore(isConnecting); - const fetchingStats = useStore(isFetchingStats); - const [isProjectsExpanded, setIsProjectsExpanded] = useState(false); - - useEffect(() => { - const fetchProjects = async () => { - if (connection.user && connection.token && connection.accountId) { - await fetchCloudflareStats(connection.token, connection.accountId); - } - }; - fetchProjects(); - }, [connection.user, connection.token, connection.accountId]); - - const handleConnect = async (event: React.FormEvent) => { - event.preventDefault(); - isConnecting.set(true); - - try { - // First verify the token and account ID - const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${connection.accountId}`, { - headers: { - Authorization: `Bearer ${connection.token}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - throw new Error('Invalid token or account ID'); - } - - const accountData = (await response.json()) as any; - updateCloudflareConnection({ - user: accountData.result, - token: connection.token, - accountId: connection.accountId, - }); - - await fetchCloudflareStats(connection.token, connection.accountId); - toast.success('Successfully connected to Cloudflare'); - } catch (error) { - console.error('Auth error:', error); - logStore.logError('Failed to authenticate with Cloudflare', { error }); - toast.error('Failed to connect to Cloudflare'); - updateCloudflareConnection({ user: null, token: '', accountId: '' }); - } finally { - isConnecting.set(false); - } - }; - - const handleDisconnect = () => { - updateCloudflareConnection({ user: null, token: '', accountId: '' }); - toast.success('Disconnected from Cloudflare'); - }; - - return ( - -
    -
    -
    - -

    Cloudflare Connection

    -
    -
    - - {connection.user ? ( -
    -
    -
    - - -
    - Connected to Cloudflare -
    -
    -
    - -
    -
    - Account: {connection.user.name || 'Cloudflare Account'} -
    -
    - - {fetchingStats ? ( -
    -
    - Fetching Cloudflare Pages projects... -
    - ) : ( -
    - - {isProjectsExpanded && connection.stats?.projects && connection.stats.projects.length > 0 ? ( -
    - {connection.stats.projects.map((project) => ( - -
    -
    -
    -
    - {project.name} -
    -
    - {project.latest_deployment ? ( - <> - - {project.latest_deployment.url} - - - -
    - {new Date(project.latest_deployment.created_on).toLocaleDateString()} -
    - - ) : ( - No deployments yet - )} -
    -
    -
    - - ))} -
    - ) : isProjectsExpanded ? ( -
    -
    - No Pages projects found in your account -
    - ) : null} -
    - )} -
    - ) : ( -
    -
    - - updateCloudflareConnection({ ...connection, token: e.target.value })} - disabled={connecting} - placeholder="Enter your Cloudflare API token" - className={classNames( - 'w-full px-3 py-2 rounded-lg text-sm', - 'bg-[#F8F8F8] dark:bg-[#999999]', - 'border border-[#E5E5E5] dark:border-[#333333]', - 'text-codinit-elements-textPrimary placeholder-codinit-elements-textTertiary', - 'focus:outline-none focus:ring-1 focus:ring-codinit-elements-borderColorActive', - 'disabled:opacity-50', - )} - /> - -
    - -
    - - updateCloudflareConnection({ ...connection, accountId: e.target.value })} - disabled={connecting} - placeholder="Enter your Cloudflare Account ID" - className={classNames( - 'w-full px-3 py-2 rounded-lg text-sm', - 'bg-[#F8F8F8] dark:bg-[#999999]', - 'border border-[#E5E5E5] dark:border-[#333333]', - 'text-codinit-elements-textPrimary placeholder-codinit-elements-textTertiary', - 'focus:outline-none focus:ring-1 focus:ring-codinit-elements-borderColorActive', - 'disabled:opacity-50', - )} - /> -
    - - Find your Account ID -
    -
    {' '} - → Account Home → Account ID -
    -
    - - -
    - )} -
    -
    - ); -} diff --git a/app/components/@settings/tabs/connections/ConnectionDiagnostics.tsx b/app/components/@settings/tabs/connections/ConnectionDiagnostics.tsx deleted file mode 100644 index 39d8e64a..00000000 --- a/app/components/@settings/tabs/connections/ConnectionDiagnostics.tsx +++ /dev/null @@ -1,595 +0,0 @@ -import React, { useState } from 'react'; -import { toast } from 'react-toastify'; -import { Button } from '~/components/ui/Button'; -import { Badge } from '~/components/ui/Badge'; -import { classNames } from '~/utils/classNames'; -import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible'; -import { CodeBracketIcon, ChevronDownIcon } from '@heroicons/react/24/outline'; - -// Helper function to safely parse JSON -const safeJsonParse = (item: string | null) => { - if (!item) { - return null; - } - - try { - return JSON.parse(item); - } catch (e) { - console.error('Failed to parse JSON from localStorage:', e); - return null; - } -}; - -/** - * A diagnostics component to help troubleshoot connection issues - */ -export default function ConnectionDiagnostics() { - const [diagnosticResults, setDiagnosticResults] = useState(null); - const [isRunning, setIsRunning] = useState(false); - const [showDetails, setShowDetails] = useState(false); - - // Run diagnostics when requested - const runDiagnostics = async () => { - try { - setIsRunning(true); - setDiagnosticResults(null); - - // Check browser-side storage - const localStorageChecks = { - githubConnection: localStorage.getItem('github_connection'), - netlifyConnection: localStorage.getItem('netlify_connection'), - vercelConnection: localStorage.getItem('vercel_connection'), - supabaseConnection: localStorage.getItem('supabase_connection'), - }; - - // Get diagnostic data from server - const response = await fetch('/api/system/diagnostics'); - - if (!response.ok) { - throw new Error(`Diagnostics API error: ${response.status}`); - } - - const serverDiagnostics = await response.json(); - - // === GitHub Checks === - const githubConnectionParsed = safeJsonParse(localStorageChecks.githubConnection); - const githubToken = githubConnectionParsed?.token; - const githubAuthHeaders = { - ...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}), - 'Content-Type': 'application/json', - }; - console.log('Testing GitHub endpoints with token:', githubToken ? 'present' : 'missing'); - - const githubEndpoints = [ - { name: 'User', url: '/api/system/git-info?action=getUser' }, - { name: 'Repos', url: '/api/system/git-info?action=getRepos' }, - { name: 'Default', url: '/api/system/git-info' }, - ]; - const githubResults = await Promise.all( - githubEndpoints.map(async (endpoint) => { - try { - const resp = await fetch(endpoint.url, { headers: githubAuthHeaders }); - return { endpoint: endpoint.name, status: resp.status, ok: resp.ok }; - } catch (error) { - return { - endpoint: endpoint.name, - error: error instanceof Error ? error.message : String(error), - ok: false, - }; - } - }), - ); - - // === Netlify Checks === - const netlifyConnectionParsed = safeJsonParse(localStorageChecks.netlifyConnection); - const netlifyToken = netlifyConnectionParsed?.token; - let netlifyUserCheck = null; - - if (netlifyToken) { - try { - const netlifyResp = await fetch('https://api.netlify.com/api/v1/user', { - headers: { Authorization: `Bearer ${netlifyToken}` }, - }); - netlifyUserCheck = { status: netlifyResp.status, ok: netlifyResp.ok }; - } catch (error) { - netlifyUserCheck = { - error: error instanceof Error ? error.message : String(error), - ok: false, - }; - } - } - - // === Vercel Checks === - const vercelConnectionParsed = safeJsonParse(localStorageChecks.vercelConnection); - const vercelToken = vercelConnectionParsed?.token; - let vercelUserCheck = null; - - if (vercelToken) { - try { - const vercelResp = await fetch('https://api.vercel.com/v2/user', { - headers: { Authorization: `Bearer ${vercelToken}` }, - }); - vercelUserCheck = { status: vercelResp.status, ok: vercelResp.ok }; - } catch (error) { - vercelUserCheck = { - error: error instanceof Error ? error.message : String(error), - ok: false, - }; - } - } - - // === Supabase Checks === - const supabaseConnectionParsed = safeJsonParse(localStorageChecks.supabaseConnection); - const supabaseUrl = supabaseConnectionParsed?.projectUrl; - const supabaseAnonKey = supabaseConnectionParsed?.anonKey; - let supabaseCheck = null; - - if (supabaseUrl && supabaseAnonKey) { - supabaseCheck = { ok: true, status: 200, message: 'URL and Key present in localStorage' }; - } else { - supabaseCheck = { ok: false, message: 'URL or Key missing in localStorage' }; - } - - // Compile results - const results = { - timestamp: new Date().toISOString(), - localStorage: { - hasGithubConnection: Boolean(localStorageChecks.githubConnection), - hasNetlifyConnection: Boolean(localStorageChecks.netlifyConnection), - hasVercelConnection: Boolean(localStorageChecks.vercelConnection), - hasSupabaseConnection: Boolean(localStorageChecks.supabaseConnection), - githubConnectionParsed, - netlifyConnectionParsed, - vercelConnectionParsed, - supabaseConnectionParsed, - }, - apiEndpoints: { - github: githubResults, - netlify: netlifyUserCheck, - vercel: vercelUserCheck, - supabase: supabaseCheck, - }, - serverDiagnostics, - }; - - setDiagnosticResults(results); - - // Display simple results - if (results.localStorage.hasGithubConnection && results.apiEndpoints.github.some((r: { ok: boolean }) => !r.ok)) { - toast.error('GitHub API connections are failing. Try reconnecting.'); - } - - if (results.localStorage.hasNetlifyConnection && netlifyUserCheck && !netlifyUserCheck.ok) { - toast.error('Netlify API connection is failing. Try reconnecting.'); - } - - if (results.localStorage.hasVercelConnection && vercelUserCheck && !vercelUserCheck.ok) { - toast.error('Vercel API connection is failing. Try reconnecting.'); - } - - if (results.localStorage.hasSupabaseConnection && supabaseCheck && !supabaseCheck.ok) { - toast.warning('Supabase connection check failed or missing details. Verify settings.'); - } - - if ( - !results.localStorage.hasGithubConnection && - !results.localStorage.hasNetlifyConnection && - !results.localStorage.hasVercelConnection && - !results.localStorage.hasSupabaseConnection - ) { - toast.info('No connection data found in browser storage.'); - } - } catch (error) { - console.error('Diagnostics error:', error); - toast.error('Error running diagnostics'); - setDiagnosticResults({ error: error instanceof Error ? error.message : String(error) }); - } finally { - setIsRunning(false); - } - }; - - // Helper to reset GitHub connection - const resetGitHubConnection = () => { - try { - localStorage.removeItem('github_connection'); - document.cookie = 'githubToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - document.cookie = 'githubUsername=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - document.cookie = 'git:github.com=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - toast.success('GitHub connection data cleared. Please refresh the page and reconnect.'); - setDiagnosticResults(null); - } catch (error) { - console.error('Error clearing GitHub data:', error); - toast.error('Failed to clear GitHub connection data'); - } - }; - - // Helper to reset Netlify connection - const resetNetlifyConnection = () => { - try { - localStorage.removeItem('netlify_connection'); - document.cookie = 'netlifyToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - toast.success('Netlify connection data cleared. Please refresh the page and reconnect.'); - setDiagnosticResults(null); - } catch (error) { - console.error('Error clearing Netlify data:', error); - toast.error('Failed to clear Netlify connection data'); - } - }; - - // Helper to reset Vercel connection - const resetVercelConnection = () => { - try { - localStorage.removeItem('vercel_connection'); - toast.success('Vercel connection data cleared. Please refresh the page and reconnect.'); - setDiagnosticResults(null); - } catch (error) { - console.error('Error clearing Vercel data:', error); - toast.error('Failed to clear Vercel connection data'); - } - }; - - // Helper to reset Supabase connection - const resetSupabaseConnection = () => { - try { - localStorage.removeItem('supabase_connection'); - toast.success('Supabase connection data cleared. Please refresh the page and reconnect.'); - setDiagnosticResults(null); - } catch (error) { - console.error('Error clearing Supabase data:', error); - toast.error('Failed to clear Supabase connection data'); - } - }; - - return ( -
    - {/* Connection Status Cards */} -
    - {/* GitHub Connection Card */} -
    -
    -
    -
    - GitHub Connection -
    -
    - {diagnosticResults ? ( - <> -
    - - {diagnosticResults.localStorage.hasGithubConnection ? 'Connected' : 'Not Connected'} - -
    - {diagnosticResults.localStorage.hasGithubConnection && ( - <> -
    -
    - User: {diagnosticResults.localStorage.githubConnectionParsed?.user?.login || 'N/A'} -
    -
    -
    - API Status:{' '} - r.ok) - ? 'default' - : 'destructive' - } - className="ml-1" - > - {diagnosticResults.apiEndpoints.github.every((r: { ok: boolean }) => r.ok) ? 'OK' : 'Failed'} - -
    - - )} - {!diagnosticResults.localStorage.hasGithubConnection && ( - - )} - - ) : ( -
    -
    -
    - Run diagnostics to check connection status -
    -
    - )} -
    - - {/* Netlify Connection Card */} -
    -
    -
    -
    - Netlify Connection -
    -
    - {diagnosticResults ? ( - <> -
    - - {diagnosticResults.localStorage.hasNetlifyConnection ? 'Connected' : 'Not Connected'} - -
    - {diagnosticResults.localStorage.hasNetlifyConnection && ( - <> -
    -
    - User:{' '} - {diagnosticResults.localStorage.netlifyConnectionParsed?.user?.full_name || - diagnosticResults.localStorage.netlifyConnectionParsed?.user?.email || - 'N/A'} -
    -
    -
    - API Status:{' '} - - {diagnosticResults.apiEndpoints.netlify?.ok ? 'OK' : 'Failed'} - -
    - - )} - {!diagnosticResults.localStorage.hasNetlifyConnection && ( - - )} - - ) : ( -
    -
    -
    - Run diagnostics to check connection status -
    -
    - )} -
    - - {/* Vercel Connection Card */} -
    -
    -
    -
    - Vercel Connection -
    -
    - {diagnosticResults ? ( - <> -
    - - {diagnosticResults.localStorage.hasVercelConnection ? 'Connected' : 'Not Connected'} - -
    - {diagnosticResults.localStorage.hasVercelConnection && ( - <> -
    -
    - User:{' '} - {diagnosticResults.localStorage.vercelConnectionParsed?.user?.username || - diagnosticResults.localStorage.vercelConnectionParsed?.user?.user?.username || - 'N/A'} -
    -
    -
    - API Status:{' '} - - {diagnosticResults.apiEndpoints.vercel?.ok ? 'OK' : 'Failed'} - -
    - - )} - {!diagnosticResults.localStorage.hasVercelConnection && ( - - )} - - ) : ( -
    -
    -
    - Run diagnostics to check connection status -
    -
    - )} -
    - - {/* Supabase Connection Card */} -
    -
    -
    -
    - Supabase Connection -
    -
    - {diagnosticResults ? ( - <> -
    - - {diagnosticResults.localStorage.hasSupabaseConnection ? 'Configured' : 'Not Configured'} - -
    - {diagnosticResults.localStorage.hasSupabaseConnection && ( - <> -
    -
    - Project URL: {diagnosticResults.localStorage.supabaseConnectionParsed?.projectUrl || 'N/A'} -
    -
    -
    - Config Status:{' '} - - {diagnosticResults.apiEndpoints.supabase?.ok ? 'OK' : 'Check Failed'} - -
    - - )} - {!diagnosticResults.localStorage.hasSupabaseConnection && ( - - )} - - ) : ( -
    -
    -
    - Run diagnostics to check connection status -
    -
    - )} -
    -
    - - {/* Action Buttons */} -
    - - - - - - - - - -
    - - {/* Details Panel */} - {diagnosticResults && ( -
    - - -
    -
    - - - Diagnostic Details - -
    - -
    -
    - -
    -
    -                  {JSON.stringify(diagnosticResults, null, 2)}
    -                
    -
    -
    -
    -
    - )} -
    - ); -} diff --git a/app/components/@settings/tabs/connections/ConnectionsTab.tsx b/app/components/@settings/tabs/connections/ConnectionsTab.tsx deleted file mode 100644 index cc732060..00000000 --- a/app/components/@settings/tabs/connections/ConnectionsTab.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { motion } from 'framer-motion'; -import React, { Suspense, useState } from 'react'; -import ConnectionDiagnostics from './ConnectionDiagnostics'; -import { Button } from '~/components/ui/Button'; -import VercelConnection from './VercelConnection'; - -// Use React.lazy for dynamic imports -const GitHubConnection = React.lazy(() => import('./GithubConnection')); -const NetlifyConnection = React.lazy(() => import('./NetlifyConnection')); -const CloudflareConnection = React.lazy(() => import('./CloudflareConnection')); - -// Loading fallback component -const LoadingFallback = () => ( -
    -
    -
    - Loading connection... -
    -
    -); - -export default function ConnectionsTab() { - const [showDiagnostics, setShowDiagnostics] = useState(false); - - return ( -
    - {/* Header */} - -
    -
    -

    - Connection Settings -

    -
    - - -

    - Manage your external service connections and integrations -

    - - {/* Diagnostics Tool - Conditionally rendered */} - {showDiagnostics && } - -
    - }> - - - }> - - - }> - - - }> - - -
    - - {/* Additional help text */} -
    -

    - - Troubleshooting Tip: -

    -

    - If you're having trouble with connections, try using the troubleshooting tool at the top of this page. It can - help diagnose and fix common connection issues. -

    -

    For persistent issues:

    -
      -
    1. Check your browser console for errors
    2. -
    3. Verify that your tokens have the correct permissions
    4. -
    5. Try clearing your browser cache and cookies
    6. -
    7. Ensure your browser allows third-party cookies if using integrations
    8. -
    -
    -
    - ); -} diff --git a/app/components/@settings/tabs/connections/GithubConnection.tsx b/app/components/@settings/tabs/connections/GithubConnection.tsx deleted file mode 100644 index 99cee4ef..00000000 --- a/app/components/@settings/tabs/connections/GithubConnection.tsx +++ /dev/null @@ -1,987 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; -import { toast } from 'react-toastify'; -import { logStore } from '~/lib/stores/logs'; -import { classNames } from '~/utils/classNames'; -import Cookies from 'js-cookie'; -import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible'; -import { Button } from '~/components/ui/Button'; -import { TextShimmer } from '~/components/ui/text-shimmer'; - -interface GitHubUserResponse { - login: string; - avatar_url: string; - html_url: string; - name: string; - bio: string; - public_repos: number; - followers: number; - following: number; - created_at: string; - public_gists: number; -} - -interface GitHubRepoInfo { - name: string; - full_name: string; - html_url: string; - description: string; - stargazers_count: number; - forks_count: number; - default_branch: string; - updated_at: string; - languages_url: string; -} - -interface GitHubOrganization { - login: string; - avatar_url: string; - html_url: string; -} - -interface GitHubEvent { - id: string; - type: string; - repo: { - name: string; - }; - created_at: string; -} - -interface GitHubLanguageStats { - [language: string]: number; -} - -interface GitHubStats { - repos: GitHubRepoInfo[]; - recentActivity: GitHubEvent[]; - languages: GitHubLanguageStats; - totalGists: number; - publicRepos: number; - privateRepos: number; - stars: number; - forks: number; - followers: number; - publicGists: number; - privateGists: number; - lastUpdated: string; - - // Keep these for backward compatibility - totalStars?: number; - totalForks?: number; - organizations?: GitHubOrganization[]; -} - -interface GitHubConnection { - user: GitHubUserResponse | null; - token: string; - tokenType: 'classic' | 'fine-grained'; - stats?: GitHubStats; - rateLimit?: { - limit: number; - remaining: number; - reset: number; - }; -} - -// Add the GitHub logo SVG component -const GithubLogo = () => ( - - - -); - -export default function GitHubConnection() { - const [connection, setConnection] = useState({ - user: null, - token: '', - tokenType: 'classic', - }); - const [isLoading, setIsLoading] = useState(true); - const [isConnecting, setIsConnecting] = useState(false); - const [isFetchingStats, setIsFetchingStats] = useState(false); - const [isStatsExpanded, setIsStatsExpanded] = useState(false); - const tokenTypeRef = React.useRef<'classic' | 'fine-grained'>('classic'); - - const fetchGithubUser = async (token: string) => { - try { - console.log('Fetching GitHub user with token:', token.substring(0, 5) + '...'); - - // Use server-side API endpoint instead of direct GitHub API call - const response = await fetch(`/api/system/git-info?action=getUser`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, // Include token in headers for validation - }, - }); - - if (!response.ok) { - console.error('Error fetching GitHub user. Status:', response.status); - throw new Error(`Error: ${response.status}`); - } - - // Get rate limit information from headers - const rateLimit = { - limit: parseInt(response.headers.get('x-ratelimit-limit') || '0'), - remaining: parseInt(response.headers.get('x-ratelimit-remaining') || '0'), - reset: parseInt(response.headers.get('x-ratelimit-reset') || '0'), - }; - - const data = await response.json(); - console.log('GitHub user API response:', data); - - const { user } = data as { user: GitHubUserResponse }; - - // Validate that we received a user object - if (!user || !user.login) { - console.error('Invalid user data received:', user); - throw new Error('Invalid user data received'); - } - - // Use the response data - setConnection((prev) => ({ - ...prev, - user, - token, - tokenType: tokenTypeRef.current, - rateLimit, - })); - - // Set cookies for client-side access - Cookies.set('githubUsername', user.login); - Cookies.set('githubToken', token); - Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' })); - - // Store connection details in localStorage - localStorage.setItem( - 'github_connection', - JSON.stringify({ - user, - token, - tokenType: tokenTypeRef.current, - }), - ); - - logStore.logInfo('Connected to GitHub', { - type: 'system', - message: `Connected to GitHub as ${user.login}`, - }); - - // Fetch additional GitHub stats - fetchGitHubStats(token); - } catch (error) { - console.error('Failed to fetch GitHub user:', error); - logStore.logError(`GitHub authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { - type: 'system', - message: 'GitHub authentication failed', - }); - - toast.error(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - throw error; // Rethrow to allow handling in the calling function - } - }; - - const fetchGitHubStats = async (token: string) => { - setIsFetchingStats(true); - - try { - // Get the current user first to ensure we have the latest value - const userResponse = await fetch('https://api.github.com/user', { - headers: { - Authorization: `${connection.tokenType === 'classic' ? 'token' : 'Bearer'} ${token}`, - }, - }); - - if (!userResponse.ok) { - if (userResponse.status === 401) { - toast.error('Your GitHub token has expired. Please reconnect your account.'); - handleDisconnect(); - - return; - } - - throw new Error(`Failed to fetch user data: ${userResponse.statusText}`); - } - - const userData = (await userResponse.json()) as any; - - // Fetch repositories with pagination - let allRepos: any[] = []; - let page = 1; - let hasMore = true; - - while (hasMore) { - const reposResponse = await fetch(`https://api.github.com/user/repos?per_page=100&page=${page}`, { - headers: { - Authorization: `${connection.tokenType === 'classic' ? 'token' : 'Bearer'} ${token}`, - }, - }); - - if (!reposResponse.ok) { - throw new Error(`Failed to fetch repositories: ${reposResponse.statusText}`); - } - - const repos = (await reposResponse.json()) as any[]; - allRepos = [...allRepos, ...repos]; - - // Check if there are more pages - const linkHeader = reposResponse.headers.get('Link'); - hasMore = linkHeader?.includes('rel="next"') ?? false; - page++; - } - - // Calculate stats - const repoStats = calculateRepoStats(allRepos); - - // Fetch recent activity - const eventsResponse = await fetch(`https://api.github.com/users/${userData.login}/events?per_page=10`, { - headers: { - Authorization: `${connection.tokenType === 'classic' ? 'token' : 'Bearer'} ${token}`, - }, - }); - - if (!eventsResponse.ok) { - throw new Error(`Failed to fetch events: ${eventsResponse.statusText}`); - } - - const events = (await eventsResponse.json()) as any[]; - const recentActivity = events.slice(0, 5).map((event: any) => ({ - id: event.id, - type: event.type, - repo: event.repo.name, - created_at: event.created_at, - })); - - // Calculate total stars and forks - const totalStars = allRepos.reduce((sum: number, repo: any) => sum + repo.stargazers_count, 0); - const totalForks = allRepos.reduce((sum: number, repo: any) => sum + repo.forks_count, 0); - const privateRepos = allRepos.filter((repo: any) => repo.private).length; - - // Update the stats in the store - const stats: GitHubStats = { - repos: repoStats.repos, - recentActivity, - languages: repoStats.languages || {}, - totalGists: repoStats.totalGists || 0, - publicRepos: userData.public_repos || 0, - privateRepos: privateRepos || 0, - stars: totalStars || 0, - forks: totalForks || 0, - followers: userData.followers || 0, - publicGists: userData.public_gists || 0, - privateGists: userData.private_gists || 0, - lastUpdated: new Date().toISOString(), - - // For backward compatibility - totalStars: totalStars || 0, - totalForks: totalForks || 0, - organizations: [], - }; - - // Get the current user first to ensure we have the latest value - const currentConnection = JSON.parse(localStorage.getItem('github_connection') || '{}'); - const currentUser = currentConnection.user || connection.user; - - // Update connection with stats - const updatedConnection: GitHubConnection = { - user: currentUser, - token, - tokenType: connection.tokenType, - stats, - rateLimit: connection.rateLimit, - }; - - // Update localStorage - localStorage.setItem('github_connection', JSON.stringify(updatedConnection)); - - // Update state - setConnection(updatedConnection); - - toast.success('GitHub stats refreshed'); - } catch (error) { - console.error('Error fetching GitHub stats:', error); - toast.error(`Failed to fetch GitHub stats: ${error instanceof Error ? error.message : 'Unknown error'}`); - } finally { - setIsFetchingStats(false); - } - }; - - const calculateRepoStats = (repos: any[]) => { - const repoStats = { - repos: repos.map((repo: any) => ({ - name: repo.name, - full_name: repo.full_name, - html_url: repo.html_url, - description: repo.description, - stargazers_count: repo.stargazers_count, - forks_count: repo.forks_count, - default_branch: repo.default_branch, - updated_at: repo.updated_at, - languages_url: repo.languages_url, - })), - - languages: {} as Record, - totalGists: 0, - }; - - repos.forEach((repo: any) => { - fetch(repo.languages_url) - .then((response) => response.json()) - .then((languages: any) => { - const typedLanguages = languages as Record; - Object.keys(typedLanguages).forEach((language) => { - if (!repoStats.languages[language]) { - repoStats.languages[language] = 0; - } - - repoStats.languages[language] += 1; - }); - }); - }); - - return repoStats; - }; - - useEffect(() => { - const loadSavedConnection = async () => { - setIsLoading(true); - - const savedConnection = localStorage.getItem('github_connection'); - - if (savedConnection) { - try { - const parsed = JSON.parse(savedConnection); - - if (!parsed.tokenType) { - parsed.tokenType = 'classic'; - } - - // Update the ref with the parsed token type - tokenTypeRef.current = parsed.tokenType; - - // Set the connection - setConnection(parsed); - - // If we have a token but no stats or incomplete stats, fetch them - if ( - parsed.user && - parsed.token && - (!parsed.stats || !parsed.stats.repos || parsed.stats.repos.length === 0) - ) { - console.log('Fetching missing GitHub stats for saved connection'); - await fetchGitHubStats(parsed.token); - } - } catch (error) { - console.error('Error parsing saved GitHub connection:', error); - localStorage.removeItem('github_connection'); - } - } else { - // Check for environment variable token - const envToken = import.meta.env.VITE_GITHUB_ACCESS_TOKEN; - - if (envToken) { - // Check if token type is specified in environment variables - const envTokenType = import.meta.env.VITE_GITHUB_TOKEN_TYPE; - console.log('Environment token type:', envTokenType); - - const tokenType = - envTokenType === 'classic' || envTokenType === 'fine-grained' - ? (envTokenType as 'classic' | 'fine-grained') - : 'classic'; - - console.log('Using token type:', tokenType); - - // Update both the state and the ref - tokenTypeRef.current = tokenType; - setConnection((prev) => ({ - ...prev, - tokenType, - })); - - try { - // Fetch user data with the environment token - await fetchGithubUser(envToken); - } catch (error) { - console.error('Failed to connect with environment token:', error); - } - } - } - - setIsLoading(false); - }; - - loadSavedConnection(); - }, []); - - // Ensure cookies are updated when connection changes - useEffect(() => { - if (!connection) { - return; - } - - const token = connection.token; - const data = connection.user; - - if (token) { - Cookies.set('githubToken', token); - Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' })); - } - - if (data) { - Cookies.set('githubUsername', data.login); - } - }, [connection]); - - // Add function to update rate limits - const updateRateLimits = async (token: string) => { - try { - const response = await fetch('https://api.github.com/rate_limit', { - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/vnd.github.v3+json', - }, - }); - - if (response.ok) { - const rateLimit = { - limit: parseInt(response.headers.get('x-ratelimit-limit') || '0'), - remaining: parseInt(response.headers.get('x-ratelimit-remaining') || '0'), - reset: parseInt(response.headers.get('x-ratelimit-reset') || '0'), - }; - - setConnection((prev) => ({ - ...prev, - rateLimit, - })); - } - } catch (error) { - console.error('Failed to fetch rate limits:', error); - } - }; - - // Add effect to update rate limits periodically - useEffect(() => { - let interval: NodeJS.Timeout; - - if (connection.token && connection.user) { - updateRateLimits(connection.token); - interval = setInterval(() => updateRateLimits(connection.token), 60000); // Update every minute - } - - return () => { - if (interval) { - clearInterval(interval); - } - }; - }, [connection.token, connection.user]); - - if (isLoading || isConnecting || isFetchingStats) { - return ; - } - - const handleConnect = async (event: React.FormEvent) => { - event.preventDefault(); - setIsConnecting(true); - - try { - // Update the ref with the current state value before connecting - tokenTypeRef.current = connection.tokenType; - - /* - * Save token type to localStorage even before connecting - * This ensures the token type is persisted even if connection fails - */ - localStorage.setItem( - 'github_connection', - JSON.stringify({ - user: null, - token: connection.token, - tokenType: connection.tokenType, - }), - ); - - // Attempt to fetch the user info which validates the token - await fetchGithubUser(connection.token); - - toast.success('Connected to GitHub successfully'); - } catch (error) { - console.error('Failed to connect to GitHub:', error); - - // Reset connection state on failure - setConnection({ user: null, token: connection.token, tokenType: connection.tokenType }); - - toast.error(`Failed to connect to GitHub: ${error instanceof Error ? error.message : 'Unknown error'}`); - } finally { - setIsConnecting(false); - } - }; - - const handleDisconnect = () => { - localStorage.removeItem('github_connection'); - - // Remove all GitHub-related cookies - Cookies.remove('githubToken'); - Cookies.remove('githubUsername'); - Cookies.remove('git:github.com'); - - // Reset the token type ref - tokenTypeRef.current = 'classic'; - setConnection({ user: null, token: '', tokenType: 'classic' }); - toast.success('Disconnected from GitHub'); - }; - - return ( - -
    -
    -
    - -

    - GitHub Connection -

    -
    -
    - - {!connection.user && ( -
    -

    - - Tip: You can also set the{' '} - - VITE_GITHUB_ACCESS_TOKEN - {' '} - environment variable to connect automatically. -

    -

    - For fine-grained tokens, also set{' '} - - VITE_GITHUB_TOKEN_TYPE=fine-grained - -

    -
    - )} -
    -
    - - -
    - -
    - - setConnection((prev) => ({ ...prev, token: e.target.value }))} - disabled={isConnecting || !!connection.user} - placeholder={`Enter your GitHub ${ - connection.tokenType === 'classic' ? 'personal access token' : 'fine-grained token' - }`} - className={classNames( - 'w-full px-3 py-2 rounded-lg text-sm', - 'bg-[#F8F8F8] dark:bg-[#999999]', - 'border border-[#E5E5E5] dark:border-[#333333]', - 'text-codinit-elements-textPrimary placeholder-codinit-elements-textTertiary', - 'focus:outline-none focus:ring-1 focus:ring-codinit-elements-borderColorActive', - 'disabled:opacity-50', - )} - /> -
    - - Get your token -
    - - - - Required scopes:{' '} - {connection.tokenType === 'classic' - ? 'repo, read:org, read:user' - : 'Repository access, Organization access'} - -
    -
    -
    - -
    - {!connection.user ? ( - - ) : ( - <> -
    -
    - - -
    - Connected to GitHub - -
    -
    - - -
    -
    - - )} -
    - - {connection.user && connection.stats && ( -
    -
    - {connection.user.login} -
    -

    - {connection.user.name || connection.user.login} -

    -

    - {connection.user.login} -

    -
    -
    - - - -
    -
    -
    - GitHub Stats -
    -
    -
    - - -
    - {/* Languages Section */} -
    -

    Top Languages

    -
    - {Object.entries(connection.stats.languages) - .sort(([, a], [, b]) => b - a) - .slice(0, 5) - .map(([language]) => ( - - {language} - - ))} -
    -
    - - {/* Additional Stats */} -
    - {[ - { - label: 'Member Since', - value: new Date(connection.user.created_at).toLocaleDateString(), - }, - { - label: 'Public Gists', - value: connection.stats.publicGists, - }, - { - label: 'Organizations', - value: connection.stats.organizations ? connection.stats.organizations.length : 0, - }, - { - label: 'Languages', - value: Object.keys(connection.stats.languages).length, - }, - ].map((stat, index) => ( -
    - {stat.label} - {stat.value} -
    - ))} -
    - - {/* Repository Stats */} -
    -
    -
    -
    Repository Stats
    -
    - {[ - { - label: 'Public Repos', - value: connection.stats.publicRepos, - }, - { - label: 'Private Repos', - value: connection.stats.privateRepos, - }, - ].map((stat, index) => ( -
    - {stat.label} - - {stat.value} - -
    - ))} -
    -
    - -
    -
    - Contribution Stats -
    -
    - {[ - { - label: 'Stars', - value: connection.stats.stars || 0, - icon: 'i-ph:star', - iconColor: 'text-codinit-elements-icon-warning', - }, - { - label: 'Forks', - value: connection.stats.forks || 0, - icon: 'i-ph:git-fork', - iconColor: 'text-codinit-elements-icon-info', - }, - { - label: 'Followers', - value: connection.stats.followers || 0, - icon: 'i-ph:users', - iconColor: 'text-codinit-elements-icon-success', - }, - ].map((stat, index) => ( -
    - {stat.label} - -
    - {stat.value} - -
    - ))} -
    -
    - -
    -
    Gists
    -
    - {[ - { - label: 'Public', - value: connection.stats.publicGists, - }, - { - label: 'Private', - value: connection.stats.privateGists || 0, - }, - ].map((stat, index) => ( -
    - {stat.label} - - {stat.value} - -
    - ))} -
    -
    - -
    - - Last updated: {new Date(connection.stats.lastUpdated).toLocaleString()} - -
    -
    -
    - - {/* Repositories Section */} -
    -

    Recent Repositories

    -
    - {connection.stats.repos.map((repo) => ( - -
    - - - ); -} - -function LoadingSpinner() { - return ( -
    -
    -
    - Loading... -
    -
    - ); -} diff --git a/app/components/@settings/tabs/connections/NetlifyConnection.tsx b/app/components/@settings/tabs/connections/NetlifyConnection.tsx deleted file mode 100644 index 7c3d6010..00000000 --- a/app/components/@settings/tabs/connections/NetlifyConnection.tsx +++ /dev/null @@ -1,731 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { toast } from 'react-toastify'; -import { classNames } from '~/utils/classNames'; -import { useStore } from '@nanostores/react'; -import { netlifyConnection, updateNetlifyConnection, initializeNetlifyConnection } from '~/lib/stores/netlify'; -import type { NetlifySite, NetlifyDeploy, NetlifyBuild, NetlifyUser } from '~/types/netlify'; -import { - CloudIcon, - BuildingLibraryIcon, - ClockIcon, - CodeBracketIcon, - CheckCircleIcon, - XCircleIcon, - TrashIcon, - ArrowPathIcon, - LockClosedIcon, - LockOpenIcon, - RocketLaunchIcon, -} from '@heroicons/react/24/outline'; -import { Button } from '~/components/ui/Button'; -import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible'; -import { formatDistanceToNow } from 'date-fns'; -import { Badge } from '~/components/ui/Badge'; - -// Add the Netlify logo SVG component at the top of the file -const NetlifyLogo = () => ( - - - -); - -// Add new interface for site actions -interface SiteAction { - name: string; - icon: React.ComponentType; - action: (siteId: string) => Promise; - requiresConfirmation?: boolean; - variant?: 'default' | 'destructive' | 'outline'; -} - -export default function NetlifyConnection() { - const connection = useStore(netlifyConnection); - const [tokenInput, setTokenInput] = useState(''); - const [fetchingStats, setFetchingStats] = useState(false); - const [sites, setSites] = useState([]); - const [deploys, setDeploys] = useState([]); - const [builds, setBuilds] = useState([]); - const [deploymentCount, setDeploymentCount] = useState(0); - const [lastUpdated, setLastUpdated] = useState(''); - const [isStatsOpen, setIsStatsOpen] = useState(false); - const [activeSiteIndex, setActiveSiteIndex] = useState(0); - const [isActionLoading, setIsActionLoading] = useState(false); - const [isConnecting, setIsConnecting] = useState(false); - - // Add site actions - const siteActions: SiteAction[] = [ - { - name: 'Clear Cache', - icon: ArrowPathIcon, - action: async (siteId: string) => { - try { - const response = await fetch(`https://api.netlify.com/api/v1/sites/${siteId}/cache`, { - method: 'POST', - headers: { - Authorization: `Bearer ${connection.token}`, - }, - }); - - if (!response.ok) { - throw new Error('Failed to clear cache'); - } - - toast.success('Site cache cleared successfully'); - } catch (err: unknown) { - const error = err instanceof Error ? err.message : 'Unknown error'; - toast.error(`Failed to clear site cache: ${error}`); - } - }, - }, - { - name: 'Delete Site', - icon: TrashIcon, - action: async (siteId: string) => { - try { - const response = await fetch(`https://api.netlify.com/api/v1/sites/${siteId}`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${connection.token}`, - }, - }); - - if (!response.ok) { - throw new Error('Failed to delete site'); - } - - toast.success('Site deleted successfully'); - fetchNetlifyStats(connection.token); - } catch (err: unknown) { - const error = err instanceof Error ? err.message : 'Unknown error'; - toast.error(`Failed to delete site: ${error}`); - } - }, - requiresConfirmation: true, - variant: 'destructive', - }, - ]; - - // Add deploy management functions - const handleDeploy = async (siteId: string, deployId: string, action: 'lock' | 'unlock' | 'publish') => { - try { - setIsActionLoading(true); - - const endpoint = - action === 'publish' - ? `https://api.netlify.com/api/v1/sites/${siteId}/deploys/${deployId}/restore` - : `https://api.netlify.com/api/v1/deploys/${deployId}/${action}`; - - const response = await fetch(endpoint, { - method: 'POST', - headers: { - Authorization: `Bearer ${connection.token}`, - }, - }); - - if (!response.ok) { - throw new Error(`Failed to ${action} deploy`); - } - - toast.success(`Deploy ${action}ed successfully`); - fetchNetlifyStats(connection.token); - } catch (err: unknown) { - const error = err instanceof Error ? err.message : 'Unknown error'; - toast.error(`Failed to ${action} deploy: ${error}`); - } finally { - setIsActionLoading(false); - } - }; - - useEffect(() => { - // Initialize connection with environment token if available - initializeNetlifyConnection(); - }, []); - - useEffect(() => { - // Check if we have a connection with a token but no stats - if (connection.user && connection.token && (!connection.stats || !connection.stats.sites)) { - fetchNetlifyStats(connection.token); - } - - // Update local state from connection - if (connection.stats) { - setSites(connection.stats.sites || []); - setDeploys(connection.stats.deploys || []); - setBuilds(connection.stats.builds || []); - setDeploymentCount(connection.stats.deploys?.length || 0); - setLastUpdated(connection.stats.lastDeployTime || ''); - } - }, [connection]); - - const handleConnect = async () => { - if (!tokenInput) { - toast.error('Please enter a Netlify API token'); - return; - } - - setIsConnecting(true); - - try { - const response = await fetch('https://api.netlify.com/api/v1/user', { - headers: { - Authorization: `Bearer ${tokenInput}`, - }, - }); - - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - - const userData = (await response.json()) as NetlifyUser; - - // Update the connection store - updateNetlifyConnection({ - user: userData, - token: tokenInput, - }); - - toast.success('Connected to Netlify successfully'); - - // Fetch stats after successful connection - fetchNetlifyStats(tokenInput); - } catch (error) { - console.error('Error connecting to Netlify:', error); - toast.error(`Failed to connect to Netlify: ${error instanceof Error ? error.message : 'Unknown error'}`); - } finally { - setIsConnecting(false); - setTokenInput(''); - } - }; - - const handleDisconnect = () => { - // Clear from localStorage - localStorage.removeItem('netlify_connection'); - - // Remove cookies - document.cookie = 'netlifyToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - - // Update the store - updateNetlifyConnection({ user: null, token: '' }); - toast.success('Disconnected from Netlify'); - }; - - const fetchNetlifyStats = async (token: string) => { - setFetchingStats(true); - - try { - // Fetch sites - const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (!sitesResponse.ok) { - throw new Error(`Failed to fetch sites: ${sitesResponse.statusText}`); - } - - const sitesData = (await sitesResponse.json()) as NetlifySite[]; - setSites(sitesData); - - // Fetch recent deploys for the first site (if any) - let deploysData: NetlifyDeploy[] = []; - let buildsData: NetlifyBuild[] = []; - let lastDeployTime = ''; - - if (sitesData && sitesData.length > 0) { - const firstSite = sitesData[0]; - - // Fetch deploys - const deploysResponse = await fetch(`https://api.netlify.com/api/v1/sites/${firstSite.id}/deploys`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (deploysResponse.ok) { - deploysData = (await deploysResponse.json()) as NetlifyDeploy[]; - setDeploys(deploysData); - setDeploymentCount(deploysData.length); - - // Get the latest deploy time - if (deploysData.length > 0) { - lastDeployTime = deploysData[0].created_at; - setLastUpdated(lastDeployTime); - - // Fetch builds for the site - const buildsResponse = await fetch(`https://api.netlify.com/api/v1/sites/${firstSite.id}/builds`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (buildsResponse.ok) { - buildsData = (await buildsResponse.json()) as NetlifyBuild[]; - setBuilds(buildsData); - } - } - } - } - - // Update the stats in the store - updateNetlifyConnection({ - stats: { - sites: sitesData, - deploys: deploysData, - builds: buildsData, - lastDeployTime, - totalSites: sitesData.length, - }, - }); - - toast.success('Netlify stats updated'); - } catch (error) { - console.error('Error fetching Netlify stats:', error); - toast.error(`Failed to fetch Netlify stats: ${error instanceof Error ? error.message : 'Unknown error'}`); - } finally { - setFetchingStats(false); - } - }; - - const renderStats = () => { - if (!connection.user || !connection.stats) { - return null; - } - - return ( -
    - - -
    -
    -
    - - Netlify Stats - -
    -
    -
    - - -
    -
    - - - {connection.stats.totalSites} Sites - - - - {deploymentCount} Deployments - - {lastUpdated && ( - - - Updated {formatDistanceToNow(new Date(lastUpdated))} ago - - )} -
    - {sites.length > 0 && ( -
    -
    -
    -

    - - Your Sites -

    - -
    -
    - {sites.map((site, index) => ( -
    { - setActiveSiteIndex(index); - }} - > -
    -
    - - - {site.name} - -
    -
    - - {site.published_deploy?.state === 'ready' ? ( - - ) : ( - - )} - - {site.published_deploy?.state || 'Unknown'} - - -
    -
    - - - - {activeSiteIndex === index && ( - <> -
    -
    - {siteActions.map((action) => ( - - ))} -
    -
    - {site.published_deploy && ( -
    -
    - - - Published {formatDistanceToNow(new Date(site.published_deploy.published_at))} ago - -
    - {site.published_deploy.branch && ( -
    - - - Branch: {site.published_deploy.branch} - -
    - )} -
    - )} - - )} -
    - ))} -
    -
    - {activeSiteIndex !== -1 && deploys.length > 0 && ( -
    -
    -

    - - Recent Deployments -

    -
    -
    - {deploys.map((deploy) => ( -
    -
    -
    - - {deploy.state === 'ready' ? ( - - ) : deploy.state === 'error' ? ( - - ) : ( - - )} - - {deploy.state} - - -
    - - {formatDistanceToNow(new Date(deploy.created_at))} ago - -
    - {deploy.branch && ( -
    - - - Branch: {deploy.branch} - -
    - )} - {deploy.deploy_url && ( - - )} -
    - - {deploy.state === 'ready' ? ( - - ) : ( - - )} -
    -
    - ))} -
    -
    - )} - {activeSiteIndex !== -1 && builds.length > 0 && ( -
    -
    -

    - - Recent Builds -

    -
    -
    - {builds.map((build) => ( -
    -
    -
    - - {build.done && !build.error ? ( - - ) : build.error ? ( - - ) : ( - - )} - - {build.done ? (build.error ? 'Failed' : 'Completed') : 'In Progress'} - - -
    - - {formatDistanceToNow(new Date(build.created_at))} ago - -
    - {build.error && ( -
    - - Error: {build.error} -
    - )} -
    - ))} -
    -
    - )} -
    - )} -
    -
    - -
    - ); - }; - - return ( -
    -
    -
    -
    -
    - -
    -

    Netlify Connection

    -
    -
    - - {!connection.user ? ( -
    - - setTokenInput(e.target.value)} - placeholder="Enter your Netlify API token" - className={classNames( - 'w-full px-3 py-2 rounded-lg text-sm', - 'bg-[#F8F8F8] dark:bg-[#999999]', - 'border border-[#E5E5E5] dark:border-[#333333]', - 'text-codinit-elements-textPrimary placeholder-codinit-elements-textTertiary', - 'focus:outline-none focus:ring-1 focus:ring-codinit-elements-borderColorActive', - 'disabled:opacity-50', - )} - /> -
    - - Get your token -
    - -
    -
    - -
    -
    - ) : ( -
    -
    - - -
    - Connected to Netlify - -
    - {renderStats()} -
    - )} -
    -
    - ); -} diff --git a/app/components/@settings/tabs/connections/VercelConnection.tsx b/app/components/@settings/tabs/connections/VercelConnection.tsx deleted file mode 100644 index bec20f8a..00000000 --- a/app/components/@settings/tabs/connections/VercelConnection.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { motion } from 'framer-motion'; -import { toast } from 'react-toastify'; -import { useStore } from '@nanostores/react'; -import { logStore } from '~/lib/stores/logs'; -import { classNames } from '~/utils/classNames'; -import { - vercelConnection, - isConnecting, - isFetchingStats, - updateVercelConnection, - fetchVercelStats, -} from '~/lib/stores/vercel'; - -export default function VercelConnection() { - const connection = useStore(vercelConnection); - const connecting = useStore(isConnecting); - const fetchingStats = useStore(isFetchingStats); - const [isProjectsExpanded, setIsProjectsExpanded] = useState(false); - - useEffect(() => { - const fetchProjects = async () => { - if (connection.user && connection.token) { - await fetchVercelStats(connection.token); - } - }; - fetchProjects(); - }, [connection.user, connection.token]); - - const handleConnect = async (event: React.FormEvent) => { - event.preventDefault(); - isConnecting.set(true); - - try { - const response = await fetch('https://api.vercel.com/v2/user', { - headers: { - Authorization: `Bearer ${connection.token}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - throw new Error('Invalid token or unauthorized'); - } - - const userData = (await response.json()) as any; - updateVercelConnection({ - user: userData.user || userData, // Handle both possible structures - token: connection.token, - }); - - await fetchVercelStats(connection.token); - toast.success('Successfully connected to Vercel'); - } catch (error) { - console.error('Auth error:', error); - logStore.logError('Failed to authenticate with Vercel', { error }); - toast.error('Failed to connect to Vercel'); - updateVercelConnection({ user: null, token: '' }); - } finally { - isConnecting.set(false); - } - }; - - const handleDisconnect = () => { - updateVercelConnection({ user: null, token: '' }); - toast.success('Disconnected from Vercel'); - }; - - console.log('connection', connection); - - return ( - -
    -
    -
    - -

    Vercel Connection

    -
    -
    - - {!connection.user ? ( -
    -
    - - updateVercelConnection({ ...connection, token: e.target.value })} - disabled={connecting} - placeholder="Enter your Vercel personal access token" - className={classNames( - 'w-full px-3 py-2 rounded-lg text-sm', - 'bg-[#F8F8F8] dark:bg-[#1A1A1A]', - 'border border-[#E5E5E5] dark:border-[#333333]', - 'text-codinit-elements-textPrimary placeholder-codinit-elements-textTertiary', - 'focus:outline-none focus:ring-1 focus:ring-codinit-elements-borderColorActive', - 'disabled:opacity-50', - )} - /> - - - -
    - ) : ( -
    -
    -
    - - -
    - Connected to Vercel - -
    -
    - -
    - {/* Debug output */} -
    {JSON.stringify(connection.user, null, 2)}
    - - User Avatar -
    -

    - {connection.user?.username || connection.user?.user?.username || 'Vercel User'} -

    -

    - {connection.user?.email || connection.user?.user?.email || 'No email available'} -

    -
    -
    - - {fetchingStats ? ( -
    -
    - Fetching Vercel projects... -
    - ) : ( -
    - - {isProjectsExpanded && connection.stats?.projects?.length ? ( -
    - {connection.stats.projects.map((project) => ( - -
    -
    -
    -
    - {project.name} -
    -
    - {project.targets?.production?.alias && project.targets.production.alias.length > 0 ? ( - <> - a.endsWith('.vercel.app') && !a.includes('-projects.vercel.app')) || project.targets.production.alias[0]}`} - target="_blank" - rel="noopener noreferrer" - className="hover:text-codinit-elements-borderColorActive" - > - {project.targets.production.alias.find( - (a: string) => a.endsWith('.vercel.app') && !a.includes('-projects.vercel.app'), - ) || project.targets.production.alias[0]} - - - -
    - {new Date(project.createdAt).toLocaleDateString()} - - - ) : project.latestDeployments && project.latestDeployments.length > 0 ? ( - <> - - {project.latestDeployments[0].url} - - - -
    - {new Date(project.latestDeployments[0].created).toLocaleDateString()} - - - ) : null} -
    -
    - {project.framework && ( -
    - -
    - {project.framework} - -
    - )} -
    - - ))} -
    - ) : isProjectsExpanded ? ( -
    -
    - No projects found in your Vercel account -
    - ) : null} -
    - )} -
    - )} -
    - - ); -} diff --git a/app/components/@settings/tabs/connections/components/GitHubAuthDialog.tsx b/app/components/@settings/tabs/connections/components/GitHubAuthDialog.tsx deleted file mode 100644 index 147b35db..00000000 --- a/app/components/@settings/tabs/connections/components/GitHubAuthDialog.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React, { useState } from 'react'; -import * as Dialog from '@radix-ui/react-dialog'; -import { motion } from 'framer-motion'; -import { toast } from 'react-toastify'; -import Cookies from 'js-cookie'; -import type { GitHubUserResponse } from '~/types/GitHub'; - -interface GitHubAuthDialogProps { - isOpen: boolean; - onClose: () => void; -} - -export function GitHubAuthDialog({ isOpen, onClose }: GitHubAuthDialogProps) { - const [token, setToken] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); - const [tokenType, setTokenType] = useState<'classic' | 'fine-grained'>('classic'); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!token.trim()) { - return; - } - - setIsSubmitting(true); - - try { - const response = await fetch('https://api.github.com/user', { - headers: { - Accept: 'application/vnd.github.v3+json', - Authorization: `Bearer ${token}`, - }, - }); - - if (response.ok) { - const userData = (await response.json()) as GitHubUserResponse; - - // Save connection data - const connectionData = { - token, - tokenType, - user: { - login: userData.login, - avatar_url: userData.avatar_url, - name: userData.name || userData.login, - }, - connected_at: new Date().toISOString(), - }; - - localStorage.setItem('github_connection', JSON.stringify(connectionData)); - - // Set cookies for API requests - Cookies.set('githubToken', token); - Cookies.set('githubUsername', userData.login); - Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' })); - - toast.success(`Successfully connected as ${userData.login}`); - setToken(''); - onClose(); - } else { - if (response.status === 401) { - toast.error('Invalid GitHub token. Please check and try again.'); - } else { - toast.error(`GitHub API error: ${response.status} ${response.statusText}`); - } - } - } catch (error) { - console.error('Error connecting to GitHub:', error); - toast.error('Failed to connect to GitHub. Please try again.'); - } finally { - setIsSubmitting(false); - } - }; - - return ( - !open && onClose()}> - - -
    - - -
    - - Access Private Repositories - - - - To access private repositories, you need to connect your GitHub account by providing a personal access - token. - - -
    -

    Connect with GitHub Token

    - -
    -
    - - setToken(e.target.value)} - placeholder="ghp_xxxxxxxxxxxxxxxxxxxx" - className="w-full px-3 py-1.5 rounded-lg border border-codinit-elements-borderColor bg-codinit-elements-background-depth-2 text-codinit-elements-textPrimary placeholder-codinit-elements-textTertiary text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
    - Get your token at{' '} - - github.com/settings/tokens - -
    -
    - -
    - -
    - - -
    -
    - - -
    -
    - -
    -

    - - Accessing Private Repositories -

    -

    - Important things to know about accessing private repositories: -

    -
      -
    • You must be granted access to the repository by its owner
    • -
    • Your GitHub token must have the 'repo' scope
    • -
    • For organization repositories, you may need additional permissions
    • -
    • No token can give you access to repositories you don't have permission for
    • -
    -
    -
    - -
    - - - -
    -
    -
    -
    -
    -
    - ); -} diff --git a/app/components/@settings/tabs/connections/components/PushToGitHubDialog.tsx b/app/components/@settings/tabs/connections/components/PushToGitHubDialog.tsx deleted file mode 100644 index cde58d7c..00000000 --- a/app/components/@settings/tabs/connections/components/PushToGitHubDialog.tsx +++ /dev/null @@ -1,671 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { toast } from 'react-toastify'; -import { motion } from 'framer-motion'; -import { Octokit } from '@octokit/rest'; -import { Dialog, DialogRoot, DialogTitle, DialogClose } from '~/components/ui/Dialog'; -import { IconButton } from '~/components/ui/IconButton'; - -// Internal imports -import { getLocalStorage } from '~/lib/persistence'; -import { classNames } from '~/utils/classNames'; -import type { GitHubUserResponse } from '~/types/GitHub'; -import { logStore } from '~/lib/stores/logs'; -import { workbenchStore } from '~/lib/stores/workbench'; -import { extractRelativePath } from '~/utils/diff'; -import { formatSize } from '~/utils/formatSize'; -import type { FileMap, File } from '~/lib/stores/files'; - -// UI Components -import { Badge, EmptyState, StatusIndicator, SearchInput } from '~/components/ui'; - -interface PushToGitHubDialogProps { - isOpen: boolean; - onClose: () => void; - onPush: (repoName: string, username?: string, token?: string, isPrivate?: boolean) => Promise; -} - -interface GitHubRepo { - name: string; - full_name: string; - html_url: string; - description: string; - stargazers_count: number; - forks_count: number; - default_branch: string; - updated_at: string; - language: string; - private: boolean; -} - -export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDialogProps) { - const [repoName, setRepoName] = useState(''); - const [isPrivate, setIsPrivate] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [user, setUser] = useState(null); - const [recentRepos, setRecentRepos] = useState([]); - const [filteredRepos, setFilteredRepos] = useState([]); - const [repoSearchQuery, setRepoSearchQuery] = useState(''); - const [isFetchingRepos, setIsFetchingRepos] = useState(false); - const [showSuccessDialog, setShowSuccessDialog] = useState(false); - const [createdRepoUrl, setCreatedRepoUrl] = useState(''); - const [pushedFiles, setPushedFiles] = useState<{ path: string; size: number }[]>([]); - - // Load GitHub connection on mount - useEffect(() => { - if (isOpen) { - const connection = getLocalStorage('github_connection'); - - if (connection?.user && connection?.token) { - setUser(connection.user); - - // Only fetch if we have both user and token - if (connection.token.trim()) { - fetchRecentRepos(connection.token); - } - } - } - }, [isOpen]); - - /* - * Filter repositories based on search query - * const debouncedSetRepoSearchQuery = useDebouncedCallback((value: string) => setRepoSearchQuery(value), 300); - */ - - useEffect(() => { - if (recentRepos.length === 0) { - setFilteredRepos([]); - return; - } - - if (!repoSearchQuery.trim()) { - setFilteredRepos(recentRepos); - return; - } - - const query = repoSearchQuery.toLowerCase().trim(); - const filtered = recentRepos.filter( - (repo) => - repo.name.toLowerCase().includes(query) || - (repo.description && repo.description.toLowerCase().includes(query)) || - (repo.language && repo.language.toLowerCase().includes(query)), - ); - - setFilteredRepos(filtered); - }, [recentRepos, repoSearchQuery]); - - const fetchRecentRepos = useCallback(async (token: string) => { - if (!token) { - logStore.logError('No GitHub token available'); - toast.error('GitHub authentication required'); - - return; - } - - try { - setIsFetchingRepos(true); - console.log('Fetching GitHub repositories with token:', token.substring(0, 5) + '...'); - - // Fetch ALL repos by paginating through all pages - let allRepos: GitHubRepo[] = []; - let page = 1; - let hasMore = true; - - while (hasMore) { - const requestUrl = `https://api.github.com/user/repos?sort=updated&per_page=100&page=${page}&affiliation=owner,organization_member`; - const response = await fetch(requestUrl, { - headers: { - Accept: 'application/vnd.github.v3+json', - Authorization: `Bearer ${token.trim()}`, - }, - }); - - if (!response.ok) { - let errorData: { message?: string } = {}; - - try { - errorData = await response.json(); - console.error('Error response data:', errorData); - } catch (e) { - errorData = { message: 'Could not parse error response' }; - console.error('Could not parse error response:', e); - } - - if (response.status === 401) { - toast.error('GitHub token expired. Please reconnect your account.'); - - // Clear invalid token - const connection = getLocalStorage('github_connection'); - - if (connection) { - localStorage.removeItem('github_connection'); - setUser(null); - } - } else if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') { - // Rate limit exceeded - const resetTime = response.headers.get('x-ratelimit-reset'); - const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleTimeString() : 'soon'; - toast.error(`GitHub API rate limit exceeded. Limit resets at ${resetDate}`); - } else { - logStore.logError('Failed to fetch GitHub repositories', { - status: response.status, - statusText: response.statusText, - error: errorData, - }); - toast.error(`Failed to fetch repositories: ${errorData.message || response.statusText}`); - } - - return; - } - - try { - const repos = (await response.json()) as GitHubRepo[]; - allRepos = allRepos.concat(repos); - - if (repos.length < 100) { - hasMore = false; - } else { - page += 1; - } - } catch (parseError) { - console.error('Error parsing JSON response:', parseError); - logStore.logError('Failed to parse GitHub repositories response', { parseError }); - toast.error('Failed to parse repository data'); - setRecentRepos([]); - - return; - } - } - setRecentRepos(allRepos); - } catch (error) { - console.error('Exception while fetching GitHub repositories:', error); - logStore.logError('Failed to fetch GitHub repositories', { error }); - toast.error('Failed to fetch recent repositories'); - } finally { - setIsFetchingRepos(false); - } - }, []); - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - - const connection = getLocalStorage('github_connection'); - - if (!connection?.token || !connection?.user) { - toast.error('Please connect your GitHub account in Settings > Connections first'); - return; - } - - if (!repoName.trim()) { - toast.error('Repository name is required'); - return; - } - - setIsLoading(true); - - try { - // Check if repository exists first - const octokit = new Octokit({ auth: connection.token }); - - try { - const { data: existingRepo } = await octokit.repos.get({ - owner: connection.user.login, - repo: repoName, - }); - - // If we get here, the repo exists - let confirmMessage = `Repository "${repoName}" already exists. Do you want to update it? This will add or modify files in the repository.`; - - // Add visibility change warning if needed - if (existingRepo.private !== isPrivate) { - const visibilityChange = isPrivate - ? 'This will also change the repository from public to private.' - : 'This will also change the repository from private to public.'; - - confirmMessage += `\n\n${visibilityChange}`; - } - - const confirmOverwrite = window.confirm(confirmMessage); - - if (!confirmOverwrite) { - setIsLoading(false); - return; - } - } catch (error) { - // 404 means repo doesn't exist, which is what we want for new repos - if (error instanceof Error && 'status' in error && error.status !== 404) { - throw error; - } - } - - const repoUrl = await onPush(repoName, connection.user.login, connection.token, isPrivate); - setCreatedRepoUrl(repoUrl); - - const files = workbenchStore.files.get(); - const filesList = Object.entries(files as FileMap) - .filter(([, dirent]) => dirent?.type === 'file' && !dirent.isBinary) - .map(([path, dirent]) => ({ - path: extractRelativePath(path), - size: new TextEncoder().encode((dirent as File).content || '').length, - })); - - setPushedFiles(filesList); - setShowSuccessDialog(true); - } catch (error: any) { - console.error('Error pushing to GitHub:', error); - - let errorMessage = 'Failed to push to GitHub. Please try again.'; - - if (error.code === 'AUTH_REQUIRED') { - errorMessage = error.message; - } else if (error.code === 'AUTH_INVALID') { - errorMessage = error.message; - } else if (error.code === 'PERMISSION_DENIED') { - errorMessage = error.message; - } else if (error.code === 'INVALID_REPO_NAME') { - errorMessage = error.message; - } else if (error.code === 'REPO_EXISTS') { - errorMessage = error.message; - } else if (error.code === 'NO_FILES') { - errorMessage = error.message; - } else if (error.code === 'NO_VALID_FILES') { - errorMessage = error.message; - } else if (error.code === 'BRANCH_NOT_FOUND') { - errorMessage = error.message; - } else if (error.code === 'CONFLICT') { - errorMessage = error.message; - } else if (error.code === 'INVALID_DATA') { - errorMessage = error.message; - } else if (error.code === 'REPO_NOT_FOUND') { - errorMessage = error.message; - } else if (error.message && error.message.length < 200) { - errorMessage = error.message; - } - - toast.error(errorMessage); - } finally { - setIsLoading(false); - } - } - - const handleClose = () => { - setRepoName(''); - setIsPrivate(false); - setShowSuccessDialog(false); - setCreatedRepoUrl(''); - onClose(); - }; - - // Success Dialog - if (showSuccessDialog) { - return ( - !open && handleClose()}> - {isOpen && ( - -
    -
    -
    -
    -
    -
    -
    -

    - Successfully pushed to GitHub -

    -

    Your code is now available on GitHub

    -
    -
    - - - -
    - -
    -

    - - Repository URL -

    -
    - - {createdRepoUrl} - - { - navigator.clipboard.writeText(createdRepoUrl); - toast.success('URL copied to clipboard'); - }} - className="p-2 text-codinit-elements-textSecondary hover:text-codinit-elements-textPrimary bg-codinit-elements-background-depth-1 rounded-lg border border-codinit-elements-borderColor" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > -
    - -
    -
    - -
    -

    - - Pushed Files ({pushedFiles.length}) -

    -
    - {pushedFiles.map((file) => ( -
    - {file.path} - - {formatSize(file.size)} - -
    - ))} -
    -
    - -
    - -
    - View Repository - - { - navigator.clipboard.writeText(createdRepoUrl); - toast.success('URL copied to clipboard'); - }} - className="px-4 py-2 rounded-lg bg-codinit-elements-background-depth-3 text-codinit-elements-textSecondary hover:bg-codinit-elements-background-depth-4 text-sm inline-flex items-center gap-2 border border-codinit-elements-borderColor" - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > -
    - Copy URL - - - Close - -
    -
    -
    - )} -
    - ); - } - - if (!user) { - return ( - !open && handleClose()}> - {isOpen && ( - -
    - - - - -
    - -

    GitHub Connection Required

    -

    - To push your code to GitHub, you need to connect your GitHub account in Settings {'>'} Connections - first. -

    -
    - - Close - - -
    - Go to Settings - -
    -
    -
    - )} -
    - ); - } - - return ( - !open && handleClose()}> - {isOpen && ( - -
    -
    - -
    - -
    - Push to GitHub -

    - Push your code to a new or existing GitHub repository -

    -
    - - - -
    - -
    -
    - {user.login} -
    -
    -
    -
    -
    -

    {user.name || user.login}

    -

    @{user.login}

    -
    -
    - -
    -
    - -
    -
    - -
    - setRepoName(e.target.value)} - placeholder="my-awesome-project" - className="w-full pl-10 px-4 py-2 rounded-lg bg-codinit-elements-background-depth-3 border border-codinit-elements-borderColor text-codinit-elements-textPrimary placeholder-codinit-elements-textTertiary focus:outline-none focus:ring-2 focus:ring-blue-500" - required - /> -
    -
    - -
    -
    - - - {filteredRepos.length} of {recentRepos.length} - -
    - -
    - setRepoSearchQuery(e.target.value)} - onClear={() => setRepoSearchQuery('')} - className="bg-codinit-elements-background-depth-3 border border-codinit-elements-borderColor text-sm" - /> -
    - - {recentRepos.length === 0 && !isFetchingRepos ? ( - - ) : ( -
    - {filteredRepos.length === 0 && repoSearchQuery.trim() !== '' ? ( - - ) : ( - filteredRepos.map((repo) => ( - setRepoName(repo.name)} - className="w-full p-3 text-left rounded-lg bg-codinit-elements-background-depth-3 hover:bg-codinit-elements-background-depth-4 transition-colors group border border-codinit-elements-borderColor hover:border-blue-500/30" - whileHover={{ scale: 1.01 }} - whileTap={{ scale: 0.99 }} - > -
    -
    -
    - - {repo.name} - -
    - {repo.private && ( - - Private - - )} -
    - {repo.description && ( -

    - {repo.description} -

    - )} -
    - {repo.language && ( - - {repo.language} - - )} - - {repo.stargazers_count.toLocaleString()} - - - {repo.forks_count.toLocaleString()} - - - {new Date(repo.updated_at).toLocaleDateString()} - -
    - - )) - )} -
    - )} -
    - - {isFetchingRepos && ( -
    - -
    - )} -
    -
    - setIsPrivate(e.target.checked)} - className="rounded border-codinit-elements-borderColor text-blue-500 focus:ring-blue-500 bg-codinit-elements-background-depth-2" - /> - -
    -

    - Private repositories are only visible to you and people you share them with -

    -
    - -
    - - Cancel - - - {isLoading ? ( - <> -
    - Pushing... - - ) : ( - <> -
    - Push to GitHub - - )} - -
    - -
    -
    - )} -
    - ); -} diff --git a/app/components/@settings/tabs/connections/components/RepositoryCard.tsx b/app/components/@settings/tabs/connections/components/RepositoryCard.tsx deleted file mode 100644 index 6270d354..00000000 --- a/app/components/@settings/tabs/connections/components/RepositoryCard.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React from 'react'; -import { motion } from 'framer-motion'; -import type { GitHubRepoInfo } from '~/types/GitHub'; - -interface RepositoryCardProps { - repo: GitHubRepoInfo; - onSelect: () => void; -} - -import { useMemo } from 'react'; - -export function RepositoryCard({ repo, onSelect }: RepositoryCardProps) { - // Use a consistent styling for all repository cards - const getCardStyle = () => { - return 'from-codinit-elements-background-depth-1 to-codinit-elements-background-depth-1 dark:from-codinit-elements-background-depth-2-dark dark:to-codinit-elements-background-depth-2-dark'; - }; - - // Format the date in a more readable format - const formatDate = (dateString: string) => { - const date = new Date(dateString); - const now = new Date(); - const diffTime = Math.abs(now.getTime() - date.getTime()); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays <= 1) { - return 'Today'; - } - - if (diffDays <= 2) { - return 'Yesterday'; - } - - if (diffDays <= 7) { - return `${diffDays} days ago`; - } - - if (diffDays <= 30) { - return `${Math.floor(diffDays / 7)} weeks ago`; - } - - return date.toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - }); - }; - - const cardStyle = useMemo(() => getCardStyle(), []); - - // const formattedDate = useMemo(() => formatDate(repo.updated_at), [repo.updated_at]); - - return ( - -
    -
    -
    - -
    -
    -

    - {repo.name} -

    -

    - - {repo.full_name.split('/')[0]} -

    -
    -
    - - - Import - -
    - - {repo.description && ( -
    -

    - {repo.description} -

    -
    - )} - -
    - {repo.private && ( - - - Private - - )} - {repo.language && ( - - - {repo.language} - - )} - - - {repo.stargazers_count.toLocaleString()} - - {repo.forks_count > 0 && ( - - - {repo.forks_count.toLocaleString()} - - )} -
    - -
    - - - Updated {formatDate(repo.updated_at)} - - - {repo.topics && repo.topics.length > 0 && ( - - {repo.topics.slice(0, 1).map((topic) => ( - - {topic} - - ))} - {repo.topics.length > 1 && +{repo.topics.length - 1}} - - )} -
    -
    - ); -} diff --git a/app/components/@settings/tabs/connections/components/RepositoryDialogContext.tsx b/app/components/@settings/tabs/connections/components/RepositoryDialogContext.tsx deleted file mode 100644 index 8a0490e2..00000000 --- a/app/components/@settings/tabs/connections/components/RepositoryDialogContext.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createContext } from 'react'; - -// Create a context to share the setShowAuthDialog function with child components -export interface RepositoryDialogContextType { - setShowAuthDialog: React.Dispatch>; -} - -// Default context value with a no-op function -export const RepositoryDialogContext = createContext({ - // This is intentionally empty as it will be overridden by the provider - setShowAuthDialog: () => { - // No operation - }, -}); diff --git a/app/components/@settings/tabs/connections/components/RepositoryList.tsx b/app/components/@settings/tabs/connections/components/RepositoryList.tsx deleted file mode 100644 index ba65d5b5..00000000 --- a/app/components/@settings/tabs/connections/components/RepositoryList.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useContext } from 'react'; -import type { GitHubRepoInfo } from '~/types/GitHub'; -import { EmptyState, StatusIndicator } from '~/components/ui'; -import { RepositoryCard } from './RepositoryCard'; -import { RepositoryDialogContext } from './RepositoryDialogContext'; - -interface RepositoryListProps { - repos: GitHubRepoInfo[]; - isLoading: boolean; - onSelect: (repo: GitHubRepoInfo) => void; - activeTab: string; -} - -export function RepositoryList({ repos, isLoading, onSelect, activeTab }: RepositoryListProps) { - // Access the parent component's setShowAuthDialog function - const { setShowAuthDialog } = useContext(RepositoryDialogContext); - - if (isLoading) { - return ( -
    - -

    - This may take a moment -

    -
    - ); - } - - if (repos.length === 0) { - if (activeTab === 'my-repos') { - return ( - setShowAuthDialog(true)} - /> - ); - } else { - return ( - - ); - } - } - - return ( -
    - {repos.map((repo) => ( - onSelect(repo)} /> - ))} -
    - ); -} diff --git a/app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx b/app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx deleted file mode 100644 index 03cf41dc..00000000 --- a/app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx +++ /dev/null @@ -1,1035 +0,0 @@ -import type { GitHubRepoInfo, GitHubContent, RepositoryStats, GitHubUserResponse } from '~/types/GitHub'; -import { useState, useEffect } from 'react'; -import { toast } from 'react-toastify'; -import * as Dialog from '@radix-ui/react-dialog'; -import { classNames } from '~/utils/classNames'; -import { getLocalStorage } from '~/lib/persistence'; -import { motion, AnimatePresence } from 'framer-motion'; -import Cookies from 'js-cookie'; - -// Import UI components -import { Input, SearchInput, Badge, FilterChip } from '~/components/ui'; - -// Import the components we've extracted -import { RepositoryList } from './RepositoryList'; -import { StatsDialog } from './StatsDialog'; -import { GitHubAuthDialog } from './GitHubAuthDialog'; -import { RepositoryDialogContext } from './RepositoryDialogContext'; - -interface GitHubTreeResponse { - tree: Array<{ - path: string; - type: string; - size?: number; - }>; -} - -interface RepositorySelectionDialogProps { - isOpen: boolean; - onClose: () => void; - onSelect: (url: string) => void; -} - -interface SearchFilters { - language?: string; - stars?: number; - forks?: number; -} - -export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: RepositorySelectionDialogProps) { - const [selectedRepository, setSelectedRepository] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [repositories, setRepositories] = useState([]); - const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [activeTab, setActiveTab] = useState<'my-repos' | 'search' | 'url'>('my-repos'); - const [customUrl, setCustomUrl] = useState(''); - const [branches, setBranches] = useState<{ name: string; default?: boolean }[]>([]); - const [selectedBranch, setSelectedBranch] = useState(''); - const [filters, setFilters] = useState({}); - const [showStatsDialog, setShowStatsDialog] = useState(false); - const [currentStats, setCurrentStats] = useState(null); - const [pendingGitUrl, setPendingGitUrl] = useState(''); - const [showAuthDialog, setShowAuthDialog] = useState(false); - - // Handle GitHub auth dialog close and refresh repositories - const handleAuthDialogClose = () => { - setShowAuthDialog(false); - - // If we're on the my-repos tab, refresh the repository list - if (activeTab === 'my-repos') { - fetchUserRepos(); - } - }; - - // Initialize GitHub connection and fetch repositories - useEffect(() => { - const savedConnection = getLocalStorage('github_connection'); - - // If no connection exists but environment variables are set, create a connection - if (!savedConnection && import.meta.env.VITE_GITHUB_ACCESS_TOKEN) { - const token = import.meta.env.VITE_GITHUB_ACCESS_TOKEN; - const tokenType = import.meta.env.VITE_GITHUB_TOKEN_TYPE === 'fine-grained' ? 'fine-grained' : 'classic'; - - // Fetch GitHub user info to initialize the connection - fetch('https://api.github.com/user', { - headers: { - Accept: 'application/vnd.github.v3+json', - Authorization: `Bearer ${token}`, - }, - }) - .then((response) => { - if (!response.ok) { - throw new Error('Invalid token or unauthorized'); - } - - return response.json(); - }) - .then((data: unknown) => { - const userData = data as GitHubUserResponse; - - // Save connection to local storage - const newConnection = { - token, - tokenType, - user: { - login: userData.login, - avatar_url: userData.avatar_url, - name: userData.name || userData.login, - }, - connected_at: new Date().toISOString(), - }; - - localStorage.setItem('github_connection', JSON.stringify(newConnection)); - - // Also save as cookies for API requests - Cookies.set('githubToken', token); - Cookies.set('githubUsername', userData.login); - Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' })); - - // Refresh repositories after connection is established - if (isOpen && activeTab === 'my-repos') { - fetchUserRepos(); - } - }) - .catch((error) => { - console.error('Failed to initialize GitHub connection from environment variables:', error); - }); - } - }, [isOpen]); - - // Fetch repositories when dialog opens or tab changes - useEffect(() => { - if (isOpen && activeTab === 'my-repos') { - fetchUserRepos(); - } - }, [isOpen, activeTab]); - - const fetchUserRepos = async () => { - const connection = getLocalStorage('github_connection'); - - if (!connection?.token) { - toast.error('Please connect your GitHub account first'); - return; - } - - setIsLoading(true); - - try { - const response = await fetch('https://api.github.com/user/repos?sort=updated&per_page=100&type=all', { - headers: { - Accept: 'application/vnd.github.v3+json', - Authorization: `Bearer ${connection.token}`, - }, - }); - - if (!response.ok) { - throw new Error('Failed to fetch repositories'); - } - - const data = await response.json(); - - // Add type assertion and validation - if ( - Array.isArray(data) && - data.every((item) => typeof item === 'object' && item !== null && 'full_name' in item) - ) { - setRepositories(data as GitHubRepoInfo[]); - } else { - throw new Error('Invalid repository data format'); - } - } catch (error) { - console.error('Error fetching repos:', error); - toast.error('Failed to fetch your repositories'); - } finally { - setIsLoading(false); - } - }; - - const handleSearch = async (query: string) => { - setIsLoading(true); - setSearchResults([]); - - try { - let searchQuery = query; - - if (filters.language) { - searchQuery += ` language:${filters.language}`; - } - - if (filters.stars) { - searchQuery += ` stars:>${filters.stars}`; - } - - if (filters.forks) { - searchQuery += ` forks:>${filters.forks}`; - } - - const response = await fetch( - `https://api.github.com/search/repositories?q=${encodeURIComponent(searchQuery)}&sort=stars&order=desc`, - { - headers: { - Accept: 'application/vnd.github.v3+json', - }, - }, - ); - - if (!response.ok) { - throw new Error('Failed to search repositories'); - } - - const data = await response.json(); - - // Add type assertion and validation - if (typeof data === 'object' && data !== null && 'items' in data && Array.isArray(data.items)) { - setSearchResults(data.items as GitHubRepoInfo[]); - } else { - throw new Error('Invalid search results format'); - } - } catch (error) { - console.error('Error searching repos:', error); - toast.error('Failed to search repositories'); - } finally { - setIsLoading(false); - } - }; - - const fetchBranches = async (repo: GitHubRepoInfo) => { - setIsLoading(true); - - try { - const connection = getLocalStorage('github_connection'); - const headers: HeadersInit = connection?.token - ? { - Accept: 'application/vnd.github.v3+json', - Authorization: `Bearer ${connection.token}`, - } - : {}; - const response = await fetch(`https://api.github.com/repos/${repo.full_name}/branches`, { - headers, - }); - - if (!response.ok) { - throw new Error('Failed to fetch branches'); - } - - const data = await response.json(); - - // Add type assertion and validation - if (Array.isArray(data) && data.every((item) => typeof item === 'object' && item !== null && 'name' in item)) { - setBranches( - data.map((branch) => ({ - name: branch.name, - default: branch.name === repo.default_branch, - })), - ); - } else { - throw new Error('Invalid branch data format'); - } - } catch (error) { - console.error('Error fetching branches:', error); - toast.error('Failed to fetch branches'); - } finally { - setIsLoading(false); - } - }; - - const handleRepoSelect = async (repo: GitHubRepoInfo) => { - setSelectedRepository(repo); - await fetchBranches(repo); - }; - - const formatGitUrl = (url: string): string => { - // Remove any tree references and ensure .git extension - const baseUrl = url - .replace(/\/tree\/[^/]+/, '') // Remove /tree/branch-name - .replace(/\/$/, '') // Remove trailing slash - .replace(/\.git$/, ''); // Remove .git if present - return `${baseUrl}.git`; - }; - - const verifyRepository = async (repoUrl: string): Promise => { - try { - // Extract branch from URL if present (format: url#branch) - let branch: string | null = null; - let cleanUrl = repoUrl; - - if (repoUrl.includes('#')) { - const parts = repoUrl.split('#'); - cleanUrl = parts[0]; - branch = parts[1]; - } - - const [owner, repo] = cleanUrl - .replace(/\.git$/, '') - .split('/') - .slice(-2); - - // Try to get token from local storage first - const connection = getLocalStorage('github_connection'); - - // If no connection in local storage, check environment variables - let headers: HeadersInit = {}; - - if (connection?.token) { - headers = { - Accept: 'application/vnd.github.v3+json', - Authorization: `Bearer ${connection.token}`, - }; - } else if (import.meta.env.VITE_GITHUB_ACCESS_TOKEN) { - // Use token from environment variables - headers = { - Accept: 'application/vnd.github.v3+json', - Authorization: `Bearer ${import.meta.env.VITE_GITHUB_ACCESS_TOKEN}`, - }; - } - - // First, get the repository info to determine the default branch - const repoInfoResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { - headers, - }); - - if (!repoInfoResponse.ok) { - if (repoInfoResponse.status === 401 || repoInfoResponse.status === 403) { - throw new Error( - `Authentication failed (${repoInfoResponse.status}). Your GitHub token may be invalid or missing the required permissions.`, - ); - } else if (repoInfoResponse.status === 404) { - throw new Error( - `Repository not found or is private (${repoInfoResponse.status}). To access private repositories, you need to connect your GitHub account or provide a valid token with appropriate permissions.`, - ); - } else { - throw new Error( - `Failed to fetch repository information: ${repoInfoResponse.statusText} (${repoInfoResponse.status})`, - ); - } - } - - const repoInfo = (await repoInfoResponse.json()) as { default_branch: string }; - let defaultBranch = repoInfo.default_branch || 'main'; - - // If a branch was specified in the URL, use that instead of the default - if (branch) { - defaultBranch = branch; - } - - // Try to fetch the repository tree using the selected branch - let treeResponse = await fetch( - `https://api.github.com/repos/${owner}/${repo}/git/trees/${defaultBranch}?recursive=1`, - { - headers, - }, - ); - - // If the selected branch doesn't work, try common branch names - if (!treeResponse.ok) { - // Try 'master' branch if default branch failed - treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/master?recursive=1`, { - headers, - }); - - // If master also fails, try 'main' branch - if (!treeResponse.ok) { - treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/main?recursive=1`, { - headers, - }); - } - - // If all common branches fail, throw an error - if (!treeResponse.ok) { - throw new Error( - 'Failed to fetch repository structure. Please check the repository URL and your access permissions.', - ); - } - } - - const treeData = (await treeResponse.json()) as GitHubTreeResponse; - - // Calculate repository stats - let totalSize = 0; - let totalFiles = 0; - const languages: { [key: string]: number } = {}; - let hasPackageJson = false; - let hasDependencies = false; - - for (const file of treeData.tree) { - if (file.type === 'blob') { - totalFiles++; - - if (file.size) { - totalSize += file.size; - } - - // Check for package.json - if (file.path === 'package.json') { - hasPackageJson = true; - - // Fetch package.json content to check dependencies - const contentResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/package.json`, { - headers, - }); - - if (contentResponse.ok) { - const content = (await contentResponse.json()) as GitHubContent; - const packageJson = JSON.parse(Buffer.from(content.content, 'base64').toString()); - hasDependencies = !!( - packageJson.dependencies || - packageJson.devDependencies || - packageJson.peerDependencies - ); - } - } - - // Detect language based on file extension - const ext = file.path.split('.').pop()?.toLowerCase(); - - if (ext) { - languages[ext] = (languages[ext] || 0) + (file.size || 0); - } - } - } - - const stats: RepositoryStats = { - totalFiles, - totalSize, - languages, - hasPackageJson, - hasDependencies, - }; - - return stats; - } catch (error) { - console.error('Error verifying repository:', error); - - // Check if it's an authentication error and show the auth dialog - const errorMessage = error instanceof Error ? error.message : 'Failed to verify repository'; - - if ( - errorMessage.includes('Authentication failed') || - errorMessage.includes('may be private') || - errorMessage.includes('Repository not found or is private') || - errorMessage.includes('Unauthorized') || - errorMessage.includes('401') || - errorMessage.includes('403') || - errorMessage.includes('404') || - errorMessage.includes('access permissions') - ) { - setShowAuthDialog(true); - } - - toast.error(errorMessage); - - return null; - } - }; - - const handleImport = async () => { - try { - let gitUrl: string; - - if (activeTab === 'url' && customUrl) { - gitUrl = formatGitUrl(customUrl); - } else if (selectedRepository) { - gitUrl = formatGitUrl(selectedRepository.html_url); - - if (selectedBranch) { - gitUrl = `${gitUrl}#${selectedBranch}`; - } - } else { - return; - } - - // Verify repository before importing - const stats = await verifyRepository(gitUrl); - - if (!stats) { - return; - } - - setCurrentStats(stats); - setPendingGitUrl(gitUrl); - setShowStatsDialog(true); - } catch (error) { - console.error('Error preparing repository:', error); - - // Check if it's an authentication error - const errorMessage = error instanceof Error ? error.message : 'Failed to prepare repository. Please try again.'; - - // Show the GitHub auth dialog for any authentication or permission errors - if ( - errorMessage.includes('Authentication failed') || - errorMessage.includes('may be private') || - errorMessage.includes('Repository not found or is private') || - errorMessage.includes('Unauthorized') || - errorMessage.includes('401') || - errorMessage.includes('403') || - errorMessage.includes('404') || - errorMessage.includes('access permissions') - ) { - // Directly show the auth dialog instead of just showing a toast - setShowAuthDialog(true); - - toast.error( -
    -

    {errorMessage}

    - -
    , - { autoClose: 10000 }, // Keep the toast visible longer - ); - } else { - toast.error(errorMessage); - } - } - }; - - const handleStatsConfirm = () => { - setShowStatsDialog(false); - - if (pendingGitUrl) { - onSelect(pendingGitUrl); - onClose(); - } - }; - - const handleFilterChange = (key: keyof SearchFilters, value: string) => { - let parsedValue: string | number | undefined = value; - - if (key === 'stars' || key === 'forks') { - parsedValue = value ? parseInt(value, 10) : undefined; - } - - setFilters((prev) => ({ ...prev, [key]: parsedValue })); - handleSearch(searchQuery); - }; - - // Handle dialog close properly - const handleClose = () => { - setIsLoading(false); // Reset loading state - setSearchQuery(''); // Reset search - setSearchResults([]); // Reset results - onClose(); - }; - - return ( - - { - if (!open) { - handleClose(); - } - }} - > - - - - {/* Header */} -
    -
    -
    - -
    -
    - - Import GitHub Repository - - - Clone a repository from GitHub to your workspace - -
    -
    - - -
    - - {/* Auth Info Banner */} -
    -
    - - - Need to access private repositories? - -
    - setShowAuthDialog(true)} - className="px-3 py-1.5 rounded-lg text-sm transition-colors flex items-center gap-1.5 shadow-sm" - style={{ - backgroundColor: 'var(--codinit-elements-button-primary-background)', - color: 'var(--codinit-elements-button-primary-text)', - }} - whileHover={{ - scale: 1.02, - backgroundColor: 'var(--codinit-elements-button-primary-backgroundHover)', - boxShadow: '0 4px 8px rgba(51, 123, 255, 0.2)', - }} - whileTap={{ scale: 0.98 }} - > - - Connect GitHub Account - -
    - - {/* Content */} -
    - {/* Tabs */} -
    -
    -
    - - - -
    -
    -
    - - {activeTab === 'url' ? ( -
    -
    -

    - - Repository URL -

    - -
    -
    - -
    - setCustomUrl(e.target.value)} - className="py-3 border border-codinit-elements-borderColor text-codinit-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-accent-500 shadow-sm" - style={{ backgroundColor: 'var(--codinit-elements-bg-depth-4)' }} - /> -
    - -
    -

    - - - You can paste any GitHub repository URL, including specific branches or tags. -
    - - Example: https://github.com/username/repository/tree/branch-name - -
    -

    -
    -
    - -
    -
    - Ready to import? -
    -
    - - - - Import Repository - -
    - ) : ( - <> - {activeTab === 'search' && ( -
    -
    -

    - - Search GitHub -

    - -
    -
    - { - setSearchQuery(e.target.value); - - if (e.target.value.length > 2) { - handleSearch(e.target.value); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter' && searchQuery.length > 2) { - handleSearch(searchQuery); - } - }} - onClear={() => { - setSearchQuery(''); - setSearchResults([]); - }} - iconClassName="text-accent-500" - className="py-3 border border-codinit-elements-borderColor text-codinit-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-accent-500 shadow-sm" - style={{ backgroundColor: 'var(--codinit-elements-bg-depth-4)' }} - loading={isLoading} - /> -
    - setFilters({})} - className="px-3 py-2 rounded-lg text-codinit-elements-textSecondary hover:text-codinit-elements-textPrimary border border-codinit-elements-borderColor shadow-sm" - style={{ backgroundColor: 'var(--codinit-elements-bg-depth-4)' }} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - title="Clear filters" - > - - -
    - -
    -
    - Filters -
    - - {/* Active filters */} - {(filters.language || filters.stars || filters.forks) && ( -
    - - {filters.language && ( - { - const newFilters = { ...filters }; - delete newFilters.language; - setFilters(newFilters); - - if (searchQuery.length > 2) { - handleSearch(searchQuery); - } - }} - /> - )} - {filters.stars && ( - ${filters.stars}`} - icon="i-ph:star" - active - onRemove={() => { - const newFilters = { ...filters }; - delete newFilters.stars; - setFilters(newFilters); - - if (searchQuery.length > 2) { - handleSearch(searchQuery); - } - }} - /> - )} - {filters.forks && ( - ${filters.forks}`} - icon="i-ph:git-fork" - active - onRemove={() => { - const newFilters = { ...filters }; - delete newFilters.forks; - setFilters(newFilters); - - if (searchQuery.length > 2) { - handleSearch(searchQuery); - } - }} - /> - )} - -
    - )} - -
    -
    -
    - -
    - { - setFilters({ ...filters, language: e.target.value }); - - if (searchQuery.length > 2) { - handleSearch(searchQuery); - } - }} - className="w-full pl-8 px-3 py-2 text-sm rounded-lg border border-codinit-elements-borderColor focus:outline-none focus:ring-2 focus:ring-accent-500" - style={{ backgroundColor: 'var(--codinit-elements-bg-depth-4)' }} - /> -
    -
    -
    - -
    - handleFilterChange('stars', e.target.value)} - className="w-full pl-8 px-3 py-2 text-sm rounded-lg border border-codinit-elements-borderColor focus:outline-none focus:ring-2 focus:ring-accent-500" - style={{ backgroundColor: 'var(--codinit-elements-bg-depth-4)' }} - /> -
    -
    -
    - -
    - handleFilterChange('forks', e.target.value)} - className="w-full pl-8 px-3 py-2 text-sm rounded-lg border border-codinit-elements-borderColor focus:outline-none focus:ring-2 focus:ring-accent-500" - style={{ backgroundColor: 'var(--codinit-elements-bg-depth-4)' }} - /> -
    -
    -
    - -
    -

    - - - Search for repositories by name, description, or topics. Use filters to narrow down - results. - -

    -
    -
    -
    - )} - -
    - {selectedRepository ? ( -
    -
    -
    - setSelectedRepository(null)} - className="p-2 rounded-lg hover:bg-white dark:hover:bg-codinit-elements-background-depth-4 text-codinit-elements-textSecondary hover:text-codinit-elements-textPrimary shadow-sm" - whileHover={{ scale: 1.1 }} - whileTap={{ scale: 0.9 }} - > - - -
    -

    - {selectedRepository.name} -

    -

    - - {selectedRepository.full_name.split('/')[0]} -

    -
    -
    - - {selectedRepository.private && ( - - Private - - )} -
    - - {selectedRepository.description && ( -
    -

    - {selectedRepository.description} -

    -
    - )} - -
    - {selectedRepository.language && ( - - {selectedRepository.language} - - )} - - {selectedRepository.stargazers_count.toLocaleString()} - - {selectedRepository.forks_count > 0 && ( - - {selectedRepository.forks_count.toLocaleString()} - - )} -
    - -
    -
    - - -
    - -
    - -
    -
    - Ready to import? -
    -
    - - - - Import {selectedRepository.name} - -
    - ) : ( - - )} -
    - - )} -
    -
    -
    - - {/* GitHub Auth Dialog */} - - - {/* Repository Stats Dialog */} - {currentStats && ( - setShowStatsDialog(false)} - onConfirm={handleStatsConfirm} - stats={currentStats} - isLargeRepo={currentStats.totalSize > 50 * 1024 * 1024} - /> - )} -
    -
    - ); -} diff --git a/app/components/@settings/tabs/connections/components/StatsDialog.tsx b/app/components/@settings/tabs/connections/components/StatsDialog.tsx deleted file mode 100644 index a3d7978e..00000000 --- a/app/components/@settings/tabs/connections/components/StatsDialog.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import * as Dialog from '@radix-ui/react-dialog'; -import { motion } from 'framer-motion'; -import type { RepositoryStats } from '~/types/GitHub'; -import { formatSize } from '~/utils/formatSize'; -import { RepositoryStats as RepoStats } from '~/components/ui'; - -interface StatsDialogProps { - isOpen: boolean; - onClose: () => void; - onConfirm: () => void; - stats: RepositoryStats; - isLargeRepo?: boolean; -} - -export function StatsDialog({ isOpen, onClose, onConfirm, stats, isLargeRepo }: StatsDialogProps) { - return ( - !open && onClose()}> - - -
    - - -
    -
    -
    - -
    -
    - - Repository Overview - - - Review repository details before importing - -
    -
    - -
    - -
    - - {isLargeRepo && ( -
    - -
    - This repository is quite large ({formatSize(stats.totalSize)}). Importing it might take a while - and could impact performance. -
    -
    - )} -
    -
    - - Cancel - - - Import Repository - -
    -
    -
    -
    -
    -
    - ); -} diff --git a/app/components/@settings/tabs/connections/types/GitHub.ts b/app/components/@settings/tabs/connections/types/GitHub.ts deleted file mode 100644 index f2f1af6b..00000000 --- a/app/components/@settings/tabs/connections/types/GitHub.ts +++ /dev/null @@ -1,95 +0,0 @@ -export interface GitHubUserResponse { - login: string; - avatar_url: string; - html_url: string; - name: string; - bio: string; - public_repos: number; - followers: number; - following: number; - public_gists: number; - created_at: string; - updated_at: string; -} - -export interface GitHubRepoInfo { - name: string; - full_name: string; - html_url: string; - description: string; - stargazers_count: number; - forks_count: number; - default_branch: string; - updated_at: string; - language: string; - languages_url: string; -} - -export interface GitHubOrganization { - login: string; - avatar_url: string; - description: string; - html_url: string; -} - -export interface GitHubEvent { - id: string; - type: string; - created_at: string; - repo: { - name: string; - url: string; - }; - payload: { - action?: string; - ref?: string; - ref_type?: string; - description?: string; - }; -} - -export interface GitHubLanguageStats { - [key: string]: number; -} - -export interface GitHubStats { - repos: GitHubRepoInfo[]; - totalStars: number; - totalForks: number; - organizations: GitHubOrganization[]; - recentActivity: GitHubEvent[]; - languages: GitHubLanguageStats; - totalGists: number; -} - -export interface GitHubConnection { - user: GitHubUserResponse | null; - token: string; - tokenType: 'classic' | 'fine-grained'; - stats?: GitHubStats; -} - -export interface GitHubTokenInfo { - token: string; - scope: string[]; - avatar_url: string; - name: string | null; - created_at: string; - followers: number; -} - -export interface GitHubRateLimits { - limit: number; - remaining: number; - reset: Date; - used: number; -} - -export interface GitHubAuthState { - username: string; - tokenInfo: GitHubTokenInfo | null; - isConnected: boolean; - isVerifying: boolean; - isLoadingRepos: boolean; - rateLimits?: GitHubRateLimits; -} diff --git a/app/components/@settings/tabs/data/DataTab.tsx b/app/components/@settings/tabs/data/DataTab.tsx deleted file mode 100644 index 32c887ff..00000000 --- a/app/components/@settings/tabs/data/DataTab.tsx +++ /dev/null @@ -1,722 +0,0 @@ -import { useState, useRef, useCallback, useEffect } from 'react'; -import { Button } from '~/components/ui/Button'; -import { ConfirmationDialog, SelectionDialog } from '~/components/ui/Dialog'; -import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '~/components/ui/Card'; -import { motion } from 'framer-motion'; -import { useDataOperations } from '~/lib/hooks/useDataOperations'; -import { openDatabase } from '~/lib/persistence/db'; -import { getAllChats, type Chat } from '~/lib/persistence/chats'; -import { DataVisualization } from './DataVisualization'; -import { classNames } from '~/utils/classNames'; -import { toast } from 'react-toastify'; -import { TextShimmer } from '~/components/ui'; - -// Create a custom hook to connect to the codinitHistory database -function useHistoryDB() { - const [db, setDb] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const initDB = async () => { - try { - setIsLoading(true); - - const database = await openDatabase(); - setDb(database || null); - setIsLoading(false); - } catch (err) { - setError(err instanceof Error ? err : new Error('Unknown error initializing database')); - setIsLoading(false); - } - }; - - initDB(); - - return () => { - if (db) { - db.close(); - } - }; - }, []); - - return { db, isLoading, error }; -} - -// Extend the Chat interface to include the missing properties -interface ExtendedChat extends Chat { - title?: string; - updatedAt?: number; -} - -// Helper function to create a chat label and description -function createChatItem(chat: Chat): ChatItem { - return { - id: chat.id, - - // Use description as title if available, or format a short ID - label: (chat as ExtendedChat).title || chat.description || `Chat ${chat.id.slice(0, 8)}`, - - // Format the description with message count and timestamp - description: `${chat.messages.length} messages - Last updated: ${new Date((chat as ExtendedChat).updatedAt || Date.parse(chat.timestamp)).toLocaleString()}`, - }; -} - -interface SettingsCategory { - id: string; - label: string; - description: string; -} - -interface ChatItem { - id: string; - label: string; - description: string; -} - -export function DataTab() { - // Use our custom hook for the codinitHistory database - const { db, isLoading: dbLoading } = useHistoryDB(); - const fileInputRef = useRef(null); - const apiKeyFileInputRef = useRef(null); - const chatFileInputRef = useRef(null); - - // State for confirmation dialogs - const [showResetInlineConfirm, setShowResetInlineConfirm] = useState(false); - const [showDeleteInlineConfirm, setShowDeleteInlineConfirm] = useState(false); - const [showSettingsSelection, setShowSettingsSelection] = useState(false); - const [showChatsSelection, setShowChatsSelection] = useState(false); - - // State for settings categories and available chats - const [settingsCategories] = useState([ - { id: 'core', label: 'Core Settings', description: 'User profile and main settings' }, - { id: 'providers', label: 'Providers', description: 'API keys and provider configurations' }, - { id: 'features', label: 'Features', description: 'Feature flags and settings' }, - { id: 'ui', label: 'UI', description: 'UI configuration and preferences' }, - { id: 'connections', label: 'Connections', description: 'External service connections' }, - { id: 'debug', label: 'Debug', description: 'Debug settings and logs' }, - { id: 'updates', label: 'Updates', description: 'Update settings and notifications' }, - ]); - - const [availableChats, setAvailableChats] = useState([]); - const [chatItems, setChatItems] = useState([]); - - // Data operations hook with codinitHistory database - const { - isExporting, - isImporting, - isResetting, - isDownloadingTemplate, - handleExportSettings, - handleExportSelectedSettings, - handleExportAllChats, - handleExportSelectedChats, - handleImportSettings, - handleImportChats, - handleResetSettings, - handleResetChats, - handleDownloadTemplate, - handleImportAPIKeys, - } = useDataOperations({ - customDb: db || undefined, // Pass the codinitHistory database, converting null to undefined - onReloadSettings: () => window.location.reload(), - onReloadChats: () => { - // Reload chats after reset - if (db) { - getAllChats(db).then((chats) => { - // Cast to ExtendedChat to handle additional properties - const extendedChats = chats as ExtendedChat[]; - setAvailableChats(extendedChats); - setChatItems(extendedChats.map((chat) => createChatItem(chat))); - }); - } - }, - onResetSettings: () => setShowResetInlineConfirm(false), - onResetChats: () => setShowDeleteInlineConfirm(false), - }); - - // Loading states for operations not provided by the hook - const [isDeleting, setIsDeleting] = useState(false); - const [isImportingKeys, setIsImportingKeys] = useState(false); - - // Load available chats - useEffect(() => { - if (db) { - console.log('Loading chats from codinitHistory database', { - name: db.name, - version: db.version, - objectStoreNames: Array.from(db.objectStoreNames), - }); - - getAllChats(db) - .then((chats) => { - console.log('Found chats:', chats.length); - - // Cast to ExtendedChat to handle additional properties - const extendedChats = chats as ExtendedChat[]; - setAvailableChats(extendedChats); - - // Create ChatItems for selection dialog - setChatItems(extendedChats.map((chat) => createChatItem(chat))); - }) - .catch((error) => { - console.error('Error loading chats:', error); - toast.error('Failed to load chats: ' + (error instanceof Error ? error.message : 'Unknown error')); - }); - } - }, [db]); - - // Handle file input changes - const handleFileInputChange = useCallback( - (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - handleImportSettings(file); - } - }, - [handleImportSettings], - ); - - const handleAPIKeyFileInputChange = useCallback( - (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - setIsImportingKeys(true); - handleImportAPIKeys(file).finally(() => setIsImportingKeys(false)); - } - }, - [handleImportAPIKeys], - ); - - const handleChatFileInputChange = useCallback( - (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - handleImportChats(file); - } - }, - [handleImportChats], - ); - - // Wrapper for reset chats to handle loading state - const handleResetChatsWithState = useCallback(() => { - setIsDeleting(true); - handleResetChats().finally(() => setIsDeleting(false)); - }, [handleResetChats]); - - return ( -
    - {/* Hidden file inputs */} - - - - - {/* Reset Settings Confirmation Dialog */} - setShowResetInlineConfirm(false)} - title="Reset All Settings?" - description="This will reset all your settings to their default values. This action cannot be undone." - confirmLabel="Reset Settings" - cancelLabel="Cancel" - variant="destructive" - isLoading={isResetting} - onConfirm={handleResetSettings} - /> - - {/* Delete Chats Confirmation Dialog */} - setShowDeleteInlineConfirm(false)} - title="Delete All Chats?" - description="This will permanently delete all your chat history. This action cannot be undone." - confirmLabel="Delete All" - cancelLabel="Cancel" - variant="destructive" - isLoading={isDeleting} - onConfirm={handleResetChatsWithState} - /> - - {/* Settings Selection Dialog */} - setShowSettingsSelection(false)} - title="Select Settings to Export" - items={settingsCategories} - onConfirm={(selectedIds) => { - handleExportSelectedSettings(selectedIds); - setShowSettingsSelection(false); - }} - confirmLabel="Export Selected" - /> - - {/* Chats Selection Dialog */} - setShowChatsSelection(false)} - title="Select Chats to Export" - items={chatItems} - onConfirm={(selectedIds) => { - handleExportSelectedChats(selectedIds); - setShowChatsSelection(false); - }} - confirmLabel="Export Selected" - /> - - {/* Chats Section */} -
    -

    Chats

    - {dbLoading ? ( -
    -
    - Loading chats database... -
    - ) : ( -
    - - -
    - -
    - - - Export All Chats - -
    - Export all your chats to a JSON file. - - - - - - - - - - -
    - -
    - - - Export Selected Chats - -
    - Choose specific chats to export. - - - - - - - - - - -
    - -
    - - - Import Chats - -
    - Import chats from a JSON file. - - - - - - - - - - -
    - -
    - - - Delete All Chats - -
    - Delete all your chat history. - - - - - - - -
    - )} -
    - - {/* Settings Section */} -
    -

    Settings

    -
    - - -
    - -
    - - - Export All Settings - -
    - Export all your settings to a JSON file. - - - - - - - - - - -
    - -
    - - - Export Selected Settings - -
    - Choose specific settings to export. - - - - - - - - - - -
    - -
    - - - Import Settings - -
    - Import settings from a JSON file. - - - - - - - - - - -
    - -
    - - - Reset All Settings - -
    - Reset all settings to their default values. - - - - - - - -
    -
    - - {/* API Keys Section */} -
    -

    API Keys

    -
    - - -
    - -
    - - - Download Template - -
    - Download a template file for your API keys. - - - - - - - - - - -
    - -
    - - - Import API Keys - -
    - Import API keys from a JSON file. - - - - - - - -
    -
    - - {/* Data Visualization */} -
    -

    Data Usage

    - - - - - -
    -
    - ); -} diff --git a/app/components/@settings/tabs/data/DataVisualization.tsx b/app/components/@settings/tabs/data/DataVisualization.tsx deleted file mode 100644 index b83c8300..00000000 --- a/app/components/@settings/tabs/data/DataVisualization.tsx +++ /dev/null @@ -1,384 +0,0 @@ -import { useState, useEffect } from 'react'; -import { - Chart as ChartJS, - CategoryScale, - LinearScale, - BarElement, - Title, - Tooltip, - Legend, - ArcElement, - PointElement, - LineElement, -} from 'chart.js'; -import { Bar, Pie } from 'react-chartjs-2'; -import type { Chat } from '~/lib/persistence/chats'; -import { classNames } from '~/utils/classNames'; - -// Register ChartJS components -ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ArcElement, PointElement, LineElement); - -type DataVisualizationProps = { - chats: Chat[]; -}; - -export function DataVisualization({ chats }: DataVisualizationProps) { - const [chatsByDate, setChatsByDate] = useState>({}); - const [messagesByRole, setMessagesByRole] = useState>({}); - const [apiKeyUsage, setApiKeyUsage] = useState>([]); - const [averageMessagesPerChat, setAverageMessagesPerChat] = useState(0); - const [isDarkMode, setIsDarkMode] = useState(false); - - useEffect(() => { - const isDark = document.documentElement.classList.contains('dark'); - setIsDarkMode(isDark); - - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.attributeName === 'class') { - setIsDarkMode(document.documentElement.classList.contains('dark')); - } - }); - }); - - observer.observe(document.documentElement, { attributes: true }); - - return () => observer.disconnect(); - }, []); - - useEffect(() => { - if (!chats || chats.length === 0) { - return; - } - - // Process chat data - const chatDates: Record = {}; - const roleCounts: Record = {}; - const apiUsage: Record = {}; - let totalMessages = 0; - - chats.forEach((chat) => { - const date = new Date(chat.timestamp).toLocaleDateString(); - chatDates[date] = (chatDates[date] || 0) + 1; - - chat.messages.forEach((message) => { - roleCounts[message.role] = (roleCounts[message.role] || 0) + 1; - totalMessages++; - - if (message.role === 'assistant') { - const providerMatch = message.content.match(/provider:\s*([\w-]+)/i); - const provider = providerMatch ? providerMatch[1] : 'unknown'; - apiUsage[provider] = (apiUsage[provider] || 0) + 1; - } - }); - }); - - const sortedDates = Object.keys(chatDates).sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); - const sortedChatsByDate: Record = {}; - sortedDates.forEach((date) => { - sortedChatsByDate[date] = chatDates[date]; - }); - - setChatsByDate(sortedChatsByDate); - setMessagesByRole(roleCounts); - setApiKeyUsage(Object.entries(apiUsage).map(([provider, count]) => ({ provider, count }))); - setAverageMessagesPerChat(totalMessages / chats.length); - }, [chats]); - - // Get theme colors from CSS variables to ensure theme consistency - const getThemeColor = (varName: string): string => { - // Get the CSS variable value from document root - if (typeof document !== 'undefined') { - return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); - } - - // Fallback for SSR - return isDarkMode ? '#FFFFFF' : '#000000'; - }; - - // Theme-aware chart colors with enhanced dark mode visibility using CSS variables - const chartColors = { - grid: isDarkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)', - text: getThemeColor('--codinit-elements-textPrimary'), - textSecondary: getThemeColor('--codinit-elements-textSecondary'), - background: getThemeColor('--codinit-elements-bg-depth-1'), - accent: getThemeColor('--codinit-elements-button-primary-text'), - border: getThemeColor('--codinit-elements-borderColor'), - }; - - const getChartColors = (index: number) => { - // Define color palettes based on Example design tokens - const baseColors = [ - // Indigo - { - base: getThemeColor('--codinit-elements-button-primary-text'), - }, - - // Pink - { - base: isDarkMode ? 'rgb(244, 114, 182)' : 'rgb(236, 72, 153)', - }, - - // Green - { - base: getThemeColor('--codinit-elements-icon-success'), - }, - - // Yellow - { - base: isDarkMode ? 'rgb(250, 204, 21)' : 'rgb(234, 179, 8)', - }, - - // Blue - { - base: isDarkMode ? 'rgb(56, 189, 248)' : 'rgb(14, 165, 233)', - }, - ]; - - // Get the base color for this index - const color = baseColors[index % baseColors.length].base; - - // Parse color and generate variations with appropriate opacity - let r = 0, - g = 0, - b = 0; - - // Handle rgb/rgba format - const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); - const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([0-9.]+)\)/); - - if (rgbMatch) { - [, r, g, b] = rgbMatch.map(Number); - } else if (rgbaMatch) { - [, r, g, b] = rgbaMatch.map(Number); - } else if (color.startsWith('#')) { - // Handle hex format - const hex = color.slice(1); - const bigint = parseInt(hex, 16); - r = (bigint >> 16) & 255; - g = (bigint >> 8) & 255; - b = bigint & 255; - } - - return { - bg: `rgba(${r}, ${g}, ${b}, ${isDarkMode ? 0.7 : 0.5})`, - border: `rgba(${r}, ${g}, ${b}, ${isDarkMode ? 0.9 : 0.8})`, - }; - }; - - const chartData = { - history: { - labels: Object.keys(chatsByDate), - datasets: [ - { - label: 'Chats Created', - data: Object.values(chatsByDate), - backgroundColor: getChartColors(0).bg, - borderColor: getChartColors(0).border, - borderWidth: 1, - }, - ], - }, - roles: { - labels: Object.keys(messagesByRole), - datasets: [ - { - label: 'Messages by Role', - data: Object.values(messagesByRole), - backgroundColor: Object.keys(messagesByRole).map((_, i) => getChartColors(i).bg), - borderColor: Object.keys(messagesByRole).map((_, i) => getChartColors(i).border), - borderWidth: 1, - }, - ], - }, - apiUsage: { - labels: apiKeyUsage.map((item) => item.provider), - datasets: [ - { - label: 'API Usage', - data: apiKeyUsage.map((item) => item.count), - backgroundColor: apiKeyUsage.map((_, i) => getChartColors(i).bg), - borderColor: apiKeyUsage.map((_, i) => getChartColors(i).border), - borderWidth: 1, - }, - ], - }, - }; - - const baseChartOptions = { - responsive: true, - maintainAspectRatio: false, - color: chartColors.text, - plugins: { - legend: { - position: 'top' as const, - labels: { - color: chartColors.text, - font: { - weight: 'bold' as const, - size: 12, - }, - padding: 16, - usePointStyle: true, - }, - }, - title: { - display: true, - color: chartColors.text, - font: { - size: 16, - weight: 'bold' as const, - }, - padding: 16, - }, - tooltip: { - titleColor: chartColors.text, - bodyColor: chartColors.text, - backgroundColor: isDarkMode - ? 'rgba(23, 23, 23, 0.8)' // Dark bg using Tailwind gray-900 - : 'rgba(255, 255, 255, 0.8)', // Light bg - borderColor: chartColors.border, - borderWidth: 1, - }, - }, - }; - - const chartOptions = { - ...baseChartOptions, - plugins: { - ...baseChartOptions.plugins, - title: { - ...baseChartOptions.plugins.title, - text: 'Chat History', - }, - }, - scales: { - x: { - grid: { - color: chartColors.grid, - drawBorder: false, - }, - border: { - display: false, - }, - ticks: { - color: chartColors.text, - font: { - weight: 500, - }, - }, - }, - y: { - grid: { - color: chartColors.grid, - drawBorder: false, - }, - border: { - display: false, - }, - ticks: { - color: chartColors.text, - font: { - weight: 500, - }, - }, - }, - }, - }; - - const pieOptions = { - ...baseChartOptions, - plugins: { - ...baseChartOptions.plugins, - title: { - ...baseChartOptions.plugins.title, - text: 'Message Distribution', - }, - legend: { - ...baseChartOptions.plugins.legend, - position: 'right' as const, - }, - datalabels: { - color: chartColors.text, - font: { - weight: 'bold' as const, - }, - }, - }, - }; - - if (chats.length === 0) { - return ( -
    -
    -

    No Data Available

    -

    - Start creating chats to see your usage statistics and data visualization. -

    -
    - ); - } - - const cardClasses = classNames( - 'p-6 rounded-lg shadow-sm', - 'bg-codinit-elements-background-depth-1', - 'border border-codinit-elements-borderColor', - ); - - const statClasses = classNames('text-3xl font-bold text-codinit-elements-textPrimary', 'flex items-center gap-3'); - - return ( -
    -
    -
    -

    Total Chats

    -
    -
    - {chats.length} -
    -
    - -
    -

    Total Messages

    -
    -
    - {Object.values(messagesByRole).reduce((sum, count) => sum + count, 0)} -
    -
    - -
    -

    Avg. Messages/Chat

    -
    -
    - {averageMessagesPerChat.toFixed(1)} -
    -
    -
    - -
    -
    -

    Chat History

    -
    - -
    -
    - -
    -

    Message Distribution

    -
    - -
    -
    -
    - - {apiKeyUsage.length > 0 && ( -
    -

    API Usage by Provider

    -
    - -
    -
    - )} -
    - ); -} diff --git a/app/components/@settings/tabs/debug/DebugTab.tsx b/app/components/@settings/tabs/debug/DebugTab.tsx deleted file mode 100644 index 0949a64a..00000000 --- a/app/components/@settings/tabs/debug/DebugTab.tsx +++ /dev/null @@ -1,2118 +0,0 @@ -import React, { useEffect, useState, useMemo, useCallback } from 'react'; -import { toast } from 'react-toastify'; -import { classNames } from '~/utils/classNames'; -import { logStore, type LogEntry } from '~/lib/stores/logs'; -import { useStore } from '@nanostores/react'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/Collapsible'; -import { Progress } from '~/components/ui/Progress'; -import { ScrollArea } from '~/components/ui/ScrollArea'; -import { Badge } from '~/components/ui/Badge'; -import { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; -import { useSettings } from '~/lib/hooks/useSettings'; - -interface SystemInfo { - os: string; - arch: string; - platform: string; - cpus: string; - memory: { - total: string; - free: string; - used: string; - percentage: number; - }; - node: string; - browser: { - name: string; - version: string; - language: string; - userAgent: string; - cookiesEnabled: boolean; - online: boolean; - platform: string; - cores: number; - }; - screen: { - width: number; - height: number; - colorDepth: number; - pixelRatio: number; - }; - time: { - timezone: string; - offset: number; - locale: string; - }; - performance: { - memory: { - jsHeapSizeLimit: number; - totalJSHeapSize: number; - usedJSHeapSize: number; - usagePercentage: number; - }; - timing: { - loadTime: number; - domReadyTime: number; - readyStart: number; - redirectTime: number; - appcacheTime: number; - unloadEventTime: number; - lookupDomainTime: number; - connectTime: number; - requestTime: number; - initDomTreeTime: number; - loadEventTime: number; - }; - navigation: { - type: number; - redirectCount: number; - }; - }; - network: { - downlink: number; - effectiveType: string; - rtt: number; - saveData: boolean; - type: string; - }; - battery?: { - charging: boolean; - chargingTime: number; - dischargingTime: number; - level: number; - }; - storage: { - quota: number; - usage: number; - persistent: boolean; - temporary: boolean; - }; -} - -interface GitHubRepoInfo { - fullName: string; - defaultBranch: string; - stars: number; - forks: number; - openIssues?: number; -} - -interface GitInfo { - local: { - commitHash: string; - branch: string; - commitTime: string; - author: string; - email: string; - remoteUrl: string; - repoName: string; - }; - github?: { - currentRepo: GitHubRepoInfo; - upstream?: GitHubRepoInfo; - }; - isForked?: boolean; -} - -interface WebAppInfo { - name: string; - version: string; - description: string; - license: string; - environment: string; - timestamp: string; - runtimeInfo: { - nodeVersion: string; - }; - dependencies: { - production: Array<{ name: string; version: string; type: string }>; - development: Array<{ name: string; version: string; type: string }>; - peer: Array<{ name: string; version: string; type: string }>; - optional: Array<{ name: string; version: string; type: string }>; - }; - gitInfo: GitInfo; -} - -// Add Ollama service status interface -interface OllamaServiceStatus { - isRunning: boolean; - lastChecked: Date; - error?: string; - models?: Array<{ - name: string; - size: string; - quantization: string; - }>; -} - -interface ExportFormat { - id: string; - label: string; - icon: string; - handler: () => void; -} - -const DependencySection = ({ - title, - deps, -}: { - title: string; - deps: Array<{ name: string; version: string; type: string }>; -}) => { - const [isOpen, setIsOpen] = useState(false); - - if (deps.length === 0) { - return null; - } - - return ( - - -
    -
    - - {title} Dependencies ({deps.length}) - -
    -
    - {isOpen ? 'Hide' : 'Show'} -
    -
    - - - -
    - {deps.map((dep) => ( -
    - {dep.name} - {dep.version} -
    - ))} -
    -
    -
    - - ); -}; - -export default function DebugTab() { - const [systemInfo, setSystemInfo] = useState(null); - const [webAppInfo, setWebAppInfo] = useState(null); - const [ollamaStatus, setOllamaStatus] = useState({ - isRunning: false, - lastChecked: new Date(), - }); - const [loading, setLoading] = useState({ - systemInfo: false, - webAppInfo: false, - errors: false, - performance: false, - }); - const [openSections, setOpenSections] = useState({ - system: false, - webapp: false, - errors: false, - performance: false, - }); - - const { providers } = useSettings(); - - // Subscribe to logStore updates - const logs = useStore(logStore.logs); - const errorLogs = useMemo(() => { - return Object.values(logs).filter( - (log): log is LogEntry => typeof log === 'object' && log !== null && 'level' in log && log.level === 'error', - ); - }, [logs]); - - // Set up error listeners when component mounts - useEffect(() => { - const handleError = (event: ErrorEvent) => { - logStore.logError(event.message, event.error, { - filename: event.filename, - lineNumber: event.lineno, - columnNumber: event.colno, - }); - }; - - const handleRejection = (event: PromiseRejectionEvent) => { - logStore.logError('Unhandled Promise Rejection', event.reason); - }; - - window.addEventListener('error', handleError); - window.addEventListener('unhandledrejection', handleRejection); - - return () => { - window.removeEventListener('error', handleError); - window.removeEventListener('unhandledrejection', handleRejection); - }; - }, []); - - // Check for errors when the errors section is opened - useEffect(() => { - if (openSections.errors) { - checkErrors(); - } - }, [openSections.errors]); - - // Load initial data when component mounts - useEffect(() => { - const loadInitialData = async () => { - await Promise.all([getSystemInfo(), getWebAppInfo()]); - }; - - loadInitialData(); - }, []); - - // Refresh data when sections are opened - useEffect(() => { - if (openSections.system) { - getSystemInfo(); - } - - if (openSections.webapp) { - getWebAppInfo(); - } - }, [openSections.system, openSections.webapp]); - - // Add periodic refresh of git info - useEffect(() => { - if (!openSections.webapp) { - return undefined; - } - - // Initial fetch - const fetchGitInfo = async () => { - try { - const response = await fetch('/api/system/git-info'); - const updatedGitInfo = (await response.json()) as GitInfo; - - setWebAppInfo((prev) => { - if (!prev) { - return null; - } - - // Only update if the data has changed - if (JSON.stringify(prev.gitInfo) === JSON.stringify(updatedGitInfo)) { - return prev; - } - - return { - ...prev, - gitInfo: updatedGitInfo, - }; - }); - } catch (error) { - console.error('Failed to fetch git info:', error); - } - }; - - fetchGitInfo(); - - // Refresh every 5 minutes instead of every second - const interval = setInterval(fetchGitInfo, 5 * 60 * 1000); - - return () => clearInterval(interval); - }, [openSections.webapp]); - - const getSystemInfo = async () => { - try { - setLoading((prev) => ({ ...prev, systemInfo: true })); - - // Get better OS detection - const userAgent = navigator.userAgent; - let detectedOS = 'Unknown'; - let detectedArch = 'unknown'; - - // Improved OS detection - if (userAgent.indexOf('Win') !== -1) { - detectedOS = 'Windows'; - } else if (userAgent.indexOf('Mac') !== -1) { - detectedOS = 'macOS'; - } else if (userAgent.indexOf('Linux') !== -1) { - detectedOS = 'Linux'; - } else if (userAgent.indexOf('Android') !== -1) { - detectedOS = 'Android'; - } else if (/iPhone|iPad|iPod/.test(userAgent)) { - detectedOS = 'iOS'; - } - - // Better architecture detection - if (userAgent.indexOf('x86_64') !== -1 || userAgent.indexOf('x64') !== -1 || userAgent.indexOf('WOW64') !== -1) { - detectedArch = 'x64'; - } else if (userAgent.indexOf('x86') !== -1 || userAgent.indexOf('i686') !== -1) { - detectedArch = 'x86'; - } else if (userAgent.indexOf('arm64') !== -1 || userAgent.indexOf('aarch64') !== -1) { - detectedArch = 'arm64'; - } else if (userAgent.indexOf('arm') !== -1) { - detectedArch = 'arm'; - } - - // Get browser info with improved detection - const browserName = (() => { - if (userAgent.indexOf('Edge') !== -1 || userAgent.indexOf('Edg/') !== -1) { - return 'Edge'; - } - - if (userAgent.indexOf('Chrome') !== -1) { - return 'Chrome'; - } - - if (userAgent.indexOf('Firefox') !== -1) { - return 'Firefox'; - } - - if (userAgent.indexOf('Safari') !== -1) { - return 'Safari'; - } - - return 'Unknown'; - })(); - - const browserVersionMatch = userAgent.match(/(Edge|Edg|Chrome|Firefox|Safari)[\s/](\d+(\.\d+)*)/); - const browserVersion = browserVersionMatch ? browserVersionMatch[2] : 'Unknown'; - - // Get performance metrics - const memory = (performance as any).memory || {}; - const timing = performance.timing; - const navigation = performance.navigation; - const connection = (navigator as any).connection || {}; - - // Try to use Navigation Timing API Level 2 when available - let loadTime = 0; - let domReadyTime = 0; - - try { - const navEntries = performance.getEntriesByType('navigation'); - - if (navEntries.length > 0) { - const navTiming = navEntries[0] as PerformanceNavigationTiming; - loadTime = navTiming.loadEventEnd - navTiming.startTime; - domReadyTime = navTiming.domContentLoadedEventEnd - navTiming.startTime; - } else { - // Fall back to older API - loadTime = timing.loadEventEnd - timing.navigationStart; - domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart; - } - } catch { - // Fall back to older API if Navigation Timing API Level 2 is not available - loadTime = timing.loadEventEnd - timing.navigationStart; - domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart; - } - - // Get battery info - let batteryInfo; - - try { - const battery = await (navigator as any).getBattery(); - batteryInfo = { - charging: battery.charging, - chargingTime: battery.chargingTime, - dischargingTime: battery.dischargingTime, - level: battery.level * 100, - }; - } catch { - console.log('Battery API not supported'); - } - - // Get storage info - let storageInfo = { - quota: 0, - usage: 0, - persistent: false, - temporary: false, - }; - - try { - const storage = await navigator.storage.estimate(); - const persistent = await navigator.storage.persist(); - storageInfo = { - quota: storage.quota || 0, - usage: storage.usage || 0, - persistent, - temporary: !persistent, - }; - } catch { - console.log('Storage API not supported'); - } - - // Get memory info from browser performance API - const performanceMemory = (performance as any).memory || {}; - const totalMemory = performanceMemory.jsHeapSizeLimit || 0; - const usedMemory = performanceMemory.usedJSHeapSize || 0; - const freeMemory = totalMemory - usedMemory; - const memoryPercentage = totalMemory ? (usedMemory / totalMemory) * 100 : 0; - - const systemInfo: SystemInfo = { - os: detectedOS, - arch: detectedArch, - platform: navigator.platform || 'unknown', - cpus: navigator.hardwareConcurrency + ' cores', - memory: { - total: formatBytes(totalMemory), - free: formatBytes(freeMemory), - used: formatBytes(usedMemory), - percentage: Math.round(memoryPercentage), - }, - node: 'browser', - browser: { - name: browserName, - version: browserVersion, - language: navigator.language, - userAgent: navigator.userAgent, - cookiesEnabled: navigator.cookieEnabled, - online: navigator.onLine, - platform: navigator.platform || 'unknown', - cores: navigator.hardwareConcurrency, - }, - screen: { - width: window.screen.width, - height: window.screen.height, - colorDepth: window.screen.colorDepth, - pixelRatio: window.devicePixelRatio, - }, - time: { - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - offset: new Date().getTimezoneOffset(), - locale: navigator.language, - }, - performance: { - memory: { - jsHeapSizeLimit: memory.jsHeapSizeLimit || 0, - totalJSHeapSize: memory.totalJSHeapSize || 0, - usedJSHeapSize: memory.usedJSHeapSize || 0, - usagePercentage: memory.totalJSHeapSize ? (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100 : 0, - }, - timing: { - loadTime, - domReadyTime, - readyStart: timing.fetchStart - timing.navigationStart, - redirectTime: timing.redirectEnd - timing.redirectStart, - appcacheTime: timing.domainLookupStart - timing.fetchStart, - unloadEventTime: timing.unloadEventEnd - timing.unloadEventStart, - lookupDomainTime: timing.domainLookupEnd - timing.domainLookupStart, - connectTime: timing.connectEnd - timing.connectStart, - requestTime: timing.responseEnd - timing.requestStart, - initDomTreeTime: timing.domInteractive - timing.responseEnd, - loadEventTime: timing.loadEventEnd - timing.loadEventStart, - }, - navigation: { - type: navigation.type, - redirectCount: navigation.redirectCount, - }, - }, - network: { - downlink: connection?.downlink || 0, - effectiveType: connection?.effectiveType || 'unknown', - rtt: connection?.rtt || 0, - saveData: connection?.saveData || false, - type: connection?.type || 'unknown', - }, - battery: batteryInfo, - storage: storageInfo, - }; - - setSystemInfo(systemInfo); - toast.success('System information updated'); - } catch (error) { - toast.error('Failed to get system information'); - console.error('Failed to get system information:', error); - } finally { - setLoading((prev) => ({ ...prev, systemInfo: false })); - } - }; - - // Helper function to format bytes to human readable format with better precision - const formatBytes = (bytes: number) => { - if (bytes === 0) { - return '0 B'; - } - - const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - - // Return with proper precision based on unit size - if (i === 0) { - return `${bytes} ${units[i]}`; - } - - return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`; - }; - - const getWebAppInfo = async () => { - try { - setLoading((prev) => ({ ...prev, webAppInfo: true })); - - const [appResponse, gitResponse] = await Promise.all([ - fetch('/api/system/app-info'), - fetch('/api/system/git-info'), - ]); - - if (!appResponse.ok || !gitResponse.ok) { - throw new Error('Failed to fetch webapp info'); - } - - const appData = (await appResponse.json()) as Omit; - const gitData = (await gitResponse.json()) as GitInfo; - - console.log('Git Info Response:', gitData); // Add logging to debug - - setWebAppInfo({ - ...appData, - gitInfo: gitData, - }); - - toast.success('WebApp information updated'); - - return true; - } catch (error) { - console.error('Failed to fetch webapp info:', error); - toast.error('Failed to fetch webapp information'); - setWebAppInfo(null); - - return false; - } finally { - setLoading((prev) => ({ ...prev, webAppInfo: false })); - } - }; - - const handleLogPerformance = async () => { - try { - setLoading((prev) => ({ ...prev, performance: true })); - - // Get performance metrics using modern Performance API - const performanceEntries = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; - const memory = (performance as any).memory; - - // Calculate timing metrics - const timingMetrics = { - loadTime: performanceEntries.loadEventEnd - performanceEntries.startTime, - domReadyTime: performanceEntries.domContentLoadedEventEnd - performanceEntries.startTime, - fetchTime: performanceEntries.responseEnd - performanceEntries.fetchStart, - redirectTime: performanceEntries.redirectEnd - performanceEntries.redirectStart, - dnsTime: performanceEntries.domainLookupEnd - performanceEntries.domainLookupStart, - tcpTime: performanceEntries.connectEnd - performanceEntries.connectStart, - ttfb: performanceEntries.responseStart - performanceEntries.requestStart, - processingTime: performanceEntries.loadEventEnd - performanceEntries.responseEnd, - }; - - // Get resource timing data - const resourceEntries = performance.getEntriesByType('resource'); - const resourceStats = { - totalResources: resourceEntries.length, - totalSize: resourceEntries.reduce((total, entry) => total + ((entry as any).transferSize || 0), 0), - totalTime: Math.max(...resourceEntries.map((entry) => entry.duration)), - }; - - // Get memory metrics - const memoryMetrics = memory - ? { - jsHeapSizeLimit: memory.jsHeapSizeLimit, - totalJSHeapSize: memory.totalJSHeapSize, - usedJSHeapSize: memory.usedJSHeapSize, - heapUtilization: (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100, - } - : null; - - // Get frame rate metrics - let fps = 0; - - if ('requestAnimationFrame' in window) { - const times: number[] = []; - let sampleCount = 0; - const maxSamples = 12; - - fps = await new Promise((resolve) => { - function calculateFPS(now: number) { - times.push(now); - sampleCount++; - - if (times.length > 10) { - const calculatedFPS = Math.round((1000 * 10) / (now - times[0])); - resolve(calculatedFPS); - - return; - } - - if (sampleCount < maxSamples) { - requestAnimationFrame(calculateFPS); - } else { - resolve(0); - } - } - - requestAnimationFrame(calculateFPS); - }); - } - - // Log all performance metrics - logStore.logSystem('Performance Metrics', { - timing: timingMetrics, - resources: resourceStats, - memory: memoryMetrics, - fps, - timestamp: new Date().toISOString(), - navigationEntry: { - type: performanceEntries.type, - redirectCount: performanceEntries.redirectCount, - }, - }); - - toast.success('Performance metrics logged'); - } catch (error) { - toast.error('Failed to log performance metrics'); - console.error('Failed to log performance metrics:', error); - } finally { - setLoading((prev) => ({ ...prev, performance: false })); - } - }; - - const checkErrors = async () => { - try { - setLoading((prev) => ({ ...prev, errors: true })); - - // Get errors from log store - const storedErrors = errorLogs; - - if (storedErrors.length === 0) { - toast.success('No errors found'); - } else { - toast.warning(`Found ${storedErrors.length} error(s)`); - } - } catch (error) { - toast.error('Failed to check errors'); - console.error('Failed to check errors:', error); - } finally { - setLoading((prev) => ({ ...prev, errors: false })); - } - }; - - const exportDebugInfo = () => { - try { - const debugData = { - timestamp: new Date().toISOString(), - system: systemInfo, - webApp: webAppInfo, - errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'), - performance: { - memory: (performance as any).memory || {}, - timing: performance.timing, - navigation: performance.navigation, - }, - }; - - const blob = new Blob([JSON.stringify(debugData, null, 2)], { type: 'application/json' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `codinit-debug-info-${new Date().toISOString()}.json`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - toast.success('Debug information exported successfully'); - } catch (error) { - console.error('Failed to export debug info:', error); - toast.error('Failed to export debug information'); - } - }; - - const exportAsCSV = () => { - try { - const debugData = { - system: systemInfo, - webApp: webAppInfo, - errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'), - performance: { - memory: (performance as any).memory || {}, - timing: performance.timing, - navigation: performance.navigation, - }, - }; - - // Convert the data to CSV format - const csvData = [ - ['Category', 'Key', 'Value'], - ...Object.entries(debugData).flatMap(([category, data]) => - Object.entries(data || {}).map(([key, value]) => [ - category, - key, - typeof value === 'object' ? JSON.stringify(value) : String(value), - ]), - ), - ]; - - // Create CSV content - const csvContent = csvData.map((row) => row.join(',')).join('\n'); - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `codinit-debug-info-${new Date().toISOString()}.csv`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - toast.success('Debug information exported as CSV'); - } catch (error) { - console.error('Failed to export CSV:', error); - toast.error('Failed to export debug information as CSV'); - } - }; - - const exportAsPDF = async () => { - try { - const { jsPDF } = await import('jspdf'); - - const debugData = { - system: systemInfo, - webApp: webAppInfo, - errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'), - performance: { - memory: (performance as any).memory || {}, - timing: performance.timing, - navigation: performance.navigation, - }, - }; - - // Create new PDF document - const doc = new jsPDF(); - const lineHeight = 7; - let yPos = 20; - const margin = 20; - const pageWidth = doc.internal.pageSize.getWidth(); - const maxLineWidth = pageWidth - 2 * margin; - - // Add key-value pair with better formatting - const addKeyValue = (key: string, value: any, indent = 0) => { - // Check if we need a new page - if (yPos > doc.internal.pageSize.getHeight() - 20) { - doc.addPage(); - yPos = margin; - } - - doc.setFontSize(10); - doc.setTextColor('#374151'); - doc.setFont('helvetica', 'bold'); - - // Format the key with proper spacing - const formattedKey = key.replace(/([A-Z])/g, ' $1').trim(); - doc.text(formattedKey + ':', margin + indent, yPos); - doc.setFont('helvetica', 'normal'); - doc.setTextColor('#6B7280'); - - let valueText; - - if (typeof value === 'object' && value !== null) { - // Skip rendering if value is empty object - if (Object.keys(value).length === 0) { - return; - } - - yPos += lineHeight; - Object.entries(value).forEach(([subKey, subValue]) => { - // Check for page break before each sub-item - if (yPos > doc.internal.pageSize.getHeight() - 20) { - doc.addPage(); - yPos = margin; - } - - const formattedSubKey = subKey.replace(/([A-Z])/g, ' $1').trim(); - addKeyValue(formattedSubKey, subValue, indent + 10); - }); - - return; - } else { - valueText = String(value); - } - - const valueX = margin + indent + doc.getTextWidth(formattedKey + ': '); - const maxValueWidth = maxLineWidth - indent - doc.getTextWidth(formattedKey + ': '); - const lines = doc.splitTextToSize(valueText, maxValueWidth); - - // Check if we need a new page for the value - if (yPos + lines.length * lineHeight > doc.internal.pageSize.getHeight() - 20) { - doc.addPage(); - yPos = margin; - } - - doc.text(lines, valueX, yPos); - yPos += lines.length * lineHeight; - }; - - // Add section header with page break check - const addSectionHeader = (title: string) => { - // Check if we need a new page - if (yPos + 20 > doc.internal.pageSize.getHeight() - 20) { - doc.addPage(); - yPos = margin; - } - - yPos += lineHeight; - doc.setFillColor('#F3F4F6'); - doc.rect(margin - 2, yPos - 5, pageWidth - 2 * (margin - 2), lineHeight + 6, 'F'); - doc.setFont('helvetica', 'bold'); - doc.setTextColor('#111827'); - doc.setFontSize(12); - doc.text(title.toUpperCase(), margin, yPos); - doc.setFont('helvetica', 'normal'); - yPos += lineHeight * 1.5; - }; - - // Add horizontal line with page break check - const addHorizontalLine = () => { - // Check if we need a new page - if (yPos + 10 > doc.internal.pageSize.getHeight() - 20) { - doc.addPage(); - yPos = margin; - - return; // Skip drawing line if we just started a new page - } - - doc.setDrawColor('#E5E5E5'); - doc.line(margin, yPos, pageWidth - margin, yPos); - yPos += lineHeight; - }; - - // Helper function to add footer to all pages - const addFooters = () => { - const totalPages = doc.internal.pages.length - 1; - - for (let i = 1; i <= totalPages; i++) { - doc.setPage(i); - doc.setFontSize(8); - doc.setTextColor('#9CA3AF'); - doc.text(`Page ${i} of ${totalPages}`, pageWidth / 2, doc.internal.pageSize.getHeight() - 10, { - align: 'center', - }); - } - }; - - // Title and Header (first page only) - doc.setFillColor('#6366F1'); - doc.rect(0, 0, pageWidth, 40, 'F'); - doc.setTextColor('#FFFFFF'); - doc.setFontSize(24); - doc.setFont('helvetica', 'bold'); - doc.text('Debug Information Report', margin, 25); - yPos = 50; - - // Timestamp and metadata - doc.setTextColor('#6B7280'); - doc.setFontSize(10); - doc.setFont('helvetica', 'normal'); - - const timestamp = new Date().toLocaleString(undefined, { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }); - doc.text(`Generated: ${timestamp}`, margin, yPos); - yPos += lineHeight * 2; - - // System Information Section - if (debugData.system) { - addSectionHeader('System Information'); - - // OS and Architecture - addKeyValue('Operating System', debugData.system.os); - addKeyValue('Architecture', debugData.system.arch); - addKeyValue('Platform', debugData.system.platform); - addKeyValue('CPU Cores', debugData.system.cpus); - - // Memory - const memory = debugData.system.memory; - addKeyValue('Memory', { - 'Total Memory': memory.total, - 'Used Memory': memory.used, - 'Free Memory': memory.free, - Usage: memory.percentage + '%', - }); - - // Browser Information - const browser = debugData.system.browser; - addKeyValue('Browser', { - Name: browser.name, - Version: browser.version, - Language: browser.language, - Platform: browser.platform, - 'Cookies Enabled': browser.cookiesEnabled ? 'Yes' : 'No', - 'Online Status': browser.online ? 'Online' : 'Offline', - }); - - // Screen Information - const screen = debugData.system.screen; - addKeyValue('Screen', { - Resolution: `${screen.width}x${screen.height}`, - 'Color Depth': screen.colorDepth + ' bit', - 'Pixel Ratio': screen.pixelRatio + 'x', - }); - - // Time Information - const time = debugData.system.time; - addKeyValue('Time Settings', { - Timezone: time.timezone, - 'UTC Offset': time.offset / 60 + ' hours', - Locale: time.locale, - }); - - addHorizontalLine(); - } - - // Web App Information Section - if (debugData.webApp) { - addSectionHeader('Web App Information'); - - // Basic Info - addKeyValue('Application', { - Name: debugData.webApp.name, - Version: debugData.webApp.version, - Environment: debugData.webApp.environment, - 'Node Version': debugData.webApp.runtimeInfo.nodeVersion, - }); - - // Git Information - if (debugData.webApp.gitInfo) { - const gitInfo = debugData.webApp.gitInfo.local; - addKeyValue('Git Information', { - Branch: gitInfo.branch, - Commit: gitInfo.commitHash, - Author: gitInfo.author, - 'Commit Time': gitInfo.commitTime, - Repository: gitInfo.repoName, - }); - - if (debugData.webApp.gitInfo.github) { - const githubInfo = debugData.webApp.gitInfo.github.currentRepo; - addKeyValue('GitHub Information', { - Repository: githubInfo.fullName, - 'Default Branch': githubInfo.defaultBranch, - Stars: githubInfo.stars, - Forks: githubInfo.forks, - 'Open Issues': githubInfo.openIssues || 0, - }); - } - } - - addHorizontalLine(); - } - - // Performance Section - if (debugData.performance) { - addSectionHeader('Performance Metrics'); - - // Memory Usage - const memory = debugData.performance.memory || {}; - const totalHeap = memory.totalJSHeapSize || 0; - const usedHeap = memory.usedJSHeapSize || 0; - const usagePercentage = memory.usagePercentage || 0; - - addKeyValue('Memory Usage', { - 'Total Heap Size': formatBytes(totalHeap), - 'Used Heap Size': formatBytes(usedHeap), - Usage: usagePercentage.toFixed(1) + '%', - }); - - // Timing Metrics - const timing = debugData.performance.timing || {}; - const navigationStart = timing.navigationStart || 0; - const loadEventEnd = timing.loadEventEnd || 0; - const domContentLoadedEventEnd = timing.domContentLoadedEventEnd || 0; - const responseEnd = timing.responseEnd || 0; - const requestStart = timing.requestStart || 0; - - const loadTime = loadEventEnd > navigationStart ? loadEventEnd - navigationStart : 0; - const domReadyTime = - domContentLoadedEventEnd > navigationStart ? domContentLoadedEventEnd - navigationStart : 0; - const requestTime = responseEnd > requestStart ? responseEnd - requestStart : 0; - - addKeyValue('Page Load Metrics', { - 'Total Load Time': (loadTime / 1000).toFixed(2) + ' seconds', - 'DOM Ready Time': (domReadyTime / 1000).toFixed(2) + ' seconds', - 'Request Time': (requestTime / 1000).toFixed(2) + ' seconds', - }); - - // Network Information - if (debugData.system?.network) { - const network = debugData.system.network; - addKeyValue('Network Information', { - 'Connection Type': network.type || 'Unknown', - 'Effective Type': network.effectiveType || 'Unknown', - 'Download Speed': (network.downlink || 0) + ' Mbps', - 'Latency (RTT)': (network.rtt || 0) + ' ms', - 'Data Saver': network.saveData ? 'Enabled' : 'Disabled', - }); - } - - addHorizontalLine(); - } - - // Errors Section - if (debugData.errors && debugData.errors.length > 0) { - addSectionHeader('Error Log'); - - debugData.errors.forEach((error: LogEntry, index: number) => { - doc.setTextColor('#DC2626'); - doc.setFontSize(10); - doc.setFont('helvetica', 'bold'); - doc.text(`Error ${index + 1}:`, margin, yPos); - yPos += lineHeight; - - doc.setFont('helvetica', 'normal'); - doc.setTextColor('#6B7280'); - addKeyValue('Message', error.message, 10); - - if (error.stack) { - addKeyValue('Stack', error.stack, 10); - } - - if (error.source) { - addKeyValue('Source', error.source, 10); - } - - yPos += lineHeight; - }); - } - - // Add footers to all pages at the end - addFooters(); - - // Save the PDF - doc.save(`codinit-debug-info-${new Date().toISOString()}.pdf`); - toast.success('Debug information exported as PDF'); - } catch (error) { - console.error('Failed to export PDF:', error); - toast.error('Failed to export debug information as PDF'); - } - }; - - const exportAsText = () => { - try { - const debugData = { - system: systemInfo, - webApp: webAppInfo, - errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'), - performance: { - memory: (performance as any).memory || {}, - timing: performance.timing, - navigation: performance.navigation, - }, - }; - - const textContent = Object.entries(debugData) - .map(([category, data]) => { - return `${category.toUpperCase()}\n${'-'.repeat(30)}\n${JSON.stringify(data, null, 2)}\n\n`; - }) - .join('\n'); - - const blob = new Blob([textContent], { type: 'text/plain' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `codinit-debug-info-${new Date().toISOString()}.txt`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - toast.success('Debug information exported as text file'); - } catch (error) { - console.error('Failed to export text file:', error); - toast.error('Failed to export debug information as text file'); - } - }; - - const exportFormats: ExportFormat[] = [ - { - id: 'json', - label: 'Export as JSON', - icon: 'i-ph:file-js', - handler: exportDebugInfo, - }, - { - id: 'csv', - label: 'Export as CSV', - icon: 'i-ph:file-csv', - handler: exportAsCSV, - }, - { - id: 'pdf', - label: 'Export as PDF', - icon: 'i-ph:file-pdf', - handler: exportAsPDF, - }, - { - id: 'txt', - label: 'Export as Text', - icon: 'i-ph:file-text', - handler: exportAsText, - }, - ]; - - // Add Ollama health check function - const checkOllamaStatus = useCallback(async () => { - try { - const ollamaProvider = providers?.Ollama; - const baseUrl = ollamaProvider?.settings?.baseUrl || 'http://127.0.0.1:11434'; - - // First check if service is running - const versionResponse = await fetch(`${baseUrl}/api/version`); - - if (!versionResponse.ok) { - throw new Error('Service not running'); - } - - // Then fetch installed models - const modelsResponse = await fetch(`${baseUrl}/api/tags`); - - const modelsData = (await modelsResponse.json()) as { - models: Array<{ name: string; size: string; quantization: string }>; - }; - - setOllamaStatus({ - isRunning: true, - lastChecked: new Date(), - models: modelsData.models, - }); - } catch { - setOllamaStatus({ - isRunning: false, - error: 'Connection failed', - lastChecked: new Date(), - models: undefined, - }); - } - }, [providers]); - - // Monitor Ollama provider status and check periodically - useEffect(() => { - const ollamaProvider = providers?.Ollama; - - if (ollamaProvider?.settings?.enabled) { - // Check immediately when provider is enabled - checkOllamaStatus(); - - // Set up periodic checks every 10 seconds - const intervalId = setInterval(checkOllamaStatus, 10000); - - return () => clearInterval(intervalId); - } - - return undefined; - }, [providers, checkOllamaStatus]); - - // Replace the existing export button with this new component - const ExportButton = () => { - const [isOpen, setIsOpen] = useState(false); - - const handleOpenChange = useCallback((open: boolean) => { - setIsOpen(open); - }, []); - - const handleFormatClick = useCallback((handler: () => void) => { - handler(); - setIsOpen(false); - }, []); - - return ( - - - - -
    - -
    - Export Debug Information - - -
    - {exportFormats.map((format) => ( - - ))} -
    -
    -
    -
    - ); - }; - - // Add helper function to get Ollama status text and color - const getOllamaStatus = () => { - const ollamaProvider = providers?.Ollama; - const isOllamaEnabled = ollamaProvider?.settings?.enabled; - - if (!isOllamaEnabled) { - return { - status: 'Disabled', - color: 'text-red-500', - bgColor: 'bg-red-500', - message: 'Ollama provider is disabled in settings', - }; - } - - if (!ollamaStatus.isRunning) { - return { - status: 'Not Running', - color: 'text-red-500', - bgColor: 'bg-red-500', - message: ollamaStatus.error || 'Ollama service is not running', - }; - } - - const modelCount = ollamaStatus.models?.length ?? 0; - - return { - status: 'Running', - color: 'text-green-500', - bgColor: 'bg-green-500', - message: `Ollama service is running with ${modelCount} installed models (Provider: Enabled)`, - }; - }; - - // Add type for status result - type StatusResult = { - status: string; - color: string; - bgColor: string; - message: string; - }; - - const status = getOllamaStatus() as StatusResult; - - return ( -
    - {/* Quick Stats Banner */} -
    - {/* Errors Card */} -
    -
    -
    -
    Errors
    -
    -
    - 0 ? 'text-red-500' : 'text-green-500')} - > - {errorLogs.length} - -
    -
    -
    0 ? 'i-ph:warning text-red-500' : 'i-ph:check-circle text-green-500', - )} - /> - {errorLogs.length > 0 ? 'Errors detected' : 'No errors detected'} -
    -
    - - {/* Memory Usage Card */} -
    -
    -
    -
    Memory Usage
    -
    -
    - 80 - ? 'text-red-500' - : (systemInfo?.memory?.percentage ?? 0) > 60 - ? 'text-yellow-500' - : 'text-green-500', - )} - > - {systemInfo?.memory?.percentage ?? 0}% - -
    - 80 - ? '[&>div]:bg-red-500' - : (systemInfo?.memory?.percentage ?? 0) > 60 - ? '[&>div]:bg-yellow-500' - : '[&>div]:bg-green-500', - )} - /> -
    -
    - Used: {systemInfo?.memory.used ?? '0 GB'} / {systemInfo?.memory.total ?? '0 GB'} -
    -
    - - {/* Page Load Time Card */} -
    -
    -
    -
    Page Load Time
    -
    -
    - 2000 - ? 'text-red-500' - : (systemInfo?.performance.timing.loadTime ?? 0) > 1000 - ? 'text-yellow-500' - : 'text-green-500', - )} - > - {systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) : '-'}s - -
    -
    -
    - DOM Ready: {systemInfo ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2) : '-'}s -
    -
    - - {/* Network Speed Card */} -
    -
    -
    -
    Network Speed
    -
    -
    - - {systemInfo?.network.downlink ?? '-'} Mbps - -
    -
    -
    - RTT: {systemInfo?.network.rtt ?? '-'} ms -
    -
    - - {/* Ollama Service Card - Now spans all 4 columns */} -
    -
    -
    -
    -
    -
    Ollama Service
    -
    {status.message}
    -
    -
    -
    -
    -
    - - {status.status} - -
    -
    -
    - {ollamaStatus.lastChecked.toLocaleTimeString()} -
    -
    -
    - -
    - {status.status === 'Running' && ollamaStatus.models && ollamaStatus.models.length > 0 ? ( - <> -
    -
    -
    - Installed Models - - {ollamaStatus.models.length} - -
    -
    -
    -
    - {ollamaStatus.models.map((model) => ( -
    -
    -
    - {model.name} -
    - - {Math.round(parseInt(model.size) / 1024 / 1024)}MB - -
    - ))} -
    -
    - - ) : ( -
    -
    -
    - {status.message} -
    -
    - )} -
    -
    -
    - - {/* Action Buttons */} -
    - - - - - - - - - -
    - - {/* System Information */} - setOpenSections((prev) => ({ ...prev, system: open }))} - className="w-full" - > - -
    -
    -
    -

    System Information

    -
    -
    -
    - - - -
    - {systemInfo ? ( -
    -
    -
    -
    - OS: - {systemInfo.os} -
    -
    -
    - Platform: - {systemInfo.platform} -
    -
    -
    - Architecture: - {systemInfo.arch} -
    -
    -
    - CPU Cores: - {systemInfo.cpus} -
    -
    -
    - Node Version: - {systemInfo.node} -
    -
    -
    - Network Type: - - {systemInfo.network.type} ({systemInfo.network.effectiveType}) - -
    -
    -
    - Network Speed: - - {systemInfo.network.downlink}Mbps (RTT: {systemInfo.network.rtt}ms) - -
    - {systemInfo.battery && ( -
    -
    - Battery: - - {systemInfo.battery.level.toFixed(1)}% {systemInfo.battery.charging ? '(Charging)' : ''} - -
    - )} -
    -
    - Storage: - - {(systemInfo.storage.usage / (1024 * 1024 * 1024)).toFixed(2)}GB /{' '} - {(systemInfo.storage.quota / (1024 * 1024 * 1024)).toFixed(2)}GB - -
    -
    -
    -
    -
    - Memory Usage: - - {systemInfo.memory.used} / {systemInfo.memory.total} ({systemInfo.memory.percentage}%) - -
    -
    -
    - Browser: - - {systemInfo.browser.name} {systemInfo.browser.version} - -
    -
    -
    - Screen: - - {systemInfo.screen.width}x{systemInfo.screen.height} ({systemInfo.screen.pixelRatio}x) - -
    -
    -
    - Timezone: - {systemInfo.time.timezone} -
    -
    -
    - Language: - {systemInfo.browser.language} -
    -
    -
    - JS Heap: - - {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '} - {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB ( - {systemInfo.performance.memory.usagePercentage.toFixed(1)}%) - -
    -
    -
    - Page Load: - - {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s - -
    -
    -
    - DOM Ready: - - {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s - -
    -
    -
    - ) : ( -
    Loading system information...
    - )} -
    - - - - {/* Performance Metrics */} - setOpenSections((prev) => ({ ...prev, performance: open }))} - className="w-full" - > - -
    -
    -
    -

    Performance Metrics

    -
    -
    -
    - - - -
    - {systemInfo && ( -
    -
    -
    - Page Load Time: - - {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s - -
    -
    - DOM Ready Time: - - {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s - -
    -
    - Request Time: - - {(systemInfo.performance.timing.requestTime / 1000).toFixed(2)}s - -
    -
    - Redirect Time: - - {(systemInfo.performance.timing.redirectTime / 1000).toFixed(2)}s - -
    -
    -
    -
    - JS Heap Usage: - - {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '} - {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB - -
    -
    - Heap Utilization: - - {systemInfo.performance.memory.usagePercentage.toFixed(1)}% - -
    -
    - Navigation Type: - - {systemInfo.performance.navigation.type === 0 - ? 'Navigate' - : systemInfo.performance.navigation.type === 1 - ? 'Reload' - : systemInfo.performance.navigation.type === 2 - ? 'Back/Forward' - : 'Other'} - -
    -
    - Redirects: - - {systemInfo.performance.navigation.redirectCount} - -
    -
    -
    - )} -
    -
    - - - {/* WebApp Information */} - setOpenSections((prev) => ({ ...prev, webapp: open }))} - className="w-full" - > - -
    -
    -
    -

    WebApp Information

    - {loading.webAppInfo && } -
    -
    -
    - - - -
    - {loading.webAppInfo ? ( -
    - -
    - ) : !webAppInfo ? ( -
    -
    -

    Failed to load WebApp information

    - -
    - ) : ( -
    -
    -

    Basic Information

    -
    -
    -
    - Name: - {webAppInfo.name} -
    -
    -
    - Version: - {webAppInfo.version} -
    -
    -
    - License: - {webAppInfo.license} -
    -
    -
    - Environment: - {webAppInfo.environment} -
    -
    -
    - Node Version: - {webAppInfo.runtimeInfo.nodeVersion} -
    -
    -
    - -
    -

    Git Information

    -
    -
    -
    - Branch: - {webAppInfo.gitInfo.local.branch} -
    -
    -
    - Commit: - {webAppInfo.gitInfo.local.commitHash} -
    -
    -
    - Author: - {webAppInfo.gitInfo.local.author} -
    -
    -
    - Commit Time: - {webAppInfo.gitInfo.local.commitTime} -
    - - {webAppInfo.gitInfo.github && ( - <> -
    -
    -
    - Repository: - - {webAppInfo.gitInfo.github.currentRepo.fullName} - {webAppInfo.gitInfo.isForked && ' (fork)'} - -
    - -
    -
    -
    - - {webAppInfo.gitInfo.github.currentRepo.stars} - -
    -
    -
    - - {webAppInfo.gitInfo.github.currentRepo.forks} - -
    -
    -
    - - {webAppInfo.gitInfo.github.currentRepo.openIssues} - -
    -
    -
    - - {webAppInfo.gitInfo.github.upstream && ( -
    -
    -
    - Upstream: - - {webAppInfo.gitInfo.github.upstream.fullName} - -
    - -
    -
    -
    - - {webAppInfo.gitInfo.github.upstream.stars} - -
    -
    -
    - - {webAppInfo.gitInfo.github.upstream.forks} - -
    -
    -
    - )} - - )} -
    -
    -
    - )} - - {webAppInfo && ( -
    -

    Dependencies

    -
    - - - - -
    -
    - )} -
    - - - - {/* Error Check */} - setOpenSections((prev) => ({ ...prev, errors: open }))} - className="w-full" - > - -
    -
    -
    -

    Error Check

    - {errorLogs.length > 0 && ( - - {errorLogs.length} Errors - - )} -
    -
    -
    - - - -
    - -
    -
    - Checks for: -
      -
    • Unhandled JavaScript errors
    • -
    • Unhandled Promise rejections
    • -
    • Runtime exceptions
    • -
    • Network errors
    • -
    -
    -
    - Status: - - {loading.errors - ? 'Checking...' - : errorLogs.length > 0 - ? `${errorLogs.length} errors found` - : 'No errors found'} - -
    - {errorLogs.length > 0 && ( -
    -
    Recent Errors:
    -
    - {errorLogs.map((error) => ( -
    -
    {error.message}
    - {error.source && ( -
    - Source: {error.source} - {error.details?.lineNumber && `:${error.details.lineNumber}`} -
    - )} - {error.stack && ( -
    {error.stack}
    - )} -
    - ))} -
    -
    - )} -
    -
    -
    -
    - -
    - ); -} diff --git a/app/components/@settings/tabs/features/FeaturesTab.tsx b/app/components/@settings/tabs/features/FeaturesTab.tsx deleted file mode 100644 index 6a9ff772..00000000 --- a/app/components/@settings/tabs/features/FeaturesTab.tsx +++ /dev/null @@ -1,311 +0,0 @@ -import { useCallback } from 'react'; -import { Switch } from '~/components/ui/Switch'; -import { useSettings } from '~/lib/hooks/useSettings'; -import { classNames } from '~/utils/classNames'; -import { toast } from 'react-toastify'; -import { PromptLibrary } from '~/lib/common/prompt-library'; -import { SettingsSection } from '~/components/@settings/shared/components/SettingsCard'; -import { SettingsList, SettingsListItem, SettingsPanel } from '~/components/@settings/shared/components/SettingsPanel'; - -interface FeatureToggle { - id: string; - title: string; - description: string; - icon: string; - enabled: boolean; - beta?: boolean; - experimental?: boolean; - tooltip?: string; -} - -export default function FeaturesTab() { - const { - autoSelectTemplate, - isLatestBranch, - contextOptimizationEnabled, - eventLogs, - setAutoSelectTemplate, - enableLatestBranch, - enableContextOptimization, - setEventLogs, - setPromptId, - promptId, - } = useSettings(); - - // Enable features by default on first load - useCallback(() => { - // Only set defaults if values are undefined - if (isLatestBranch === undefined) { - enableLatestBranch(false); // Default: OFF - Don't auto-update from main branch - } - - if (contextOptimizationEnabled === undefined) { - enableContextOptimization(true); // Default: ON - Enable context optimization - } - - if (autoSelectTemplate === undefined) { - setAutoSelectTemplate(true); // Default: ON - Enable auto-select templates - } - - if (promptId === undefined) { - setPromptId('default'); // Default: 'default' - } - - if (eventLogs === undefined) { - setEventLogs(true); // Default: ON - Enable event logging - } - }, []); // Only run once on component mount - - const handleToggleFeature = useCallback( - (id: string, enabled: boolean) => { - switch (id) { - case 'latestBranch': { - enableLatestBranch(enabled); - toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`); - break; - } - - case 'autoSelectTemplate': { - setAutoSelectTemplate(enabled); - toast.success(`Auto select template ${enabled ? 'enabled' : 'disabled'}`); - break; - } - - case 'contextOptimization': { - enableContextOptimization(enabled); - toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`); - break; - } - - case 'eventLogs': { - setEventLogs(enabled); - toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`); - break; - } - - default: - break; - } - }, - [enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs], - ); - - const features: { stable: FeatureToggle[]; beta: FeatureToggle[] } = { - stable: [ - { - id: 'latestBranch', - title: 'Main Branch Updates', - description: 'Get the latest updates from the main branch', - icon: 'i-ph:git-branch', - enabled: isLatestBranch, - tooltip: 'Enabled by default to receive updates from the main development branch', - }, - { - id: 'autoSelectTemplate', - title: 'Auto Select Template', - description: 'Automatically select starter template', - icon: 'i-ph:selection', - enabled: autoSelectTemplate, - tooltip: 'Enabled by default to automatically select the most appropriate starter template', - }, - { - id: 'contextOptimization', - title: 'Context Optimization', - description: 'Optimize context for better responses', - icon: 'i-ph:brain', - enabled: contextOptimizationEnabled, - tooltip: 'Enabled by default for improved AI responses', - }, - { - id: 'eventLogs', - title: 'Event Logging', - description: 'Enable detailed event logging and history', - icon: 'i-ph:list-bullets', - enabled: eventLogs, - tooltip: 'Enabled by default to record detailed logs of system events and user actions', - }, - ], - beta: [], - }; - - return ( -
    - - - - {features.stable.map((feature, _index) => ( - -
    -
    -
    -
    -
    -
    -

    - {feature.title} -

    - {feature.beta && ( - - Beta - - )} - {feature.experimental && ( - - Experimental - - )} -
    -

    - {feature.description} -

    - {feature.tooltip && ( -
    - 💡 - {feature.tooltip} -
    - )} -
    -
    -
    - handleToggleFeature(feature.id, checked)} - /> -
    - - ))} - - - - - {features.beta.length > 0 && ( - - - - {features.beta.map((feature, _index) => ( - -
    -
    -
    -
    -
    -
    -

    - {feature.title} -

    - {feature.beta && ( - - Beta - - )} - {feature.experimental && ( - - Experimental - - )} -
    -

    - {feature.description} -

    - {feature.tooltip && ( -
    - 💡 - {feature.tooltip} -
    - )} -
    -
    -
    - handleToggleFeature(feature.id, checked)} - /> -
    - - ))} - - - - )} - - - -
    -
    -
    -
    -
    -

    System Prompt Template

    -

    - Choose a prompt from the library to use as the system prompt for AI interactions -

    -
    -
    - -
    -
    - - -
    - ); -} diff --git a/app/components/@settings/tabs/notifications/NotificationsTab.tsx b/app/components/@settings/tabs/notifications/NotificationsTab.tsx deleted file mode 100644 index d0e2d6fd..00000000 --- a/app/components/@settings/tabs/notifications/NotificationsTab.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; -import { logStore } from '~/lib/stores/logs'; -import { useStore } from '@nanostores/react'; -import { formatDistanceToNow } from 'date-fns'; -import { classNames } from '~/utils/classNames'; -import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; - -interface NotificationDetails { - type?: string; - message?: string; - currentVersion?: string; - latestVersion?: string; - branch?: string; - updateUrl?: string; -} - -type FilterType = 'all' | 'system' | 'error' | 'warning' | 'update' | 'info' | 'provider' | 'network'; - -const NotificationsTab = () => { - const [filter, setFilter] = useState('all'); - const logs = useStore(logStore.logs); - - useEffect(() => { - const startTime = performance.now(); - - return () => { - const duration = performance.now() - startTime; - logStore.logPerformanceMetric('NotificationsTab', 'mount-duration', duration); - }; - }, []); - - const handleClearNotifications = () => { - const count = Object.keys(logs).length; - logStore.logInfo('Cleared notifications', { - type: 'notification_clear', - message: `Cleared ${count} notifications`, - clearedCount: count, - component: 'notifications', - }); - logStore.clearLogs(); - }; - - const handleUpdateAction = (updateUrl: string) => { - logStore.logInfo('Update link clicked', { - type: 'update_click', - message: 'User clicked update link', - updateUrl, - component: 'notifications', - }); - window.open(updateUrl, '_blank'); - }; - - const handleFilterChange = (newFilter: FilterType) => { - logStore.logInfo('Notification filter changed', { - type: 'filter_change', - message: `Filter changed to ${newFilter}`, - previousFilter: filter, - newFilter, - component: 'notifications', - }); - setFilter(newFilter); - }; - - const filteredLogs = Object.values(logs) - .filter((log) => { - if (filter === 'all') { - return true; - } - - if (filter === 'update') { - return log.details?.type === 'update'; - } - - if (filter === 'system') { - return log.category === 'system'; - } - - if (filter === 'provider') { - return log.category === 'provider'; - } - - if (filter === 'network') { - return log.category === 'network'; - } - - return log.level === filter; - }) - .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); - - const getNotificationStyle = (level: string, type?: string) => { - if (type === 'update') { - return { - icon: 'i-ph:arrow-circle-up', - color: 'text-blue-500 dark:text-blue-400', - bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20', - }; - } - - switch (level) { - case 'error': - return { - icon: 'i-ph:warning-circle', - color: 'text-red-500 dark:text-red-400', - bg: 'hover:bg-red-500/10 dark:hover:bg-red-500/20', - }; - case 'warning': - return { - icon: 'i-ph:warning', - color: 'text-yellow-500 dark:text-yellow-400', - bg: 'hover:bg-yellow-500/10 dark:hover:bg-yellow-500/20', - }; - case 'info': - return { - icon: 'i-ph:info', - color: 'text-blue-500 dark:text-blue-400', - bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20', - }; - default: - return { - icon: 'i-ph:bell', - color: 'text-gray-500 dark:text-gray-400', - bg: 'hover:bg-gray-500/10 dark:hover:bg-gray-500/20', - }; - } - }; - - const renderNotificationDetails = (details: NotificationDetails) => { - if (details.type === 'update') { - return ( -
    -

    {details.message}

    -
    -

    Current Version: {details.currentVersion}

    -

    Latest Version: {details.latestVersion}

    -

    Branch: {details.branch}

    -
    - -
    - ); - } - - return details.message ?

    {details.message}

    : null; - }; - - const filterOptions: { id: FilterType; label: string; icon: string; color: string }[] = [ - { id: 'all', label: 'All Notifications', icon: 'i-ph:bell', color: '#9333ea' }, - { id: 'system', label: 'System', icon: 'i-ph:gear', color: '#6b7280' }, - { id: 'update', label: 'Updates', icon: 'i-ph:arrow-circle-up', color: '#9333ea' }, - { id: 'error', label: 'Errors', icon: 'i-ph:warning-circle', color: '#ef4444' }, - { id: 'warning', label: 'Warnings', icon: 'i-ph:warning', color: '#f59e0b' }, - { id: 'info', label: 'Information', icon: 'i-ph:info', color: '#3b82f6' }, - { id: 'provider', label: 'Providers', icon: 'i-ph:robot', color: '#10b981' }, - { id: 'network', label: 'Network', icon: 'i-ph:wifi-high', color: '#6366f1' }, - ]; - - return ( -
    -
    - - - - - - - - {filterOptions.map((option) => ( - handleFilterChange(option.id)} - > -
    -
    -
    - {option.label} - - ))} - - - - - -
    - -
    - {filteredLogs.length === 0 ? ( - - -
    -

    No Notifications

    -

    You're all caught up!

    -
    -
    - ) : ( - filteredLogs.map((log) => { - const style = getNotificationStyle(log.level, log.details?.type); - return ( - -
    -
    - -
    -

    {log.message}

    - {log.details && renderNotificationDetails(log.details as NotificationDetails)} -

    - Category: {log.category} - {log.subCategory ? ` > ${log.subCategory}` : ''} -

    -
    -
    - -
    -
    - ); - }) - )} -
    -
    - ); -}; - -export default NotificationsTab; diff --git a/app/components/@settings/tabs/profile/ProfileTab.tsx b/app/components/@settings/tabs/profile/ProfileTab.tsx deleted file mode 100644 index 7e1a380b..00000000 --- a/app/components/@settings/tabs/profile/ProfileTab.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { useState, useCallback } from 'react'; -import { useStore } from '@nanostores/react'; -import { classNames } from '~/utils/classNames'; -import { profileStore, updateProfile } from '~/lib/stores/profile'; -import { toast } from 'react-toastify'; -import { debounce } from '~/utils/debounce'; - -export default function ProfileTab() { - const profile = useStore(profileStore); - const [isUploading, setIsUploading] = useState(false); - - // Create debounced update functions - const debouncedUpdate = useCallback( - debounce((field: 'username' | 'bio', value: string) => { - updateProfile({ [field]: value }); - toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`); - }, 1000), - [], - ); - - const handleAvatarUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - - if (!file) { - return; - } - - try { - setIsUploading(true); - - // Convert the file to base64 - const reader = new FileReader(); - - reader.onloadend = () => { - const base64String = reader.result as string; - updateProfile({ avatar: base64String }); - setIsUploading(false); - toast.success('Profile picture updated'); - }; - - reader.onerror = () => { - console.error('Error reading file:', reader.error); - setIsUploading(false); - toast.error('Failed to update profile picture'); - }; - reader.readAsDataURL(file); - } catch (error) { - console.error('Error uploading avatar:', error); - setIsUploading(false); - toast.error('Failed to update profile picture'); - } - }; - - const handleProfileUpdate = (field: 'username' | 'bio', value: string) => { - // Update the store immediately for UI responsiveness - updateProfile({ [field]: value }); - - // Debounce the toast notification - debouncedUpdate(field, value); - }; - - return ( -
    -
    - {/* Personal Information Section */} -
    - {/* Avatar Upload */} -
    -
    - {profile.avatar ? ( - Profile - ) : ( -
    - )} - -